diff --git a/src/screens/Browse/BrowseSettings.js b/src/screens/Browse/BrowseSettings.js index 37b0dddc..a9392a0b 100644 --- a/src/screens/Browse/BrowseSettings.js +++ b/src/screens/Browse/BrowseSettings.js @@ -23,7 +23,7 @@ const BrowseSettings = ({ navigation }) => { showNovelUpdates = true, showMyAnimeList = true, } = useSelector((state) => state.sourceReducer); - const languages = ["English", "Spanish"]; + const languages = ["English", "Spanish", "Japanese"]; const { searchAllSources = false } = useSettings(); const renderItem = ({ item }) => { diff --git a/src/services/Source/source.js b/src/services/Source/source.js index 27d6bd67..8fe3140b 100644 --- a/src/services/Source/source.js +++ b/src/services/Source/source.js @@ -1,4 +1,3 @@ -import { showToast } from "../../hooks/showToast"; import { getSource } from "../../sources/sources"; export const fetchSources = async () => { @@ -11,8 +10,6 @@ export const fetchSources = async () => { }; export const fetchNovel = async (sourceId, novelUrl) => { - if (sourceId === 37) showToast("LNMTL might take around 20-30 seconds."); - const source = getSource(sourceId); const res = await source.parseNovelAndChapters(novelUrl); diff --git a/src/sources/en/lnmtl.js b/src/sources/en/lnmtl.js index 74e8371f..61711746 100644 --- a/src/sources/en/lnmtl.js +++ b/src/sources/en/lnmtl.js @@ -1,4 +1,5 @@ import cheerio from "react-native-cheerio"; +import { showToast } from "../../hooks/showToast"; import { htmlToText } from "../helpers/htmlToText"; const baseUrl = "https://lnmtl.com/"; @@ -35,6 +36,8 @@ const popularNovels = async (page) => { }; const parseNovelAndChapters = async (novelUrl) => { + showToast("LNMTL might take around 20-30 seconds."); + const url = baseUrl + "novel/" + novelUrl; const result = await fetch(url); @@ -130,7 +133,10 @@ const parseChapter = async (novelUrl, chapterUrl) => { $(".original").remove(); let chapterText = $(".chapter-body").html(); - chapterText = chapterName + "\n\n" + htmlToText(chapterText); + chapterText = + chapterName + + "\n\n" + + htmlToText(chapterText, { removeLineBreaks: false }); let nextChapter = null; let prevChapter = null; diff --git a/src/sources/helpers/htmlToText.js b/src/sources/helpers/htmlToText.js index 3c9f5353..83842e03 100644 --- a/src/sources/helpers/htmlToText.js +++ b/src/sources/helpers/htmlToText.js @@ -1,40 +1,41 @@ -const htmlToText = (html) => { - return ( - html - // Remove line breaks - .replace(/(?:\n|\r\n|\r)/gi, "") - // Turn
's into single line breaks. - .replace(/<\s*br[^>]*>/gi, "\n") - // Turn 's into line breaks. - .replace(/<\s*\/li[^>]*>/gi, "\n") - // Turn

's into double line breaks. - .replace(/<\s*p[^>]*>/gi, "\n\n") - // Remove content in script tags. - .replace(/<\s*script[^>]*>[\s\S]*?<\/script>/gim, "") - // Remove content in style tags. - .replace(/<\s*style[^>]*>[\s\S]*?<\/style>/gim, "") - // Remove content in comments. - .replace(//gim, "") - // Format anchor tags properly. - .replace( - /<\s*a[^>]*href=['"](.*?)['"][^>]*>([\s\S]*?)<\/\s*a\s*>/gi, - "$2" - ) - .replace(/<\s*img[^>]*src=['"](.*?)['"][^>]*>/gi, "[$1]") - // Remove all remaining tags. - .replace(/(<([^>]+)>)/gi, "") - // Make sure there are never more than two - // consecutive linebreaks. - .replace(/\n{2,}/g, "\n\n") - // Remove tabs. - .replace(/\t/g, "") - // Remove newlines at the beginning of the text. - .replace(/^\n+/m, "") - // Replace multiple spaces with a single space. - .replace(/ {2,}/g, " ") - // Decode HTML entities. - .replace(/&([^;]+);/g, decodeHtmlEntity) - ); +const htmlToText = (html, options = {}) => { + const { removeLineBreaks = true } = options; + + text = removeLineBreaks && html.replace(/(?:\n|\r\n|\r)/gi, ""); + text = html + // Turn
's into single line breaks. + .replace(/<\s*br[^>]*>/gi, "\n") + // Turn 's into line breaks. + .replace(/<\s*\/li[^>]*>/gi, "\n") + // Turn

's into double line breaks. + .replace(/<\s*p[^>]*>/gi, "\n\n") + // Remove content in script tags. + .replace(/<\s*script[^>]*>[\s\S]*?<\/script>/gim, "") + // Remove content in style tags. + .replace(/<\s*style[^>]*>[\s\S]*?<\/style>/gim, "") + // Remove content in comments. + .replace(//gim, "") + // Format anchor tags properly. + .replace( + /<\s*a[^>]*href=['"](.*?)['"][^>]*>([\s\S]*?)<\/\s*a\s*>/gi, + "$2" + ) + .replace(/<\s*img[^>]*src=['"](.*?)['"][^>]*>/gi, "[$1]") + // Remove all remaining tags. + .replace(/(<([^>]+)>)/gi, "") + // Make sure there are never more than two + // consecutive linebreaks. + .replace(/\n{2,}/g, "\n\n") + // Remove tabs. + .replace(/\t/g, "") + // Remove newlines at the beginning of the text. + .replace(/^\n+/m, "") + // Replace multiple spaces with a single space. + .replace(/ {2,}/g, " ") + // Decode HTML entities. + .replace(/&([^;]+);/g, decodeHtmlEntity); + + return text; }; const decodeHtmlEntity = (m, n) => { diff --git a/src/sources/multisrc/MadaraGenerator.js b/src/sources/multisrc/MadaraGenerator.js new file mode 100644 index 00000000..7693f34e --- /dev/null +++ b/src/sources/multisrc/MadaraGenerator.js @@ -0,0 +1,72 @@ +import MadaraScraper from "./madara/MadaraScraper"; + +const getPath = (extensionId) => { + const path = { + 38: { novels: "manga", novel: "manga", chapter: "manga" }, + 39: { novels: "series", novel: "series", chapter: "series" }, + 40: { novels: "novels", novel: "novel", chapter: "novel" }, + 41: { novels: "manga", novel: "manga", chapter: "manga" }, + 42: { novels: "all-novels", novel: "manga", chapter: "manga" }, + 43: { novels: "series", novel: "series", chapter: "series" }, + 44: { novels: "series", novel: "series", chapter: "series" }, + 45: { novels: "novel", novel: "novel", chapter: "novel" }, + }; + + return path[extensionId]; +}; + +export const SkyNovelScraper = new MadaraScraper( + 38, + "https://skynovel.org/", + "SkyNovel", + getPath(38) +); + +export const NovelCakeScraper = new MadaraScraper( + 39, + "https://novelcake.com/", + "NovelCake", + getPath(39) +); + +export const NovelsRockScraper = new MadaraScraper( + 40, + "https://novelsrock.com/", + "NovelsRock", + getPath(40) +); + +export const ZinnNovelScraper = new MadaraScraper( + 41, + "https://zinnovel.com/", + "ZinnNovel", + getPath(41) +); + +export const NovelTranslateScraper = new MadaraScraper( + 42, + "https://noveltranslate.com/", + "NovelTranslate", + getPath(42) +); + +export const LunarLettersScraper = new MadaraScraper( + 43, + "https://www.lunarletters.com/", + "LunarLetters", + getPath(43) +); + +export const SleepyTranslationsScraper = new MadaraScraper( + 44, + "https://sleepytranslations.com/", + "SleepyTranslations", + getPath(44) +); + +export const FreeNovelScraper = new MadaraScraper( + 45, + "https://freenovel.me/", + "FreeNovelMe", + getPath(45) +); diff --git a/src/sources/multisrc/madara/MadaraScraper.js b/src/sources/multisrc/madara/MadaraScraper.js new file mode 100644 index 00000000..bdee0eb1 --- /dev/null +++ b/src/sources/multisrc/madara/MadaraScraper.js @@ -0,0 +1,220 @@ +import cheerio from "react-native-cheerio"; +import { htmlToText } from "../../helpers/htmlToText"; + +class MadaraScraper { + constructor(sourceId, baseUrl, sourceName, path) { + this.sourceId = sourceId; + this.baseUrl = baseUrl; + this.sourceName = sourceName; + this.path = path; + + // this.novelsScraper = this.novelsScraper.bind(this); + // this.novelScraper = this.novelScraper.bind(this); + // this.chapterScraper = this.chapterScraper.bind(this); + // this.searchScraper = this.searchScraper.bind(this); + } + + async popularNovels(page) { + const totalPages = 100; + let url = + this.baseUrl + + this.path.novels + + "/page/" + + page + + "/?m_orderby=rating"; + let sourceId = this.sourceId; + + const result = await fetch(url); + const body = await result.text(); + + const $ = cheerio.load(body); + + let novels = []; + + $(".manga-title-badges").remove(); + + $(".page-item-detail").each(function (result) { + const novelName = $(this).find(".post-title").text().trim(); + let image = $(this).find("img"); + const novelCover = image.attr("data-src") || image.attr("src"); + + let novelUrl = $(this) + .find(".post-title") + .find("a") + .attr("href") + .split("/")[4]; + + const novel = { + sourceId, + novelName, + novelCover, + novelUrl, + }; + + novels.push(novel); + }); + + return { totalPages, novels }; + } + + async parseNovelAndChapters(novelUrl) { + const url = `${this.baseUrl}${this.path.novel}/${novelUrl}/`; + + const result = await fetch(url); + const body = await result.text(); + + let $ = cheerio.load(body); + + let novel = {}; + + novel.sourceId = this.sourceId; + + novel.sourceName = this.sourceName; + + novel.url = url; + + novel.novelUrl = novelUrl; + + $(".manga-title-badges").remove(); + + novel.novelName = $(".post-title > h1").text().trim(); + + novel.novelCover = + $(".summary_image > a > img").attr("data-src") || + $(".summary_image > a > img").attr("src"); + + $(".post-content_item").each(function (result) { + const detailName = $(this) + .find(".summary-heading > h5") + .text() + .trim(); + const detail = $(this).find(".summary-content").text().trim(); + + switch (detailName) { + case "Genre(s)": + novel.genre = detail.replace(/[\t\n]/g, ","); + break; + case "Author(s)": + novel.author = detail; + break; + case "Status": + novel.status = detail; + break; + } + }); + + novel.summary = $("div.summary__content").text().trim(); + + let novelChapters = []; + + const novelId = + $(".rating-post-id").attr("value") || + $("#manga-chapters-holder").attr("data-id"); + + let formData = new FormData(); + formData.append("action", "manga_get_chapters"); + formData.append("manga", novelId); + + const data = await fetch(this.baseUrl + "wp-admin/admin-ajax.php", { + method: "POST", + body: formData, + }); + const text = await data.text(); + + $ = cheerio.load(text); + + $(".wp-manga-chapter").each(function (result) { + const chapterName = $(this) + .find("a") + .text() + .replace(/[\t\n]/g, "") + .trim(); + + const releaseDate = $(this) + .find("span") + .text() + .replace("Free", "") + .trim(); + + const chapterUrl = $(this).find("a").attr("href").split("/")[5]; + + novelChapters.push({ chapterName, releaseDate, chapterUrl }); + }); + + console.log(novel); + + novel.chapters = novelChapters.reverse(); + + return novel; + } + + async parseChapter(novelUrl, chapterUrl) { + let sourceId = this.sourceId; + + const url = `${this.baseUrl}${this.path.chapter}/${novelUrl}/${chapterUrl}`; + + const result = await fetch(url); + const body = await result.text(); + + const $ = cheerio.load(body); + + let chapterName = + $(".text-center").text() || $("#chapter-heading").text(); + + let chapterText = $(".text-left").html(); + chapterText = htmlToText(chapterText); + + let nextChapter = null; + let prevChapter = null; + + const chapter = { + sourceId, + novelUrl, + chapterUrl, + chapterName, + chapterText, + nextChapter, + prevChapter, + }; + + return chapter; + } + + async searchNovels(searchTerm) { + const url = `${this.baseUrl}?s=${searchTerm}&post_type=wp-manga`; + + const result = await fetch(url); + const body = await result.text(); + + const $ = cheerio.load(body); + + let novels = []; + let sourceId = this.sourceId; + + $(".c-tabs-item__content").each(function (result) { + const novelName = $(this).find(".post-title").text().trim(); + + let image = $(this).find("img"); + const novelCover = image.attr("data-src") || image.attr("src"); + + let novelUrl = $(this) + .find(".post-title") + .find("a") + .attr("href") + .split("/")[4]; + + const novel = { + sourceId, + novelName, + novelCover, + novelUrl, + }; + + novels.push(novel); + }); + + return novels; + } +} + +export default MadaraScraper; diff --git a/src/sources/sources.js b/src/sources/sources.js index f9834069..1c0efe91 100644 --- a/src/sources/sources.js +++ b/src/sources/sources.js @@ -34,6 +34,16 @@ import RoyalRoadScraper from "./en/royalroad"; import ScribbleHubScraper from "./en/scribblehub"; import SyosetuScraper from "./jp/syosetu"; import LNMTLScraper from "./en/lnmtl"; +import { + FreeNovelScraper, + LunarLettersScraper, + NovelCakeScraper, + NovelsRockScraper, + NovelTranslateScraper, + SkyNovelScraper, + SleepyTranslationsScraper, + ZinnNovelScraper, +} from "./multisrc/MadaraGenerator"; export const sources = [ /** @@ -230,6 +240,62 @@ export const sources = [ icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/lnmtl/icon.png?raw=true", lang: "English", }, + { + sourceId: 38, + sourceUrl: "https://skynovel.org/", + sourceName: "SkyNovel", + icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/multisrc/madara/icons/skynovel.png?raw=true", + lang: "English", + }, + { + sourceId: 39, + sourceUrl: "https://novelcake.com/", + sourceName: "NovelCake", + icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/multisrc/madara/icons/novelcake.png?raw=true", + lang: "English", + }, + { + sourceId: 40, + sourceUrl: "https://novelsrock.com/", + sourceName: "NovelsRock", + icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/multisrc/madara/icons/novelsrock.png?raw=true", + lang: "English", + }, + { + sourceId: 41, + sourceUrl: "https://zinnovel.com/", + sourceName: "ZinnNovel", + icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/multisrc/madara/icons/zinnovel.png?raw=true", + lang: "English", + }, + { + sourceId: 42, + sourceUrl: "https://noveltranslate.com/", + sourceName: "NovelTranslate", + icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/multisrc/madara/icons/noveltranslate.png?raw=true", + lang: "English", + }, + { + sourceId: 43, + sourceUrl: "https://www.lunarletters.com/", + sourceName: "LunarLetters", + icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/multisrc/madara/icons/lunarletters.png?raw=true", + lang: "English", + }, + { + sourceId: 44, + sourceUrl: "https://sleepytranslations.com/", + sourceName: "SleepyTranslations", + icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/multisrc/madara/icons/sleepytranslations.png?raw=true", + lang: "English", + }, + { + sourceId: 45, + sourceUrl: "https://freenovel.me/", + sourceName: "FreeNovelMe", + icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/multisrc/madara/icons/freenovel.png?raw=true", + lang: "English", + }, /** * Spanish @@ -414,5 +480,29 @@ export const getSource = (sourceId) => { case 37: { return LNMTLScraper; } + case 38: { + return SkyNovelScraper; + } + case 39: { + return NovelCakeScraper; + } + case 40: { + return NovelsRockScraper; + } + case 41: { + return ZinnNovelScraper; + } + case 42: { + return NovelTranslateScraper; + } + case 43: { + return LunarLettersScraper; + } + case 44: { + return SleepyTranslationsScraper; + } + case 45: { + return FreeNovelScraper; + } } };