diff --git a/.gitignore b/.gitignore
index 9b9798c..a0b9104 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,6 @@
local.properties
/app/release/
.idea
-beta/
\ No newline at end of file
+beta/
+
+/downloader/
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 56d6814..28869da 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -12,6 +12,8 @@
+
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index dc8131f..457142d 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -52,4 +52,11 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/ranobe/ranobe/App.java b/app/src/main/java/org/ranobe/ranobe/App.java
index 31c751b..29fc43a 100644
--- a/app/src/main/java/org/ranobe/ranobe/App.java
+++ b/app/src/main/java/org/ranobe/ranobe/App.java
@@ -20,7 +20,6 @@ public static Context getContext() {
@Override
public void onCreate() {
super.onCreate();
-
Log.d(Ranobe.DEBUG, "app launched");
DynamicColors.applyToActivitiesIfAvailable(this);
App.context = getApplicationContext();
diff --git a/app/src/main/java/org/ranobe/ranobe/sources/SourceManager.java b/app/src/main/java/org/ranobe/ranobe/sources/SourceManager.java
index 1c61bac..11d092b 100644
--- a/app/src/main/java/org/ranobe/ranobe/sources/SourceManager.java
+++ b/app/src/main/java/org/ranobe/ranobe/sources/SourceManager.java
@@ -2,11 +2,17 @@
import org.ranobe.ranobe.sources.en.AllNovel;
import org.ranobe.ranobe.sources.en.AzyNovel;
+import org.ranobe.ranobe.sources.en.BoxNovel;
import org.ranobe.ranobe.sources.en.LightNovelBtt;
+import org.ranobe.ranobe.sources.en.LightNovelHeaven;
import org.ranobe.ranobe.sources.en.LightNovelPub;
+import org.ranobe.ranobe.sources.en.Neovel;
+import org.ranobe.ranobe.sources.en.NewNovel;
import org.ranobe.ranobe.sources.en.Ranobe;
import org.ranobe.ranobe.sources.en.ReadLightNovel;
+import org.ranobe.ranobe.sources.en.ReadWebNovels;
import org.ranobe.ranobe.sources.en.VipNovel;
+import org.ranobe.ranobe.sources.en.WuxiaWorld;
import org.ranobe.ranobe.sources.ru.RanobeHub;
import java.util.HashMap;
@@ -35,6 +41,12 @@ public static HashMap> getSources() {
sources.put(6, Ranobe.class);
sources.put(7, AllNovel.class);
sources.put(8, AzyNovel.class);
+ sources.put(9, LightNovelHeaven.class);
+ sources.put(10, NewNovel.class);
+ sources.put(11, ReadWebNovels.class);
+ sources.put(12, BoxNovel.class);
+ sources.put(13, WuxiaWorld.class);
+ sources.put(14, Neovel.class);
return sources;
}
diff --git a/app/src/main/java/org/ranobe/ranobe/sources/en/BoxNovel.java b/app/src/main/java/org/ranobe/ranobe/sources/en/BoxNovel.java
new file mode 100644
index 0000000..7434746
--- /dev/null
+++ b/app/src/main/java/org/ranobe/ranobe/sources/en/BoxNovel.java
@@ -0,0 +1,152 @@
+package org.ranobe.ranobe.sources.en;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.ranobe.models.Chapter;
+import org.ranobe.ranobe.models.DataSource;
+import org.ranobe.ranobe.models.Filter;
+import org.ranobe.ranobe.models.Lang;
+import org.ranobe.ranobe.models.Novel;
+import org.ranobe.ranobe.network.HttpClient;
+import org.ranobe.ranobe.sources.Source;
+import org.ranobe.ranobe.util.NumberUtils;
+import org.ranobe.ranobe.util.SourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class BoxNovel implements Source {
+ private static final String baseUrl = "https://boxnovel.com";
+ private static final int sourceId = 12;
+
+ private String cleanImg(String cover) {
+ return cover.replaceAll("/-\\d+x\\d+.\\w{3}/gm", ".jpg");
+ }
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "Box Novel";
+ source.lang = Lang.eng;
+ source.dev = "ap-atul";
+ source.logo = "https://boxnovel.com/wp-content/uploads/2018/04/box-icon-250x250.png";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws IOException {
+ List items = new ArrayList<>();
+ String web = baseUrl.concat("/page/").concat(String.valueOf(page));
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+
+ for (Element element : doc.select(".page-item-detail")) {
+ String url = element.select(".h5 > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = element.select(".h5 > a").text().trim();
+ item.cover = cleanImg(element.select("img").attr("data-src").trim());
+ items.add(item);
+ }
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, new HashMap<>()));
+
+ novel.sourceId = sourceId;
+ novel.name = doc.select(".post-title > h1").text().trim();
+ novel.cover = cleanImg(doc.select(".summary_image > a > img").attr("data-src").trim());
+ novel.summary = doc.select("div.summary__content").text().replaceAll("\n", "\n\n").trim();
+ novel.rating = NumberUtils.toFloat(doc.select(".total_votes").text().trim());
+ novel.authors = Arrays.asList(doc.select(".author-content > a").text().split(","));
+
+ List genres = new ArrayList<>();
+ for (Element element : doc.select(".genres-content > a")) {
+ genres.add(element.text().trim());
+ }
+ novel.genres = genres;
+
+ for (Element element : doc.select(".post-content_item")) {
+ String header = element.select(".summary-heading > h5").text().trim();
+ String content = element.select(".summary-content").text().trim();
+
+ if (header.equalsIgnoreCase("Status")) {
+ novel.status = content;
+ } else if (header.equalsIgnoreCase("Alternative")) {
+ novel.alternateNames = Arrays.asList(content.split(","));
+ } else if (header.equalsIgnoreCase("Release")) {
+ novel.year = NumberUtils.toInt(content);
+ }
+ }
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws IOException {
+ List items = new ArrayList<>();
+ String web = novel.url.concat("ajax/chapters");
+ Element doc = Jsoup.parse(HttpClient.POST(web, new HashMap<>(), new HashMap<>()));
+
+ for (Element element : doc.select(".wp-manga-chapter")) {
+ Chapter item = new Chapter(novel.url);
+
+ item.url = element.select("a").attr("href").trim();
+ item.name = element.select("a").text().trim();
+ item.id = NumberUtils.toFloat(item.name);
+ item.updated = element.select("span.chapter-release-date").text().trim();
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(chapter.url, new HashMap<>()));
+ Element main = doc.select(".reading-content").first();
+
+ if (main == null) {
+ return null;
+ }
+
+ chapter.content = "";
+ chapter.content = String.join("\n\n", main.select("p").eachText());
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws IOException {
+ List items = new ArrayList<>();
+
+ if (filters.hashKeyword()) {
+ String web = SourceUtils.buildUrl(baseUrl, "/page/", String.valueOf(page), "/?s=", filters.getKeyword(), "&post_type=wp-manga");
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+ for (Element element : doc.select(".c-tabs-item__content")) {
+ String url = element.select(".tab-thumb > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.url = url;
+ item.name = element.select(".post-title > h3 > a").text().trim();
+ item.cover = cleanImg(element.select("img.img-responsive").attr("data-src").trim());
+
+ items.add(item);
+ }
+ }
+ }
+
+ return items;
+ }
+}
diff --git a/app/src/main/java/org/ranobe/ranobe/sources/en/LightNovelHeaven.java b/app/src/main/java/org/ranobe/ranobe/sources/en/LightNovelHeaven.java
new file mode 100644
index 0000000..98f1722
--- /dev/null
+++ b/app/src/main/java/org/ranobe/ranobe/sources/en/LightNovelHeaven.java
@@ -0,0 +1,148 @@
+package org.ranobe.ranobe.sources.en;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.ranobe.models.Chapter;
+import org.ranobe.ranobe.models.DataSource;
+import org.ranobe.ranobe.models.Filter;
+import org.ranobe.ranobe.models.Lang;
+import org.ranobe.ranobe.models.Novel;
+import org.ranobe.ranobe.network.HttpClient;
+import org.ranobe.ranobe.sources.Source;
+import org.ranobe.ranobe.util.NumberUtils;
+import org.ranobe.ranobe.util.SourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class LightNovelHeaven implements Source {
+ private final String baseUrl = "https://lightnovelheaven.com/";
+ private final int sourceId = 9;
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "Light Novel Heaven";
+ source.lang = Lang.eng;
+ source.dev = "ap-atul";
+ source.logo = "https://lightnovelheaven.com/wp-content/uploads/2020/07/cropped-mid-2-192x192.png";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws IOException {
+ List items = new ArrayList<>();
+ String web = baseUrl.concat("/page/").concat(String.valueOf(page));
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+
+ for (Element element : doc.select(".page-item-detail")) {
+ String url = element.select(".h5 > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = element.select(".h5 > a").text().trim();
+ item.cover = element.select("img").attr("data-src").trim();
+ items.add(item);
+ }
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, new HashMap<>()));
+
+ novel.sourceId = sourceId;
+ novel.name = doc.select(".post-title > h1").text().trim();
+ novel.cover = doc.select(".summary_image > a > img").attr("data-src").trim();
+ novel.summary = String.join("\n\n", doc.select("div.summary__content").select("p").eachText());
+ novel.rating = NumberUtils.toFloat(doc.select(".total_votes").text().trim());
+ novel.authors = Arrays.asList(doc.select(".author-content > a").text().split(","));
+
+ List genres = new ArrayList<>();
+ for (Element element : doc.select(".genres-content > a")) {
+ genres.add(element.text().trim());
+ }
+ novel.genres = genres;
+
+ for (Element element : doc.select(".post-content_item")) {
+ String header = element.select(".summary-heading > h5").text().trim();
+ String content = element.select(".summary-content").text().trim();
+
+ if (header.equalsIgnoreCase("Status")) {
+ novel.status = content;
+ } else if (header.equalsIgnoreCase("Alternative")) {
+ novel.alternateNames = Arrays.asList(content.split(","));
+ } else if (header.equalsIgnoreCase("Release")) {
+ novel.year = NumberUtils.toInt(content);
+ }
+ }
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws IOException {
+ List items = new ArrayList<>();
+ String web = novel.url.concat("ajax/chapters");
+ Element doc = Jsoup.parse(HttpClient.POST(web, new HashMap<>(), new HashMap<>()));
+
+ for (Element element : doc.select(".wp-manga-chapter")) {
+ Chapter item = new Chapter(novel.url);
+
+ item.url = element.select("a").attr("href").trim();
+ item.name = element.select("a").text().trim();
+ item.id = NumberUtils.toFloat(item.name);
+ item.updated = element.select("span.chapter-release-date").text().trim();
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(chapter.url, new HashMap<>()));
+ Element main = doc.select(".reading-content").first();
+
+ if (main == null) {
+ return null;
+ }
+
+ chapter.content = "";
+ chapter.content = String.join("\n\n", main.select("p").eachText());
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws IOException {
+ List items = new ArrayList<>();
+
+ if (filters.hashKeyword()) {
+ String web = SourceUtils.buildUrl(baseUrl, "/page/", String.valueOf(page), "/?s=", filters.getKeyword(), "&post_type=wp-manga");
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+ for (Element element : doc.select(".c-tabs-item__content")) {
+ String url = element.select(".tab-thumb > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.url = url;
+ item.name = element.select(".post-title > h3 > a").text().trim();
+ item.cover = element.select("img.img-responsive").attr("data-src").trim();
+
+ items.add(item);
+ }
+ }
+ }
+
+ return items;
+ }
+}
diff --git a/app/src/main/java/org/ranobe/ranobe/sources/en/Neovel.java b/app/src/main/java/org/ranobe/ranobe/sources/en/Neovel.java
new file mode 100644
index 0000000..307ac9a
--- /dev/null
+++ b/app/src/main/java/org/ranobe/ranobe/sources/en/Neovel.java
@@ -0,0 +1,171 @@
+package org.ranobe.ranobe.sources.en;
+
+import android.util.Base64;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.ranobe.models.Chapter;
+import org.ranobe.ranobe.models.DataSource;
+import org.ranobe.ranobe.models.Filter;
+import org.ranobe.ranobe.models.Lang;
+import org.ranobe.ranobe.models.Novel;
+import org.ranobe.ranobe.network.HttpClient;
+import org.ranobe.ranobe.sources.Source;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class Neovel implements Source {
+ private static final String BASE_URL = "https://neovel.io";
+ private static final int SOURCE_ID = 14;
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = SOURCE_ID;
+ source.url = BASE_URL;
+ source.name = "Neovel";
+ source.lang = Lang.eng;
+ source.dev = "ap-atul";
+ source.logo = "https://neovel.io/favicon-32x32.png";
+ return source;
+ }
+
+ private String getCoverImage(String bookId) {
+ return String.format(Locale.getDefault(), "https://neovel.io/V2/book/image?bookId=%s&oldApp=false&imageExtension=2", bookId);
+ }
+
+ private String getStatus(int completion) {
+ switch (completion) {
+ case 1: return "Ongoing";
+ case 3: return "Completed";
+ default: return "N.A.";
+ }
+ }
+
+ private int getYear(String date) {
+ Pattern pattern = Pattern.compile("/\\d{4}/gm");
+ Matcher m = pattern.matcher(date);
+ if(m.find()) {
+ return Integer.parseInt(m.group());
+ }
+ return 0;
+ }
+
+ private List jsonArrayToString(JSONArray arr) throws JSONException {
+ List values = new ArrayList<>();
+ for(int i = 0; i < arr.length(); i++) {
+ values.add(arr.getString(0));
+ }
+ return values;
+ }
+
+ private String encodeKeyword(String keyword) {
+ byte[] inputData = keyword.getBytes(); // Default encoding is UTF-8
+ return Base64.encodeToString(inputData, Base64.DEFAULT);
+ }
+
+ @Override
+ public List novels(int page) throws Exception {
+ String web = BASE_URL +"/V2/books/search?language=EN&filter=0&name=&sort=6&page=".concat(String.valueOf(page)) + "&onlyOffline=true&genreIds=0&genreCombining=0&tagIds=0&tagCombining=0&minChapterCount=0&maxChapterCount=9999&completion=5&onlyPremium=false&blacklistedTagIds=&onlyMature=false";
+ return parseNovels(web);
+ }
+
+ private List parseNovels(String web) throws Exception {
+ List items = new ArrayList<>();
+ JSONArray data = new JSONArray(HttpClient.GET(web, new HashMap<>()));
+
+ for(int i = 0; i < data.length(); i++) {
+ JSONObject d = data.getJSONObject(i);
+ String url = BASE_URL + "/" + d.getString("id");
+
+ Novel item = new Novel(url);
+ item.sourceId = SOURCE_ID;
+ item.name = d.getString("name");
+ item.cover = getCoverImage(d.getString("id"));
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws Exception {
+ String bookId = novel.url.replace(BASE_URL, "").replace("/", "").trim();
+ String url = String.format("https://neovel.io/V1/page/book?bookId=%s&language=EN", bookId);
+ JSONObject data = new JSONObject(HttpClient.GET(url, new HashMap<>()));
+ JSONObject bookDetails = data.getJSONObject("bookDto");
+
+ novel.sourceId = SOURCE_ID;
+ novel.name = bookDetails.getString("name");
+ novel.cover = getCoverImage(bookId);
+ novel.summary = bookDetails.getString("bookDescription");
+ novel.rating = Float.parseFloat(String.valueOf(bookDetails.getDouble("rating")));
+ novel.authors = jsonArrayToString(bookDetails.getJSONArray("authors"));
+ novel.status = getStatus(bookDetails.getInt("completion"));
+ novel.year = getYear(bookDetails.getString("postDate"));
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws Exception {
+ List items = new ArrayList<>();
+ String bookId = novel.url.replace(BASE_URL, "").replace("/", "").trim();
+ String url = String.format("https://neovel.io/V5/chapters?bookId=%s&language=EN", bookId);
+ JSONArray data = new JSONArray(HttpClient.GET(url, new HashMap<>()));
+
+ for(int i = 0; i < data.length(); i++) {
+ JSONObject o = data.getJSONObject(i);
+ String u = BASE_URL + "/chapter/" + o.getString("chapterId");
+
+ Chapter item = new Chapter(novel.url);
+ item.url = u;
+ item.name = o.getString("chapterName");
+ item.id = Float.parseFloat(o.getString("chapterNumber"));
+ item.updated = o.getString("postDate");
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws Exception {
+ String chapterId = chapter.url.replace(BASE_URL, "").replace("/chapter/", "").trim();
+ String url = String.format("https://neovel.io/V2/chapter/content?chapterId=%s", chapterId);
+ JSONObject data = new JSONObject(HttpClient.GET(url, new HashMap<>()));
+ Element doc = Jsoup.parse(data.getString("chapterContent"));
+ List paras = new ArrayList<>();
+
+ for(String bits: doc.wholeText().split("\n")){
+ String text = bits.trim();
+ if (text.length() > 0) {
+ paras.add(text);
+ }
+ }
+
+ chapter.content = String.join("\n\n", paras);
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws Exception {
+ List items = new ArrayList<>();
+
+ if (filters.hashKeyword()) {
+ String encodedKeyword = encodeKeyword(filters.getKeyword()).trim();
+ String web = BASE_URL +"/V2/books/search?language=EN&filter=0&name=" + encodedKeyword + "&sort=6&page=".concat(String.valueOf(page - 1)) + "&onlyOffline=true&genreIds=0&genreCombining=0&tagIds=0&tagCombining=0&minChapterCount=0&maxChapterCount=9999&completion=5&onlyPremium=false&blacklistedTagIds=&onlyMature=false";
+ return parseNovels(web);
+ }
+
+ return items;
+ }
+}
diff --git a/app/src/main/java/org/ranobe/ranobe/sources/en/NewNovel.java b/app/src/main/java/org/ranobe/ranobe/sources/en/NewNovel.java
new file mode 100644
index 0000000..4daf92e
--- /dev/null
+++ b/app/src/main/java/org/ranobe/ranobe/sources/en/NewNovel.java
@@ -0,0 +1,153 @@
+package org.ranobe.ranobe.sources.en;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.ranobe.models.Chapter;
+import org.ranobe.ranobe.models.DataSource;
+import org.ranobe.ranobe.models.Filter;
+import org.ranobe.ranobe.models.Lang;
+import org.ranobe.ranobe.models.Novel;
+import org.ranobe.ranobe.network.HttpClient;
+import org.ranobe.ranobe.sources.Source;
+import org.ranobe.ranobe.util.NumberUtils;
+import org.ranobe.ranobe.util.SourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class NewNovel implements Source {
+ private static final String baseUrl = "https://newnovel.org/";
+ private static final int sourceId = 10;
+
+ private String cleanImg(String cover) {
+ return cover.replaceAll("/-\\d+x\\d+.\\w{3}/gm", ".jpg");
+ }
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "New Novel";
+ source.lang = Lang.eng;
+ source.dev = "ap-atul";
+ source.logo = "https://newnovel.org/wp-content/uploads/2022/05/coollogo_com-9657259.png";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws IOException {
+ List items = new ArrayList<>();
+ String web = baseUrl.concat("/page/").concat(String.valueOf(page));
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+
+ for (Element element : doc.select(".page-item-detail")) {
+ String url = element.select(".h5 > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = element.select(".h5 > a").text().trim();
+ item.cover = cleanImg(element.select("img").attr("src").trim());
+ items.add(item);
+ }
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, new HashMap<>()));
+
+ novel.sourceId = sourceId;
+ novel.name = doc.select(".post-title > h1").text().trim();
+ novel.cover = cleanImg(doc.select(".summary_image > a > img").attr("Src").trim());
+ doc.select(".j_synopsis").select("br").append("::");
+ novel.summary = doc.select("div.summary__content > div.mb48").text().replaceAll("::", "\n\n").trim();
+ novel.rating = NumberUtils.toFloat(doc.select(".total_votes").text().trim());
+ novel.authors = Arrays.asList(doc.select(".author-content > a").text().split(","));
+
+ List genres = new ArrayList<>();
+ for (Element element : doc.select(".genres-content > a")) {
+ genres.add(element.text().trim());
+ }
+ novel.genres = genres;
+
+ for (Element element : doc.select(".post-content_item")) {
+ String header = element.select(".summary-heading > h5").text().trim();
+ String content = element.select(".summary-content").text().trim();
+
+ if (header.equalsIgnoreCase("Status")) {
+ novel.status = content;
+ } else if (header.equalsIgnoreCase("Alternative")) {
+ novel.alternateNames = Arrays.asList(content.split(","));
+ } else if (header.equalsIgnoreCase("Release")) {
+ novel.year = NumberUtils.toInt(content);
+ }
+ }
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws IOException {
+ List items = new ArrayList<>();
+ String web = novel.url.concat("ajax/chapters");
+ Element doc = Jsoup.parse(HttpClient.POST(web, new HashMap<>(), new HashMap<>()));
+
+ for (Element element : doc.select(".wp-manga-chapter")) {
+ Chapter item = new Chapter(novel.url);
+
+ item.url = element.select("a").attr("href").trim();
+ item.name = element.select("a").text().trim();
+ item.id = NumberUtils.toFloat(item.name);
+ item.updated = element.select("span.chapter-release-date").text().trim();
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(chapter.url, new HashMap<>()));
+ Element main = doc.select(".reading-content").first();
+
+ if (main == null) {
+ return null;
+ }
+
+ chapter.content = "";
+ chapter.content = String.join("\n\n", main.select("p").eachText());
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws IOException {
+ List items = new ArrayList<>();
+
+ if (filters.hashKeyword()) {
+ String web = SourceUtils.buildUrl(baseUrl, "/page/", String.valueOf(page), "/?s=", filters.getKeyword(), "&post_type=wp-manga");
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+ for (Element element : doc.select(".c-tabs-item__content")) {
+ String url = element.select(".tab-thumb > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.url = url;
+ item.name = element.select(".post-title > h3 > a").text().trim();
+ item.cover = cleanImg(element.select("img.img-responsive").attr("src").trim());
+
+ items.add(item);
+ }
+ }
+ }
+
+ return items;
+ }
+}
diff --git a/app/src/main/java/org/ranobe/ranobe/sources/en/ReadWebNovels.java b/app/src/main/java/org/ranobe/ranobe/sources/en/ReadWebNovels.java
new file mode 100644
index 0000000..fd77a15
--- /dev/null
+++ b/app/src/main/java/org/ranobe/ranobe/sources/en/ReadWebNovels.java
@@ -0,0 +1,152 @@
+package org.ranobe.ranobe.sources.en;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.ranobe.models.Chapter;
+import org.ranobe.ranobe.models.DataSource;
+import org.ranobe.ranobe.models.Filter;
+import org.ranobe.ranobe.models.Lang;
+import org.ranobe.ranobe.models.Novel;
+import org.ranobe.ranobe.network.HttpClient;
+import org.ranobe.ranobe.sources.Source;
+import org.ranobe.ranobe.util.NumberUtils;
+import org.ranobe.ranobe.util.SourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class ReadWebNovels implements Source {
+ private static final String baseUrl = "https://readwebnovels.net/";
+ private static final int sourceId = 11;
+
+ private String cleanImg(String cover) {
+ return cover.replaceAll("/-\\d+x\\d+.\\w{3}/gm", ".jpg");
+ }
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "Read Web Novels";
+ source.lang = Lang.eng;
+ source.dev = "ap-atul";
+ source.logo = "https://readwebnovels.net/wp-content/uploads/2020/01/cropped-boo1k-180x180.png";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws IOException {
+ List items = new ArrayList<>();
+ String web = baseUrl.concat("/page/").concat(String.valueOf(page));
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+
+ for (Element element : doc.select(".page-item-detail")) {
+ String url = element.select(".h5 > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = element.select(".h5 > a").text().trim();
+ item.cover = cleanImg(element.select("img").attr("src").trim());
+ items.add(item);
+ }
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, new HashMap<>()));
+
+ novel.sourceId = sourceId;
+ novel.name = doc.select(".post-title > h1").text().trim();
+ novel.cover = cleanImg(doc.select(".summary_image > a > img").attr("Src").trim());
+ novel.summary = doc.select("div.summary__content").text().replaceAll("\n", "\n\n").trim();
+ novel.rating = NumberUtils.toFloat(doc.select(".total_votes").text().trim());
+ novel.authors = Arrays.asList(doc.select(".author-content > a").text().split(","));
+
+ List genres = new ArrayList<>();
+ for (Element element : doc.select(".genres-content > a")) {
+ genres.add(element.text().trim());
+ }
+ novel.genres = genres;
+
+ for (Element element : doc.select(".post-content_item")) {
+ String header = element.select(".summary-heading > h5").text().trim();
+ String content = element.select(".summary-content").text().trim();
+
+ if (header.equalsIgnoreCase("Status")) {
+ novel.status = content;
+ } else if (header.equalsIgnoreCase("Alternative")) {
+ novel.alternateNames = Arrays.asList(content.split(","));
+ } else if (header.equalsIgnoreCase("Release")) {
+ novel.year = NumberUtils.toInt(content);
+ }
+ }
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws IOException {
+ List items = new ArrayList<>();
+ String web = novel.url.concat("ajax/chapters");
+ Element doc = Jsoup.parse(HttpClient.POST(web, new HashMap<>(), new HashMap<>()));
+
+ for (Element element : doc.select(".wp-manga-chapter")) {
+ Chapter item = new Chapter(novel.url);
+
+ item.url = element.select("a").attr("href").trim();
+ item.name = element.select("a").text().trim();
+ item.id = NumberUtils.toFloat(item.name);
+ item.updated = element.select("span.chapter-release-date").text().trim();
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(chapter.url, new HashMap<>()));
+ Element main = doc.select(".reading-content").first();
+
+ if (main == null) {
+ return null;
+ }
+
+ chapter.content = "";
+ chapter.content = String.join("\n\n", main.select("p").eachText());
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws IOException {
+ List items = new ArrayList<>();
+
+ if (filters.hashKeyword()) {
+ String web = SourceUtils.buildUrl(baseUrl, "/page/", String.valueOf(page), "/?s=", filters.getKeyword(), "&post_type=wp-manga");
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+ for (Element element : doc.select(".c-tabs-item__content")) {
+ String url = element.select(".tab-thumb > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.url = url;
+ item.name = element.select(".post-title > h3 > a").text().trim();
+ item.cover = cleanImg(element.select("img.img-responsive").attr("src").trim());
+
+ items.add(item);
+ }
+ }
+ }
+
+ return items;
+ }
+}
diff --git a/app/src/main/java/org/ranobe/ranobe/sources/en/WuxiaWorld.java b/app/src/main/java/org/ranobe/ranobe/sources/en/WuxiaWorld.java
new file mode 100644
index 0000000..ee0c2fa
--- /dev/null
+++ b/app/src/main/java/org/ranobe/ranobe/sources/en/WuxiaWorld.java
@@ -0,0 +1,152 @@
+package org.ranobe.ranobe.sources.en;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.ranobe.models.Chapter;
+import org.ranobe.ranobe.models.DataSource;
+import org.ranobe.ranobe.models.Filter;
+import org.ranobe.ranobe.models.Lang;
+import org.ranobe.ranobe.models.Novel;
+import org.ranobe.ranobe.network.HttpClient;
+import org.ranobe.ranobe.sources.Source;
+import org.ranobe.ranobe.util.NumberUtils;
+import org.ranobe.ranobe.util.SourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class WuxiaWorld implements Source {
+ private static final String baseUrl = "https://wuxiaworld.site";
+ private static final int sourceId = 13;
+
+ private String cleanImg(String cover) {
+ return cover.replaceAll("/-\\d+x\\d+.\\w{3}/gm", ".jpg");
+ }
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "Wuxia World";
+ source.lang = Lang.eng;
+ source.dev = "ap-atul";
+ source.logo = "https://wuxiaworld.site/wp-content/uploads/2019/04/favicon-1.ico";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws IOException {
+ List items = new ArrayList<>();
+ String web = baseUrl.concat("/page/").concat(String.valueOf(page));
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+
+ for (Element element : doc.select(".page-item-detail")) {
+ String url = element.select(".h5 > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = element.select(".h5 > a").text().trim();
+ item.cover = cleanImg(element.select("img").attr("data-src").trim());
+ items.add(item);
+ }
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, new HashMap<>()));
+
+ novel.sourceId = sourceId;
+ novel.name = doc.select(".post-title > h1").text().trim();
+ novel.cover = cleanImg(doc.select(".summary_image > a > img").attr("data-src").trim());
+ novel.summary = doc.select("div.summary__content").text().replaceAll("\n", "\n\n").trim();
+ novel.rating = NumberUtils.toFloat(doc.select(".total_votes").text().trim());
+ novel.authors = Arrays.asList(doc.select(".author-content > a").text().split(","));
+
+ List genres = new ArrayList<>();
+ for (Element element : doc.select(".genres-content > a")) {
+ genres.add(element.text().trim());
+ }
+ novel.genres = genres;
+
+ for (Element element : doc.select(".post-content_item")) {
+ String header = element.select(".summary-heading > h5").text().trim();
+ String content = element.select(".summary-content").text().trim();
+
+ if (header.equalsIgnoreCase("Status")) {
+ novel.status = content;
+ } else if (header.equalsIgnoreCase("Alternative")) {
+ novel.alternateNames = Arrays.asList(content.split(","));
+ } else if (header.equalsIgnoreCase("Release")) {
+ novel.year = NumberUtils.toInt(content);
+ }
+ }
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws IOException {
+ List items = new ArrayList<>();
+ String web = novel.url.concat("ajax/chapters");
+ Element doc = Jsoup.parse(HttpClient.POST(web, new HashMap<>(), new HashMap<>()));
+
+ for (Element element : doc.select(".wp-manga-chapter")) {
+ Chapter item = new Chapter(novel.url);
+
+ item.url = element.select("a").attr("href").trim();
+ item.name = element.select("a").text().trim();
+ item.id = NumberUtils.toFloat(item.name);
+ item.updated = element.select("span.chapter-release-date").text().trim();
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(chapter.url, new HashMap<>()));
+ Element main = doc.select(".reading-content").first();
+
+ if (main == null) {
+ return null;
+ }
+
+ chapter.content = "";
+ chapter.content = String.join("\n\n", main.select("p").eachText());
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws IOException {
+ List items = new ArrayList<>();
+
+ if (filters.hashKeyword()) {
+ String web = SourceUtils.buildUrl(baseUrl, "/page/", String.valueOf(page), "/?s=", filters.getKeyword(), "&post_type=wp-manga");
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+ for (Element element : doc.select(".c-tabs-item__content")) {
+ String url = element.select(".tab-thumb > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.url = url;
+ item.name = element.select(".post-title > h3 > a").text().trim();
+ item.cover = cleanImg(element.select("img.img-responsive").attr("data-src").trim());
+
+ items.add(item);
+ }
+ }
+ }
+
+ return items;
+ }
+}
diff --git a/app/src/main/java/org/ranobe/ranobe/ui/explore/adapter/SourceAdapter.java b/app/src/main/java/org/ranobe/ranobe/ui/explore/adapter/SourceAdapter.java
index ac12b61..494279f 100644
--- a/app/src/main/java/org/ranobe/ranobe/ui/explore/adapter/SourceAdapter.java
+++ b/app/src/main/java/org/ranobe/ranobe/ui/explore/adapter/SourceAdapter.java
@@ -33,7 +33,7 @@ public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType)
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
DataSource source = sources.get(position);
- holder.binding.sourceId.setText(String.valueOf(source.sourceId));
+ holder.binding.sourceId.setText(String.format(Locale.getDefault(), "%02d", source.sourceId));
holder.binding.sourceName.setText(source.name);
holder.binding.sourceContent.setText(String.format(
Locale.getDefault(),
diff --git a/app/src/main/java/org/ranobe/ranobe/ui/reader/adapter/PageAdapter.java b/app/src/main/java/org/ranobe/ranobe/ui/reader/adapter/PageAdapter.java
index d732de6..69b8cd5 100644
--- a/app/src/main/java/org/ranobe/ranobe/ui/reader/adapter/PageAdapter.java
+++ b/app/src/main/java/org/ranobe/ranobe/ui/reader/adapter/PageAdapter.java
@@ -67,7 +67,7 @@ public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
}
holder.binding.content.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize);
- holder.binding.chapterTitle.setText(String.format(Locale.getDefault(), "Chapter %.0f", chapter.id));
+ holder.binding.chapterTitle.setText(String.format(Locale.getDefault(), "Chapter %.1f", chapter.id));
holder.binding.content.setText(chapter.content);
holder.binding.pageLayout.setLayoutParams(params);
holder.binding.content.setLayoutParams(params);
diff --git a/app/src/main/java/org/ranobe/ranobe/ui/search/Search.java b/app/src/main/java/org/ranobe/ranobe/ui/search/Search.java
index 8cc1714..92d01a2 100644
--- a/app/src/main/java/org/ranobe/ranobe/ui/search/Search.java
+++ b/app/src/main/java/org/ranobe/ranobe/ui/search/Search.java
@@ -88,6 +88,8 @@ private void runSearch(String keyword) {
Filter filter = new Filter();
filter.addFilter(Filter.FILTER_KEYWORD, keyword);
+ results.clear();
+ resultAdapter.notifyDataSetChanged();
viewModel.search(dataSources, filter, 1).observe(getViewLifecycleOwner(), result -> {
results.clear();
results.putAll(result);
diff --git a/app/src/main/java/org/ranobe/ranobe/ui/search/viewmodel/SearchViewModel.java b/app/src/main/java/org/ranobe/ranobe/ui/search/viewmodel/SearchViewModel.java
index b882e11..c78a71c 100644
--- a/app/src/main/java/org/ranobe/ranobe/ui/search/viewmodel/SearchViewModel.java
+++ b/app/src/main/java/org/ranobe/ranobe/ui/search/viewmodel/SearchViewModel.java
@@ -34,7 +34,7 @@ public MutableLiveData>> search(List>() {
@Override
public void onComplete(List result) {
- if (result.size() > 0) {
+ if (!result.isEmpty()) {
LinkedHashMap> old = results.getValue();
if (old == null)
old = new LinkedHashMap<>();
diff --git a/app/src/main/res/layout/item_source.xml b/app/src/main/res/layout/item_source.xml
index cd6e4b7..72e2865 100644
--- a/app/src/main/res/layout/item_source.xml
+++ b/app/src/main/res/layout/item_source.xml
@@ -4,7 +4,6 @@
android:id="@+id/source_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="10dp"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true"
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 9117eda..52d8744 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,7 +1,6 @@
\ No newline at end of file
diff --git a/core/.gitignore b/core/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/core/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/build.gradle b/core/build.gradle
new file mode 100644
index 0000000..fcbfb49
--- /dev/null
+++ b/core/build.gradle
@@ -0,0 +1,31 @@
+plugins {
+ id 'com.android.library'
+}
+
+android {
+ namespace 'org.ranobe.core'
+ compileSdk 33
+
+ defaultConfig {
+ minSdk 21
+ targetSdk 33
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation 'org.jsoup:jsoup:1.15.3'
+ implementation "com.squareup.okhttp3:okhttp:4.10.0"
+}
\ No newline at end of file
diff --git a/core/consumer-rules.pro b/core/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/core/proguard-rules.pro b/core/proguard-rules.pro
new file mode 100644
index 0000000..67ea765
--- /dev/null
+++ b/core/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1d26c87
--- /dev/null
+++ b/core/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/core/src/main/java/org/ranobe/core/config/Ranobe.java b/core/src/main/java/org/ranobe/core/config/Ranobe.java
new file mode 100644
index 0000000..40528c4
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/config/Ranobe.java
@@ -0,0 +1,10 @@
+package org.ranobe.core.config;
+
+
+@SuppressWarnings({
+ "squid:S3599", "squid:S2386", "squid:S1171", "squid:S1118"
+})
+public class Ranobe {
+ public static final String DEBUG = "ranobe-debug";
+
+}
diff --git a/core/src/main/java/org/ranobe/core/models/Chapter.java b/core/src/main/java/org/ranobe/core/models/Chapter.java
new file mode 100644
index 0000000..f6fb9fa
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/models/Chapter.java
@@ -0,0 +1,70 @@
+package org.ranobe.core.models;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class Chapter implements Parcelable {
+ public String url;
+ public String novelUrl;
+ public String content;
+ public String name;
+ public String updated;
+ public float id;
+
+ public Chapter() {
+ this.url = "";
+ }
+
+ public Chapter(String novelUrl) {
+ this.novelUrl = novelUrl;
+ this.url = "";
+ }
+
+ protected Chapter(Parcel in) {
+ url = in.readString();
+ novelUrl = in.readString();
+ content = in.readString();
+ name = in.readString();
+ updated = in.readString();
+ id = in.readFloat();
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public Chapter createFromParcel(Parcel in) {
+ return new Chapter(in);
+ }
+
+ @Override
+ public Chapter[] newArray(int size) {
+ return new Chapter[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return "Chapter{" +
+ "url='" + url + '\'' +
+ ", content='" + content + '\'' +
+ ", name='" + name + '\'' +
+ ", updated='" + updated + '\'' +
+ ", id=" + id +
+ ", novelUrl=" + novelUrl +
+ '}';
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeString(url);
+ parcel.writeString(novelUrl);
+ parcel.writeString(content);
+ parcel.writeString(name);
+ parcel.writeString(updated);
+ parcel.writeFloat(id);
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/models/DataSource.java b/core/src/main/java/org/ranobe/core/models/DataSource.java
new file mode 100644
index 0000000..eb0281a
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/models/DataSource.java
@@ -0,0 +1,19 @@
+package org.ranobe.core.models;
+
+
+public class DataSource {
+ public int sourceId;
+ public String url;
+ public String name;
+ public String lang;
+ public String logo;
+ public String dev;
+
+ @Override
+ public String toString() {
+ return "DataSource{" +
+ "sourceId=" + sourceId +
+ ", url='" + url + '\'' +
+ '}';
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/models/Filter.java b/core/src/main/java/org/ranobe/core/models/Filter.java
new file mode 100644
index 0000000..2e9a485
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/models/Filter.java
@@ -0,0 +1,39 @@
+package org.ranobe.core.models;
+
+import java.util.HashMap;
+import java.util.Objects;
+
+public class Filter {
+ public static final String FILTER_KEYWORD = "keyword";
+ private final HashMap params;
+
+ public Filter() {
+ params = new HashMap<>();
+ }
+
+ public void addFilter(String key, String val) {
+ params.put(key, val);
+ }
+
+ public boolean hashKeyword() {
+ String val = params.get(FILTER_KEYWORD);
+ return val != null;
+ }
+
+ public String getKeyword() {
+ return params.get(FILTER_KEYWORD);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Filter filter = (Filter) o;
+ return Objects.equals(params, filter.params);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(params);
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/models/Lang.java b/core/src/main/java/org/ranobe/core/models/Lang.java
new file mode 100644
index 0000000..e923781
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/models/Lang.java
@@ -0,0 +1,6 @@
+package org.ranobe.core.models;
+
+public class Lang {
+ public static final String eng = "en-us";
+ public static final String ru = "ru";
+}
diff --git a/core/src/main/java/org/ranobe/core/models/Novel.java b/core/src/main/java/org/ranobe/core/models/Novel.java
new file mode 100644
index 0000000..15f371a
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/models/Novel.java
@@ -0,0 +1,97 @@
+package org.ranobe.core.models;
+
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+
+import org.ranobe.core.util.SourceUtils;
+
+import java.util.List;
+
+public class Novel implements Parcelable {
+ public long id;
+ public int sourceId;
+ public String name;
+ public String cover;
+ public String url;
+
+ public String status;
+ public String summary;
+ public List alternateNames;
+ public List authors;
+ public List genres;
+ public float rating;
+ public int year;
+
+ public Novel(String url) {
+ this.id = SourceUtils.generateId(url);
+ this.url = url;
+ }
+
+ protected Novel(Parcel in) {
+ id = in.readLong();
+ sourceId = in.readInt();
+ name = in.readString();
+ cover = in.readString();
+ url = in.readString();
+ status = in.readString();
+ summary = in.readString();
+ alternateNames = in.createStringArrayList();
+ authors = in.createStringArrayList();
+ genres = in.createStringArrayList();
+ rating = in.readFloat();
+ year = in.readInt();
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public Novel createFromParcel(Parcel in) {
+ return new Novel(in);
+ }
+
+ @Override
+ public Novel[] newArray(int size) {
+ return new Novel[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return "Novel{" +
+ "id=" + id +
+ ", sourceId=" + sourceId +
+ ", name='" + name + '\'' +
+ ", cover='" + cover + '\'' +
+ ", url='" + url + '\'' +
+ ", status='" + status + '\'' +
+ ", summary='" + summary + '\'' +
+ ", alternateNames=" + alternateNames +
+ ", authors=" + authors +
+ ", genres=" + genres +
+ ", rating=" + rating +
+ ", year=" + year +
+ '}';
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeLong(id);
+ parcel.writeInt(sourceId);
+ parcel.writeString(name);
+ parcel.writeString(cover);
+ parcel.writeString(url);
+ parcel.writeString(status);
+ parcel.writeString(summary);
+ parcel.writeStringList(alternateNames);
+ parcel.writeStringList(authors);
+ parcel.writeStringList(genres);
+ parcel.writeFloat(rating);
+ parcel.writeInt(year);
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/models/ReaderTheme.java b/core/src/main/java/org/ranobe/core/models/ReaderTheme.java
new file mode 100644
index 0000000..9cc3b58
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/models/ReaderTheme.java
@@ -0,0 +1,21 @@
+package org.ranobe.core.models;
+
+import android.graphics.Color;
+
+public class ReaderTheme {
+ private final int text;
+ private final int background;
+
+ public ReaderTheme(String text, String background) {
+ this.text = Color.parseColor(text);
+ this.background = Color.parseColor(background);
+ }
+
+ public int getText() {
+ return text;
+ }
+
+ public int getBackground() {
+ return background;
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/network/HttpClient.java b/core/src/main/java/org/ranobe/core/network/HttpClient.java
new file mode 100644
index 0000000..ac310f8
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/network/HttpClient.java
@@ -0,0 +1,80 @@
+package org.ranobe.core.network;
+
+import android.content.Context;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import okhttp3.Cache;
+import okhttp3.CacheControl;
+import okhttp3.FormBody;
+import okhttp3.Interceptor;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+public class HttpClient {
+ private static OkHttpClient client = null;
+ private static Context context;
+
+ public static void initialize(Context context) {
+ HttpClient.context = context;
+ }
+
+ private static OkHttpClient client() {
+ if (client == null) {
+ File cacheDir = new File(HttpClient.context.getCacheDir(), "cache-files");
+ Cache cache = new Cache(cacheDir, 10 * 1024 * 1024); //10 MiB
+ client = new OkHttpClient
+ .Builder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(30, TimeUnit.SECONDS)
+ .addNetworkInterceptor(new CacheInterceptor()).cache(cache).build();
+ }
+ return client;
+ }
+
+ public static String GET(String url, HashMap headers) throws IOException {
+ Request.Builder builder = new Request.Builder().url(url);
+ for (Map.Entry entry : headers.entrySet()) {
+ builder.addHeader(entry.getKey(), entry.getValue());
+ }
+ ResponseBody response = HttpClient.client().newCall(builder.build()).execute().body();
+ return response == null ? "" : response.string();
+ }
+
+ public static String POST(String url, HashMap headers, HashMap form) throws IOException {
+ Request.Builder builder = new Request.Builder().url(url);
+ for (Map.Entry entry : headers.entrySet()) {
+ builder.addHeader(entry.getKey(), entry.getValue());
+ }
+ FormBody.Builder formBody = new FormBody.Builder();
+ for (Map.Entry entry : form.entrySet()) {
+ formBody.add(entry.getKey(), entry.getValue());
+ }
+ ResponseBody response = HttpClient.client().newCall(builder.post(formBody.build()).build()).execute().body();
+ return response == null ? "" : response.string();
+ }
+
+ public static class CacheInterceptor implements Interceptor {
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ Response response = chain.proceed(chain.request());
+
+ CacheControl cacheControl = new CacheControl.Builder()
+ .maxAge(15, TimeUnit.MINUTES) // 15 minutes cache
+ .build();
+
+ return response.newBuilder()
+ .removeHeader("Pragma")
+ .removeHeader("Cache-Control")
+ .header("Cache-Control", cacheControl.toString())
+ .build();
+ }
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/network/repository/Repository.java b/core/src/main/java/org/ranobe/core/network/repository/Repository.java
new file mode 100644
index 0000000..534df48
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/network/repository/Repository.java
@@ -0,0 +1,87 @@
+package org.ranobe.core.network.repository;
+
+import org.ranobe.core.models.Chapter;
+import org.ranobe.core.models.Filter;
+import org.ranobe.core.models.Novel;
+import org.ranobe.core.sources.Source;
+import org.ranobe.core.sources.SourceManager;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class Repository {
+ private final Executor executor;
+ private final Source source;
+
+ public Repository(int sourceId) {
+ this.executor = Executors.newCachedThreadPool();
+ this.source = SourceManager.getSource(sourceId);
+ }
+
+ public Repository(Source source) {
+ this.executor = Executors.newCachedThreadPool();
+ this.source = source;
+ }
+
+ public void novels(int page, Callback> callback) {
+ executor.execute(() -> {
+ try {
+ List result = source.novels(page);
+ callback.onComplete(result);
+ } catch (Exception e) {
+ callback.onError(e);
+ }
+ });
+ }
+
+ public void details(Novel novel, Callback callback) {
+ executor.execute(() -> {
+ try {
+ Novel result = source.details(novel);
+ callback.onComplete(result);
+ } catch (Exception e) {
+ callback.onError(e);
+ }
+ });
+ }
+
+ public void chapters(Novel novel, Callback> callback) {
+ executor.execute(() -> {
+ try {
+ List items = source.chapters(novel);
+ callback.onComplete(items);
+ } catch (Exception e) {
+ callback.onError(e);
+ }
+ });
+ }
+
+ public void chapter(Chapter chapter, Callback callback) {
+ executor.execute(() -> {
+ try {
+ Chapter item = source.chapter(chapter);
+ callback.onComplete(item);
+ } catch (Exception e) {
+ callback.onError(e);
+ }
+ });
+ }
+
+ public void search(Filter filter, int page, Callback> callback) {
+ executor.execute(() -> {
+ try {
+ List items = source.search(filter, page);
+ callback.onComplete(items);
+ } catch (Exception e) {
+ callback.onError(e);
+ }
+ });
+ }
+
+ public interface Callback {
+ void onComplete(T result);
+
+ void onError(Exception e);
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/sources/Source.java b/core/src/main/java/org/ranobe/core/sources/Source.java
new file mode 100644
index 0000000..4a59cf8
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/sources/Source.java
@@ -0,0 +1,28 @@
+package org.ranobe.core.sources;
+
+
+import org.ranobe.core.models.Chapter;
+import org.ranobe.core.models.DataSource;
+import org.ranobe.core.models.Filter;
+import org.ranobe.core.models.Novel;
+
+import java.util.List;
+
+public interface Source {
+ DataSource metadata();
+
+ // get the list of novels based on page
+ List novels(int page) throws Exception;
+
+ // get all the fields for a single novel
+ Novel details(Novel novel) throws Exception;
+
+ // get all chapters for a novel
+ List chapters(Novel novel) throws Exception;
+
+ // get content of the chapter from the url
+ Chapter chapter(Chapter chapter) throws Exception;
+
+ // search novels
+ List search(Filter filters, int page) throws Exception;
+}
diff --git a/core/src/main/java/org/ranobe/core/sources/SourceManager.java b/core/src/main/java/org/ranobe/core/sources/SourceManager.java
new file mode 100644
index 0000000..c8a9b60
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/sources/SourceManager.java
@@ -0,0 +1,82 @@
+package org.ranobe.core.sources;
+
+
+import org.ranobe.core.sources.en.AllNovel;
+import org.ranobe.core.sources.en.AzyNovel;
+import org.ranobe.core.sources.en.BoxNovel;
+import org.ranobe.core.sources.en.LightNovelBtt;
+import org.ranobe.core.sources.en.LightNovelHeaven;
+import org.ranobe.core.sources.en.LightNovelPub;
+import org.ranobe.core.sources.en.NewNovel;
+import org.ranobe.core.sources.en.ReadLightNovel;
+import org.ranobe.core.sources.en.ReadWebNovels;
+import org.ranobe.core.sources.en.VipNovel;
+import org.ranobe.core.sources.en.WuxiaWorld;
+import org.ranobe.core.sources.ru.RanobeHub;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class SourceManager {
+ public static Source getSource(int sourceId) {
+ try {
+ Class> klass = getSources().get(sourceId);
+ if (klass == null) {
+ throw new ClassNotFoundException("Source not found with source id : " + sourceId);
+ }
+ return (Source) klass.newInstance();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return new ReadLightNovel();
+ }
+ }
+
+ public static Source getSourceByDomain(String domain) {
+ try {
+ Class> klass = getSourcesByDomain().get(domain);
+ if (klass == null) {
+ throw new ClassNotFoundException("Source not found with domain : " + domain);
+ }
+ return (Source) klass.newInstance();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static Map> getSources() {
+ HashMap> sources = new HashMap<>();
+ sources.put(1, ReadLightNovel.class);
+ sources.put(2, VipNovel.class);
+ sources.put(3, LightNovelBtt.class);
+ sources.put(4, LightNovelPub.class);
+ sources.put(5, RanobeHub.class);
+ sources.put(7, AllNovel.class);
+ sources.put(8, AzyNovel.class);
+ sources.put(9, LightNovelHeaven.class);
+ sources.put(10, NewNovel.class);
+ sources.put(11, ReadWebNovels.class);
+ sources.put(12, BoxNovel.class);
+ sources.put(13, WuxiaWorld.class);
+
+ return sources;
+ }
+
+ public static Map> getSourcesByDomain() {
+ HashMap> sources = new HashMap<>();
+ sources.put("readlightnovel.me", ReadLightNovel.class);
+ sources.put("vipnovel.com", VipNovel.class);
+ sources.put("lightnovelbtt.com", LightNovelBtt.class);
+ sources.put("light-novelpub.com", LightNovelPub.class);
+ sources.put("ranobehub.org", RanobeHub.class);
+ sources.put("allnovel.org", AllNovel.class);
+ sources.put("azynovel.com", AzyNovel.class);
+ sources.put("lightnovelheaven.com", LightNovelHeaven.class);
+ sources.put("newnovel.org", NewNovel.class);
+ sources.put("readwebnovels.net", ReadWebNovels.class);
+ sources.put("boxnovel.com", BoxNovel.class);
+ sources.put("wuxiaworld.site", WuxiaWorld.class);
+
+ return sources;
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/sources/en/AllNovel.java b/core/src/main/java/org/ranobe/core/sources/en/AllNovel.java
new file mode 100644
index 0000000..66c062d
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/sources/en/AllNovel.java
@@ -0,0 +1,136 @@
+package org.ranobe.core.sources.en;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.core.models.Chapter;
+import org.ranobe.core.models.DataSource;
+import org.ranobe.core.models.Filter;
+import org.ranobe.core.models.Lang;
+import org.ranobe.core.models.Novel;
+import org.ranobe.core.network.HttpClient;
+import org.ranobe.core.sources.Source;
+import org.ranobe.core.util.NumberUtils;
+import org.ranobe.core.util.SourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class AllNovel implements Source {
+
+ private final String baseUrl = "https://allnovel.org";
+ private final int sourceId = 7;
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "All Novel";
+ source.lang = Lang.eng;
+ source.dev = "punpun";
+ source.logo = "https://allnovel.org/uploads/thumbs/logo23232-21abb9ad59-98b3a84b69aa4c92e8b001282e110775.png";
+ return source;
+ }
+
+
+ @Override
+ public List novels(int page) throws Exception {
+ String web = baseUrl + "/most-popular?page=" + page;
+ return parse(HttpClient.GET(web, new HashMap<>()));
+ }
+
+
+ private List parse(String body) throws IOException {
+ List items = new ArrayList<>();
+ Element doc = Jsoup.parse(body).select("div.col-truyen-main.archive").first();
+
+ if (doc == null) return items;
+
+ for (Element element : doc.select("div.row")) {
+ String url = element.select("h3.truyen-title > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = element.select("h3.truyen-title > a").text().trim();
+ Element img = Jsoup.parse(HttpClient.GET(baseUrl + url, new HashMap<>()));
+ item.cover = baseUrl + img.select("div.books img").attr("src");
+
+ items.add(item);
+ }
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws Exception {
+ Element doc = Jsoup.parse(HttpClient.GET(baseUrl + novel.url, new HashMap<>()));
+ novel.sourceId = sourceId;
+ novel.name = doc.select("div.books h3.title").text().trim();
+ novel.cover = baseUrl + doc.select("div.books img").attr("src").trim();
+ novel.summary = doc.select("div.desc-text > p").text().trim();
+ novel.rating = NumberUtils.toFloat(doc.select("input#rateVal").attr("value")) / 2;
+
+
+ for (Element element : doc.select("div.info")) {
+
+ novel.authors = Arrays.asList(element.select("div:eq(0) > a").text().split(","));
+ List genres = new ArrayList<>();
+ for (Element a : element.select("div:eq(2) > a")) genres.add(a.text());
+ novel.genres = genres;
+ novel.status = element.select("div:eq(4) > a").text();
+
+ }
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws Exception {
+ List items = new ArrayList<>();
+ Element novelId = Jsoup.parse(HttpClient.GET(baseUrl + novel.url, new HashMap<>())); // getNovelId
+ String id = novelId.select("div#rating").attr("data-novel-id");
+
+ String base = baseUrl.concat("/ajax-chapter-option?novelId=").concat(id);
+ Element doc = Jsoup.parse(HttpClient.GET(base, new HashMap<>()));
+
+ for (Element element : doc.select("select option")) {
+ Chapter item = new Chapter(novel.url);
+
+ item.url = element.attr("value").trim();
+ item.name = element.text().trim();
+ item.id = NumberUtils.toFloat(item.name);
+ items.add(item);
+ }
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws Exception {
+ Element doc = Jsoup.parse(HttpClient.GET(baseUrl + chapter.url, new HashMap<>()));
+
+ chapter.url = baseUrl + chapter.url;
+ chapter.content = "";
+
+ doc.select("div.chapter-c").select("p").append("::");
+ chapter.content = SourceUtils.cleanContent(
+ doc.select("div.chapter-c").text().replaceAll("::", "\n\n").trim()
+ );
+
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws Exception {
+ if (filters.hashKeyword()) {
+ String keyword = filters.getKeyword();
+ String web = SourceUtils.buildUrl(baseUrl, "/search?keyword=", keyword, "&page=", String.valueOf(page));
+ return parse(HttpClient.GET(web, new HashMap<>()));
+ }
+ return new ArrayList<>();
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/sources/en/AzyNovel.java b/core/src/main/java/org/ranobe/core/sources/en/AzyNovel.java
new file mode 100644
index 0000000..9c04426
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/sources/en/AzyNovel.java
@@ -0,0 +1,128 @@
+package org.ranobe.core.sources.en;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.core.models.Chapter;
+import org.ranobe.core.models.DataSource;
+import org.ranobe.core.models.Filter;
+import org.ranobe.core.models.Lang;
+import org.ranobe.core.models.Novel;
+import org.ranobe.core.network.HttpClient;
+import org.ranobe.core.sources.Source;
+import org.ranobe.core.util.NumberUtils;
+import org.ranobe.core.util.SourceUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class AzyNovel implements Source {
+
+ private final String baseUrl = "https://www.azynovel.com";
+ private final int sourceId = 8;
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "AzyNovel";
+ source.lang = Lang.eng;
+ source.dev = "punpun";
+ source.logo = "https://www.azynovel.com/img/azynovel_icon_64.png";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws Exception {
+ String web = baseUrl + "/popular-novels?page=" + page;
+ return parse(HttpClient.GET(web, new HashMap<>()));
+ }
+
+ private List parse(String body) {
+ List items = new ArrayList<>();
+ Element doc = Jsoup.parse(body).select("div.columns.is-multiline").first();
+ if (doc == null) return items;
+
+ for (Element element : doc.select("a.box.is-shadowless")) {
+ String url = element.attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = element.select("div.content > p.gtitle").attr("title").trim();
+ item.cover = element.select("img.athumbnail").attr("data-src");
+
+
+ items.add(item);
+ }
+
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws Exception {
+ Element doc = Jsoup.parse(HttpClient.GET(baseUrl + novel.url, new HashMap<>()));
+ novel.sourceId = sourceId;
+ novel.name = doc.select("article.media div.media-content h1:eq(0)").text().trim();
+ novel.cover = doc.select("div.media-left img").attr("data-src").trim();
+ novel.summary = doc.select("div.content > div:eq(1)").text().trim();
+ novel.rating = NumberUtils.toFloat("9") / 2;
+
+
+ novel.authors = Arrays.asList(doc.select("article.media div.media-content p:eq(1) a").text().split(","));
+ List genres = new ArrayList<>();
+ for (Element a : doc.select("article.media div.media-content p:eq(3) a"))
+ genres.add(a.text());
+ novel.genres = genres;
+ novel.status = "unknown";
+
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws Exception {
+ List items = new ArrayList<>();
+ String base = baseUrl.concat(novel.url);
+ Element doc = Jsoup.parse(HttpClient.GET(base, new HashMap<>()));
+
+ for (Element element : doc.select("div.chapter-list a")) {
+ Chapter item = new Chapter(novel.url);
+
+ item.url = element.attr("href").trim();
+ item.name = element.text().trim();
+ item.id = NumberUtils.toFloat(item.name);
+ items.add(item);
+ }
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws Exception {
+ Element doc = Jsoup.parse(HttpClient.GET(baseUrl + chapter.url, new HashMap<>()));
+
+ chapter.url = baseUrl + chapter.url;
+ chapter.content = "";
+
+ doc.select("div.columns div div:eq(4)").select("p").append("::");
+ chapter.content = SourceUtils.cleanContent(
+ doc.select("div.columns div div:eq(4)").text().replaceAll("::", "\n\n").trim()
+ );
+
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws Exception {
+ if (filters.hashKeyword()) {
+ String keyword = filters.getKeyword();
+ String web = SourceUtils.buildUrl(baseUrl, "/search?q=", keyword, "&page=", String.valueOf(page));
+ return parse(HttpClient.GET(web, new HashMap<>()));
+ }
+ return new ArrayList<>();
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/sources/en/BoxNovel.java b/core/src/main/java/org/ranobe/core/sources/en/BoxNovel.java
new file mode 100644
index 0000000..21b19a0
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/sources/en/BoxNovel.java
@@ -0,0 +1,152 @@
+package org.ranobe.core.sources.en;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.core.models.Chapter;
+import org.ranobe.core.models.DataSource;
+import org.ranobe.core.models.Filter;
+import org.ranobe.core.models.Lang;
+import org.ranobe.core.models.Novel;
+import org.ranobe.core.network.HttpClient;
+import org.ranobe.core.sources.Source;
+import org.ranobe.core.util.NumberUtils;
+import org.ranobe.core.util.SourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class BoxNovel implements Source {
+ private static final String baseUrl = "https://boxnovel.com";
+ private static final int sourceId = 12;
+
+ private String cleanImg(String cover) {
+ return cover.replaceAll("/-\\d+x\\d+.\\w{3}/gm", ".jpg");
+ }
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "Box Novel";
+ source.lang = Lang.eng;
+ source.dev = "ap-atul";
+ source.logo = "https://boxnovel.com/wp-content/uploads/2018/04/box-icon-250x250.png";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws IOException {
+ List items = new ArrayList<>();
+ String web = baseUrl.concat("/page/").concat(String.valueOf(page));
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+
+ for (Element element : doc.select(".page-item-detail")) {
+ String url = element.select(".h5 > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = element.select(".h5 > a").text().trim();
+ item.cover = cleanImg(element.select("img").attr("data-src").trim());
+ items.add(item);
+ }
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, new HashMap<>()));
+
+ novel.sourceId = sourceId;
+ novel.name = doc.select(".post-title > h1").text().trim();
+ novel.cover = cleanImg(doc.select(".summary_image > a > img").attr("data-src").trim());
+ novel.summary = doc.select("div.summary__content").text().replaceAll("\n", "\n\n").trim();
+ novel.rating = NumberUtils.toFloat(doc.select(".total_votes").text().trim());
+ novel.authors = Arrays.asList(doc.select(".author-content > a").text().split(","));
+
+ List genres = new ArrayList<>();
+ for (Element element : doc.select(".genres-content > a")) {
+ genres.add(element.text().trim());
+ }
+ novel.genres = genres;
+
+ for (Element element : doc.select(".post-content_item")) {
+ String header = element.select(".summary-heading > h5").text().trim();
+ String content = element.select(".summary-content").text().trim();
+
+ if (header.equalsIgnoreCase("Status")) {
+ novel.status = content;
+ } else if (header.equalsIgnoreCase("Alternative")) {
+ novel.alternateNames = Arrays.asList(content.split(","));
+ } else if (header.equalsIgnoreCase("Release")) {
+ novel.year = NumberUtils.toInt(content);
+ }
+ }
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws IOException {
+ List items = new ArrayList<>();
+ String web = novel.url.concat("ajax/chapters");
+ Element doc = Jsoup.parse(HttpClient.POST(web, new HashMap<>(), new HashMap<>()));
+
+ for (Element element : doc.select(".wp-manga-chapter")) {
+ Chapter item = new Chapter(novel.url);
+
+ item.url = element.select("a").attr("href").trim();
+ item.name = element.select("a").text().trim();
+ item.id = NumberUtils.toFloat(item.name);
+ item.updated = element.select("span.chapter-release-date").text().trim();
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(chapter.url, new HashMap<>()));
+ Element main = doc.select(".reading-content").first();
+
+ if (main == null) {
+ return null;
+ }
+
+ chapter.content = "";
+ chapter.content = String.join("\n\n", main.select("p").eachText());
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws IOException {
+ List items = new ArrayList<>();
+
+ if (filters.hashKeyword()) {
+ String web = SourceUtils.buildUrl(baseUrl, "/page/", String.valueOf(page), "/?s=", filters.getKeyword(), "&post_type=wp-manga");
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+ for (Element element : doc.select(".c-tabs-item__content")) {
+ String url = element.select(".tab-thumb > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.url = url;
+ item.name = element.select(".post-title > h3 > a").text().trim();
+ item.cover = cleanImg(element.select("img.img-responsive").attr("data-src").trim());
+
+ items.add(item);
+ }
+ }
+ }
+
+ return items;
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/sources/en/LightNovelBtt.java b/core/src/main/java/org/ranobe/core/sources/en/LightNovelBtt.java
new file mode 100644
index 0000000..711ef10
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/sources/en/LightNovelBtt.java
@@ -0,0 +1,131 @@
+package org.ranobe.core.sources.en;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.core.models.Chapter;
+import org.ranobe.core.models.DataSource;
+import org.ranobe.core.models.Filter;
+import org.ranobe.core.models.Lang;
+import org.ranobe.core.models.Novel;
+import org.ranobe.core.network.HttpClient;
+import org.ranobe.core.sources.Source;
+import org.ranobe.core.util.NumberUtils;
+import org.ranobe.core.util.SourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class LightNovelBtt implements Source {
+ private final String baseUrl = "https://lightnovelbtt.com";
+ private final int sourceId = 3;
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "Light Novel Btt";
+ source.lang = Lang.eng;
+ source.dev = "ap-atul";
+ source.logo = "https://lightnovelbtt.com/Content/images/favicon/android-icon-192x192.png";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws IOException {
+ String web = baseUrl.concat("/?page=").concat(String.valueOf(page)).concat("&typegroup=0");
+ return parse(HttpClient.GET(web, new HashMap<>()));
+ }
+
+ private String httpsImage(String url) {
+ return url.replace("http://", "https://");
+ }
+
+ private List parse(String body) {
+ List items = new ArrayList<>();
+ Element doc = Jsoup.parse(body).select("div.items").first();
+
+ if (doc == null) return items;
+
+ for (Element element : doc.select("div.item")) {
+ String url = element.select("div.box_img > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = element.select("div.title").text().trim();
+ item.cover = httpsImage(element.select("img").attr("data-src").trim());
+ items.add(item);
+ }
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws Exception {
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, new HashMap<>()));
+
+ novel.sourceId = sourceId;
+ novel.name = doc.select("h1.title-detail").text().trim();
+ novel.cover = httpsImage(
+ doc.select("div.detail-info").select("img").attr("data-src").trim()
+ );
+ doc.select("p#summary").select("br").append("::");
+ novel.summary = doc.select("p#summary").text().replaceAll("::", "\n\n").trim();
+ novel.authors = Arrays.asList(doc.select("li.author > p.col-xs-10 > a").text().trim().split(","));
+ doc.select("li.kind > p").select("a").append("::");
+ novel.genres = Arrays.asList(doc.select("li.kind > p").text().split("::"));
+ novel.status = doc.select("li.status > p.col-xs-10").text().trim();
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws Exception {
+ List items = new ArrayList<>();
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, new HashMap<>()));
+
+ for (Element element : doc.select("div.list-chapter").select("li.row")) {
+ Chapter item = new Chapter(novel.url);
+
+ if (element.hasClass("heading")) {
+ continue;
+ }
+
+ item.url = element.select("a").attr("href").trim();
+ item.name = element.select("a").text().trim();
+ item.id = NumberUtils.toFloat(item.name);
+ item.updated = element.select("div.col-xs-4").text().trim();
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws Exception {
+ Element doc = Jsoup.parse(HttpClient.GET(chapter.url, new HashMap<>()));
+
+ chapter.content = "";
+ doc.select("div.reading-detail").select("p").append("::");
+ chapter.content = SourceUtils.cleanContent(
+ doc.select("div.reading-detail").text().replaceAll("::", "\n\n").trim()
+ );
+
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws IOException {
+ if (filters.hashKeyword()) {
+ String keyword = filters.getKeyword();
+ String web = SourceUtils.buildUrl(baseUrl, "/find-story?keyword=", keyword, "&page=", String.valueOf(page));
+ return parse(HttpClient.GET(web, new HashMap<>()));
+ }
+ return new ArrayList<>();
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/sources/en/LightNovelHeaven.java b/core/src/main/java/org/ranobe/core/sources/en/LightNovelHeaven.java
new file mode 100644
index 0000000..17417b6
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/sources/en/LightNovelHeaven.java
@@ -0,0 +1,148 @@
+package org.ranobe.core.sources.en;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.core.models.Chapter;
+import org.ranobe.core.models.DataSource;
+import org.ranobe.core.models.Filter;
+import org.ranobe.core.models.Lang;
+import org.ranobe.core.models.Novel;
+import org.ranobe.core.network.HttpClient;
+import org.ranobe.core.sources.Source;
+import org.ranobe.core.util.NumberUtils;
+import org.ranobe.core.util.SourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class LightNovelHeaven implements Source {
+ private final String baseUrl = "https://lightnovelheaven.com/";
+ private final int sourceId = 9;
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "Light Novel Heaven";
+ source.lang = Lang.eng;
+ source.dev = "ap-atul";
+ source.logo = "https://lightnovelheaven.com/wp-content/uploads/2020/07/cropped-mid-2-192x192.png";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws IOException {
+ List items = new ArrayList<>();
+ String web = baseUrl.concat("/page/").concat(String.valueOf(page));
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+
+ for (Element element : doc.select(".page-item-detail")) {
+ String url = element.select(".h5 > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = element.select(".h5 > a").text().trim();
+ item.cover = element.select("img").attr("data-src").trim();
+ items.add(item);
+ }
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, new HashMap<>()));
+
+ novel.sourceId = sourceId;
+ novel.name = doc.select(".post-title > h1").text().trim();
+ novel.cover = doc.select(".summary_image > a > img").attr("data-src").trim();
+ novel.summary = String.join("\n\n", doc.select("div.summary__content").select("p").eachText());
+ novel.rating = NumberUtils.toFloat(doc.select(".total_votes").text().trim());
+ novel.authors = Arrays.asList(doc.select(".author-content > a").text().split(","));
+
+ List genres = new ArrayList<>();
+ for (Element element : doc.select(".genres-content > a")) {
+ genres.add(element.text().trim());
+ }
+ novel.genres = genres;
+
+ for (Element element : doc.select(".post-content_item")) {
+ String header = element.select(".summary-heading > h5").text().trim();
+ String content = element.select(".summary-content").text().trim();
+
+ if (header.equalsIgnoreCase("Status")) {
+ novel.status = content;
+ } else if (header.equalsIgnoreCase("Alternative")) {
+ novel.alternateNames = Arrays.asList(content.split(","));
+ } else if (header.equalsIgnoreCase("Release")) {
+ novel.year = NumberUtils.toInt(content);
+ }
+ }
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws IOException {
+ List items = new ArrayList<>();
+ String web = novel.url.concat("ajax/chapters");
+ Element doc = Jsoup.parse(HttpClient.POST(web, new HashMap<>(), new HashMap<>()));
+
+ for (Element element : doc.select(".wp-manga-chapter")) {
+ Chapter item = new Chapter(novel.url);
+
+ item.url = element.select("a").attr("href").trim();
+ item.name = element.select("a").text().trim();
+ item.id = NumberUtils.toFloat(item.name);
+ item.updated = element.select("span.chapter-release-date").text().trim();
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(chapter.url, new HashMap<>()));
+ Element main = doc.select(".reading-content").first();
+
+ if (main == null) {
+ return null;
+ }
+
+ chapter.content = "";
+ chapter.content = String.join("\n\n", main.select("p").eachText());
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws IOException {
+ List items = new ArrayList<>();
+
+ if (filters.hashKeyword()) {
+ String web = SourceUtils.buildUrl(baseUrl, "/page/", String.valueOf(page), "/?s=", filters.getKeyword(), "&post_type=wp-manga");
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+ for (Element element : doc.select(".c-tabs-item__content")) {
+ String url = element.select(".tab-thumb > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.url = url;
+ item.name = element.select(".post-title > h3 > a").text().trim();
+ item.cover = element.select("img.img-responsive").attr("data-src").trim();
+
+ items.add(item);
+ }
+ }
+ }
+
+ return items;
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/sources/en/LightNovelPub.java b/core/src/main/java/org/ranobe/core/sources/en/LightNovelPub.java
new file mode 100644
index 0000000..a848273
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/sources/en/LightNovelPub.java
@@ -0,0 +1,138 @@
+package org.ranobe.core.sources.en;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.core.models.Chapter;
+import org.ranobe.core.models.DataSource;
+import org.ranobe.core.models.Filter;
+import org.ranobe.core.models.Lang;
+import org.ranobe.core.models.Novel;
+import org.ranobe.core.network.HttpClient;
+import org.ranobe.core.sources.Source;
+import org.ranobe.core.util.NumberUtils;
+import org.ranobe.core.util.SourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class LightNovelPub implements Source {
+ private final String baseUrl = "https://light-novelpub.com";
+ private final int sourceId = 4;
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "Light Novel Pub";
+ source.lang = Lang.eng;
+ source.dev = "ap-atul";
+ source.logo = "https://light-novelpub.com/img/favicon.ico";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws IOException {
+ String web = baseUrl.concat("/sort/hot-lightnovelpub-update/?page=").concat(String.valueOf(page));
+ return parse(HttpClient.GET(web, new HashMap<>()));
+ }
+
+ private List parse(String body) {
+ List items = new ArrayList<>();
+ Element doc = Jsoup.parse(body).select("div.list-novel").first();
+
+ if (doc == null) return items;
+
+ for (Element element : doc.select("div.row")) {
+ String url = element.select("h3.novel-title > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = element.select("h3.novel-title > a").text().trim();
+ item.cover = element.select("img").attr("src").replace("_200_89", "");
+ items.add(item);
+ }
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws Exception {
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, new HashMap<>()));
+
+ novel.sourceId = sourceId;
+ novel.name = doc.select("h3.title").text().trim();
+ novel.cover = doc.select("div.book").select("img").attr("src").trim();
+ novel.summary = doc.select("div.desc-text").text().trim();
+ novel.rating = NumberUtils.toFloat(doc.select("span[itemprop=ratingValue]").text()) / 2;
+
+ for (Element element : doc.select("ul.info-meta > li")) {
+ String header = element.select("h3").text();
+
+ if (header.equalsIgnoreCase("Author:")) {
+ novel.authors = Arrays.asList(element.select("a").text().split(","));
+ } else if (header.equalsIgnoreCase("Genre:")) {
+ List genres = new ArrayList<>();
+ for (Element a : element.select("a")) genres.add(a.text());
+ novel.genres = genres;
+ } else if (header.equalsIgnoreCase("Alternative names:")) {
+ novel.alternateNames = Arrays.asList(element.select("a").text().split(","));
+ } else if (header.equalsIgnoreCase("Status:")) {
+ novel.status = element.select("a").text().trim();
+ }
+ }
+
+ return novel;
+ }
+
+ private String getNovelId(String url) {
+ String[] parts = url.split("/");
+ return parts[parts.length - 1];
+ }
+
+ @Override
+ public List chapters(Novel novel) throws Exception {
+ List items = new ArrayList<>();
+ String base = baseUrl.concat("/ajax/chapter-archive?novelId=").concat(getNovelId(novel.url));
+ Element doc = Jsoup.parse(HttpClient.GET(base, new HashMap<>()));
+
+ for (Element element : doc.select("a")) {
+ Chapter item = new Chapter(novel.url);
+
+ item.url = element.attr("href").trim();
+ item.name = element.attr("title").trim();
+ item.id = NumberUtils.toFloat(item.name);
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws Exception {
+ Element doc = Jsoup.parse(HttpClient.GET(chapter.url, new HashMap<>()));
+
+ chapter.content = "";
+ doc.select("div.chr-c").select("p").append("::");
+ chapter.content = SourceUtils.cleanContent(
+ doc.select("div.chr-c").text().replaceAll("::", "\n\n").trim()
+ );
+
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws IOException {
+ if (filters.hashKeyword()) {
+ String keyword = filters.getKeyword();
+ String web = SourceUtils.buildUrl(baseUrl, "/search?keyword=", keyword, "&page=", String.valueOf(page));
+ return parse(HttpClient.GET(web, new HashMap<>()));
+ }
+ return new ArrayList<>();
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/sources/en/NewNovel.java b/core/src/main/java/org/ranobe/core/sources/en/NewNovel.java
new file mode 100644
index 0000000..8583e46
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/sources/en/NewNovel.java
@@ -0,0 +1,153 @@
+package org.ranobe.core.sources.en;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.core.models.Chapter;
+import org.ranobe.core.models.DataSource;
+import org.ranobe.core.models.Filter;
+import org.ranobe.core.models.Lang;
+import org.ranobe.core.models.Novel;
+import org.ranobe.core.network.HttpClient;
+import org.ranobe.core.sources.Source;
+import org.ranobe.core.util.NumberUtils;
+import org.ranobe.core.util.SourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class NewNovel implements Source {
+ private static final String baseUrl = "https://newnovel.org/";
+ private static final int sourceId = 10;
+
+ private String cleanImg(String cover) {
+ return cover.replaceAll("/-\\d+x\\d+.\\w{3}/gm", ".jpg");
+ }
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "New Novel";
+ source.lang = Lang.eng;
+ source.dev = "ap-atul";
+ source.logo = "https://newnovel.org/wp-content/uploads/2022/05/coollogo_com-9657259.png";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws IOException {
+ List items = new ArrayList<>();
+ String web = baseUrl.concat("/page/").concat(String.valueOf(page));
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+
+ for (Element element : doc.select(".page-item-detail")) {
+ String url = element.select(".h5 > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = element.select(".h5 > a").text().trim();
+ item.cover = cleanImg(element.select("img").attr("src").trim());
+ items.add(item);
+ }
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, new HashMap<>()));
+
+ novel.sourceId = sourceId;
+ novel.name = doc.select(".post-title > h1").text().trim();
+ novel.cover = cleanImg(doc.select(".summary_image > a > img").attr("Src").trim());
+ doc.select(".j_synopsis").select("br").append("::");
+ novel.summary = doc.select("div.summary__content > div.mb48").text().replaceAll("::", "\n\n").trim();
+ novel.rating = NumberUtils.toFloat(doc.select(".total_votes").text().trim());
+ novel.authors = Arrays.asList(doc.select(".author-content > a").text().split(","));
+
+ List genres = new ArrayList<>();
+ for (Element element : doc.select(".genres-content > a")) {
+ genres.add(element.text().trim());
+ }
+ novel.genres = genres;
+
+ for (Element element : doc.select(".post-content_item")) {
+ String header = element.select(".summary-heading > h5").text().trim();
+ String content = element.select(".summary-content").text().trim();
+
+ if (header.equalsIgnoreCase("Status")) {
+ novel.status = content;
+ } else if (header.equalsIgnoreCase("Alternative")) {
+ novel.alternateNames = Arrays.asList(content.split(","));
+ } else if (header.equalsIgnoreCase("Release")) {
+ novel.year = NumberUtils.toInt(content);
+ }
+ }
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws IOException {
+ List items = new ArrayList<>();
+ String web = novel.url.concat("ajax/chapters");
+ Element doc = Jsoup.parse(HttpClient.POST(web, new HashMap<>(), new HashMap<>()));
+
+ for (Element element : doc.select(".wp-manga-chapter")) {
+ Chapter item = new Chapter(novel.url);
+
+ item.url = element.select("a").attr("href").trim();
+ item.name = element.select("a").text().trim();
+ item.id = NumberUtils.toFloat(item.name);
+ item.updated = element.select("span.chapter-release-date").text().trim();
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(chapter.url, new HashMap<>()));
+ Element main = doc.select(".reading-content").first();
+
+ if (main == null) {
+ return null;
+ }
+
+ chapter.content = "";
+ chapter.content = String.join("\n\n", main.select("p").eachText());
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws IOException {
+ List items = new ArrayList<>();
+
+ if (filters.hashKeyword()) {
+ String web = SourceUtils.buildUrl(baseUrl, "/page/", String.valueOf(page), "/?s=", filters.getKeyword(), "&post_type=wp-manga");
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+ for (Element element : doc.select(".c-tabs-item__content")) {
+ String url = element.select(".tab-thumb > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.url = url;
+ item.name = element.select(".post-title > h3 > a").text().trim();
+ item.cover = cleanImg(element.select("img.img-responsive").attr("src").trim());
+
+ items.add(item);
+ }
+ }
+ }
+
+ return items;
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/sources/en/ReadLightNovel.java b/core/src/main/java/org/ranobe/core/sources/en/ReadLightNovel.java
new file mode 100644
index 0000000..bfc1134
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/sources/en/ReadLightNovel.java
@@ -0,0 +1,150 @@
+package org.ranobe.core.sources.en;
+
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.ranobe.core.models.Chapter;
+import org.ranobe.core.models.DataSource;
+import org.ranobe.core.models.Filter;
+import org.ranobe.core.models.Lang;
+import org.ranobe.core.models.Novel;
+import org.ranobe.core.network.HttpClient;
+import org.ranobe.core.sources.Source;
+import org.ranobe.core.util.NumberUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+
+public class ReadLightNovel implements Source {
+ public final String baseUrl = "https://www.readlightnovel.me";
+ public final HashMap HEADERS = new HashMap() {{
+ put("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 6.1; rv:2.2) Gecko/20110201");
+ put("Cache-Control", "public max-age=604800");
+ put("x-requested-with", "XMLHttpRequest");
+ }};
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = 1;
+ source.url = baseUrl;
+ source.name = "Read Light Novel";
+ source.lang = Lang.eng;
+ source.dev = "ap-atul";
+ source.logo = "https://www.readlightnovel.me/assets/images/logo-new-day.png";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws IOException {
+ String url = baseUrl + "/top-novels/new/" + page;
+ String body = HttpClient.GET(url, HEADERS);
+ return parse(body);
+ }
+
+ public List parse(String body) {
+ List items = new ArrayList<>();
+ Element doc = Jsoup.parse(body);
+
+ for (Element element : doc.select("div.top-novel-block")) {
+ String url = element.select("div.top-novel-header > h2 > a").attr("href").trim();
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = 1;
+ item.name = element.select("div.top-novel-header > h2 > a").text().trim();
+ item.url = element.select("div.top-novel-header > h2 > a").attr("href").trim();
+ item.cover = element.select("div.top-novel-cover > a > img").attr("src").trim();
+ items.add(item);
+ }
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, HEADERS));
+
+ novel.sourceId = 1;
+ novel.name = doc.select("div.novel-cover > a > img").attr("alt").trim();
+ novel.cover = doc.select("div.novel-cover > a > img").attr("src").trim();
+
+ for (Element element : doc.select("div.novel-detail-item")) {
+ String header = element.select("div.novel-detail-header").text().trim();
+ String value = element.select("div.novel-detail-body").text().trim();
+
+ if (header.toLowerCase().contains("alternative")) {
+ novel.alternateNames = Arrays.asList(value.split("\n"));
+ } else if (header.toLowerCase().contains("author")) {
+ novel.authors = Arrays.asList(value.split("\n"));
+ } else if (header.toLowerCase().contains("genre")) {
+ novel.genres = Arrays.asList(value.split(" "));
+ } else if (header.toLowerCase().contains("rating")) {
+ novel.rating = NumberUtils.toFloat(value);
+ } else if (header.toLowerCase().contains("description")) {
+ String summary = "";
+ for (Element ele : element.select("div.novel-detail-body > p")) {
+ summary = summary.concat(ele.text().trim()).concat("\n\n");
+ }
+ novel.summary = summary;
+ } else if (header.toLowerCase().contains("status")) {
+ novel.status = value;
+ } else if (header.toLowerCase().contains("year")) {
+ novel.year = NumberUtils.toInt(value);
+ }
+ }
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws IOException {
+ List items = new ArrayList<>();
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, HEADERS));
+
+ Elements main = doc.select("div.tab-content");
+ for (Element element : main.select("a")) {
+ Chapter item = new Chapter(novel.url);
+
+ item.name = element.text().trim();
+ item.id = NumberUtils.toFloat(item.name);
+ item.url = element.attr("href").trim();
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws IOException {
+ chapter.content = "";
+ Element doc = Jsoup.parse(HttpClient.GET(chapter.url, HEADERS));
+
+ for (Element element : doc.select("div#chapterhidden > p")) {
+ String text = element.text().trim();
+ chapter.content = chapter.content.concat("\n\n").concat(text);
+ }
+ return chapter;
+ }
+
+ @Override
+ // returns only 50 results
+ public List search(Filter filters, int page) throws IOException {
+ String url = "https://www.readlightnovel.me/detailed-search-210922";
+ if (page > 1) return new ArrayList<>();
+ if (filters.hashKeyword()) {
+ String keyword = filters.getKeyword();
+ HashMap form = new HashMap<>();
+ form.put("keyword", keyword);
+ form.put("search", "1");
+ String body = HttpClient.POST(url, HEADERS, form);
+ return parse(body);
+ }
+ return new ArrayList<>();
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/sources/en/ReadWebNovels.java b/core/src/main/java/org/ranobe/core/sources/en/ReadWebNovels.java
new file mode 100644
index 0000000..8b6013c
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/sources/en/ReadWebNovels.java
@@ -0,0 +1,152 @@
+package org.ranobe.core.sources.en;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.core.models.Chapter;
+import org.ranobe.core.models.DataSource;
+import org.ranobe.core.models.Filter;
+import org.ranobe.core.models.Lang;
+import org.ranobe.core.models.Novel;
+import org.ranobe.core.network.HttpClient;
+import org.ranobe.core.sources.Source;
+import org.ranobe.core.util.NumberUtils;
+import org.ranobe.core.util.SourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class ReadWebNovels implements Source {
+ private static final String baseUrl = "https://readwebnovels.net";
+ private static final int sourceId = 11;
+
+ private String cleanImg(String cover) {
+ return cover.replaceAll("/-\\d+x\\d+.\\w{3}/gm", ".jpg");
+ }
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "Read Web Novels";
+ source.lang = Lang.eng;
+ source.dev = "ap-atul";
+ source.logo = "https://readwebnovels.net/wp-content/uploads/2020/01/cropped-boo1k-180x180.png";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws IOException {
+ List items = new ArrayList<>();
+ String web = baseUrl.concat("/page/").concat(String.valueOf(page));
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+
+ for (Element element : doc.select(".page-item-detail")) {
+ String url = element.select(".h5 > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = element.select(".h5 > a").text().trim();
+ item.cover = cleanImg(element.select("img").attr("src").trim());
+ items.add(item);
+ }
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, new HashMap<>()));
+
+ novel.sourceId = sourceId;
+ novel.name = doc.select(".post-title > h1").text().trim();
+ novel.cover = cleanImg(doc.select(".summary_image > a > img").attr("Src").trim());
+ novel.summary = doc.select("div.summary__content").text().replaceAll("\n", "\n\n").trim();
+ novel.rating = NumberUtils.toFloat(doc.select(".total_votes").text().trim());
+ novel.authors = Arrays.asList(doc.select(".author-content > a").text().split(","));
+
+ List genres = new ArrayList<>();
+ for (Element element : doc.select(".genres-content > a")) {
+ genres.add(element.text().trim());
+ }
+ novel.genres = genres;
+
+ for (Element element : doc.select(".post-content_item")) {
+ String header = element.select(".summary-heading > h5").text().trim();
+ String content = element.select(".summary-content").text().trim();
+
+ if (header.equalsIgnoreCase("Status")) {
+ novel.status = content;
+ } else if (header.equalsIgnoreCase("Alternative")) {
+ novel.alternateNames = Arrays.asList(content.split(","));
+ } else if (header.equalsIgnoreCase("Release")) {
+ novel.year = NumberUtils.toInt(content);
+ }
+ }
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws IOException {
+ List items = new ArrayList<>();
+ String web = novel.url.concat("ajax/chapters");
+ Element doc = Jsoup.parse(HttpClient.POST(web, new HashMap<>(), new HashMap<>()));
+
+ for (Element element : doc.select(".wp-manga-chapter")) {
+ Chapter item = new Chapter(novel.url);
+
+ item.url = element.select("a").attr("href").trim();
+ item.name = element.select("a").text().trim();
+ item.id = NumberUtils.toFloat(item.name);
+ item.updated = element.select("span.chapter-release-date").text().trim();
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(chapter.url, new HashMap<>()));
+ Element main = doc.select(".reading-content").first();
+
+ if (main == null) {
+ return null;
+ }
+
+ chapter.content = "";
+ chapter.content = String.join("\n\n", main.select("p").eachText());
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws IOException {
+ List items = new ArrayList<>();
+
+ if (filters.hashKeyword()) {
+ String web = SourceUtils.buildUrl(baseUrl, "/page/", String.valueOf(page), "/?s=", filters.getKeyword(), "&post_type=wp-manga");
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+ for (Element element : doc.select(".c-tabs-item__content")) {
+ String url = element.select(".tab-thumb > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.url = url;
+ item.name = element.select(".post-title > h3 > a").text().trim();
+ item.cover = cleanImg(element.select("img.img-responsive").attr("src").trim());
+
+ items.add(item);
+ }
+ }
+ }
+
+ return items;
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/sources/en/VipNovel.java b/core/src/main/java/org/ranobe/core/sources/en/VipNovel.java
new file mode 100644
index 0000000..f0f3464
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/sources/en/VipNovel.java
@@ -0,0 +1,155 @@
+package org.ranobe.core.sources.en;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.core.models.Chapter;
+import org.ranobe.core.models.DataSource;
+import org.ranobe.core.models.Filter;
+import org.ranobe.core.models.Lang;
+import org.ranobe.core.models.Novel;
+import org.ranobe.core.network.HttpClient;
+import org.ranobe.core.sources.Source;
+import org.ranobe.core.util.NumberUtils;
+import org.ranobe.core.util.SourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class VipNovel implements Source {
+ private final String baseUrl = "https://vipnovel.com";
+ private final int sourceId = 2;
+
+ private String cleanImg(String cover) {
+ return cover.replaceAll("/-\\d+x\\d+.\\w{3}/gm", ".jpg");
+ }
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "Vip Novel";
+ source.lang = Lang.eng;
+ source.dev = "ap-atul";
+ source.logo = "https://vipnovel.com/wp-content/uploads/2019/02/cropped-51918204_414359882667265_8706934217017131008_n-180x180.png";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws IOException {
+ List items = new ArrayList<>();
+ String web = baseUrl.concat("/page/").concat(String.valueOf(page));
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+
+ for (Element element : doc.select(".page-item-detail")) {
+ String url = element.select(".h5 > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = element.select(".h5 > a").text().trim();
+ item.cover = cleanImg(element.select("img").attr("src").trim());
+ items.add(item);
+ }
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, new HashMap<>()));
+
+ novel.sourceId = sourceId;
+ novel.name = doc.select(".post-title > h1").text().trim();
+ novel.cover = cleanImg(doc.select(".summary_image > a > img").attr("Src").trim());
+ doc.select(".j_synopsis").select("br").append("::");
+ novel.summary = doc.select("div.summary__content > div.mb48").text().replaceAll("::", "\n\n").trim();
+ novel.rating = NumberUtils.toFloat(doc.select(".total_votes").text().trim());
+ novel.authors = Arrays.asList(doc.select(".author-content > a").text().split(","));
+
+ List genres = new ArrayList<>();
+ for (Element element : doc.select(".genres-content > a")) {
+ genres.add(element.text().trim());
+ }
+ novel.genres = genres;
+
+ for (Element element : doc.select(".post-content_item")) {
+ String header = element.select(".summary-heading > h5").text().trim();
+ String content = element.select(".summary-content").text().trim();
+
+ if (header.equalsIgnoreCase("Status")) {
+ novel.status = content;
+ } else if (header.equalsIgnoreCase("Alternative")) {
+ novel.alternateNames = Arrays.asList(content.split(","));
+ } else if (header.equalsIgnoreCase("Release")) {
+ novel.year = NumberUtils.toInt(content);
+ }
+ }
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws IOException {
+ List items = new ArrayList<>();
+ String web = novel.url.concat("ajax/chapters");
+ Element doc = Jsoup.parse(HttpClient.POST(web, new HashMap<>(), new HashMap<>()));
+
+ for (Element element : doc.select(".wp-manga-chapter")) {
+ Chapter item = new Chapter(novel.url);
+
+ item.url = element.select("a").attr("href").trim();
+ item.name = element.select("a").text().trim();
+ item.id = NumberUtils.toFloat(item.name);
+ item.updated = element.select("span.chapter-release-date").text().trim();
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(chapter.url, new HashMap<>()));
+ Element main = doc.select(".reading-content").first();
+
+ if (main == null) {
+ return null;
+ }
+
+ chapter.content = "";
+ main.select("p").append("::");
+ chapter.content = SourceUtils.cleanContent(main.text().replaceAll("::", "\n\n").trim());
+
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws IOException {
+ List items = new ArrayList<>();
+
+ if (filters.hashKeyword()) {
+ String web = SourceUtils.buildUrl(baseUrl, "/page/", String.valueOf(page), "/?s=", filters.getKeyword(), "&post_type=wp-manga");
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+ for (Element element : doc.select(".c-tabs-item__content")) {
+ String url = element.select(".tab-thumb > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.url = url;
+ item.name = element.select(".post-title > h3 > a").text().trim();
+ item.cover = cleanImg(element.select("img.img-responsive").attr("src").trim());
+
+ items.add(item);
+ }
+ }
+ }
+
+ return items;
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/sources/en/WuxiaWorld.java b/core/src/main/java/org/ranobe/core/sources/en/WuxiaWorld.java
new file mode 100644
index 0000000..b154e7e
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/sources/en/WuxiaWorld.java
@@ -0,0 +1,152 @@
+package org.ranobe.core.sources.en;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.core.models.Chapter;
+import org.ranobe.core.models.DataSource;
+import org.ranobe.core.models.Filter;
+import org.ranobe.core.models.Lang;
+import org.ranobe.core.models.Novel;
+import org.ranobe.core.network.HttpClient;
+import org.ranobe.core.sources.Source;
+import org.ranobe.core.util.NumberUtils;
+import org.ranobe.core.util.SourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class WuxiaWorld implements Source {
+ private static final String baseUrl = "https://wuxiaworld.site";
+ private static final int sourceId = 13;
+
+ private String cleanImg(String cover) {
+ return cover.replaceAll("/-\\d+x\\d+.\\w{3}/gm", ".jpg");
+ }
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "Wuxia World";
+ source.lang = Lang.eng;
+ source.dev = "ap-atul";
+ source.logo = "https://wuxiaworld.site/wp-content/uploads/2019/04/favicon-1.ico";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws IOException {
+ List items = new ArrayList<>();
+ String web = baseUrl.concat("/page/").concat(String.valueOf(page));
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+
+ for (Element element : doc.select(".page-item-detail")) {
+ String url = element.select(".h5 > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = element.select(".h5 > a").text().trim();
+ item.cover = cleanImg(element.select("img").attr("data-src").trim());
+ items.add(item);
+ }
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, new HashMap<>()));
+
+ novel.sourceId = sourceId;
+ novel.name = doc.select(".post-title > h1").text().trim();
+ novel.cover = cleanImg(doc.select(".summary_image > a > img").attr("data-src").trim());
+ novel.summary = doc.select("div.summary__content").text().replaceAll("\n", "\n\n").trim();
+ novel.rating = NumberUtils.toFloat(doc.select(".total_votes").text().trim());
+ novel.authors = Arrays.asList(doc.select(".author-content > a").text().split(","));
+
+ List genres = new ArrayList<>();
+ for (Element element : doc.select(".genres-content > a")) {
+ genres.add(element.text().trim());
+ }
+ novel.genres = genres;
+
+ for (Element element : doc.select(".post-content_item")) {
+ String header = element.select(".summary-heading > h5").text().trim();
+ String content = element.select(".summary-content").text().trim();
+
+ if (header.equalsIgnoreCase("Status")) {
+ novel.status = content;
+ } else if (header.equalsIgnoreCase("Alternative")) {
+ novel.alternateNames = Arrays.asList(content.split(","));
+ } else if (header.equalsIgnoreCase("Release")) {
+ novel.year = NumberUtils.toInt(content);
+ }
+ }
+
+ return novel;
+ }
+
+ @Override
+ public List chapters(Novel novel) throws IOException {
+ List items = new ArrayList<>();
+ String web = novel.url.concat("ajax/chapters");
+ Element doc = Jsoup.parse(HttpClient.POST(web, new HashMap<>(), new HashMap<>()));
+
+ for (Element element : doc.select(".wp-manga-chapter")) {
+ Chapter item = new Chapter(novel.url);
+
+ item.url = element.select("a").attr("href").trim();
+ item.name = element.select("a").text().trim();
+ item.id = NumberUtils.toFloat(item.name);
+ item.updated = element.select("span.chapter-release-date").text().trim();
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(chapter.url, new HashMap<>()));
+ Element main = doc.select(".reading-content").first();
+
+ if (main == null) {
+ return null;
+ }
+
+ chapter.content = "";
+ chapter.content = String.join("\n\n", main.select("p").eachText());
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws IOException {
+ List items = new ArrayList<>();
+
+ if (filters.hashKeyword()) {
+ String web = SourceUtils.buildUrl(baseUrl, "/page/", String.valueOf(page), "/?s=", filters.getKeyword(), "&post_type=wp-manga");
+ Element doc = Jsoup.parse(HttpClient.GET(web, new HashMap<>()));
+ for (Element element : doc.select(".c-tabs-item__content")) {
+ String url = element.select(".tab-thumb > a").attr("href").trim();
+
+ if (url.length() > 0) {
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.url = url;
+ item.name = element.select(".post-title > h3 > a").text().trim();
+ item.cover = cleanImg(element.select("img.img-responsive").attr("data-src").trim());
+
+ items.add(item);
+ }
+ }
+ }
+
+ return items;
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/sources/ru/RanobeHub.java b/core/src/main/java/org/ranobe/core/sources/ru/RanobeHub.java
new file mode 100644
index 0000000..f8e9fb9
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/sources/ru/RanobeHub.java
@@ -0,0 +1,171 @@
+package org.ranobe.core.sources.ru;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.ranobe.core.models.Chapter;
+import org.ranobe.core.models.DataSource;
+import org.ranobe.core.models.Filter;
+import org.ranobe.core.models.Lang;
+import org.ranobe.core.models.Novel;
+import org.ranobe.core.network.HttpClient;
+import org.ranobe.core.sources.Source;
+import org.ranobe.core.util.NumberUtils;
+import org.ranobe.core.util.SourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class RanobeHub implements Source {
+ private final String baseUrl = "https://ranobehub.org/";
+ private final int sourceId = 5;
+
+ @Override
+ public DataSource metadata() {
+ DataSource source = new DataSource();
+ source.sourceId = sourceId;
+ source.url = baseUrl;
+ source.name = "Ranobehub — ранобэ на русском онлайн";
+ source.lang = Lang.ru;
+ source.dev = "ap-atul";
+ source.logo = "https://ranobehub.org/favicon.png";
+ return source;
+ }
+
+ @Override
+ public List novels(int page) throws Exception {
+ String web = baseUrl.concat("api/search?page=").concat(String.valueOf(page)).concat("&take=40");
+ List items = new ArrayList<>();
+ String json = HttpClient.GET(web, new HashMap<>());
+
+ JSONArray novels = new JSONObject(json).getJSONArray("resource");
+ for (int i = 0; i < novels.length(); i++) {
+ JSONObject novel = novels.getJSONObject(i);
+
+ String url = novel.getString("url");
+ Novel item = new Novel(url);
+ item.sourceId = sourceId;
+ item.name = novel.getJSONObject("names").getString("rus");
+ item.cover = novel.getJSONObject("poster").getString("medium")
+ .replace("medium", "big");
+ items.add(item);
+ }
+
+ return items;
+ }
+
+ @Override
+ public Novel details(Novel novel) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(novel.url, new HashMap<>()));
+
+ novel.sourceId = sourceId;
+ novel.name = doc.select("h1.ui.huge.header").text().trim();
+ novel.alternateNames = Arrays.asList(doc.select("h2.ui.header.medium").text().trim().split(","));
+ novel.cover = doc.select("img.__posterbox").attr("data-src").replace("medium", "big").trim();
+ doc.select("div.book-description").select("p").append("::");
+ novel.summary = doc.select("div.book-description").text().replaceAll("::", "\n\n").trim();
+
+ List authors = new ArrayList<>();
+ for (Element element : doc.select("book-author")) {
+ authors.add(element.select("a").text().trim());
+ }
+ novel.authors = authors;
+ novel.year = NumberUtils.toInt(doc.select("div.book-meta-value").select("a").text().trim());
+
+ List genres = new ArrayList<>();
+ for (Element element : doc.select("div.book-meta-value.book-tags > a")) {
+ genres.add(element.text().trim());
+ }
+ novel.genres = genres;
+
+ for (Element element : doc.select("div.book-meta-row")) {
+ String header = element.select("div.book-meta-key").text().trim();
+ if (header.contains("перевода")) {
+ novel.status = element.select("div.book-meta-value").select("a").text().trim();
+ }
+ }
+ return novel;
+ }
+
+ private String getNovelId(String url) {
+ String[] parts = url.split("/");
+ String last = parts[parts.length - 1];
+ return String.valueOf(NumberUtils.toInt(last));
+ }
+
+ @Override
+ public List chapters(Novel novel) throws Exception {
+ List items = new ArrayList<>();
+ String web = baseUrl.concat("api/ranobe/").concat(getNovelId(novel.url)).concat("/contents");
+ String json = HttpClient.GET(web, new HashMap<>());
+
+ JSONArray vols = new JSONObject(json).getJSONArray("volumes");
+
+ for (int i = 0; i < vols.length(); i++) {
+ JSONArray chaps = vols.getJSONObject(i).getJSONArray("chapters");
+
+ for (int j = 0; j < chaps.length(); j++) {
+ JSONObject chapter = chaps.getJSONObject(j);
+
+ Chapter item = new Chapter(novel.url);
+ item.url = chapter.getString("url");
+ item.name = chapter.getString("name");
+ item.id = items.size() + 1;
+ item.updated = SourceUtils.getDate(chapter.getInt("changed_at"));
+ items.add(item);
+ }
+ }
+ return items;
+ }
+
+ @Override
+ public Chapter chapter(Chapter chapter) throws IOException {
+ Element doc = Jsoup.parse(HttpClient.GET(chapter.url, new HashMap<>()));
+ chapter.content = "";
+
+ for (Element element : doc.select("div.ui.text.container")) {
+ if (element.hasAttr("data-container")) {
+ element.select("p").append("::");
+ chapter.content = SourceUtils.cleanContent(
+ element.text().replaceAll("::", "\n\n").trim()
+ );
+ }
+ }
+
+ return chapter;
+ }
+
+ @Override
+ public List search(Filter filters, int page) throws Exception {
+ if (page > 1) return new ArrayList<>();
+
+ List items = new ArrayList<>();
+ if (filters.hashKeyword()) {
+ String keyword = filters.getKeyword();
+ String web = SourceUtils.buildUrl(baseUrl, "api/fulltext/global?query=", keyword, "&take=100");
+ String json = HttpClient.GET(web, new HashMap<>());
+ JSONArray response = new JSONArray(json);
+
+ for (int i = 0; i < response.length(); i++) {
+ if (response.getJSONObject(i).getJSONObject("meta").getString("key").equals("ranobe")) {
+ JSONArray novels = response.getJSONObject(i).getJSONArray("data");
+ for (int j = 0; j < novels.length(); j++) {
+ JSONObject novel = novels.getJSONObject(j);
+
+ Novel item = new Novel(novel.getString("url"));
+ item.sourceId = sourceId;
+ item.name = novel.getJSONObject("names").getString("rus");
+ item.cover = novel.getString("image").replace("small", "big");
+ items.add(item);
+ }
+ break;
+ }
+ }
+ }
+ return items;
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/util/ListUtils.java b/core/src/main/java/org/ranobe/core/util/ListUtils.java
new file mode 100644
index 0000000..de0cb36
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/util/ListUtils.java
@@ -0,0 +1,26 @@
+package org.ranobe.core.util;
+
+
+import org.ranobe.core.models.Chapter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ListUtils {
+ public static List searchByName(String keyword, List items) {
+ List result = new ArrayList<>();
+ for (Chapter item : items) {
+ if (item.name.toLowerCase().contains(keyword)) {
+ result.add(item);
+ }
+ }
+ return result;
+ }
+
+ public static List sortById(List items) {
+ List sorted = new ArrayList<>(items);
+ Collections.sort(sorted, (a, b) -> Float.compare(a.id, b.id));
+ return sorted;
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/util/NumberUtils.java b/core/src/main/java/org/ranobe/core/util/NumberUtils.java
new file mode 100644
index 0000000..6b0fc7d
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/util/NumberUtils.java
@@ -0,0 +1,30 @@
+package org.ranobe.core.util;
+
+import java.util.Random;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class NumberUtils {
+ private static final Pattern floatingPoint = Pattern.compile("[+-]?\\d+(\\.\\d+)?");
+ private static final String allNumbersRegex = "\\D";
+
+ public static float toFloat(String value) {
+ if (value == null) return 0F;
+
+ Matcher matcher = floatingPoint.matcher(value);
+ if (!matcher.find()) return 0F;
+
+ String number = matcher.group();
+ return number.length() == 0 ? 0F : Float.parseFloat(number);
+ }
+
+ public static int toInt(String value) {
+ if (value == null) return 0;
+ String number = value.replaceAll(allNumbersRegex, "");
+ return number.length() > 0 ? Integer.parseInt(number) : 0;
+ }
+
+ public static int getRandom(int size) {
+ return new Random().nextInt(size);
+ }
+}
diff --git a/core/src/main/java/org/ranobe/core/util/SourceUtils.java b/core/src/main/java/org/ranobe/core/util/SourceUtils.java
new file mode 100644
index 0000000..2c14517
--- /dev/null
+++ b/core/src/main/java/org/ranobe/core/util/SourceUtils.java
@@ -0,0 +1,38 @@
+package org.ranobe.core.util;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class SourceUtils {
+ public static Long generateId(String url) {
+ long hash = 1125899906842597L;
+ for (int i = 0; i < url.length(); i++) {
+ hash = 31 * hash + url.charAt(i);
+ }
+ return hash;
+ }
+
+ public static String cleanContent(String raw) {
+ return raw.replaceAll("\n\n", "\n");
+ }
+
+ // does simple concatenation and nothing else
+ public static String buildUrl(String... args) {
+ String url = "";
+ for (String a : args) {
+ if (a == null) {
+ a = "";
+ }
+ url = url.concat(a);
+ }
+ return url;
+ }
+
+ public static String getDate(int timestamp) {
+ try {
+ return SimpleDateFormat.getDateTimeInstance().format(new Date(timestamp * 1000L));
+ } catch (Exception e) {
+ return "";
+ }
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index 70fd110..9008151 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -10,7 +10,12 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
+ maven {
+ url 'https://github.com/psiegman/mvn-repo/raw/master/releases'
+ }
}
}
rootProject.name = "ranobe"
include ':app'
+include ':core'
+include ':downloader'