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'