Beta phase (#801)
* beta host * Refactor: url -> path (#757) * refactor: url -> path * clean up * fix: json branch * PagePlugin (#769) * paginated novel support * fix: truyenfull * fix: tunovel, remove pageList * parsePage support for webtest (#773) * add parsePage api * add parsePage UI * add totalPages * (FR src) Beta plugin & Added filters (#775) * noveldeglace: - added filters - added latest - defaultCover fallback - change id to noveldeglace.com - added complementary infos in summary - added cover artist - chapters are now parts - show volume number for multi volume novels - better search - fix search page number - refactored for plugin beta - releaseDate as Date.toString() - separted parseNovels - separated getCheerio - loadedCheerio => $ for shorter lines & simpler test on web * chireads: - fix different parsing for mobile version - added filters - added author - added status - added released date - refactored for plugin beta - separted parseNovels - separated getCheerio - loadedCheerio => $ for shorter lines & simpler test on web * wpmangastream => lightnovelwp rework & new sources (#776) * Added sources: - Knoxt (EN) - Baca Light Novel (ID) - AllNovelRead (ES) - DaoTranslate (EN) - جنة الروايات (AR) - Universal Novel (EN) - NovelTR (TR) old sources auto-generated: - GenesisTls (EN) - NobleMTL (EN) wpmangastream => lightnovelwp: - added doc - added filters in a separte file - semi-automatic filter generator - made it compatible with version 1.0.X and 1.1.X versions of the theme - loadedCheerio => $ - icons in "/icons/lightnovel/id.png" - code cleanup * removed auto-implemented sources * Added README.md to lightnovelwp * removed ai generated text for kolnovel * added novelsknight * added centralNovel * pandamtl accual name * adding folder plugin/arabic * Fix: Vietnamese plugins (#777) * fix: lightnovelvn * add nettruyen * add truyenchu * fix: truyenconect * Fix: Migrate plugins (#771) * Add novel.ovh (#756) * fix * test * fix * date fix + master merge (#761) * date fix * Added Brazilian Portuguese E-Novel (#758) * fixed single digit dates (#767) Co-authored-by: NaddH1 <naderhorani@hotmail.com> * fix * test * fix order * Update renovels.ts * require_chapters * Delete plugins/english/genesistl.broken.ts * Update novelTL.ts --------- Co-authored-by: NaddH1 <naderhorani@hotmail.com> * (src update) ENG (#772) * allnovelfull * bestlightnovel * divinedaolibrary * earlynovel * lightnovelpub * L * M * novelbin * novelfull * novelbuddy * N S * novel updates * async sleep * rename file * pagePlugin * Feat: minify js dir (#781) * Multisrc: HotNovelPub (#784) * HotNovelPub * fix search * Squashed commit of the following: commit 3b759c68eddc06c0adfb959f775de069a85adca7 Author: nyagami <hoangquan05112002@gmail.com> Date: Sat Feb 17 20:33:47 2024 +0700 Feat: minify js dir (#781) * jsmin changed to terser * test * why I did it. * test * Update ranobelib.ts * fix page * Update ranobelib.ts * updated modules * Update terser.json Co-authored-by: nyagami <hoangquan05112002@gmail.com> * Update scripts/multisrc/hotnovelpub/template.ts Co-authored-by: nyagami <hoangquan05112002@gmail.com> * terser api --------- Co-authored-by: nyagami <hoangquan05112002@gmail.com> * Added sources & fix sources (#787) * lightnovelwp: - fix author name - added more localisation of strings - README typo - error on !r.ok new message * noveldeglace: - fix chapterNumber for parts - better dates - better search * Added sources: - White Moonlight Novels (EN) - System Translation (EN) * Resized centralnovel.png * Add Faqwiki.us source (#792) * Add Faqwiki.us source (beta branch) * Fix Path, Fix Chapter Parsing, Fix Novel Title * search novel path fix * fix chapter parsing. some chapter in this site store their whole text in 1-4 <p>, * Add WTR-LAB Source (#797) * Add WTR-LAB Source (Beta Branch) * Add other language from WTR-LAB source * change lang to sourceLang * remove id es tr lang from WTR-LAB * Update plugins/english/wtrlab.ts agree Co-authored-by: nyagami <hoangquan05112002@gmail.com> * Update plugins/english/wtrlab.ts agree Co-authored-by: nyagami <hoangquan05112002@gmail.com> --------- Co-authored-by: nyagami <hoangquan05112002@gmail.com> * revert: scripts to master --------- Co-authored-by: Justin COLLON <48163201+error7404@users.noreply.github.com> Co-authored-by: Rider21 <58046032+Rider21@users.noreply.github.com> Co-authored-by: NaddH1 <naderhorani@hotmail.com> Co-authored-by: K1ngfish3r <trulyihu@gmail.com> Co-authored-by: bregassa <bregas.sat1@gmail.com>
This commit is contained in:
@@ -1,315 +0,0 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
|
||||
class KolNovel implements Plugin.PluginBase {
|
||||
id = "kolnovel";
|
||||
name = "KolNovel";
|
||||
icon = "multisrc/wpmangastream/icons/kolnovel.png";
|
||||
site = "https://kolnovel.lol/";
|
||||
version = "1.0.0";
|
||||
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = `${this.site}series/?page=${pageNo}`;
|
||||
|
||||
if (filters.genres.value.length)
|
||||
link += filters.genres.value.map((i) => `&genre[]=${i}`).join("");
|
||||
if (filters.type.value.length)
|
||||
link += filters.type.value.map((i) => `&lang[]=${i}`).join("");
|
||||
|
||||
link += "&status=" + (filters?.status ? filters.status : "");
|
||||
|
||||
link += "&order=" + (filters?.order ? filters.order : "popular");
|
||||
const headers = new Headers();
|
||||
const body = await fetchApi(link, { headers }).then((result) =>
|
||||
result.text()
|
||||
);
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio("article.maindet").each(function () {
|
||||
const novelName = loadedCheerio(this).find("h2").text();
|
||||
let image = loadedCheerio(this).find("img");
|
||||
const novelCover = image.attr("data-src") || image.attr("src");
|
||||
const novelUrl = loadedCheerio(this).find("h2 a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const headers = new Headers();
|
||||
const result = await fetch(url, { headers });
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio("h1.entry-title").text();
|
||||
|
||||
novel.cover =
|
||||
loadedCheerio("img.wp-post-image").attr("data-src") ||
|
||||
loadedCheerio("img.wp-post-image").attr("src");
|
||||
|
||||
loadedCheerio("div.serl:nth-child(3) > span").each(function () {
|
||||
const detailName = loadedCheerio(this).text().trim();
|
||||
const detail = loadedCheerio(this).next().text().trim();
|
||||
|
||||
switch (detailName) {
|
||||
case "الكاتب":
|
||||
case "Author":
|
||||
novel.author = detail;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
novel.status = loadedCheerio("div.sertostat > span").attr("class");
|
||||
|
||||
novel.genres = loadedCheerio(".sertogenre")
|
||||
.children("a")
|
||||
.map((i, el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join(",");
|
||||
|
||||
novel.summary = loadedCheerio(".sersys")
|
||||
.find("br")
|
||||
.replaceWith("\n")
|
||||
.end()
|
||||
.text();
|
||||
|
||||
let chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio(".eplister")
|
||||
.find("li")
|
||||
.each(function () {
|
||||
const chapterName =
|
||||
loadedCheerio(this).find(".epl-num").text() +
|
||||
" - " +
|
||||
loadedCheerio(this).find(".epl-title").text();
|
||||
|
||||
const releaseDate = loadedCheerio(this)
|
||||
.find(".epl-date")
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
const chapterUrl = loadedCheerio(this).find("a").attr("href");
|
||||
|
||||
if (!chapterUrl) return;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
});
|
||||
});
|
||||
|
||||
novel.chapters = chapter.reverse();
|
||||
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const headers = new Headers();
|
||||
const result = await fetch(chapterUrl, { headers });
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
loadedCheerio(".epcontent > div, i").remove();
|
||||
let ignore = loadedCheerio("article > style").text().trim().split(",");
|
||||
ignore.push(...(ignore.pop()?.match(/^\.\w+/) || []));
|
||||
ignore.map((tag) => loadedCheerio(`p${tag}`).remove());
|
||||
|
||||
let chapterText = loadedCheerio(".epcontent").html() || "";
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo?: number | undefined
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}?s=${searchTerm}`;
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(url, { headers });
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio("article.maindet").each(function () {
|
||||
const novelName = loadedCheerio(this).find("h2").text();
|
||||
let image = loadedCheerio(this).find("img");
|
||||
const novelCover = image.attr("data-src") || image.attr("src");
|
||||
const novelUrl = loadedCheerio(this).find("h2 a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
novels.push({
|
||||
name: novelName,
|
||||
url: novelUrl,
|
||||
cover: novelCover,
|
||||
});
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return await fetchFile(url);
|
||||
}
|
||||
filters = {
|
||||
order: {
|
||||
label: "ترتيب حسب",
|
||||
options: [
|
||||
{ label: "الإعداد الأولي", value: "" },
|
||||
|
||||
{ label: "A-Z", value: "title" },
|
||||
|
||||
{ label: "Z-A", value: "titlereverse" },
|
||||
|
||||
{ label: "أخر التحديثات", value: "update" },
|
||||
|
||||
{ label: "أخر ما تم إضافته", value: "latest" },
|
||||
|
||||
{ label: "الرائجة", value: "popular" },
|
||||
],
|
||||
value: "",
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
status: {
|
||||
value: "",
|
||||
label: "الحالة",
|
||||
options: [
|
||||
{ label: "All", value: "" },
|
||||
|
||||
{ label: "Ongoing", value: "ongoing" },
|
||||
|
||||
{ label: "Hiatus", value: "hiatus" },
|
||||
|
||||
{ label: "Completed", value: "completed" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
type: {
|
||||
value: [],
|
||||
label: "النوع",
|
||||
options: [
|
||||
{ label: "إنجليزية", value: "english" },
|
||||
|
||||
{ label: "روايةلايت", value: "light-novel" },
|
||||
|
||||
{ label: "روايةويب", value: "web-novel" },
|
||||
|
||||
{ label: "صينية", value: "chinese" },
|
||||
|
||||
{ label: "عربية", value: "arabic" },
|
||||
|
||||
{ label: "كورية", value: "korean" },
|
||||
|
||||
{ label: "يابانية", value: "japanese" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
genres: {
|
||||
value: [],
|
||||
label: "تصنيف",
|
||||
options: [
|
||||
{ label: "Wuxia", value: "wuxia" },
|
||||
|
||||
{ label: "Xianxia", value: "xianxia" },
|
||||
|
||||
{ label: "XUANHUAN", value: "xuanhuan" },
|
||||
|
||||
{ label: "أكشن", value: "action" },
|
||||
|
||||
{ label: "إثارة", value: "excitement" },
|
||||
|
||||
{ label: "إنتقالالىعالمأخر", value: "isekai" },
|
||||
|
||||
{ label: "إيتشي", value: "etchi" },
|
||||
|
||||
{ label: "الخيالالعلمي", value: "sci-fi" },
|
||||
|
||||
{ label: "بوليسي", value: "policy" },
|
||||
|
||||
{ label: "تاريخي", value: "historical" },
|
||||
|
||||
{ label: "تحقيقات", value: "%d8%aa%d8%ad%d9%82%d9%8a%d9%82" },
|
||||
|
||||
{ label: "تقمصشخصيات", value: "rpg" },
|
||||
|
||||
{ label: "جريمة", value: "crime" },
|
||||
|
||||
{ label: "جوسى", value: "josei" },
|
||||
|
||||
{ label: "حريم", value: "harem" },
|
||||
|
||||
{ label: "حياةمدرسية", value: "school-life" },
|
||||
|
||||
{ label: "خيالي(فانتازيا)", value: "fantasy" },
|
||||
|
||||
{ label: "دراما", value: "drama" },
|
||||
|
||||
{ label: "رعب", value: "horror" },
|
||||
|
||||
{ label: "رومانسي", value: "romantic" },
|
||||
|
||||
{ label: "سحر", value: "magic" },
|
||||
|
||||
{ label: "سينن", value: "senen" },
|
||||
|
||||
{ label: "شريحةمنالحياة", value: "slice-of-life" },
|
||||
|
||||
{ label: "شوجو", value: "shojo" },
|
||||
|
||||
{ label: "شونين", value: "shonen" },
|
||||
|
||||
{ label: "طبي", value: "medical" },
|
||||
|
||||
{ label: "ظواهرخارقةللطبيعة", value: "supernatural" },
|
||||
|
||||
{ label: "غموض", value: "mysteries" },
|
||||
|
||||
{ label: "فنونالقتال", value: "martial-arts" },
|
||||
|
||||
{ label: "قوىخارقة", value: "superpower" },
|
||||
|
||||
{ label: "كوميدي", value: "comedy" },
|
||||
|
||||
{ label: "مأساوي", value: "tragedy" },
|
||||
|
||||
{ label: "مابعدالكارثة", value: "after-the-disaster" },
|
||||
|
||||
{ label: "مغامرة", value: "adventure" },
|
||||
|
||||
{ label: "ميكا", value: "mechanical" },
|
||||
|
||||
{ label: "ناضج", value: "mature" },
|
||||
|
||||
{ label: "نفسي", value: "psychological" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { isUrlAbsolute } from "@libs/isAbsoluteUrl";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Chapter, Novel, Plugin } from "@typings/plugin";
|
||||
|
||||
export const id = "LNR.org";
|
||||
export const name = "LightNovelReader";
|
||||
export const icon = "src/en/lightnovelreader/icon.png";
|
||||
export const version = "1.0.0";
|
||||
export const site = "https://lightnovelreader.org";
|
||||
|
||||
|
||||
const baseUrl = site;
|
||||
|
||||
export const popularNovels: Plugin.popularNovels = async function (page) {
|
||||
const url = baseUrl + "/ranking/top-rated/" + page;
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Novel.Item[] = [];
|
||||
|
||||
loadedCheerio(".category-items.ranking-category.cm-list > ul > li").each(
|
||||
function () {
|
||||
let novelUrl = loadedCheerio(this).find("a").attr("href");
|
||||
|
||||
if (novelUrl && !isUrlAbsolute(novelUrl)) {
|
||||
novelUrl = baseUrl + novelUrl;
|
||||
}
|
||||
|
||||
if (novelUrl) {
|
||||
const novelName = loadedCheerio(this)
|
||||
.find(".category-name a")
|
||||
.text()
|
||||
.trim();
|
||||
let novelCover = loadedCheerio(this)
|
||||
.find(".category-img img")
|
||||
.attr("src");
|
||||
|
||||
if (novelCover && !isUrlAbsolute(novelCover)) {
|
||||
novelCover = baseUrl + novelCover;
|
||||
}
|
||||
|
||||
const novel = {
|
||||
url: novelUrl,
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
|
||||
async function (novelUrl) {
|
||||
const url = novelUrl;
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Novel.instance = {
|
||||
url: novelUrl,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio(".section-header-title > h2").text();
|
||||
|
||||
let novelCover = loadedCheerio(".novels-detail img").attr("src");
|
||||
|
||||
novel.cover = novelCover
|
||||
? isUrlAbsolute(novelCover)
|
||||
? novelCover
|
||||
: baseUrl + novelCover
|
||||
: undefined;
|
||||
|
||||
novel.summary = loadedCheerio(
|
||||
"div.container > div > div.col-12.col-xl-9 > div > div:nth-child(5) > div"
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
novel.author = loadedCheerio(
|
||||
"div.novels-detail-right > ul > li:nth-child(6) > .novels-detail-right-in-right > a"
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
novel.genres = loadedCheerio(
|
||||
"body > section:nth-child(4) > div > div > div.col-12.col-xl-9 > div > div:nth-child(2) > div > div.novels-detail-right > ul > li:nth-child(3) > div.novels-detail-right-in-right"
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
novel.status = loadedCheerio(
|
||||
"div.novels-detail-right > ul > li:nth-child(2) > .novels-detail-right-in-right"
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
loadedCheerio(".cm-tabs-content > ul > li").each(function () {
|
||||
let chapterUrl = loadedCheerio(this).find("a").attr("href");
|
||||
|
||||
if (chapterUrl && !isUrlAbsolute(chapterUrl)) {
|
||||
chapterUrl = baseUrl + chapterUrl;
|
||||
}
|
||||
|
||||
if (chapterUrl) {
|
||||
const chapterName = loadedCheerio(this).find("a").text().trim();
|
||||
const releaseDate = null;
|
||||
|
||||
const chapter: Chapter.Item = {
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
};
|
||||
|
||||
novel.chapters?.push(chapter);
|
||||
}
|
||||
});
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
|
||||
const url = chapterUrl;
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const chapterText = loadedCheerio("#chapterText").html() || "";
|
||||
|
||||
return chapterText;
|
||||
};
|
||||
|
||||
export const searchNovels: Plugin.searchNovels = async function (searchTerm) {
|
||||
const url =
|
||||
baseUrl + `/search/autocomplete?dataType=json&query=${searchTerm}`;
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.json() as { results: ResultsEntity[] };
|
||||
|
||||
const novels: Novel.Item[] = [];
|
||||
|
||||
body.results.forEach((item) =>
|
||||
novels.push({
|
||||
url: item.link,
|
||||
name: item.original_title,
|
||||
cover: item.image,
|
||||
})
|
||||
);
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
export const fetchImage: Plugin.fetchImage = async function (url) {
|
||||
return await fetchFile(url, {});
|
||||
};
|
||||
|
||||
interface ResultsEntity {
|
||||
overview: string;
|
||||
original_title: string;
|
||||
link: string;
|
||||
image: string;
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { Filters } from "@libs/filterInputs";
|
||||
|
||||
class NovelFull implements Plugin.PluginBase {
|
||||
id = "NF.me";
|
||||
name = "NovelFullMe";
|
||||
site = "https://novelfull.me/";
|
||||
version = "1.0.0";
|
||||
filters?: Filters | undefined;
|
||||
icon = "src/en/novelfullme/icon.png";
|
||||
baseUrl = this.site;
|
||||
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}popular?page=${pageNo}`;
|
||||
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".book-item").each((idx, ele) => {
|
||||
const novelName = loadedCheerio(ele).find(".title").text();
|
||||
const novelCover =
|
||||
"https:" + loadedCheerio(ele).find("img").attr("data-src");
|
||||
const novelUrl =
|
||||
this.baseUrl +
|
||||
loadedCheerio(ele).find(".title a").attr("href")?.substring(1);
|
||||
|
||||
const novel = { name: novelName, cover: novelCover, url: novelUrl };
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
let novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
name: "",
|
||||
cover: "",
|
||||
author: "",
|
||||
status: "",
|
||||
genres: "",
|
||||
summary: "",
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio(".name h1").text().trim();
|
||||
|
||||
novel.cover =
|
||||
"https:" + loadedCheerio(".img-cover img").attr("data-src");
|
||||
|
||||
novel.summary = loadedCheerio(
|
||||
"body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.mt-1 > div.section.box.mt-1.summary > div.section-body > p.content"
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
novel.author = "Unknown";
|
||||
|
||||
novel.status = loadedCheerio(
|
||||
"body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(1) > a > span"
|
||||
)
|
||||
.text()
|
||||
?.trim();
|
||||
|
||||
novel.genres = loadedCheerio(
|
||||
"body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(2)"
|
||||
)
|
||||
.text()
|
||||
?.replace("Genres :", "")
|
||||
.replace(/[\s\n]+/g, " ")
|
||||
.trim();
|
||||
|
||||
let chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
const chaptersUrl =
|
||||
novelUrl.replace(this.baseUrl, "https://novelfull.me/api/novels/") +
|
||||
"/chapters?source=detail";
|
||||
|
||||
const chaptersRequest = await fetchApi(chaptersUrl);
|
||||
const chaptersHtml = await chaptersRequest.text();
|
||||
|
||||
loadedCheerio = parseHTML(chaptersHtml);
|
||||
|
||||
loadedCheerio("li").each(function () {
|
||||
const chapterName = loadedCheerio(this)
|
||||
.find(".chapter-title")
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
const releaseDate = loadedCheerio(this)
|
||||
.find(".chapter-update")
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
const chapterUrl = loadedCheerio(this).find("a").attr("href");
|
||||
|
||||
if (!chapterUrl) return;
|
||||
|
||||
chapters.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
});
|
||||
});
|
||||
|
||||
novel.chapters = chapters.reverse();
|
||||
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const url = chapterUrl;
|
||||
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
loadedCheerio("#listen-chapter").remove();
|
||||
loadedCheerio("#google_translate_element").remove();
|
||||
|
||||
const chapterText = loadedCheerio(".chapter__content").html() || '';
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}search?status=all&sort=views&q=${searchTerm}`;
|
||||
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".book-item").each((idx, ele) => {
|
||||
const novelName = loadedCheerio(ele).find(".title").text();
|
||||
const novelCover =
|
||||
"https:" + loadedCheerio(ele).find("img").attr("data-src");
|
||||
const novelUrl =
|
||||
this.baseUrl +
|
||||
loadedCheerio(ele).find(".title a").attr("href")?.substring(1);
|
||||
|
||||
const novel = { name: novelName, cover: novelCover, url: novelUrl };
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new NovelFull();
|
||||
+130
-42
@@ -1,50 +1,84 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { Filters } from "@libs/filterInputs";
|
||||
import { Filters, FilterTypes } from "@libs/filterInputs";
|
||||
|
||||
class NovelsOnline implements Plugin.PluginBase {
|
||||
id = "NO.net";
|
||||
name = "novelsOnline";
|
||||
site = "https://novelsonline.net";
|
||||
icon = "src/coverNotAvailable.jpg";
|
||||
filters?: Filters | undefined;
|
||||
icon = "src/en/novelsonline/icon.png";
|
||||
version = "1.0.0";
|
||||
|
||||
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
return []; /** TO DO */
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = this.site;
|
||||
if (filters.genre.value !== '')
|
||||
link += `/category/${filters.genre.value}/`;
|
||||
else
|
||||
link += `/top-novel/`;
|
||||
|
||||
link += page
|
||||
link += `?change_type=${filters.sort.value}`
|
||||
|
||||
const body = await fetchApi(link).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const novel: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio('.top-novel-block').each((i,el) => {
|
||||
const novelName = loadedCheerio(el).find('h2').text();
|
||||
const novelCover = loadedCheerio(el).find('.top-novel-cover img').attr('src');
|
||||
const novelUrl = loadedCheerio(el).find('h2 a').attr('href');
|
||||
if (!novelUrl) return;
|
||||
|
||||
novel.push({
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
path: novelUrl.replace(this.site, ''),
|
||||
})
|
||||
})
|
||||
return novel;
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
let novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(this.site + novelPath).then((res) => res.text());
|
||||
let $ = parseHTML(result);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: $("h1").text() || 'Untitled',
|
||||
cover: $(".novel-cover").find("a > img").attr("src"),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
const result = await fetchApi(novelUrl).then((res) => res.text());
|
||||
|
||||
let $ = parseHTML(result);
|
||||
$('.novel-detail-item').each((i,el) => {
|
||||
let detailName = $(el).find('h6').text();
|
||||
let detail = $(el).find('.novel-detail-body');
|
||||
|
||||
novel.name = $("h1").text();
|
||||
novel.cover = $(".novel-cover").find("a > img").attr("src");
|
||||
novel.author = $(
|
||||
"div.novel-details > div:nth-child(5) > div.novel-detail-body"
|
||||
)
|
||||
.find("li")
|
||||
.map((_, el) => $(el).text())
|
||||
.get()
|
||||
.join(", ");
|
||||
|
||||
novel.genres = $(
|
||||
"div.novel-details > div:nth-child(2) > div.novel-detail-body"
|
||||
)
|
||||
.find("li")
|
||||
.map((_, el) => $(el).text())
|
||||
.get()
|
||||
.join(",");
|
||||
|
||||
novel.summary = $(
|
||||
"div.novel-right > div > div:nth-child(1) > div.novel-detail-body"
|
||||
).text();
|
||||
switch(detailName){
|
||||
case "Description":
|
||||
novel.summary = detail.text();
|
||||
break;
|
||||
case "Genre":
|
||||
novel.genres = detail.find('li')
|
||||
.map((_, el) => $(el).text())
|
||||
.get()
|
||||
.join(', ');
|
||||
break;
|
||||
case "Author(s)":
|
||||
novel.author = detail.find('li')
|
||||
.map((_, el) => $(el).text())
|
||||
.get()
|
||||
.join(', ');
|
||||
break;
|
||||
case "Status":
|
||||
novel.status = detail.text().trim();
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
novel.chapters = $("ul.chapter-chs > li > a")
|
||||
.map((_, el) => {
|
||||
@@ -53,22 +87,23 @@ class NovelsOnline implements Plugin.PluginBase {
|
||||
|
||||
return {
|
||||
name: chapterName,
|
||||
releaseTime: "",
|
||||
url: chapterUrl,
|
||||
path: chapterUrl?.replace(this.site, ''),
|
||||
} as Plugin.ChapterItem;
|
||||
})
|
||||
.get();
|
||||
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(chapterUrl).then((res) => res.text());
|
||||
let loadedCheerio = parseHTML(result);
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(this.site + chapterPath).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(result);
|
||||
|
||||
const chapterText = loadedCheerio("#contentall").html() || "";
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const result = await fetchApi(
|
||||
"https://novelsonline.net/sResults.php",
|
||||
@@ -84,7 +119,7 @@ class NovelsOnline implements Plugin.PluginBase {
|
||||
},
|
||||
).then((res) => res.text());
|
||||
|
||||
let $ = parseHTML(result);
|
||||
const $ = parseHTML(result);
|
||||
|
||||
const headers = $("li");
|
||||
return headers
|
||||
@@ -93,23 +128,76 @@ class NovelsOnline implements Plugin.PluginBase {
|
||||
const novelUrl = $(h).find("a").attr("href");
|
||||
const novelCover = $(h).find("img").attr("src");
|
||||
|
||||
if (!novelUrl) {
|
||||
return null;
|
||||
}
|
||||
if (!novelUrl) return
|
||||
|
||||
return {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
path: novelUrl.replace(this.site, ''),
|
||||
};
|
||||
})
|
||||
.get()
|
||||
.filter((sr) => sr !== null)
|
||||
}
|
||||
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url);
|
||||
}
|
||||
|
||||
filters = {
|
||||
sort: {
|
||||
value: "top_rated",
|
||||
label: "Sort by",
|
||||
options: [
|
||||
{ label: "Top Rated", value: "top_rated" },
|
||||
{ label: "Most Viewed", value: "view" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
genre: {
|
||||
value: "",
|
||||
label: "Category",
|
||||
options: [
|
||||
{ label: "None", value: "" },
|
||||
{ label: "Action", value: "action" },
|
||||
{ label: "Adventure", value: "adventure" },
|
||||
{ label: "Celebrity", value: "celebrity" },
|
||||
{ label: "Comedy", value: "comedy" },
|
||||
{ label: "Drama", value: "drama" },
|
||||
{ label: "Ecchi", value: "ecchi" },
|
||||
{ label: "Fantasy", value: "fantasy" },
|
||||
{ label: "Gender Bender", value: "gender-bender" },
|
||||
{ label: "Harem", value: "harem" },
|
||||
{ label: "Historical", value: "historical" },
|
||||
{ label: "Horror", value: "horror" },
|
||||
{ label: "Josei", value: "josei" },
|
||||
{ label: "Martial Arts", value: "martial-arts" },
|
||||
{ label: "Mature", value: "mature" },
|
||||
{ label: "Mecha", value: "mecha" },
|
||||
{ label: "Mystery", value: "mystery" },
|
||||
{ label: "Psychological", value: "psychological" },
|
||||
{ label: "Romance", value: "romance" },
|
||||
{ label: "School Life", value: "school-life" },
|
||||
{ label: "Sci-fi", value: "sci-fi" },
|
||||
{ label: "Seinen", value: "seinen" },
|
||||
{ label: "Shotacon", value: "shotacon" },
|
||||
{ label: "Shoujo", value: "shoujo" },
|
||||
{ label: "Shoujo Ai", value: "shoujo-ai" },
|
||||
{ label: "Shounen", value: "shounen" },
|
||||
{ label: "Shounen Ai", value: "shounen-ai" },
|
||||
{ label: "Slice of Life", value: "slice-of-life" },
|
||||
{ label: "Sports", value: "sports" },
|
||||
{ label: "Supernatural", value: "supernatural" },
|
||||
{ label: "Tragedy", value: "tragedy" },
|
||||
{ label: "Wuxia", value: "wuxia" },
|
||||
{ label: "Xianxia", value: "xianxia" },
|
||||
{ label: "Xuanhuan", value: "xuanhuan" },
|
||||
{ label: "Yaoi", value: "yaoi" },
|
||||
{ label: "Yuri", value: "yuri" }
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new NovelsOnline();
|
||||
|
||||
@@ -15,23 +15,20 @@ class AllNovelFullPlugin implements Plugin.PluginBase {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".col-truyen-main .list-truyen .row").each((i,el) => {
|
||||
const novelUrl = loadedCheerio(el).find("h3.truyen-title > a").attr("href");
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novelName = loadedCheerio(el)
|
||||
.find("h3.truyen-title > a")
|
||||
.text();
|
||||
|
||||
let novelCover = loadedCheerio(el).find("img.cover").attr("src");
|
||||
let novelUrl = loadedCheerio(el).find("h3.truyen-title > a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
if (novelUrl && !isUrlAbsolute(novelUrl)) {
|
||||
novelUrl = this.site + novelUrl;
|
||||
}
|
||||
if (novelCover && !isUrlAbsolute(novelCover)){
|
||||
novelCover = this.site + novelCover;
|
||||
}
|
||||
|
||||
const novel = {
|
||||
url: novelUrl,
|
||||
path: novelUrl,
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
};
|
||||
@@ -53,41 +50,35 @@ class AllNovelFullPlugin implements Plugin.PluginBase {
|
||||
|
||||
link += `?page=${pageNo}`;
|
||||
|
||||
const headers = new Headers();
|
||||
const body = await fetchApi(link, { headers }).then((result) =>
|
||||
result.text()
|
||||
);
|
||||
const result = await fetchApi(link);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(url, { headers });
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const url = this.site + novelPath;
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
path: novelPath,
|
||||
name: loadedCheerio(".book > img").attr("alt") || 'Untitled',
|
||||
cover: this.site + loadedCheerio(".book > img").attr("src"),
|
||||
summary: loadedCheerio(".desc-text").text().trim(),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio(".book > img").attr("alt");
|
||||
|
||||
novel.cover = this.site + loadedCheerio(".book > img").attr("src");
|
||||
|
||||
novel.summary = loadedCheerio(".desc-text").text().trim();
|
||||
|
||||
loadedCheerio(".info > div").each((i,el) => {
|
||||
const detailName = loadedCheerio(el).find("h3").text();
|
||||
const detail = loadedCheerio(el)
|
||||
.find('a')
|
||||
.map((a,ex) => loadedCheerio(ex).text())
|
||||
.toArray()
|
||||
.join(', ');
|
||||
.join(',');
|
||||
|
||||
switch (detailName) {
|
||||
case 'Author:':
|
||||
@@ -106,7 +97,7 @@ class AllNovelFullPlugin implements Plugin.PluginBase {
|
||||
const getChapters = async (id: string) => {
|
||||
const chaptersUrl = this.site + "/ajax/chapter-option?novelId=" + id;
|
||||
|
||||
const data = await fetchApi(chaptersUrl, { headers });
|
||||
const data = await fetchApi(chaptersUrl);
|
||||
const chapters = await data.text();
|
||||
|
||||
loadedCheerio = parseHTML(chapters);
|
||||
@@ -115,16 +106,13 @@ class AllNovelFullPlugin implements Plugin.PluginBase {
|
||||
|
||||
loadedCheerio("select > option").each((i,el) => {
|
||||
const chapterName = loadedCheerio(el).text();
|
||||
const releaseDate = null;
|
||||
const cUrl = loadedCheerio(el).attr("value");
|
||||
const chapterUrl = this.site + cUrl
|
||||
const chapterUrl = loadedCheerio(el).attr("value");
|
||||
|
||||
if (!cUrl) return;
|
||||
if (!chapterUrl ) return;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
path: chapterUrl,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -138,9 +126,8 @@ class AllNovelFullPlugin implements Plugin.PluginBase {
|
||||
return novel;
|
||||
};
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(chapterUrl, { headers });
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(this.site + chapterPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
@@ -152,11 +139,10 @@ class AllNovelFullPlugin implements Plugin.PluginBase {
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo: number
|
||||
page: number
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/search?keyword=${searchTerm}&page=${pageNo}`;
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(url, { headers });
|
||||
const url = `${this.site}/search?keyword=${searchTerm}&page=${page}`;
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
+202
-103
@@ -1,52 +1,34 @@
|
||||
import { load as cheerioload } from "cheerio";
|
||||
import { CheerioAPI, load as parseHTML} from "cheerio";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { defaultCover } from "@libs/defaultCover";
|
||||
import { fetchFile } from "@libs/fetch";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
|
||||
class BLN implements Plugin.PluginBase {
|
||||
id = "BLN.com";
|
||||
id = "BLN";
|
||||
name = "BestLightNovel";
|
||||
icon = "src/en/bestlightnovel/icon.png";
|
||||
site = "https://bestlightnovel.com/";
|
||||
filter?: undefined;
|
||||
version = "1.0.0";
|
||||
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
options: Plugin.PopularNovelsOptions
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url =
|
||||
this.site +
|
||||
"novel_list?type=topview&category=all&state=all&page=1" +
|
||||
|
||||
|
||||
const result = await fetch(url);
|
||||
if (!result.ok) {
|
||||
console.error(await result.text());
|
||||
// TODO: Cloudflare protection or other error
|
||||
return [];
|
||||
}
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerioload(body);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".update_item.list_category").each(function () {
|
||||
const novelName = loadedCheerio(this).find("h3 > a").text();
|
||||
const novelCover = loadedCheerio(this).find("img").attr("src");
|
||||
parseNovels(loadedCheerio: CheerioAPI) {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".update_item.list_category").each((i,el) => {
|
||||
const novelUrl = loadedCheerio(el).find("h3 > a").attr("href");
|
||||
if (!novelUrl) {
|
||||
// TODO: Handle error
|
||||
console.error("No novel url!");
|
||||
return;
|
||||
}
|
||||
|
||||
const novelName = loadedCheerio(el).find("h3 > a").text();
|
||||
const novelCover = loadedCheerio(el).find("img").attr("src");
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
name: novelName,
|
||||
path: novelUrl.replace(this.site, ''),
|
||||
cover: novelCover,
|
||||
};
|
||||
|
||||
@@ -55,126 +37,243 @@ class BLN implements Plugin.PluginBase {
|
||||
|
||||
return novels;
|
||||
}
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
|
||||
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = this.site + "novel_list?"
|
||||
link += 'type=' + filters.type.value;
|
||||
link += '&category=' + filters.category.value;
|
||||
link += '&state=' + filters.status.value;
|
||||
link += '&page=' + page;
|
||||
|
||||
const result = await fetch(link);
|
||||
if (!result.ok) {
|
||||
console.error(await result.text());
|
||||
console.error(await result.text());
|
||||
// TODO: Cloudflare protection
|
||||
// TODO: Cloudflare protection or other error
|
||||
return [];
|
||||
}
|
||||
const body = await result.text();
|
||||
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
|
||||
let novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
name: "",
|
||||
cover: "",
|
||||
author: "",
|
||||
status: NovelStatus.Unknown,
|
||||
genres: "",
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetch(this.site + novelPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: loadedCheerio(".truyen_info_right h1").text().trim() || "",
|
||||
cover: loadedCheerio(".info_image img").attr("src") || defaultCover,
|
||||
summary: loadedCheerio("#noidungm").text()?.trim() || "",
|
||||
chapters: [],
|
||||
};
|
||||
loadedCheerio('ul.truyen_info_right > li').each(function() {
|
||||
const detailName = loadedCheerio(this).find('span').text();
|
||||
const detail = loadedCheerio(this)
|
||||
.find('a')
|
||||
.map((a,ex) => loadedCheerio(ex).text())
|
||||
.toArray()
|
||||
.join(', ');
|
||||
|
||||
switch(detailName) {
|
||||
case "Author(s): ":
|
||||
novel.author = detail;
|
||||
break;
|
||||
case "GENRES: ":
|
||||
novel.genres = detail;
|
||||
break;
|
||||
case "STATUS : ":
|
||||
novel.status =
|
||||
detail === "Ongoing"
|
||||
? NovelStatus.Ongoing
|
||||
: detail === "Completed"
|
||||
? NovelStatus.Completed
|
||||
: NovelStatus.Unknown;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
novel.name = loadedCheerio(".truyen_info_right h1").text().trim();
|
||||
novel.cover =
|
||||
loadedCheerio(".info_image img").attr("src") || defaultCover;
|
||||
novel.summary = loadedCheerio("#noidungm").text()?.trim();
|
||||
novel.author = loadedCheerio(
|
||||
"#main_body > div.cotgiua > div.truyen_info_wrapper > div.truyen_info > div.entry-header > div.truyen_if_wrap > ul > li:nth-child(2) > a"
|
||||
)
|
||||
.text()
|
||||
const chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
|
||||
let status = loadedCheerio(
|
||||
"#main_body > div.cotgiua > div.truyen_info_wrapper > div.truyen_info > div.entry-header > div.truyen_if_wrap > ul > li:nth-child(4) > a"
|
||||
)
|
||||
.text()
|
||||
?.trim();
|
||||
|
||||
novel.status =
|
||||
status === "ONGOING"
|
||||
? NovelStatus.Ongoing
|
||||
: status === "COMPLETED"
|
||||
? NovelStatus.Completed
|
||||
: NovelStatus.Unknown;
|
||||
|
||||
let novelChapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio(".chapter-list div.row").each(function () {
|
||||
const chapterName = loadedCheerio(this).find("a").text().trim();
|
||||
const releaseDate = loadedCheerio(this)
|
||||
loadedCheerio(".chapter-list div.row").each((i, el) => {
|
||||
const chapterName = loadedCheerio(el).find("a").text().trim();
|
||||
const releaseDate = loadedCheerio(el)
|
||||
.find("span:last")
|
||||
.text()
|
||||
.trim();
|
||||
.trim();
|
||||
|
||||
const months = [
|
||||
"jan",
|
||||
"feb",
|
||||
"mar",
|
||||
"apr",
|
||||
"may",
|
||||
"jun",
|
||||
"jul",
|
||||
"aug",
|
||||
"sep",
|
||||
"oct",
|
||||
"nov",
|
||||
"dec",
|
||||
].join("|");
|
||||
const rx = new RegExp(`(${months})-(\\d{1,2})-(\\d{2})`,'i')
|
||||
.exec(releaseDate);
|
||||
if (!rx) return;
|
||||
const year = 2000 + +rx[3];
|
||||
const month = months.indexOf(rx[1].toLowerCase());
|
||||
const day = +rx[2];
|
||||
|
||||
const chapterUrl = loadedCheerio(el).find("a").attr("href");
|
||||
if (!chapterUrl) {
|
||||
// TODO: Handle error
|
||||
console.error("No chapter url!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
releaseTime: new Date(year, month, day).toISOString(),
|
||||
path: chapterUrl.replace(this.site, ''),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
novel.chapters = chapter.reverse();
|
||||
|
||||
return novel;
|
||||
}
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetch(this.site + chapterPath);
|
||||
const body = await result.text();
|
||||
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const chapterText = loadedCheerio("#vung_doc").html() || "";
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
searchTerm: string,
|
||||
page: number
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}search_novels/${searchTerm}?page=${page}`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
|
||||
let loadedCheerio = cheerioload(body);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".update_item.list_category").each(function () {
|
||||
const novelName = loadedCheerio(this).find("h3 > a").text();
|
||||
const novelCover = loadedCheerio(this).find("img").attr("src");
|
||||
const novelUrl = loadedCheerio(this).find("h3 > a").attr("href");
|
||||
|
||||
if (!novelUrl) {
|
||||
// TODO: Handle error
|
||||
console.error("No novel url!");
|
||||
return;
|
||||
}
|
||||
|
||||
const novel = { name: novelName, cover: novelCover, url: novelUrl };
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return await fetchFile(url);
|
||||
}
|
||||
|
||||
filters = {
|
||||
status: {
|
||||
label: "Status",
|
||||
value: "all",
|
||||
options: [
|
||||
{ label: "ALL", value: "all" },
|
||||
{ label: "Completed", value: "completed" },
|
||||
{ label: "Ongoing", value: "ongoing" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
type: {
|
||||
value: "topview",
|
||||
label: "Type",
|
||||
options: [
|
||||
{ label: "Recently updated", value: "latest" },
|
||||
{ label: "Newest", value: "newest" },
|
||||
{ label: "Top view", value: "topview" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
category: {
|
||||
label: "Category",
|
||||
value: "all",
|
||||
options: [
|
||||
{ label: "ALL", value: "all" },
|
||||
{ label: "Action", value: "1" },
|
||||
{ label: "Adventure", value: "2" },
|
||||
{ label: "Animals", value: "65" },
|
||||
{ label: "Arts", value: "40" },
|
||||
{ label: "Biographies", value: "41" },
|
||||
{ label: "Business", value: "42" },
|
||||
{ label: "Chinese", value: "3" },
|
||||
{ label: "Comedy", value: "4" },
|
||||
{ label: "Computers", value: "43" },
|
||||
{ label: "Crafts, Hobbies", value: "45" },
|
||||
{ label: "Drama", value: "5" },
|
||||
{ label: "Education", value: "46" },
|
||||
{ label: "English", value: "6" },
|
||||
{ label: "Entertainment", value: "47" },
|
||||
{ label: "Fantasy", value: "7" },
|
||||
{ label: "Fiction", value: "48" },
|
||||
{ label: "Gender Bender", value: "8" },
|
||||
{ label: "Harem", value: "9" },
|
||||
{ label: "Historical", value: "10" },
|
||||
{ label: "History", value: "49" },
|
||||
{ label: "Home", value: "50" },
|
||||
{ label: "Horror", value: "11" },
|
||||
{ label: "Humor", value: "51" },
|
||||
{ label: "Investing", value: "52" },
|
||||
{ label: "Josei", value: "12" },
|
||||
{ label: "Korean", value: "13" },
|
||||
{ label: "Literature", value: "53" },
|
||||
{ label: "Lolicon", value: "14" },
|
||||
{ label: "Martial Arts", value: "15" },
|
||||
{ label: "Mature", value: "16" },
|
||||
{ label: "Mecha", value: "17" },
|
||||
{ label: "Memoirs", value: "54" },
|
||||
{ label: "Mystery", value: "18" },
|
||||
{ label: "Original", value: "19" },
|
||||
{ label: "Other Books", value: "66" },
|
||||
{ label: "Philosophy", value: "55" },
|
||||
{ label: "Photography", value: "56" },
|
||||
{ label: "Politics", value: "57" },
|
||||
{ label: "Professional", value: "58" },
|
||||
{ label: "Psychological", value: "20" },
|
||||
{ label: "Reference", value: "59" },
|
||||
{ label: "Reincarnation", value: "21" },
|
||||
{ label: "Religion", value: "60" },
|
||||
{ label: "Romance", value: "22" },
|
||||
{ label: "School Life", value: "23" },
|
||||
{ label: "School Stories", value: "67" },
|
||||
{ label: "Sci-Fi", value: "24" },
|
||||
{ label: "Seinen", value: "25" },
|
||||
{ label: "Short Stories", value: "68" },
|
||||
{ label: "Shotacon", value: "26" },
|
||||
{ label: "Shoujo", value: "27" },
|
||||
{ label: "Shoujo Ai", value: "28" },
|
||||
{ label: "Shounen", value: "29" },
|
||||
{ label: "Shounen Ai", value: "30" },
|
||||
{ label: "Slice Of Life", value: "31" },
|
||||
{ label: "Smut", value: "32" },
|
||||
{ label: "Social Science", value: "61" },
|
||||
{ label: "Spirituality", value: "62" },
|
||||
{ label: "Sports", value: "33" },
|
||||
{ label: "Supernatural", value: "34" },
|
||||
{ label: "Teaser", value: "69" },
|
||||
{ label: "Technical", value: "63" },
|
||||
{ label: "Technology", value: "64" },
|
||||
{ label: "Tragedy", value: "35" },
|
||||
{ label: "Virtual Reality", value: "36" },
|
||||
{ label: "Wuxia", value: "37" },
|
||||
{ label: "Xianxia", value: "38" },
|
||||
{ label: "Xuanhuan", value: "39" },
|
||||
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new BLN();
|
||||
|
||||
@@ -10,23 +10,24 @@ class DDLPlugin implements Plugin.PluginBase {
|
||||
site = "https://www.divinedaolibrary.com/";
|
||||
version = "1.0.0";
|
||||
icon = "src/en/divinedaolibrary/icon.png";
|
||||
filters?: undefined;
|
||||
|
||||
parseNovels(loadedCheerio: CheerioAPI, searchTerm?: string){
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio("#main")
|
||||
.find("li")
|
||||
.each(function () {
|
||||
const novelName = loadedCheerio(this).find("a").text();
|
||||
.each((i,el) => {
|
||||
const novelName = loadedCheerio(el).find("a").text();
|
||||
const novelCover = defaultCover;
|
||||
const novelUrl = loadedCheerio(this).find(" a").attr("href");
|
||||
const novelUrl = loadedCheerio(el).find(" a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
path: novelUrl.replace(this.site, ''),
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
@@ -42,38 +43,31 @@ class DDLPlugin implements Plugin.PluginBase {
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions
|
||||
options: Plugin.PopularNovelsOptions
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = this.site + "novels";
|
||||
|
||||
const headers = new Headers();
|
||||
const body = await fetchApi(link, { headers }).then((result) =>
|
||||
result.text()
|
||||
);
|
||||
const body = await fetchApi(link).then((res) => res.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(url, { headers });
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(this.site + novelPath);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
path: novelPath,
|
||||
name: loadedCheerio("h1.entry-title").text().trim() || "Untitled",
|
||||
cover:
|
||||
loadedCheerio(".entry-content").find("img").attr("data-ezsrc") ||
|
||||
defaultCover,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio("h1.entry-title").text().trim();
|
||||
|
||||
novel.cover =
|
||||
loadedCheerio(".entry-content").find("img").attr("data-ezsrc") ||
|
||||
defaultCover;
|
||||
|
||||
novel.summary = loadedCheerio("#main > article > div > p:nth-child(6)")
|
||||
.text()
|
||||
.trim();
|
||||
@@ -87,17 +81,15 @@ class DDLPlugin implements Plugin.PluginBase {
|
||||
|
||||
loadedCheerio("#main")
|
||||
.find("li > span > a")
|
||||
.each(function () {
|
||||
const chapterName = loadedCheerio(this).text().trim();
|
||||
const releaseDate = null;
|
||||
const chapterUrl = loadedCheerio(this).attr("href");
|
||||
.each((i, el) => {
|
||||
const chapterName = loadedCheerio(el).text().trim();
|
||||
const chapterUrl = loadedCheerio(el).attr("href");
|
||||
|
||||
if (!chapterUrl) return;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
path: chapterUrl.replace(this.site, ''),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,9 +98,8 @@ class DDLPlugin implements Plugin.PluginBase {
|
||||
return novel;
|
||||
};
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(chapterUrl, { headers });
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(this.site + chapterPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
@@ -131,9 +122,8 @@ class DDLPlugin implements Plugin.PluginBase {
|
||||
pageNo: number
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + "novels";
|
||||
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(url, { headers });
|
||||
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
@@ -2,9 +2,8 @@ import { CheerioAPI, load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { defaultCover } from "@libs/defaultCover";
|
||||
|
||||
class EarlyNovelPlugin implements Plugin.PluginBase {
|
||||
class EarlyNovelPlugin implements Plugin.PagePlugin {
|
||||
id = "earlynovel";
|
||||
name = "Early Novel";
|
||||
version = "1.0.0";
|
||||
@@ -16,27 +15,43 @@ class EarlyNovelPlugin implements Plugin.PluginBase {
|
||||
|
||||
loadedCheerio(".col-truyen-main > .list-truyen > .row").each(
|
||||
(i, el) => {
|
||||
const nUrl = loadedCheerio(el).find("h3.truyen-title > a").attr("href");
|
||||
|
||||
const novelUrl = this.site + nUrl
|
||||
const novelUrl = loadedCheerio(el).find("h3.truyen-title > a").attr("href");
|
||||
const novelName = loadedCheerio(el)
|
||||
.find("h3.truyen-title > a")
|
||||
.text();
|
||||
const novelCover = loadedCheerio(el).find("img.cover").attr("src");
|
||||
console.log(novelCover);
|
||||
const novelCover = loadedCheerio(el).find('.lazyimg').attr("data-image");
|
||||
|
||||
if (!nUrl) return;
|
||||
if (!novelUrl) return;
|
||||
novels.push({
|
||||
url: novelUrl,
|
||||
path: novelUrl,
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
parseChapters(loadedCheerio:CheerioAPI){
|
||||
const chapter: Plugin.ChapterItem[] = [];
|
||||
loadedCheerio("ul.list-chapter > li").each((i,el) => {
|
||||
const chapterName = loadedCheerio(el)
|
||||
.find(".chapter-text")
|
||||
.text()
|
||||
.trim();
|
||||
const chapterUrl = loadedCheerio(el)
|
||||
.find("a")
|
||||
.attr("href")
|
||||
?.slice(1);
|
||||
if (!chapterUrl) return;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
path: chapterUrl
|
||||
})});
|
||||
return chapter;
|
||||
}
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
|
||||
@@ -50,33 +65,32 @@ class EarlyNovelPlugin implements Plugin.PluginBase {
|
||||
|
||||
link += `?page=${pageNo}`;
|
||||
|
||||
const headers = new Headers();
|
||||
const body = await fetchApi(link, { headers }).then((result) =>
|
||||
result.text()
|
||||
);
|
||||
const body = await fetchApi(link).then((res) => res.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
console.log(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(url, { headers });
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
|
||||
const result = await fetchApi(this.site + novelPath);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
loadedCheerio('.glyphicon-menu-right').closest('li').remove();
|
||||
const pagenav = loadedCheerio('.page-nav').prev().find('a');
|
||||
const lastPageStr = pagenav.attr('title')?.match(/(\d+)/);
|
||||
const lastPage = Number(lastPageStr?.[1] || "0");
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
const novel: Plugin.SourceNovel & {totalPages: number}= {
|
||||
path: novelPath,
|
||||
name: loadedCheerio(".book > img").attr("alt") || "Untitled",
|
||||
cover: loadedCheerio(".book > img").attr("src"),
|
||||
summary: loadedCheerio(".desc-text").text().trim(),
|
||||
chapters: [],
|
||||
totalPages: lastPage
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio(".book > img").attr("alt");
|
||||
|
||||
novel.cover = loadedCheerio(".book > img").attr("src");
|
||||
|
||||
novel.summary = loadedCheerio(".desc-text").text().trim();
|
||||
|
||||
loadedCheerio(".info > div > h3").each(function () {
|
||||
let detailName = loadedCheerio(this).text();
|
||||
let detail = loadedCheerio(this)
|
||||
@@ -98,58 +112,20 @@ class EarlyNovelPlugin implements Plugin.PluginBase {
|
||||
}
|
||||
});
|
||||
|
||||
//! Doesn't work since there are multiple pages and can't find source API
|
||||
//# Since cannot find sourceAPI i try similar function to lightnovelpub
|
||||
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
|
||||
const lastPageStr = loadedCheerio('a:contains("Last ")')
|
||||
.attr("title")
|
||||
?.match(/(\d+)/g);
|
||||
const lastPage = Number(lastPageStr?.[1] || "0");
|
||||
|
||||
const getChapters = async () => {
|
||||
const chapter: Plugin.ChapterItem[] = [];
|
||||
for (let i = 1; i <= lastPage; i++) {
|
||||
const chaptersUrl = `${novelUrl}?page=${i}`;
|
||||
const data = await fetchApi(chaptersUrl, { headers });
|
||||
const chapters = await data.text();
|
||||
|
||||
loadedCheerio = parseHTML(chapters);
|
||||
|
||||
loadedCheerio("ul.list-chapter > li").each((i,el) => {
|
||||
const chapterName = loadedCheerio(el)
|
||||
.find(".chapter-text")
|
||||
.text()
|
||||
.trim();
|
||||
const releaseDate = null;
|
||||
const cUrl = loadedCheerio(el)
|
||||
.find("a")
|
||||
.attr("href")
|
||||
?.slice(1);
|
||||
if (!cUrl) return;
|
||||
const chapterUrl = this.site + cUrl;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
});
|
||||
});
|
||||
|
||||
await delay(1000);
|
||||
}
|
||||
|
||||
return chapter;
|
||||
}
|
||||
|
||||
novel.chapters = await getChapters();
|
||||
|
||||
novel.chapters = this.parseChapters(loadedCheerio);
|
||||
return novel;
|
||||
};
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(chapterUrl, { headers });
|
||||
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
||||
const url = this.site + novelPath + '?page=' + page;
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const chapters = this.parseChapters(loadedCheerio);
|
||||
return {chapters}
|
||||
}
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(this.site + chapterPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
@@ -164,8 +140,7 @@ class EarlyNovelPlugin implements Plugin.PluginBase {
|
||||
pageNo: number
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}search?keyword=${searchTerm}`;
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(url, { headers });
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
@@ -14,37 +14,39 @@ class FaqWikiUs implements Plugin.PluginBase {
|
||||
page: number,
|
||||
{ showLatestNovels }: Plugin.PopularNovelsOptions,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const sort = showLatestNovels
|
||||
? ""
|
||||
: "";
|
||||
|
||||
const body = await fetchApi(this.site + sort).then((res) => res.text());
|
||||
|
||||
const body = await fetchApi(this.site).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = loadedCheerio(".wp-block-table > table > tbody > tr")
|
||||
.map((index, element) => {
|
||||
const name = loadedCheerio(element).find('a').text().replace(/–\s+Chapter\s+List\s+–.*$/i,'');
|
||||
let cover = loadedCheerio(element).find("img").attr("data-ezsrc");
|
||||
.map((index, element) => {
|
||||
const name = loadedCheerio(element).find('a').text().replace(/–\s+Chapter\s+List\s+–.*$/i,'');
|
||||
let cover = loadedCheerio(element).find("img").attr("data-ezsrc");
|
||||
|
||||
// Remove the appended query string
|
||||
if (cover) {
|
||||
cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic!
|
||||
}
|
||||
// Remove the appended query string
|
||||
if (cover) {
|
||||
cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic!
|
||||
}
|
||||
|
||||
const url = "" + loadedCheerio(element).find("a").attr("href");
|
||||
const path = "" + loadedCheerio(element).find("a").attr("href")?.toString().replace(this.site, "").trim();
|
||||
|
||||
return { name, cover, url };
|
||||
})
|
||||
.get();
|
||||
|
||||
return { name, cover, path };
|
||||
})
|
||||
.get()
|
||||
.filter((novel) => novel.name && novel.path);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(url: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
async parseNovel(path: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(this.site + path).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = { url };
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path,
|
||||
name: ""
|
||||
};
|
||||
novel.name = loadedCheerio(".entry-title").text().replace("Novel – All Chapters", "").trim();
|
||||
|
||||
let cover = loadedCheerio(".wp-block-image").find("img").attr("data-ezsrc");
|
||||
@@ -60,7 +62,7 @@ class FaqWikiUs implements Plugin.PluginBase {
|
||||
const chapters: Plugin.ChapterItem[] = loadedCheerio(".lcp_catlist > li > a")
|
||||
.map((chapterIndex, element) => ({
|
||||
name: loadedCheerio(element).text().replace(novel.name + "", "").replace("Novel" + "", "").trim(),
|
||||
url: ""+loadedCheerio(element).attr("href"),
|
||||
path: ""+loadedCheerio(element).attr("href")?.toString().replace(this.site, "").trim(),
|
||||
releaseTime: null,
|
||||
chapterNumber: chapterIndex + 1,
|
||||
}))
|
||||
@@ -70,8 +72,8 @@ class FaqWikiUs implements Plugin.PluginBase {
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const body = await fetchApi(chapterUrl).then((res) => res.text());
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const body = await fetchApi(this.site + chapterPath).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
loadedCheerio("span").remove();
|
||||
|
||||
@@ -85,8 +87,8 @@ class FaqWikiUs implements Plugin.PluginBase {
|
||||
return text;
|
||||
}).get().join('\n\n');
|
||||
}
|
||||
|
||||
else {
|
||||
|
||||
else {
|
||||
// Multiple paragraphs case
|
||||
chapterContent = chapterParagraphs.map((index, element) => {
|
||||
const text = loadedCheerio(element).text().trim();
|
||||
@@ -95,33 +97,30 @@ class FaqWikiUs implements Plugin.PluginBase {
|
||||
}
|
||||
|
||||
return chapterContent;
|
||||
}
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
const body = await fetchApi(this.site).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = loadedCheerio(".wp-block-table > table > tbody > tr")
|
||||
.map((index, element) => {
|
||||
const name = loadedCheerio(element).find('a').text().replace(/–\s+Chapter\s+List\s+–.*$/i,'');
|
||||
let cover = loadedCheerio(element).find("img").attr("data-ezsrc");
|
||||
.map((index, element) => {
|
||||
const name = loadedCheerio(element).find('a').text().replace(/–\s+Chapter\s+List\s+–.*$/i,'');
|
||||
let cover = loadedCheerio(element).find("img").attr("data-ezsrc");
|
||||
|
||||
// Remove the appended query string
|
||||
if (cover) {
|
||||
cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic!
|
||||
}
|
||||
// Remove the appended query string
|
||||
if (cover) {
|
||||
cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic!
|
||||
}
|
||||
|
||||
const url = "" + loadedCheerio(element).find("a").attr("href");
|
||||
const path = "" + loadedCheerio(element).find("a").attr("href")?.toString().replace(this.site, "").trim();
|
||||
|
||||
return { name, cover, url };
|
||||
})
|
||||
.get();
|
||||
|
||||
const normalizedQuery = searchTerm.toLowerCase();
|
||||
return novels.filter(novel => novel.name.toLowerCase().includes(normalizedQuery)
|
||||
);
|
||||
return { name, cover, path };
|
||||
})
|
||||
.get().filter((novel) => novel.name && novel.path);
|
||||
const normalizedQuery = searchTerm.toLowerCase();
|
||||
return novels.filter(novel => novel.name.toLowerCase().includes(normalizedQuery));
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { isUrlAbsolute } from "@libs/isAbsoluteUrl";
|
||||
import { Novel, Plugin } from "@typings/plugin";
|
||||
|
||||
export const id = "freenovelupdates";
|
||||
export const name = "Free Novel Updates (Broken)";
|
||||
export const version = "1.0.0";
|
||||
export const icon = "src/en/freenovelupdates/icon.png";
|
||||
export const site = "https://www.freenovelupdates.com";
|
||||
|
||||
|
||||
const pluginId = id;
|
||||
const baseUrl = site;
|
||||
|
||||
export const popularNovels: Plugin.popularNovels = async function (page) {
|
||||
const url = `${baseUrl}genres/light-novel-1002`;
|
||||
|
||||
const body = await fetchApi(url, {}, pluginId).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Novel.Item[] = [];
|
||||
|
||||
loadedCheerio(".books-item").each(function () {
|
||||
let novelUrl = loadedCheerio(this).find("a").attr("href");
|
||||
|
||||
if (novelUrl && !isUrlAbsolute(novelUrl)) {
|
||||
novelUrl = baseUrl + novelUrl;
|
||||
}
|
||||
|
||||
if (novelUrl) {
|
||||
const novelName = loadedCheerio(this).find(".title").text().trim();
|
||||
let novelCover = loadedCheerio(this).find("img").attr("src");
|
||||
|
||||
if (novelCover && !isUrlAbsolute(novelCover)) {
|
||||
novelCover = baseUrl + novelCover;
|
||||
}
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
}
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
|
||||
async function (novelUrl) {
|
||||
const url = novelUrl;
|
||||
|
||||
const body = await fetchApi(url, {}, pluginId).then((r) => r.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Novel.instance = {
|
||||
url,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio("h1").text();
|
||||
|
||||
let novelCover = loadedCheerio(".img > img").attr("src");
|
||||
|
||||
novel.cover = novelCover
|
||||
? isUrlAbsolute(novelCover)
|
||||
? novelCover
|
||||
: baseUrl + novelCover
|
||||
: undefined;
|
||||
|
||||
novel.summary = loadedCheerio(".description-content").text().trim();
|
||||
|
||||
novel.author = loadedCheerio(".author").text().trim();
|
||||
|
||||
novel.genres = loadedCheerio(".category").text().trim();
|
||||
|
||||
novel.status = loadedCheerio(".status").text().trim();
|
||||
|
||||
loadedCheerio(".chapter").each(function () {
|
||||
let chapterUrl = loadedCheerio(this).find("a").attr("href");
|
||||
|
||||
if (chapterUrl && !isUrlAbsolute(chapterUrl)) {
|
||||
chapterUrl = baseUrl + chapterUrl;
|
||||
}
|
||||
|
||||
if (chapterUrl) {
|
||||
const chapterName = loadedCheerio(this).find("a").text().trim();
|
||||
const releaseDate = null;
|
||||
|
||||
const chapter = {
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
};
|
||||
|
||||
novel.chapters?.push(chapter);
|
||||
}
|
||||
});
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
|
||||
const body = await fetchApi(chapterUrl, {}, pluginId).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const chapterText = loadedCheerio(".content").html() || "";
|
||||
|
||||
return chapterText;
|
||||
};
|
||||
|
||||
export const searchNovels: Plugin.searchNovels = async function (searchTerm) {
|
||||
return [];
|
||||
};
|
||||
|
||||
export const fetchImage: Plugin.fetchImage = async function (url) {
|
||||
const headers = {
|
||||
Referer: baseUrl,
|
||||
};
|
||||
return await fetchFile(url, { headers: headers });
|
||||
};
|
||||
@@ -23,22 +23,27 @@ class FreeWebNovel implements Plugin.PluginBase {
|
||||
|
||||
const novels: Plugin.NovelItem[] = loadedCheerio(".li-row")
|
||||
.map((index, element) => ({
|
||||
name: loadedCheerio(element).find(".tit").text(),
|
||||
name: loadedCheerio(element).find(".tit").text() || "",
|
||||
cover: loadedCheerio(element).find("img").attr("src"),
|
||||
url: this.site + loadedCheerio(element).find("h3 > a").attr("href"),
|
||||
path: loadedCheerio(element).find("h3 > a").attr("href") || "",
|
||||
}))
|
||||
.get();
|
||||
.get()
|
||||
.filter((novel) => novel.name && novel.path);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(url: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(this.site + novelPath).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = { url };
|
||||
novel.name = loadedCheerio("h1.tit").text();
|
||||
novel.cover = loadedCheerio(".pic > img").attr("src");
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: loadedCheerio("h1.tit").text(),
|
||||
cover: loadedCheerio(".pic > img").attr("src"),
|
||||
summary: loadedCheerio(".inner").text().trim(),
|
||||
};
|
||||
|
||||
novel.genres = loadedCheerio("[title=Genre]")
|
||||
.next()
|
||||
.text()
|
||||
@@ -54,12 +59,14 @@ class FreeWebNovel implements Plugin.PluginBase {
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "");
|
||||
|
||||
novel.summary = loadedCheerio(".inner").text().trim();
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = loadedCheerio("#idData > li > a")
|
||||
.map((chapterIndex, element) => ({
|
||||
name: loadedCheerio(element).attr("title") || "Chapter " + (chapterIndex + 1),
|
||||
url: this.site + loadedCheerio(element).attr("href"),
|
||||
name:
|
||||
loadedCheerio(element).attr("title") ||
|
||||
"Chapter " + (chapterIndex + 1),
|
||||
path:
|
||||
loadedCheerio(element).attr("href") ||
|
||||
novelPath.replace(".html", "/chapter-" + (chapterIndex + 1) + ".html"),
|
||||
releaseTime: null,
|
||||
chapterNumber: chapterIndex + 1,
|
||||
}))
|
||||
@@ -69,17 +76,15 @@ class FreeWebNovel implements Plugin.PluginBase {
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const body = await fetchApi(chapterUrl).then((res) => res.text());
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const body = await fetchApi(this.site + chapterPath).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const chapterText = loadedCheerio("div.txt").html() || "";
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
const body = await fetchApi(this.site + "/search/", {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
@@ -87,17 +92,18 @@ class FreeWebNovel implements Plugin.PluginBase {
|
||||
Origin: this.site,
|
||||
},
|
||||
method: "POST",
|
||||
body: "searchkey=" + encodeURIComponent(searchTerm)
|
||||
body: "searchkey=" + encodeURIComponent(searchTerm),
|
||||
}).then((res) => res.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const novels: Plugin.NovelItem[] = loadedCheerio(".li-row > .li > .con")
|
||||
.map((index, element) => ({
|
||||
name: loadedCheerio(element).find(".tit").text(),
|
||||
name: loadedCheerio(element).find(".tit").text() || "",
|
||||
cover: loadedCheerio(element).find(".pic > a > img").attr("src"),
|
||||
url: this.site + loadedCheerio(element).find("h3 > a").attr("href"),
|
||||
path: loadedCheerio(element).find("h3 > a").attr("href") || "",
|
||||
}))
|
||||
.get();
|
||||
.get()
|
||||
.filter((novel) => novel.name && novel.path);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
import { CheerioAPI, load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import dayjs from "dayjs";
|
||||
class Genesis implements Plugin.PluginBase {
|
||||
id = "genesistls";
|
||||
name = "GenesisTLs";
|
||||
icon = "src/en/genesistls/icon.png";
|
||||
site = "https://genesistls.com/";
|
||||
version = "1.0.0";
|
||||
parseNovels(loadedCheerio: CheerioAPI) {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio("article.bs").each(function () {
|
||||
const novelName = loadedCheerio(this).find(".ntitle").text().trim();
|
||||
let image = loadedCheerio(this).find("img");
|
||||
const novelCover = image.attr("data-src") || image.attr("src");
|
||||
const novelUrl = loadedCheerio(this).find("a").attr("href");
|
||||
if (!novelUrl) return;
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
};
|
||||
novels.push(novel);
|
||||
});
|
||||
return novels;
|
||||
}
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = `${this.site}series/?page=${pageNo}`;
|
||||
if (filters.genres.value.length)
|
||||
link += filters.genres.value.map((i) => `&genre[]=${i}`).join("");
|
||||
if (filters.type.value.length)
|
||||
link += filters.type.value.map((i) => `&type[]=${i}`).join("");
|
||||
link += "&status=" + filters.status.value;
|
||||
link += "&order=" + filters.order.value;
|
||||
const body = await fetchApi(link).then((result) =>
|
||||
result.text()
|
||||
);
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
let loadedCheerio = parseHTML(body);
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
chapters: [],
|
||||
};
|
||||
novel.name = loadedCheerio(".entry-title").text();
|
||||
novel.cover =
|
||||
loadedCheerio("img.wp-post-image").attr("data-src") ||
|
||||
loadedCheerio("img.wp-post-image").attr("src");
|
||||
loadedCheerio("div.spe > span").each(function () {
|
||||
const detailName = loadedCheerio(this).find("b").text().trim();
|
||||
const detail = loadedCheerio(this)
|
||||
.find("b")
|
||||
.remove()
|
||||
.end()
|
||||
.text()
|
||||
.trim();
|
||||
switch (detailName) {
|
||||
case "Author:":
|
||||
novel.author = detail;
|
||||
break;
|
||||
case "Status:":
|
||||
novel.status = detail;
|
||||
break;
|
||||
}
|
||||
});
|
||||
novel.genres = loadedCheerio(".genxed")
|
||||
.text()
|
||||
.trim()
|
||||
.replace(/\s/g, ",");
|
||||
loadedCheerio('div[itemprop="description"] h3,p.a,strong').remove();
|
||||
novel.summary = loadedCheerio('div[itemprop="description"]')
|
||||
.find("br")
|
||||
.replaceWith("\n")
|
||||
.end()
|
||||
.text();
|
||||
let chapter: Plugin.ChapterItem[] = [];
|
||||
loadedCheerio(".eplister")
|
||||
.find("li")
|
||||
.each(function () {
|
||||
const chapterUrl = loadedCheerio(this).find("a").attr("href");
|
||||
if (!chapterUrl) return;
|
||||
const chapterpaid = loadedCheerio(this).find('.epl-price').text();
|
||||
if (chapterpaid !== "Free") return
|
||||
const chapterName =
|
||||
loadedCheerio(this).find(".epl-num").text() +
|
||||
" - " +
|
||||
loadedCheerio(this).find(".epl-title").text();
|
||||
const releaseDate = loadedCheerio(this)
|
||||
.find(".epl-date")
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
|
||||
if (!releaseDate) return;
|
||||
const months = [
|
||||
"jan",
|
||||
"feb",
|
||||
"mar",
|
||||
"apr",
|
||||
"may",
|
||||
"jun",
|
||||
"jul",
|
||||
"aug",
|
||||
"sep",
|
||||
"oct",
|
||||
"nov",
|
||||
"dec",
|
||||
];
|
||||
const rx = new RegExp(
|
||||
`(${months.join("|")}).*? (\\d{1,2}).*?(\\d{4})`,
|
||||
"i"
|
||||
).exec(releaseDate);
|
||||
if (!rx) return;
|
||||
const year = +rx[3];
|
||||
const month = months.indexOf(rx[1].toLowerCase());
|
||||
const day = +rx[2];
|
||||
|
||||
if (month < 0) return;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: new Date(year, month, day).toISOString(),
|
||||
url: chapterUrl,
|
||||
});
|
||||
|
||||
});
|
||||
novel.chapters = chapter.reverse();
|
||||
return novel;
|
||||
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(chapterUrl);
|
||||
const body = await result.text();
|
||||
const loadedCheerio = parseHTML(body);
|
||||
let chapterText = loadedCheerio(".epcontent").html() || "";
|
||||
return chapterText;
|
||||
}
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo: number
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}page/${pageNo}/?s=${searchTerm}`;
|
||||
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return await fetchFile(url);
|
||||
}
|
||||
filters = {
|
||||
order: {
|
||||
label: "Sort By",
|
||||
options: [
|
||||
{ label: "Default", value: "" },
|
||||
{ label: "A-Z", value: "title" },
|
||||
{ label: "Z-A", value: "titlereverse" },
|
||||
{ label: "Latest Update", value: "update" },
|
||||
{ label: "Latest Added", value: "latest" },
|
||||
{ label: "Popular", value: "popular" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
value: "",
|
||||
},
|
||||
status: {
|
||||
label: "Status",
|
||||
options: [
|
||||
{ label: "All", value: "" },
|
||||
{ label: "Ongoing", value: "ongoing" },
|
||||
{ label: "Hiatus", value: "hiatus" },
|
||||
{ label: "Completed", value: "completed" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
value: "",
|
||||
},
|
||||
type: {
|
||||
label: "Type",
|
||||
options: [
|
||||
{ label: "korean novel", value: "korean-novel" },
|
||||
{ label: "Web Novel", value: "web-novel" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
value: [],
|
||||
},
|
||||
genres: {
|
||||
label: "Genres",
|
||||
options: [
|
||||
|
||||
{ label: "Academy", value: "academy" },
|
||||
{ label: "Action", value: "action" },
|
||||
{ label: "Adult", value: "adult" },
|
||||
{ label: "Adventure", value: "adventure" },
|
||||
{ label: "Another World", value: "another-world" },
|
||||
{ label: "Comdey", value: "comdey" },
|
||||
{ label: "Comedy", value: "comedy" },
|
||||
{ label: "Dark Fantasy", value: "dark-fantasy" },
|
||||
{ label: "Drama", value: "drama" },
|
||||
{ label: "Fantasy", value: "fantasy" },
|
||||
{ label: "Fantasy Fusion", value: "fantasy-fusion" },
|
||||
{ label: "Harem", value: "harem" },
|
||||
{ label: "Historical", value: "historical" },
|
||||
{ label: "Horror", value: "horror" },
|
||||
{ label: "Hunter", value: "hunter" },
|
||||
{ label: "Light Novel", value: "light-novel" },
|
||||
{ label: "Martial Arts", value: "martial-arts" },
|
||||
{ label: "Mature", value: "mature" },
|
||||
{ label: "Misunderstanding", value: "misunderstanding" },
|
||||
{ label: "Modern", value: "modern" },
|
||||
{ label: "Munchkin", value: "munchkin" },
|
||||
{ label: "Murim", value: "murim" },
|
||||
{ label: "mystery", value: "mystery" },
|
||||
{ label: "No Harem", value: "no-harem" },
|
||||
{ label: "NO NTR", value: "no-ntr" },
|
||||
{ label: "obsession", value: "obsession" },
|
||||
{ label: "Possession", value: "possession" },
|
||||
{ label: "Psychological", value: "psychological" },
|
||||
{ label: "Regression", value: "regression" },
|
||||
{ label: "Regret", value: "regret" },
|
||||
{ label: "reincarnation", value: "reincarnation" },
|
||||
{ label: "Romance", value: "romance" },
|
||||
{ label: "School Life", value: "school-life" },
|
||||
{ label: "Seinen", value: "seinen" },
|
||||
{ label: "Slice of life", value: "slice-of-life" },
|
||||
{ label: "Supernatural", value: "supernatural" },
|
||||
{ label: "Tragedy", value: "tragedy" },
|
||||
{ label: "Transmigrated to Game", value: "transmigrated-to-game" },
|
||||
{ label: "Transmigration", value: "transmigration" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
value: [],
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
export default new Genesis();
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { Filters } from "@libs/filterInputs";
|
||||
import { Filters, FilterTypes } from "@libs/filterInputs";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
|
||||
class LightNovelPub implements Plugin.PluginBase {
|
||||
class LightNovelPub implements Plugin.PagePlugin {
|
||||
id = "lightnovelpub";
|
||||
name = "LightNovelPub";
|
||||
version = "1.0.0";
|
||||
icon = "src/en/lightnovelpub/icon.png";
|
||||
filters?: Filters | undefined;
|
||||
site = "https://www.lightnovelpub.com/";
|
||||
baseUrl = this.site;
|
||||
headers = {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}browse/all/popular/all/${pageNo}`;
|
||||
|
||||
const body = await fetchApi(url).then((r) => r.text());
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = `${this.site}browse/`;
|
||||
link += `${filters.genres.value}/`;
|
||||
link += `${filters.order.value}/`;
|
||||
link += `${filters.status.value}/`;
|
||||
link += page;
|
||||
|
||||
const body = await fetchApi(link).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
@@ -34,16 +40,16 @@ class LightNovelPub implements Plugin.PluginBase {
|
||||
.trim();
|
||||
const novelCover = loadedCheerio(ele).find("img").attr("data-src");
|
||||
const novelUrl =
|
||||
this.baseUrl +
|
||||
loadedCheerio(ele)
|
||||
.find(".novel-title > a")
|
||||
.attr("href")
|
||||
?.substring(1);
|
||||
|
||||
if (!novelUrl) return;
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
path: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
@@ -51,67 +57,62 @@ class LightNovelPub implements Plugin.PluginBase {
|
||||
|
||||
return novels;
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const body = await fetchApi(url).then((r) => r.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
|
||||
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const totalChapters = parseInt(loadedCheerio(".header-stats span:first strong").text(), 10);
|
||||
|
||||
const novel: Plugin.SourceNovel & {totalPages: number}= {
|
||||
path: novelPath,
|
||||
name: loadedCheerio("h1.novel-title").text().trim() || "Untitled",
|
||||
cover: loadedCheerio("figure.cover > img").attr("data-src"),
|
||||
author: loadedCheerio(".author > a > span").text(),
|
||||
summary: loadedCheerio(".summary > .content").text().trim(),
|
||||
status: loadedCheerio(".header-stats span:last strong").text(),
|
||||
totalPages: Math.ceil(totalChapters/100),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio("h1.novel-title").text().trim();
|
||||
novel.genres = loadedCheerio('.categories ul li')
|
||||
.map((a,ex) => loadedCheerio(ex).text().trim())
|
||||
.toArray()
|
||||
.join(',');
|
||||
|
||||
novel.cover = loadedCheerio("figure.cover > img").attr("data-src");
|
||||
|
||||
novel.genres = "";
|
||||
|
||||
loadedCheerio(".categories > ul > li > a").each(function () {
|
||||
novel.genres += loadedCheerio(this).text() + ",";
|
||||
});
|
||||
|
||||
novel.genres = novel.genres.slice(0, -1);
|
||||
|
||||
loadedCheerio("div.header-stats > span").each(function () {
|
||||
if (loadedCheerio(this).find("small").text() === "Status") {
|
||||
novel.status = loadedCheerio(this).find("strong").text();
|
||||
}
|
||||
});
|
||||
|
||||
novel.author = loadedCheerio(".author > a > span").text();
|
||||
|
||||
novel.summary = loadedCheerio(".summary > .content").text().trim();
|
||||
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
await delay(1000);
|
||||
const chapterListUrl = novelUrl + '/chapters?chorder=desc';
|
||||
|
||||
const chapterListBody = await fetchApi(chapterListUrl).then(r => r.text());
|
||||
const loadedChapterList = parseHTML(chapterListBody);
|
||||
|
||||
|
||||
const lastNumber = parseInt(
|
||||
loadedChapterList('ul.chapter-list > li:nth-child(1)').attr('data-chapterno') ?? '0'
|
||||
);
|
||||
const lastChapterUrl = loadedChapterList('ul.chapter-list > li:nth-child(1) > a').attr('href');
|
||||
|
||||
if(lastChapterUrl){
|
||||
const chapterUrlPrefix = this.baseUrl + lastChapterUrl.split('/').slice(1, -1).join('/');
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
for(let i = 1; i <= lastNumber; i++){
|
||||
chapters.push({
|
||||
chapterNumber: i,
|
||||
name: 'Chapter ' + i,
|
||||
url: chapterUrlPrefix + '/chapter-' + i,
|
||||
})
|
||||
}
|
||||
novel.chapters = chapters;
|
||||
}
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const body = await fetchApi(chapterUrl).then((r) => r.text());
|
||||
|
||||
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
||||
const url = this.site + novelPath + '/chapters/page-' + page;
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const chapter: Plugin.ChapterItem[] = [];
|
||||
loadedCheerio('.chapter-list li').each(function () {
|
||||
const chapterName =
|
||||
'Chapter ' +
|
||||
loadedCheerio(this).find('.chapter-no').text().trim() +
|
||||
' - ';
|
||||
loadedCheerio(this).find('.chapter-title').text().trim();
|
||||
|
||||
const releaseDate = loadedCheerio(this)
|
||||
.find('.chapter-update')
|
||||
.attr('datetime');
|
||||
|
||||
const chapterUrl = loadedCheerio(this).find('a').attr('href')?.substring(1);
|
||||
if (!chapterUrl) return;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
path: chapterUrl,
|
||||
releaseTime: dayjs(releaseDate).toISOString(),
|
||||
})});
|
||||
const chapters = chapter;
|
||||
return {chapters};
|
||||
}
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
@@ -119,9 +120,10 @@ class LightNovelPub implements Plugin.PluginBase {
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}lnsearchlive`;
|
||||
const link = `${this.baseUrl}search`;
|
||||
const url = `${this.site}lnsearchlive`;
|
||||
const link = `${this.site}search`;
|
||||
const response = await fetchApi(link).then((r) => r.text());
|
||||
const token = parseHTML(response);
|
||||
let verifytoken = token("#novelSearchForm > input").attr("value");
|
||||
@@ -152,22 +154,93 @@ class LightNovelPub implements Plugin.PluginBase {
|
||||
.text()
|
||||
.trim();
|
||||
const novelCover = loadedCheerio(ele).find("img").attr("src");
|
||||
const novelUrl =
|
||||
this.baseUrl + loadedCheerio(ele).find("a").attr("href")?.substring(1);
|
||||
|
||||
const novelUrl = loadedCheerio(ele).find("a").attr("href")?.substring(1);
|
||||
if (!novelUrl) return;
|
||||
novels.push({
|
||||
name: novelName,
|
||||
url: novelUrl,
|
||||
path: novelUrl,
|
||||
cover: novelCover,
|
||||
});
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url, {headers: this.headers});
|
||||
}
|
||||
|
||||
filters = {
|
||||
order: {
|
||||
value: "popular",
|
||||
label: "Order by",
|
||||
options: [
|
||||
{ label: "New", value: "new" },
|
||||
{ label: "Popular", value: "popular" },
|
||||
{ label: "Updates", value: "updated" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
status: {
|
||||
value: "all",
|
||||
label: "Status",
|
||||
options: [
|
||||
{ label: "All", value: "all" },
|
||||
{ label: "Completed", value: "completed" },
|
||||
{ label: "Ongoing", value: "ongoing" }
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
genres: {
|
||||
value: "all",
|
||||
label: "Genre",
|
||||
options: [
|
||||
{ label: "All", value: "all" },
|
||||
{ label: "Action", value: "action" },
|
||||
{ label: "Adventure", value: "adventure" },
|
||||
{ label: "Drama", value: "drama" },
|
||||
{ label: "Fantasy", value: "fantasy" },
|
||||
{ label: "Harem", value: "harem" },
|
||||
{ label: "Martial Arts", value: "martial-arts" },
|
||||
{ label: "Mature", value: "mature" },
|
||||
{ label: "Romance", value: "romance" },
|
||||
{ label: "Tragedy", value: "tragedy" },
|
||||
{ label: "Xuanhuan", value: "xuanhuan" },
|
||||
{ label: "Ecchi", value: "ecchi" },
|
||||
{ label: "Comedy", value: "comedy" },
|
||||
{ label: "Slice of Life", value: "slice-of-life" },
|
||||
{ label: "Mystery", value: "mystery" },
|
||||
{ label: "Supernatural", value: "supernatural" },
|
||||
{ label: "Psychological", value: "psychological" },
|
||||
{ label: "Sci-fi", value: "sci-fi" },
|
||||
{ label: "Xianxia", value: "xianxia" },
|
||||
{ label: "School Life", value: "school-life" },
|
||||
{ label: "Josei", value: "josei" },
|
||||
{ label: "Wuxia", value: "wuxia" },
|
||||
{ label: "Shounen", value: "shounen" },
|
||||
{ label: "Horror", value: "horror" },
|
||||
{ label: "Mecha", value: "mecha" },
|
||||
{ label: "Historical", value: "historical" },
|
||||
{ label: "Shoujo", value: "shoujo" },
|
||||
{ label: "Adult", value: "adult" },
|
||||
{ label: "Seinen", value: "seinen" },
|
||||
{ label: "Sports", value: "sports" },
|
||||
{ label: "Lolicon", value: "lolicon" },
|
||||
{ label: "Gender Bender", value: "gender-bender" },
|
||||
{ label: "Shounen Ai", value: "shounen-ai" },
|
||||
{ label: "Yaoi", value: "yaoi" },
|
||||
{ label: "Video Games", value: "video-games" },
|
||||
{ label: "Smut", value: "smut" },
|
||||
{ label: "Magical Realism", value: "magical-realism" },
|
||||
{ label: "Eastern Fantasy", value: "eastern-fantasy" },
|
||||
{ label: "Contemporary Romance", value: "contemporary-romance" },
|
||||
{ label: "Fantasy Romance", value: "fantasy-romance" },
|
||||
{ label: "Shoujo Ai", value: "shoujo-ai" },
|
||||
{ label: "Yuri", value: "yuri" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new LightNovelPub();
|
||||
@@ -1,25 +1,31 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { Filters } from "@libs/filterInputs";
|
||||
import { Filters, FilterTypes } from "@libs/filterInputs";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
|
||||
class LightNovelWorld implements Plugin.PluginBase {
|
||||
class LightNovelWorld implements Plugin.PagePlugin {
|
||||
id = "lightnovelworld";
|
||||
name = "LightNovelWorld";
|
||||
site = "https://www.webnovelworld.org/";
|
||||
version = "1.0.0";
|
||||
filters?: Filters | undefined;
|
||||
icon = "src/en/lightnovelworld/icon.png";
|
||||
baseUrl = this.site;
|
||||
headers = {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}browse/all/popular/all/${pageNo}`;
|
||||
|
||||
const body = await fetchApi(url).then((r) => r.text());
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = `${this.site}browse/`;
|
||||
link += `${filters.genres.value}/`;
|
||||
link += `${filters.order.value}/`;
|
||||
link += `${filters.status.value}/`;
|
||||
link += page;
|
||||
|
||||
const body = await fetchApi(link).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
@@ -34,16 +40,16 @@ class LightNovelWorld implements Plugin.PluginBase {
|
||||
.trim();
|
||||
const novelCover = loadedCheerio(ele).find("img").attr("data-src");
|
||||
const novelUrl =
|
||||
this.baseUrl +
|
||||
loadedCheerio(ele)
|
||||
.find(".novel-title > a")
|
||||
.attr("href")
|
||||
?.substring(1);
|
||||
|
||||
if (!novelUrl) return;
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
path: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
@@ -51,67 +57,62 @@ class LightNovelWorld implements Plugin.PluginBase {
|
||||
|
||||
return novels;
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const body = await fetchApi(url).then((r) => r.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
|
||||
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const totalChapters = parseInt(loadedCheerio(".header-stats span:first strong").text(), 10);
|
||||
|
||||
const novel: Plugin.SourceNovel & {totalPages: number}= {
|
||||
path: novelPath,
|
||||
name: loadedCheerio("h1.novel-title").text().trim() || "Untitled",
|
||||
cover: loadedCheerio("figure.cover > img").attr("data-src"),
|
||||
author: loadedCheerio(".author > a > span").text(),
|
||||
summary: loadedCheerio(".summary > .content").text().trim(),
|
||||
status: loadedCheerio(".header-stats span:last strong").text(),
|
||||
totalPages: Math.ceil(totalChapters/100),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio("h1.novel-title").text().trim();
|
||||
novel.genres = loadedCheerio('.categories ul li')
|
||||
.map((a,ex) => loadedCheerio(ex).text().trim())
|
||||
.toArray()
|
||||
.join(',');
|
||||
|
||||
novel.cover = loadedCheerio("figure.cover > img").attr("data-src");
|
||||
|
||||
novel.genres = "";
|
||||
|
||||
loadedCheerio(".categories > ul > li > a").each(function () {
|
||||
novel.genres += loadedCheerio(this).text() + ",";
|
||||
});
|
||||
|
||||
novel.genres = novel.genres.slice(0, -1);
|
||||
|
||||
loadedCheerio("div.header-stats > span").each(function () {
|
||||
if (loadedCheerio(this).find("small").text() === "Status") {
|
||||
novel.status = loadedCheerio(this).find("strong").text();
|
||||
}
|
||||
});
|
||||
|
||||
novel.author = loadedCheerio(".author > a > span").text();
|
||||
|
||||
novel.summary = loadedCheerio(".summary > .content").text().trim();
|
||||
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
await delay(1000);
|
||||
const chapterListUrl = novelUrl + '/chapters?chorder=desc';
|
||||
|
||||
const chapterListBody = await fetchApi(chapterListUrl).then(r => r.text());
|
||||
const loadedChapterList = parseHTML(chapterListBody);
|
||||
|
||||
|
||||
const lastNumber = parseInt(
|
||||
loadedChapterList('ul.chapter-list > li:nth-child(1)').attr('data-chapterno') ?? '0'
|
||||
);
|
||||
const lastChapterUrl = loadedChapterList('ul.chapter-list > li:nth-child(1) > a').attr('href');
|
||||
|
||||
if(lastChapterUrl){
|
||||
const chapterUrlPrefix = this.baseUrl + lastChapterUrl.split('/').slice(1, -1).join('/');
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
for(let i = 1; i <= lastNumber; i++){
|
||||
chapters.push({
|
||||
chapterNumber: i,
|
||||
name: 'Chapter ' + i,
|
||||
url: chapterUrlPrefix + '/chapter-' + i,
|
||||
})
|
||||
}
|
||||
novel.chapters = chapters;
|
||||
}
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const body = await fetchApi(chapterUrl).then((r) => r.text());
|
||||
|
||||
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
||||
const url = this.site + novelPath + '/chapters/page-' + page;
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const chapter: Plugin.ChapterItem[] = [];
|
||||
loadedCheerio('.chapter-list li').each(function () {
|
||||
const chapterName =
|
||||
'Chapter ' +
|
||||
loadedCheerio(this).find('.chapter-no').text().trim() +
|
||||
' - ';
|
||||
loadedCheerio(this).find('.chapter-title').text().trim();
|
||||
|
||||
const releaseDate = loadedCheerio(this)
|
||||
.find('.chapter-update')
|
||||
.attr('datetime');
|
||||
|
||||
const chapterUrl = loadedCheerio(this).find('a').attr('href')?.substring(1);
|
||||
if (!chapterUrl) return;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
path: chapterUrl,
|
||||
releaseTime: dayjs(releaseDate).toISOString(),
|
||||
})});
|
||||
const chapters = chapter;
|
||||
return {chapters};
|
||||
}
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
@@ -119,9 +120,10 @@ class LightNovelWorld implements Plugin.PluginBase {
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}lnsearchlive`;
|
||||
const link = `${this.baseUrl}search`;
|
||||
const url = `${this.site}lnsearchlive`;
|
||||
const link = `${this.site}search`;
|
||||
const response = await fetchApi(link).then((r) => r.text());
|
||||
const token = parseHTML(response);
|
||||
let verifytoken = token("#novelSearchForm > input").attr("value");
|
||||
@@ -152,22 +154,93 @@ class LightNovelWorld implements Plugin.PluginBase {
|
||||
.text()
|
||||
.trim();
|
||||
const novelCover = loadedCheerio(ele).find("img").attr("src");
|
||||
const novelUrl =
|
||||
this.baseUrl + loadedCheerio(ele).find("a").attr("href")?.substring(1);
|
||||
|
||||
const novelUrl = loadedCheerio(ele).find("a").attr("href")?.substring(1);
|
||||
if (!novelUrl) return;
|
||||
novels.push({
|
||||
name: novelName,
|
||||
url: novelUrl,
|
||||
path: novelUrl,
|
||||
cover: novelCover,
|
||||
});
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url, {headers: this.headers});
|
||||
}
|
||||
|
||||
filters = {
|
||||
order: {
|
||||
value: "popular",
|
||||
label: "Order by",
|
||||
options: [
|
||||
{ label: "New", value: "new" },
|
||||
{ label: "Popular", value: "popular" },
|
||||
{ label: "Updates", value: "updated" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
status: {
|
||||
value: "all",
|
||||
label: "Status",
|
||||
options: [
|
||||
{ label: "All", value: "all" },
|
||||
{ label: "Completed", value: "completed" },
|
||||
{ label: "Ongoing", value: "ongoing" }
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
genres: {
|
||||
value: "all",
|
||||
label: "Genre",
|
||||
options: [
|
||||
{ label: "All", value: "all" },
|
||||
{ label: "Action", value: "action" },
|
||||
{ label: "Adventure", value: "adventure" },
|
||||
{ label: "Drama", value: "drama" },
|
||||
{ label: "Fantasy", value: "fantasy" },
|
||||
{ label: "Harem", value: "harem" },
|
||||
{ label: "Martial Arts", value: "martial-arts" },
|
||||
{ label: "Mature", value: "mature" },
|
||||
{ label: "Romance", value: "romance" },
|
||||
{ label: "Tragedy", value: "tragedy" },
|
||||
{ label: "Xuanhuan", value: "xuanhuan" },
|
||||
{ label: "Ecchi", value: "ecchi" },
|
||||
{ label: "Comedy", value: "comedy" },
|
||||
{ label: "Slice of Life", value: "slice-of-life" },
|
||||
{ label: "Mystery", value: "mystery" },
|
||||
{ label: "Supernatural", value: "supernatural" },
|
||||
{ label: "Psychological", value: "psychological" },
|
||||
{ label: "Sci-fi", value: "sci-fi" },
|
||||
{ label: "Xianxia", value: "xianxia" },
|
||||
{ label: "School Life", value: "school-life" },
|
||||
{ label: "Josei", value: "josei" },
|
||||
{ label: "Wuxia", value: "wuxia" },
|
||||
{ label: "Shounen", value: "shounen" },
|
||||
{ label: "Horror", value: "horror" },
|
||||
{ label: "Mecha", value: "mecha" },
|
||||
{ label: "Historical", value: "historical" },
|
||||
{ label: "Shoujo", value: "shoujo" },
|
||||
{ label: "Adult", value: "adult" },
|
||||
{ label: "Seinen", value: "seinen" },
|
||||
{ label: "Sports", value: "sports" },
|
||||
{ label: "Lolicon", value: "lolicon" },
|
||||
{ label: "Gender Bender", value: "gender-bender" },
|
||||
{ label: "Shounen Ai", value: "shounen-ai" },
|
||||
{ label: "Yaoi", value: "yaoi" },
|
||||
{ label: "Video Games", value: "video-games" },
|
||||
{ label: "Smut", value: "smut" },
|
||||
{ label: "Magical Realism", value: "magical-realism" },
|
||||
{ label: "Eastern Fantasy", value: "eastern-fantasy" },
|
||||
{ label: "Contemporary Romance", value: "contemporary-romance" },
|
||||
{ label: "Fantasy Romance", value: "fantasy-romance" },
|
||||
{ label: "Shoujo Ai", value: "shoujo-ai" },
|
||||
{ label: "Yuri", value: "yuri" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new LightNovelWorld();
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Chapter, Novel, Plugin } from "@typings/plugin";
|
||||
|
||||
const pluginId = "lnmtl";
|
||||
const baseUrl = "https://lnmtl.com/";
|
||||
|
||||
export const popularNovels: Plugin.popularNovels = async function (page) {
|
||||
let url = baseUrl + "novel?page=" + page;
|
||||
|
||||
const body = await fetchApi(url, {}, pluginId).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Novel.Item[] = [];
|
||||
|
||||
loadedCheerio(".media").each(function () {
|
||||
const novelName = loadedCheerio(this).find("h4").text();
|
||||
const novelCover = loadedCheerio(this).find("img").attr("src");
|
||||
const novelUrl = loadedCheerio(this).find("h4 > a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
export const parseNovelAndChapters: Plugin.parseNovelAndChapters =
|
||||
async function (novelUrl) {
|
||||
const url = novelUrl;
|
||||
|
||||
const body = await fetchApi(url, {}, pluginId).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Novel.instance = {
|
||||
url,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio(".novel-name").text();
|
||||
|
||||
novel.cover = loadedCheerio("div.novel").find("img").attr("src");
|
||||
|
||||
novel.summary = loadedCheerio("div.description").text().trim();
|
||||
|
||||
loadedCheerio(".panel-body > dl").each(function () {
|
||||
let detailName = loadedCheerio(this).find("dt").text().trim();
|
||||
let detail = loadedCheerio(this).find("dd").text().trim();
|
||||
|
||||
switch (detailName) {
|
||||
case "Authors":
|
||||
novel.author = detail;
|
||||
break;
|
||||
case "Current status":
|
||||
novel.status = detail;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
novel.genres = loadedCheerio('.panel-heading:contains(" Genres ")')
|
||||
.next()
|
||||
.text()
|
||||
.trim()
|
||||
.replace(/\s\s/g, ",");
|
||||
|
||||
let volumes = JSON.parse(
|
||||
loadedCheerio("main")
|
||||
.next()
|
||||
.html()
|
||||
.match(/lnmtl.volumes = \[(.*?)\]/).[0]
|
||||
.replace("lnmtl.volumes = ", "")
|
||||
);
|
||||
|
||||
let chapters: Chapter.Item[] = [];
|
||||
|
||||
volumes = volumes.map((volume) => volume.id);
|
||||
|
||||
for (const volume of volumes) {
|
||||
let volumeData = await fetchApi(
|
||||
`https://lnmtl.com/chapter?page=1&volumeId=${volume}`
|
||||
);
|
||||
volumeData = await volumeData.json();
|
||||
|
||||
// volumeData = volumeData.data.map((volume) => volume.slug);
|
||||
|
||||
for (let i = 1; i <= volumeData.last_page; i++) {
|
||||
let chapterData = await fetchApi(
|
||||
`https://lnmtl.com/chapter?page=${i}&volumeId=${volume}`
|
||||
);
|
||||
chapterData = await chapterData.json();
|
||||
|
||||
chapterData = chapterData.data.map((chapter) => ({
|
||||
name: `#${chapter.number} ${chapter.title}`,
|
||||
url: `${baseUrl}chapter/${chapter.slug}`,
|
||||
releaseTime: chapter.created_at,
|
||||
}));
|
||||
|
||||
chapters.push(...chapterData);
|
||||
}
|
||||
}
|
||||
|
||||
novel.chapters = chapters;
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
|
||||
const body = await fetchApi(chapterUrl, {}, pluginId).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
loadedCheerio(".original").replaceWith("<br><br>");
|
||||
|
||||
let chapterText = loadedCheerio(".chapter-body").html();
|
||||
|
||||
if (!chapterText) {
|
||||
chapterText = loadedCheerio(".alert.alert-warning").text();
|
||||
}
|
||||
|
||||
return chapterText;
|
||||
};
|
||||
|
||||
export const searchNovels: Plugin.searchNovels = async function (searchTerm) {
|
||||
const url = "https://lnmtl.com/term";
|
||||
|
||||
const body = await fetchApi(url, {}, pluginId).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels:Novel.Item[] = loadedCheerio("footer")
|
||||
.next()
|
||||
.next()
|
||||
.html()
|
||||
.match(/local: \[(.*?)\]/)[0]
|
||||
.replace("local: ", "");
|
||||
|
||||
novels = JSON.parse(novels);
|
||||
|
||||
novels = novels.filter((novel) =>
|
||||
novel.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
novels = novels.map((novel) => ({
|
||||
name: novel.name,
|
||||
url: novel.slug,
|
||||
cover: novel.image,
|
||||
}));
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
export const fetchImage: Plugin.fetchImage = async function (url) {
|
||||
const headers = {
|
||||
Referer: baseUrl,
|
||||
};
|
||||
return await fetchFile(url, { headers: headers });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
id: pluginId,
|
||||
name: "LNMTL",
|
||||
version: "1.0.0",
|
||||
icon: "src/en/lnmtl/icon.png",
|
||||
site: baseUrl,
|
||||
protected: true,
|
||||
fetchImage,
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
+77
-55
@@ -3,19 +3,22 @@ import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
|
||||
class LnMTLPlugin implements Plugin.PluginBase {
|
||||
class LnMTLPlugin implements Plugin.PagePlugin {
|
||||
id = "lnmtl";
|
||||
name = "LnMTL";
|
||||
icon = "src/en/lnmtl/icon.png";
|
||||
site = "https://lnmtl.com/";
|
||||
version = "1.0.0";
|
||||
|
||||
async sleep (ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = this.site + "novel?";
|
||||
|
||||
link += `orderBy=${filters.order.value}`;
|
||||
link += `&order=${filters.sort.value}`;
|
||||
link += `&filter=${filters.storyStatus.value}`;
|
||||
@@ -28,17 +31,17 @@ class LnMTLPlugin implements Plugin.PluginBase {
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".media").each(function () {
|
||||
const novelName = loadedCheerio(this).find("h4").text();
|
||||
const novelCover = loadedCheerio(this).find("img").attr("src");
|
||||
const novelUrl = loadedCheerio(this).find("h4 > a").attr("href");
|
||||
loadedCheerio(".media").each((i,el) => {
|
||||
const novelName = loadedCheerio(el).find("h4").text();
|
||||
const novelCover = loadedCheerio(el).find("img").attr("src");
|
||||
const novelUrl = loadedCheerio(el).find("h4 > a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
path: novelUrl.replace(this.site, ''),
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
@@ -47,24 +50,29 @@ class LnMTLPlugin implements Plugin.PluginBase {
|
||||
return novels;
|
||||
};
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const result = await fetchApi(url);
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
|
||||
const result = await fetchApi(this.site + novelPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
const volumes = JSON.parse(
|
||||
loadedCheerio("main")
|
||||
.next()
|
||||
.html()
|
||||
?.match(/lnmtl.volumes = \[(.*?)\]/)![0]
|
||||
?.replace("lnmtl.volumes = ", "") || ""
|
||||
);
|
||||
|
||||
const novel: Plugin.SourceNovel & {totalPages: number} = {
|
||||
path: novelPath,
|
||||
name: loadedCheerio(".novel-name").text() || 'Untitled',
|
||||
cover: loadedCheerio("div.novel").find("img").attr("src"),
|
||||
summary: loadedCheerio("div.description").text().trim(),
|
||||
totalPages: volumes.length,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio(".novel-name").text();
|
||||
|
||||
novel.cover = loadedCheerio("div.novel").find("img").attr("src");
|
||||
|
||||
novel.summary = loadedCheerio("div.description").text().trim();
|
||||
|
||||
loadedCheerio(".panel-body > dl").each(function () {
|
||||
const detailName = loadedCheerio(this).find("dt").text().trim();
|
||||
const detail = loadedCheerio(this).find("dd").text().trim();
|
||||
@@ -86,52 +94,66 @@ class LnMTLPlugin implements Plugin.PluginBase {
|
||||
.toArray()
|
||||
.join(",");
|
||||
|
||||
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
let volumes = JSON.parse(
|
||||
return novel;
|
||||
};
|
||||
|
||||
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
||||
const result = await fetchApi(this.site + novelPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const volumes = JSON.parse(
|
||||
loadedCheerio("main")
|
||||
.next()
|
||||
.html()
|
||||
?.match(/lnmtl.volumes = \[(.*?)\]/)![0]
|
||||
?.replace("lnmtl.volumes = ", "") || ""
|
||||
);
|
||||
)[+page-1];
|
||||
|
||||
const chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
volumes = volumes.map((volume: { id: number; }) => volume.id);
|
||||
|
||||
for (const volume of volumes) {
|
||||
const volumeData = await fetchApi(
|
||||
`${this.site}chapter?page=1&volumeId=${volume}`
|
||||
);
|
||||
const volumePage = await volumeData.json();
|
||||
|
||||
// volumeData = volumeData.data.map((volume) => volume.slug);
|
||||
|
||||
for (let i = 1; i <= volumePage.last_page; i++) {
|
||||
const chapterData = await fetchApi(
|
||||
`${this.site}chapter?page=${i}&volumeId=${volume}`
|
||||
);
|
||||
const chapterInfo = await chapterData.json();
|
||||
|
||||
const chapterDetails = chapterInfo.data.map(
|
||||
(chapter: { number: number; title: string; slug: string; created_at: string; }) => ({
|
||||
name: `#${chapter.number} ${chapter.title}`,
|
||||
url: `${this.site}chapter/${chapter.slug}`,
|
||||
releaseTime: new Date (chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert
|
||||
}));
|
||||
|
||||
chapter.push(...chapterDetails);
|
||||
}
|
||||
await delay(1000);
|
||||
interface ChapterEntry {
|
||||
number: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
novel.chapters = chapter;
|
||||
await this.sleep(1000);
|
||||
const volumeData = await fetchApi(
|
||||
`${this.site}chapter?volumeId=${volumes.id}`
|
||||
);
|
||||
const volumePage = await volumeData.json();
|
||||
const firstPage = volumePage.data.map(
|
||||
(chapter: ChapterEntry) => ({
|
||||
name: `#${chapter.number} - ${chapter.title}`,
|
||||
path: `chapter/${chapter.slug}`,
|
||||
releaseTime: new Date (chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert
|
||||
}))
|
||||
chapter.push(...firstPage);
|
||||
|
||||
return novel;
|
||||
};
|
||||
for (let i = 2; i <= volumePage.last_page; i++) {
|
||||
await this.sleep(1000);
|
||||
const chapterData = await fetchApi(
|
||||
`${this.site}chapter?page=${i}&volumeId=${volumes.id}`
|
||||
);
|
||||
const chapterInfo = await chapterData.json();
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(chapterUrl);
|
||||
const chapterDetails = chapterInfo.data.map(
|
||||
(chapter: ChapterEntry) => ({
|
||||
name: `#${chapter.number} ${chapter.title}`,
|
||||
path: `chapter/${chapter.slug}`,
|
||||
releaseTime: new Date (chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert
|
||||
}));
|
||||
|
||||
chapter.push(...chapterDetails);
|
||||
}
|
||||
const chapters = chapter;
|
||||
return {chapters};
|
||||
}
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(this.site + chapterPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
@@ -171,11 +193,11 @@ class LnMTLPlugin implements Plugin.PluginBase {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
nov.map((res: { name: string; slug: string; image: string; }) => {
|
||||
const novelName = res.name;
|
||||
const novelUrl = `${this.site}novel/${res.slug}`;
|
||||
const novelUrl = `novel/${res.slug}`;
|
||||
const novelCover = res.image;
|
||||
|
||||
const novel = {
|
||||
url: novelUrl,
|
||||
path: novelUrl,
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
};
|
||||
|
||||
+96
-60
@@ -1,7 +1,7 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { Filters } from "@libs/filterInputs";
|
||||
import { Filters, FilterTypes } from "@libs/filterInputs";
|
||||
|
||||
class MTLNovel implements Plugin.PluginBase {
|
||||
id = "mtlnovel";
|
||||
@@ -9,30 +9,38 @@ class MTLNovel implements Plugin.PluginBase {
|
||||
version = "1.0.0";
|
||||
icon = "src/en/mtlnovel/icon.png";
|
||||
site = "https://www.mtlnovel.com/";
|
||||
baseUrl = this.site;
|
||||
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}alltime-rank/page/${pageNo}`;
|
||||
const body = await fetchApi(url).then((result) => result.text());
|
||||
console.log(body);
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = `${this.site}novel-list/?`;
|
||||
link += `orderby=${filters.order.value}`;
|
||||
link += `&order=${filters.sort.value}`;
|
||||
link += `&status=${filters.storyStatus.value}`;
|
||||
link += `&pg=${page}`
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append('Alt-Used', 'www.mtlnovel.com');
|
||||
const body = await fetch(link, {headers}).then((result) => result.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio("div.box.wide").each(function () {
|
||||
const novelName = loadedCheerio(this)
|
||||
loadedCheerio("div.box.wide").each((i,el) => {
|
||||
const novelName = loadedCheerio(el)
|
||||
.find("a.list-title")
|
||||
.text()
|
||||
.slice(4);
|
||||
const novelCover = loadedCheerio(this).find("amp-img").attr("src");
|
||||
const novelUrl = loadedCheerio(this).find("a.list-title").attr("href");
|
||||
const novelCover = loadedCheerio(el).find("amp-img").attr("src");
|
||||
const novelUrl = loadedCheerio(el).find("a.list-title").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
path: novelUrl.replace(this.site, ''),
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
@@ -40,65 +48,56 @@ class MTLNovel implements Plugin.PluginBase {
|
||||
|
||||
return novels;
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const headers = new Headers();
|
||||
headers.append('Referer', `${this.site}novel-list/`)
|
||||
headers.append('Alt-Used', 'www.mtlnovel.com');
|
||||
|
||||
let headers = {
|
||||
Referer: `${this.baseUrl}alltime-rank/`,
|
||||
};
|
||||
|
||||
const body = await fetchApi(
|
||||
url,
|
||||
const body = await fetch(
|
||||
this.site + novelPath,
|
||||
{
|
||||
method: "GET",
|
||||
headers: headers,
|
||||
headers
|
||||
},
|
||||
).then((result) => result.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
path: novelPath,
|
||||
name: loadedCheerio("h1.entry-title").text() || 'Untitled',
|
||||
cover: loadedCheerio(".nov-head > amp-img").attr("src"),
|
||||
summary: loadedCheerio("div.desc > h2").next().text().trim(),
|
||||
author: loadedCheerio("#author").text(),
|
||||
status: loadedCheerio("#status").text(),
|
||||
genres: loadedCheerio("#genre").text().replace(/\s+/g, ""),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio("h1.entry-title").text();
|
||||
const chapterListUrl = this.site + novelPath + "chapter-list/";
|
||||
|
||||
novel.cover = loadedCheerio(".nov-head > amp-img").attr("src");
|
||||
|
||||
novel.summary = loadedCheerio("div.desc > h2").next().text().trim();
|
||||
|
||||
novel.author = loadedCheerio("#author").text();
|
||||
|
||||
novel.status = loadedCheerio("#status").text();
|
||||
|
||||
novel.genres = loadedCheerio("#genre").text().replace(/\s+/g, "");
|
||||
|
||||
const chapterListUrl = url + "chapter-list/";
|
||||
|
||||
async function getChapters() {
|
||||
const listResult = await fetch(chapterListUrl);
|
||||
const getChapters = async () => {
|
||||
const listResult = await fetch(chapterListUrl, {headers});
|
||||
const listBody = await listResult.text();
|
||||
|
||||
loadedCheerio = parseHTML(listBody);
|
||||
|
||||
let chapter: Plugin.ChapterItem[] = [];
|
||||
const chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio("div.ch-list")
|
||||
.find("a.ch-link")
|
||||
.each(function () {
|
||||
const chapterName = loadedCheerio(this)
|
||||
.each((i,el) => {
|
||||
const chapterName = loadedCheerio(el)
|
||||
.text()
|
||||
.replace("~ ", "");
|
||||
const releaseDate = null;
|
||||
const chapterUrl = loadedCheerio(this).attr("href");
|
||||
if (chapterUrl) {
|
||||
chapter.push({
|
||||
url: chapterUrl,
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
});
|
||||
}
|
||||
const chapterUrl = loadedCheerio(el).attr("href");
|
||||
if (!chapterUrl) return;
|
||||
chapter.push({
|
||||
path: chapterUrl.replace(this.site, ''),
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
});
|
||||
|
||||
});
|
||||
return chapter.reverse();
|
||||
}
|
||||
@@ -107,8 +106,11 @@ class MTLNovel implements Plugin.PluginBase {
|
||||
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const body = await fetchApi(chapterUrl).then((r) => r.text());
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const headers = new Headers();
|
||||
headers.append('Alt-Used', 'www.mtlnovel.com');
|
||||
const body = await fetch(this.site + chapterPath, {headers}).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
@@ -116,17 +118,20 @@ class MTLNovel implements Plugin.PluginBase {
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const headers = new Headers();
|
||||
headers.append('Alt-Used', 'www.mtlnovel.com');
|
||||
const searchUrl =
|
||||
this.baseUrl +
|
||||
this.site +
|
||||
"wp-admin/admin-ajax.php?action=autosuggest&q=" +
|
||||
searchTerm +
|
||||
"&__amp_source_origin=https%3A%2F%2Fwww.mtlnovel.com";
|
||||
|
||||
const res = await fetch(searchUrl);
|
||||
const res = await fetch(searchUrl, {headers});
|
||||
const result = await res.json();
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
interface SearchEntry {
|
||||
title: string;
|
||||
thumbnail: string;
|
||||
@@ -135,12 +140,9 @@ class MTLNovel implements Plugin.PluginBase {
|
||||
result.items[0].results.map((item: SearchEntry) => {
|
||||
const novelName = item.title.replace(/<\/?strong>/g, "");
|
||||
const novelCover = item.thumbnail;
|
||||
const novelUrl = item.permalink.replace(
|
||||
"https://www.mtlnovel.com/",
|
||||
""
|
||||
);
|
||||
const novelUrl = item.permalink.replace(this.site, '');
|
||||
|
||||
const novel = { name: novelName, cover: novelCover, url: novelUrl };
|
||||
const novel = { name: novelName, cover: novelCover, path: novelUrl };
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
@@ -148,8 +150,42 @@ class MTLNovel implements Plugin.PluginBase {
|
||||
return novels;
|
||||
}
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url);
|
||||
const headers = new Headers();
|
||||
headers.append('Alt-Used', 'www.mtlnovel.com');
|
||||
return fetchFile(url, {headers});
|
||||
}
|
||||
|
||||
filters = {
|
||||
order: {
|
||||
value: "view",
|
||||
label: "Order by",
|
||||
options: [
|
||||
{ label: "Date", value: "date" },
|
||||
{ label: "Name", value: "name" },
|
||||
{ label: "Rating", value: "rating" },
|
||||
{ label: "View", value: "view" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
sort: {
|
||||
value: "desc",
|
||||
label: "Sort by",
|
||||
options: [
|
||||
{ label: "Descending", value: "desc" },
|
||||
{ label: "Ascending", value: "asc" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
storyStatus: {
|
||||
value: "all",
|
||||
label: "Status",
|
||||
options: [
|
||||
{ label: "All", value: "all" },
|
||||
{ label: "Ongoing", value: "ongoing" },
|
||||
{ label: "Complete", value: "completed" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
export default new MTLNovel();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { CheerioAPI, load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { Filters } from "@libs/filterInputs";
|
||||
@@ -9,27 +9,20 @@ class MTLReader implements Plugin.PluginBase {
|
||||
version = "1.0.0";
|
||||
icon = "src/en/mtlreader/icon.png";
|
||||
site = "https://mtlreader.com/";
|
||||
baseUrl = this.site;
|
||||
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}novels?page=${pageNo}`;
|
||||
parseNovels(loadedCheerio: CheerioAPI){
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
const body = await fetchApi(url).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".col-md-4.col-sm-4").each(function () {
|
||||
const novelName = loadedCheerio(this).find("h5").text();
|
||||
const novelCover = loadedCheerio(this).find("img").attr("src");
|
||||
const novelUrl = loadedCheerio(this).find("h5 > a").attr("href");
|
||||
loadedCheerio(".col-md-4").each((i,el) => {
|
||||
const novelName = loadedCheerio(el).find("h5").text();
|
||||
const novelCover = loadedCheerio(el).find("img").attr("src");
|
||||
const novelUrl = loadedCheerio(el).find("h5 > a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
path: novelUrl.replace(this.site, ''),
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
@@ -37,43 +30,46 @@ class MTLReader implements Plugin.PluginBase {
|
||||
|
||||
return novels;
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
|
||||
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}novels?page=${pageNo}`;
|
||||
|
||||
const body = await fetchApi(url).then((r) => r.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
path: novelPath,
|
||||
name: loadedCheerio(".agent-title").text().trim() || "Untitled",
|
||||
cover: loadedCheerio(".agent-p-img > img").attr("src"),
|
||||
summary: loadedCheerio("#editdescription").text().trim(),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio(".agent-title").text().trim();
|
||||
|
||||
novel.cover = loadedCheerio(".agent-p-img > img").attr("src");
|
||||
|
||||
novel.summary = loadedCheerio("#editdescription").text().trim();
|
||||
|
||||
novel.author = loadedCheerio("i.fa-user")
|
||||
.parent()
|
||||
.text()
|
||||
.replace("Author: ", "")
|
||||
.trim();
|
||||
|
||||
let chapter: Plugin.ChapterItem[] = [];
|
||||
const chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio("tr.spaceUnder").each(function () {
|
||||
const chapterName = loadedCheerio(this).find("a").text().trim();
|
||||
const releaseDate = null;
|
||||
const chapterUrl = loadedCheerio(this).find("a").attr("href");
|
||||
loadedCheerio("tr.spaceUnder").each((i,el) => {
|
||||
const chapterName = loadedCheerio(el).find("a").text().trim();
|
||||
const chapterUrl = loadedCheerio(el).find("a").attr("href");
|
||||
|
||||
if (!chapterUrl) return;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
path: chapterUrl.replace(this.site, ''),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,8 +77,8 @@ class MTLReader implements Plugin.PluginBase {
|
||||
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const body = await fetchApi(chapterUrl).then((r) => r.text());
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
@@ -92,32 +88,16 @@ class MTLReader implements Plugin.PluginBase {
|
||||
return chapterText;
|
||||
}
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}search?input=${searchTerm}`;
|
||||
const body = await fetchApi(this.site).then((r) => r.text());
|
||||
const tokenCheerio = parseHTML(body);
|
||||
const token = tokenCheerio('input[name="_token"]').attr('value');
|
||||
|
||||
const body = await fetchApi(url).then((r) => r.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".col-md-4.col-sm-4").each(function () {
|
||||
const novelName = loadedCheerio(this).find("h5").text();
|
||||
const novelCover = loadedCheerio(this).find("img").attr("src");
|
||||
const novelUrl = loadedCheerio(this).find("h5 > a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
const searchUrl = `${this.site}search?_token=${token}&input=${searchTerm}`;
|
||||
const seacrhBody = await fetchApi(searchUrl).then((r) => r.text());
|
||||
const loadedCheerio = parseHTML(seacrhBody);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url);
|
||||
}
|
||||
|
||||
@@ -1,327 +0,0 @@
|
||||
import { CheerioAPI, load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
|
||||
class NobleMTL implements Plugin.PluginBase {
|
||||
id = "noblemtl";
|
||||
name = "NobleMTL";
|
||||
icon = "src/en/noblemtl/icon.png";
|
||||
site = "https://noblemtl.com/";
|
||||
version = "1.0.0";
|
||||
|
||||
parseNovels(loadedCheerio: CheerioAPI) {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio("article.bs").each(function () {
|
||||
const novelName = loadedCheerio(this).find(".ntitle").text().trim();
|
||||
let image = loadedCheerio(this).find("img");
|
||||
const novelCover = image.attr("data-src") || image.attr("src");
|
||||
const novelUrl = loadedCheerio(this).find("a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = `${this.site}series/?page=${pageNo}`;
|
||||
|
||||
if (filters.genres.value.length)
|
||||
link += filters.genres.value.map((i) => `&genre[]=${i}`).join("");
|
||||
|
||||
if (filters.type.value.length)
|
||||
link += filters.type.value.map((i) => `&lang[]=${i}`).join("");
|
||||
|
||||
link += "&status=" + filters.status.value;
|
||||
|
||||
link += "&order=" + filters.order.value;
|
||||
|
||||
const headers = new Headers();
|
||||
const body = await fetchApi(link, { headers }).then((result) =>
|
||||
result.text()
|
||||
);
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(url, { headers });
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio(".entry-title").text();
|
||||
|
||||
novel.cover =
|
||||
loadedCheerio("img.wp-post-image").attr("data-src") ||
|
||||
loadedCheerio("img.wp-post-image").attr("src");
|
||||
|
||||
loadedCheerio("div.spe > span").each(function () {
|
||||
const detailName = loadedCheerio(this).find("b").text().trim();
|
||||
const detail = loadedCheerio(this)
|
||||
.find("b")
|
||||
.remove()
|
||||
.end()
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
switch (detailName) {
|
||||
case "Author:":
|
||||
novel.author = detail;
|
||||
break;
|
||||
case "Status:":
|
||||
novel.status = detail;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
novel.genres = loadedCheerio(".genxed")
|
||||
.text()
|
||||
.trim()
|
||||
.replace(/\s/g, ",");
|
||||
|
||||
loadedCheerio('div[itemprop="description"] h3,p.a,strong').remove();
|
||||
novel.summary = loadedCheerio('div[itemprop="description"]')
|
||||
.find("br")
|
||||
.replaceWith("\n")
|
||||
.end()
|
||||
.text();
|
||||
|
||||
let chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio(".eplister")
|
||||
.find("li")
|
||||
.each(function () {
|
||||
const chapterUrl = loadedCheerio(this).find("a").attr("href");
|
||||
if (!chapterUrl) return;
|
||||
const chapterName =
|
||||
loadedCheerio(this).find(".epl-num").text() +
|
||||
" - " +
|
||||
loadedCheerio(this).find(".epl-title").text();
|
||||
const releaseDate = loadedCheerio(this)
|
||||
.find(".epl-date")
|
||||
.text()
|
||||
.trim(); // Feb 25, 2024
|
||||
|
||||
if (!releaseDate) return;
|
||||
const months = [
|
||||
"jan",
|
||||
"feb",
|
||||
"mar",
|
||||
"apr",
|
||||
"may",
|
||||
"jun",
|
||||
"jul",
|
||||
"aug",
|
||||
"sep",
|
||||
"oct",
|
||||
"nov",
|
||||
"dec",
|
||||
];
|
||||
const rx = new RegExp(
|
||||
`(${months.join("|")}).*? (\\d{2}).*?(\\d{4})`,
|
||||
"i"
|
||||
).exec(releaseDate);
|
||||
if (!rx) return;
|
||||
const year = +rx[3];
|
||||
const month = months.indexOf(rx[1].toLowerCase());
|
||||
const day = +rx[2];
|
||||
|
||||
if (month < 0) return;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: new Date(year, month, day).toISOString(),
|
||||
url: chapterUrl,
|
||||
});
|
||||
});
|
||||
|
||||
novel.chapters = chapter.reverse();
|
||||
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(chapterUrl, { headers });
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let chapterText = loadedCheerio(".epcontent").html() || "";
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo: number
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}page/${pageNo}/?s=${searchTerm}`;
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(url, { headers });
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return await fetchFile(url);
|
||||
}
|
||||
|
||||
filters = {
|
||||
order: {
|
||||
label: "Sort By",
|
||||
options: [
|
||||
{ label: "Default", value: "" },
|
||||
{ label: "A-Z", value: "title" },
|
||||
{ label: "Z-A", value: "titlereverse" },
|
||||
{ label: "Latest Update", value: "update" },
|
||||
{ label: "Latest Added", value: "latest" },
|
||||
{ label: "Popular", value: "popular" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
value: "",
|
||||
},
|
||||
|
||||
status: {
|
||||
label: "Status",
|
||||
options: [
|
||||
{ label: "All", value: "" },
|
||||
{ label: "Ongoing", value: "ongoing" },
|
||||
{ label: "Hiatus", value: "hiatus" },
|
||||
{ label: "Completed", value: "completed" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
value: "",
|
||||
},
|
||||
type: {
|
||||
label: "Type",
|
||||
options: [
|
||||
{ label: "Chinese novel", value: "chinese-novel" },
|
||||
{ label: "habyeol", value: "habyeol" },
|
||||
{ label: "korean novel", value: "korean-novel" },
|
||||
{ label: "Web Novel", value: "web-novel" },
|
||||
{ label: "삼심", value: "%ec%82%bc%ec%8b%ac" },
|
||||
{ label: "호곡", value: "%ed%98%b8%ea%b3%a1" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
value: [],
|
||||
},
|
||||
genres: {
|
||||
label: "Genres",
|
||||
options: [
|
||||
{ label: "A.I", value: "a.i" },
|
||||
{ label: "Academy", value: "academy" },
|
||||
{ label: "Action", value: "action" },
|
||||
{ label: "Adult", value: "adult" },
|
||||
{ label: "Adventure", value: "adventure" },
|
||||
{ label: "Alternative History", value: "alternative-history" },
|
||||
{ label: "Another World", value: "another-world" },
|
||||
{ label: "Apocalypse", value: "apocalypse" },
|
||||
{ label: "Bromance", value: "bromance" },
|
||||
{ label: "Comedy", value: "comedy" },
|
||||
{ label: "Dark fantasy", value: "dark-fantasy" },
|
||||
{ label: "Demons", value: "demons" },
|
||||
{ label: "Drama", value: "drama" },
|
||||
{ label: "Dystopia", value: "dystopia" },
|
||||
{ label: "Ecchi", value: "ecchi" },
|
||||
{ label: "Entertainment", value: "entertainment" },
|
||||
{ label: "Exhaustion", value: "exhaustion" },
|
||||
{ label: "Fanfiction", value: "fanfiction" },
|
||||
{ label: "fantasy", value: "fantasy" },
|
||||
{ label: "finance", value: "finance" },
|
||||
{ label: "Full color", value: "full-color" },
|
||||
{ label: "Game", value: "game" },
|
||||
{ label: "Gender Bender", value: "gender-bender" },
|
||||
{ label: "Genius", value: "genius" },
|
||||
{ label: "Harem", value: "harem" },
|
||||
{ label: "Hero", value: "hero" },
|
||||
{ label: "Historical", value: "historical" },
|
||||
{ label: "Hunter", value: "hunter" },
|
||||
{ label: "korean novel", value: "korean-novel" },
|
||||
{ label: "Light Novel", value: "light-novel" },
|
||||
{
|
||||
label: "List Adventure Manga Genres",
|
||||
value: "list-adventure-manga-genres",
|
||||
},
|
||||
{ label: "Long Strip", value: "long-strip" },
|
||||
{ label: "Love comedy", value: "love-comedy" },
|
||||
{ label: "magic", value: "magic" },
|
||||
{ label: "Manhua", value: "manhua" },
|
||||
{ label: "Martial Arts", value: "martial-arts" },
|
||||
{ label: "Mature", value: "mature" },
|
||||
{ label: "Medieval", value: "medieval" },
|
||||
{ label: "Misunderstanding", value: "misunderstanding" },
|
||||
{ label: "Modern", value: "modern" },
|
||||
{ label: "modern fantasy", value: "modern-fantasy" },
|
||||
{ label: "music", value: "music" },
|
||||
{ label: "Mystery", value: "mystery" },
|
||||
{ label: "Necromancy", value: "necromancy" },
|
||||
{ label: "No Romance", value: "no-romance" },
|
||||
{ label: "NTL", value: "ntl" },
|
||||
{ label: "o", value: "o" },
|
||||
{ label: "Obsession", value: "obsession" },
|
||||
{ label: "Politics", value: "politics" },
|
||||
{ label: "Possession", value: "possession" },
|
||||
{ label: "Programming", value: "programming" },
|
||||
{ label: "Psychological", value: "psychological" },
|
||||
{ label: "Pure Love", value: "pure-love" },
|
||||
{ label: "Redemption", value: "redemption" },
|
||||
{ label: "Regression", value: "regression" },
|
||||
{ label: "Regret", value: "regret" },
|
||||
{ label: "Reincarnation", value: "reincarnation" },
|
||||
{ label: "Revenge", value: "revenge" },
|
||||
{ label: "Romance", value: "romance" },
|
||||
{ label: "Romance Fanrasy", value: "romance-fanrasy" },
|
||||
{ label: "Salvation", value: "salvation" },
|
||||
{ label: "School Life", value: "school-life" },
|
||||
{ label: "Sci-fi", value: "sci-fi" },
|
||||
{ label: "Science fiction", value: "science-fiction" },
|
||||
{ label: "Seinen", value: "seinen" },
|
||||
{ label: "Shounen", value: "shounen" },
|
||||
{ label: "Slice of Life", value: "slice-of-life" },
|
||||
{ label: "Soft yandere", value: "soft-yandere" },
|
||||
{ label: "Sports", value: "sports" },
|
||||
{ label: "Supernatural", value: "supernatural" },
|
||||
{ label: "Survival", value: "survival" },
|
||||
{ label: "system", value: "system" },
|
||||
{ label: "Time limit", value: "time-limit" },
|
||||
{ label: "Tragedy", value: "tragedy" },
|
||||
{ label: "Transmigration", value: "transmigration" },
|
||||
{ label: "TS", value: "ts" },
|
||||
{ label: "Tsundere", value: "tsundere" },
|
||||
{ label: "Unique", value: "unique" },
|
||||
{ label: "Wholesome", value: "wholesome" },
|
||||
{ label: "Wuxia", value: "wuxia" },
|
||||
{ label: "Xuanhuan", value: "xuanhuan" },
|
||||
{ label: "Yandere", value: "yandere" },
|
||||
{ label: "Yuri", value: "yuri" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
value: [],
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new NobleMTL();
|
||||
+134
-65
@@ -1,29 +1,21 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { CheerioAPI, load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { Filters } from "@libs/filterInputs";
|
||||
import { Filters, FilterTypes } from "@libs/filterInputs";
|
||||
|
||||
class NovelBin implements Plugin.PluginBase {
|
||||
id = "novelbin";
|
||||
name = "Novel Bin";
|
||||
icon = "src/en/novelbin/icon.png";
|
||||
site = "https://novelmax.net/";
|
||||
filters?: Filters | undefined;
|
||||
site = "https://binnovel.com/";
|
||||
version = "1.0.0";
|
||||
baseUrl = this.site;
|
||||
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}sort/p/?page=${pageNo}`;
|
||||
parseNovels(loadedCheerio:CheerioAPI){
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
const body = await fetchApi(url).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".col-novel-main .list-novel .row").each(function () {
|
||||
const novelName = loadedCheerio(this).find("h3.novel-title > a").text();
|
||||
const novelCover = loadedCheerio(this).find("img.cover").attr("src");
|
||||
const novelUrl = loadedCheerio(this)
|
||||
loadedCheerio(".col-novel-main .list-novel .row").each((i,el) => {
|
||||
const novelName = loadedCheerio(el).find("h3.novel-title > a").text();
|
||||
const novelCover = loadedCheerio(el).find("img.cover").attr("src");
|
||||
const novelUrl = loadedCheerio(el)
|
||||
.find("h3.novel-title > a")
|
||||
.attr("href");
|
||||
|
||||
@@ -32,7 +24,7 @@ class NovelBin implements Plugin.PluginBase {
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
path: novelUrl.replace(this.site, ''),
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
@@ -40,23 +32,38 @@ class NovelBin implements Plugin.PluginBase {
|
||||
|
||||
return novels;
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const body = await fetchApi(url).then((r) => r.text());
|
||||
|
||||
async popularNovels(page: number, {filters}: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
let link = this.site;
|
||||
if (filters.genre.value !== '')
|
||||
link += `gen/${filters.genre.value}`;
|
||||
else
|
||||
link += `sort/${filters.sort.value}`;
|
||||
|
||||
if (filters.complete.value)
|
||||
link += `/completed`;
|
||||
|
||||
link += `?page=${page}`;
|
||||
|
||||
const body = await fetchApi(link).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
path: novelPath,
|
||||
name: loadedCheerio("div.book > img").attr("alt") || "Untitled",
|
||||
cover: loadedCheerio("div.book > img").attr("src"),
|
||||
summary: loadedCheerio("div.desc-text").text().trim(),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio("div.book > img").attr("alt");
|
||||
|
||||
novel.cover = loadedCheerio("div.book > img").attr("src");
|
||||
|
||||
novel.summary = loadedCheerio("div.desc-text").text().trim();
|
||||
|
||||
loadedCheerio("ul.info > li > h3").each(function () {
|
||||
let detailName = loadedCheerio(this).text();
|
||||
let detail = loadedCheerio(this)
|
||||
@@ -82,26 +89,24 @@ class NovelBin implements Plugin.PluginBase {
|
||||
|
||||
const getChapters = async (id: string) => {
|
||||
const chapterListUrl =
|
||||
this.baseUrl + "ajax/chapter-archive?novelId=" + id;
|
||||
this.site + "ajax/chapter-archive?novelId=" + id;
|
||||
|
||||
const data = await fetchApi(chapterListUrl);
|
||||
const chapterdata = await data.text();
|
||||
|
||||
loadedCheerio = parseHTML(chapterdata);
|
||||
|
||||
let chapter: Plugin.ChapterItem[] = [];
|
||||
const chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio("ul.list-chapter > li").each(function () {
|
||||
const chapterName = loadedCheerio(this).find("a").attr("title");
|
||||
const releaseDate = null;
|
||||
const chapterUrl = loadedCheerio(this).find("a").attr("href");
|
||||
loadedCheerio("ul.list-chapter > li").each((i,el) => {
|
||||
const chapterName = loadedCheerio(el).find("a").attr("title");
|
||||
const chapterUrl = loadedCheerio(el).find("a").attr("href");
|
||||
|
||||
if (!chapterName || !chapterUrl) return;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
path: chapterUrl.replace(this.site, ''),
|
||||
});
|
||||
});
|
||||
return chapter;
|
||||
@@ -113,50 +118,114 @@ class NovelBin implements Plugin.PluginBase {
|
||||
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const body = await fetchApi(chapterUrl).then((r) => r.text());
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
loadedCheerio('#chr-content > div,h6,p[style="display: none;"]').remove();
|
||||
let chapterText = loadedCheerio("#chr-content").html() || '';
|
||||
const chapterText = loadedCheerio("#chr-content").html() || '';
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}search/?keyword=${searchTerm}`;
|
||||
const url = `${this.site}search/?keyword=${searchTerm}`;
|
||||
const body = await fetchApi(url).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio("div.col-novel-main > div.list-novel > .row").each(
|
||||
function () {
|
||||
const novelUrl = loadedCheerio(this)
|
||||
.find("h3.novel-title > a")
|
||||
.attr("href");
|
||||
|
||||
const novelName = loadedCheerio(this)
|
||||
.find("h3.novel-title > a")
|
||||
.text();
|
||||
const novelCover = loadedCheerio(this).find("img").attr("src");
|
||||
|
||||
if (novelUrl) {
|
||||
novels.push({
|
||||
url: novelUrl,
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return novels;
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url);
|
||||
}
|
||||
|
||||
filters = {
|
||||
sort: {
|
||||
value: "popular-novels",
|
||||
label: "Sort by",
|
||||
options: [
|
||||
{ label: "Latest Release", value: "update" },
|
||||
{ label: "Hot Novel", value: "hot-novels" },
|
||||
{ label: "Completed Novel", value: "c" },
|
||||
{ label: "Most Popular", value: "popular-novels" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
genre: {
|
||||
value: "",
|
||||
label: "Genres",
|
||||
options: [
|
||||
{ label: "None", value: "" },
|
||||
{ label: "Action", value: "action" },
|
||||
{ label: "Adult", value: "adult" },
|
||||
{ label: "Adventure", value: "adventure" },
|
||||
{ label: "Anime", value: "anime" },
|
||||
{ label: "Arts", value: "arts" },
|
||||
{ label: "Comedy", value: "comedy" },
|
||||
{ label: "Drama", value: "drama" },
|
||||
{ label: "Eastern", value: "eastern" },
|
||||
{ label: "Ecchi", value: "ecchi" },
|
||||
{ label: "Fan-fiction", value: "fan-fiction" },
|
||||
{ label: "Fanfiction", value: "fanfiction" },
|
||||
{ label: "Fantas", value: "fantas" },
|
||||
{ label: "Fantasy", value: "fantasy" },
|
||||
{ label: "Game", value: "game" },
|
||||
{ label: "Gender bender", value: "gender-bender" },
|
||||
{ label: "Harem", value: "harem" },
|
||||
{ label: "Hentai", value: "hentai" },
|
||||
{ label: "Historical", value: "historical" },
|
||||
{ label: "Horror", value: "horror" },
|
||||
{ label: "Isekai", value: "isekai" },
|
||||
{ label: "Josei", value: "josei" },
|
||||
{ label: "Lgbt+", value: "lgbt+" },
|
||||
{ label: "Litrpg", value: "litrpg" },
|
||||
{ label: "Magic", value: "magic" },
|
||||
{ label: "Magical realism", value: "magical-realism" },
|
||||
{ label: "Manhua", value: "manhua" },
|
||||
{ label: "Martial arts", value: "martial-arts" },
|
||||
{ label: "Mature", value: "mature" },
|
||||
{ label: "Mecha", value: "mecha" },
|
||||
{ label: "Military", value: "military" },
|
||||
{ label: "Modern life", value: "modern-life" },
|
||||
{ label: "Movies", value: "movies" },
|
||||
{ label: "Mystery", value: "mystery" },
|
||||
{ label: "Portal fantasy", value: "portal-fantasy" },
|
||||
{ label: "Psychological", value: "psychological" },
|
||||
{ label: "Realistic fiction", value: "realistic-fiction" },
|
||||
{ label: "Reincarnation", value: "reincarnation" },
|
||||
{ label: "Romance", value: "romance" },
|
||||
{ label: "School life", value: "school-life" },
|
||||
{ label: "Sci-fi", value: "sci-fi" },
|
||||
{ label: "Seinen", value: "seinen" },
|
||||
{ label: "Shoujo", value: "shoujo" },
|
||||
{ label: "Shoujo ai", value: "shoujo-ai" },
|
||||
{ label: "Shounen", value: "shounen" },
|
||||
{ label: "Shounen ai", value: "shounen-ai" },
|
||||
{ label: "Slice of life", value: "slice-of-life" },
|
||||
{ label: "Smut", value: "smut" },
|
||||
{ label: "Sports", value: "sports" },
|
||||
{ label: "Supernatural", value: "supernatural" },
|
||||
{ label: "System", value: "system" },
|
||||
{ label: "Tragedy", value: "tragedy" },
|
||||
{ label: "Urban life", value: "urban-life" },
|
||||
{ label: "Video games", value: "video-games" },
|
||||
{ label: "Wuxia", value: "wuxia" },
|
||||
{ label: "Xianxia", value: "xianxia" },
|
||||
{ label: "Xuanhuan", value: "xuanhuan" },
|
||||
{ label: "Yaoi", value: "yaoi" },
|
||||
{ label: "Yuri", value: "yuri" }
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
complete: {
|
||||
label: "Show Completed Novels Only",
|
||||
value: false,
|
||||
type: FilterTypes.Switch,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new NovelBin();
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import { CheerioAPI, load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { Filters } from "@libs/filterInputs";
|
||||
|
||||
class NovelBuddy implements Plugin.PluginBase {
|
||||
id = "novelbuddy";
|
||||
name = "NovelBuddy.io";
|
||||
site = "https://novelbuddy.io/";
|
||||
version = "1.0.0";
|
||||
icon = "src/en/novelbuddy/icon.png";
|
||||
filters?: undefined; //TODO: Filters, Filters in Filters required
|
||||
|
||||
parseNovels(loadedCheerio: CheerioAPI){
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".book-item").each((idx, ele) => {
|
||||
const novelName = loadedCheerio(ele).find(".title").text();
|
||||
const novelCover =
|
||||
"https:" + loadedCheerio(ele).find("img").attr("data-src");
|
||||
const novelUrl = loadedCheerio(ele).find(".title a").attr("href")?.substring(1);
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = { name: novelName, cover: novelCover, path: novelUrl };
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
options: Plugin.PopularNovelsOptions
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}popular?page=${pageNo}`;
|
||||
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(this.site + novelPath);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: loadedCheerio(".name h1").text().trim() || "Untitled",
|
||||
cover: "https:" + loadedCheerio(".img-cover img").attr("data-src"),
|
||||
summary: loadedCheerio('.section-body.summary').text().trim(),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
loadedCheerio('.meta.box p').each((i,el) => {
|
||||
const detailName = loadedCheerio(el).find('strong').text();
|
||||
const detail = loadedCheerio(el).find('a');
|
||||
|
||||
switch(detailName){
|
||||
case "Authors :":
|
||||
novel.author = detail.find('span')
|
||||
.map((a,ex) => loadedCheerio(ex).text())
|
||||
.toArray()
|
||||
.join(', ');
|
||||
break;
|
||||
case "Status :":
|
||||
novel.status = detail.text();
|
||||
break;
|
||||
case "Genres :":
|
||||
novel.genres = detail.text().trim();
|
||||
break;
|
||||
}
|
||||
})
|
||||
const novelId = loadedCheerio('script')
|
||||
.text()
|
||||
.match(/bookId = (\d+);/)![1];
|
||||
const chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
const getChapters = async (id: string) => {
|
||||
const chapterListUrl = `${this.site}api/manga/${id}/chapters?source=detail`;
|
||||
const data = await fetchApi(chapterListUrl);
|
||||
const chapterlist = await data.text();
|
||||
console.log(chapterlist);
|
||||
loadedCheerio = parseHTML(chapterlist);
|
||||
|
||||
loadedCheerio("li").each((i,el) => {
|
||||
const chapterName = loadedCheerio(el)
|
||||
.find(".chapter-title")
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
const releaseDate = loadedCheerio(el)
|
||||
.find(".chapter-update")
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
const months = [
|
||||
"jan",
|
||||
"feb",
|
||||
"mar",
|
||||
"apr",
|
||||
"may",
|
||||
"jun",
|
||||
"jul",
|
||||
"aug",
|
||||
"sep",
|
||||
"oct",
|
||||
"nov",
|
||||
"dec",
|
||||
].join("|");
|
||||
const rx = new RegExp(`(${months}) (\\d{1,2}), (\\d{4})`,'i')
|
||||
.exec(releaseDate);
|
||||
if (!rx) return;
|
||||
const year = +rx[3];
|
||||
const month = months.indexOf(rx[1].toLowerCase());
|
||||
const day = +rx[2];
|
||||
|
||||
const chapterUrl = loadedCheerio(el).find("a").attr("href")?.slice(1);
|
||||
|
||||
if (!chapterUrl) return;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: new Date(year, month, day).toISOString(),
|
||||
path: chapterUrl,
|
||||
});
|
||||
});
|
||||
return chapter;
|
||||
}
|
||||
|
||||
novel.chapters = (await getChapters(novelId)).reverse();
|
||||
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(this.site + chapterPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
loadedCheerio("#listen-chapter").remove();
|
||||
loadedCheerio("#google_translate_element").remove();
|
||||
|
||||
const chapterText = loadedCheerio(".chapter__content").html() || '';
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}search?q=${searchTerm}&page=${page}`;
|
||||
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new NovelBuddy();
|
||||
@@ -1,167 +0,0 @@
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Chapter, Novel, Plugin } from "@typings/plugin";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
|
||||
export const id = "novelforest";
|
||||
export const name = "Novel Forest (Broken)";
|
||||
export const version = "1.0.0";
|
||||
export const icon = "src/en/novelforest/icon.png";
|
||||
export const site = "https://novelforest.com/";
|
||||
|
||||
|
||||
const pluginId = id;
|
||||
const baseUrl = site;
|
||||
|
||||
export const popularNovels: Plugin.popularNovels = async function (page) {
|
||||
const url = `${baseUrl}popular?page=${page}`;
|
||||
|
||||
const body = await fetchApi(url, {}, pluginId).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Novel.Item[] = [];
|
||||
|
||||
loadedCheerio(".book-item").each(function () {
|
||||
const novelName = loadedCheerio(this).find(".title").text();
|
||||
const novelCover =
|
||||
"https:" + loadedCheerio(this).find("img").attr("data-src");
|
||||
const novelUrl =
|
||||
baseUrl +
|
||||
loadedCheerio(this).find(".title a").attr("href")?.substring(1);
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
export const parseNovelAndChapters: Plugin.parseNovelAndChapters = async (
|
||||
novelUrl
|
||||
) => {
|
||||
const url = novelUrl;
|
||||
|
||||
const body = await fetchApi(url, {}, pluginId).then((r) => r.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Novel.instance = {
|
||||
url,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio(".name h1").text().trim();
|
||||
|
||||
novel.cover = "https:" + loadedCheerio(".img-cover img").attr("data-src");
|
||||
|
||||
novel.summary = loadedCheerio(
|
||||
"body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.mt-1 > div.section.box.mt-1.summary > div.section-body > p.content"
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
novel.author = loadedCheerio(
|
||||
"body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(1) > a > span"
|
||||
)
|
||||
.text()
|
||||
?.trim();
|
||||
|
||||
novel.status = loadedCheerio(
|
||||
"body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(2) > a > span"
|
||||
)
|
||||
.text()
|
||||
?.trim();
|
||||
|
||||
novel.genres = loadedCheerio(
|
||||
"body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(3)"
|
||||
)
|
||||
.text()
|
||||
?.replace("Genres :", "")
|
||||
.replace(/[\s\n]+/g, " ")
|
||||
.trim();
|
||||
|
||||
let chapter: Chapter.Item[] = [];
|
||||
|
||||
const chaptersUrl =
|
||||
novelUrl.replace(baseUrl, "https://novelforest.com/api/novels/") +
|
||||
"/chapters?source=detail";
|
||||
|
||||
const chaptersRequest = await fetch(chaptersUrl);
|
||||
const chaptersHtml = await chaptersRequest.text();
|
||||
|
||||
loadedCheerio = parseHTML(chaptersHtml);
|
||||
|
||||
loadedCheerio("li").each(function () {
|
||||
const chapterName = loadedCheerio(this)
|
||||
.find(".chapter-title")
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
const releaseDate = loadedCheerio(this)
|
||||
.find(".chapter-update")
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
const chapterUrl =
|
||||
baseUrl + loadedCheerio(this).find("a").attr("href")?.substring(1);
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
});
|
||||
});
|
||||
|
||||
novel.chapters = chapter;
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
export const parseChapter: Plugin.parseChapter = async function (chapterUrl) {
|
||||
const body = await fetchApi(chapterUrl, {}, pluginId).then((r) => r.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
loadedCheerio("#listen-chapter").remove();
|
||||
loadedCheerio("#google_translate_element").remove();
|
||||
|
||||
const chapterText = loadedCheerio(".chapter__content").html();
|
||||
|
||||
return chapterText;
|
||||
};
|
||||
|
||||
export const searchNovels: Plugin.searchNovels = async function (searchTerm) {
|
||||
const url = `${baseUrl}search?q=${searchTerm}`;
|
||||
|
||||
const body = await fetchApi(url, {}, pluginId).then((r) => r.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Novel.Item[] = [];
|
||||
|
||||
loadedCheerio(".book-item").each(function () {
|
||||
const novelName = loadedCheerio(this).find(".title").text();
|
||||
const novelCover =
|
||||
"https:" + loadedCheerio(this).find("img").attr("data-src");
|
||||
const novelUrl =
|
||||
baseUrl +
|
||||
loadedCheerio(this).find(".title a").attr("href")?.substring(1);
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
export const fetchImage: Plugin.fetchImage = fetchFile;
|
||||
+107
-75
@@ -1,7 +1,7 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { CheerioAPI, load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { Filters } from "@libs/filterInputs";
|
||||
import { Filters, FilterTypes } from "@libs/filterInputs";
|
||||
|
||||
|
||||
class NovelFull implements Plugin.PluginBase {
|
||||
@@ -10,16 +10,9 @@ class NovelFull implements Plugin.PluginBase {
|
||||
version = "1.0.0";
|
||||
icon = "src/en/novelfull/icon.png";
|
||||
site = "https://novelfull.com/";
|
||||
baseUrl = this.site;
|
||||
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}most-popular?page=${pageNo}`;
|
||||
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
parseNovels(loadedCheerio:CheerioAPI){
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".col-truyen-main .list-truyen .row").each((idx, ele) => {
|
||||
const novelName = loadedCheerio(ele)
|
||||
@@ -27,19 +20,19 @@ class NovelFull implements Plugin.PluginBase {
|
||||
.text();
|
||||
|
||||
const novelCover =
|
||||
this.baseUrl + loadedCheerio(ele).find("img").attr("src")?.slice(1);
|
||||
this.site + loadedCheerio(ele).find("img").attr("src")?.slice(1);
|
||||
|
||||
const novelUrl =
|
||||
this.baseUrl +
|
||||
loadedCheerio(ele)
|
||||
.find("h3.truyen-title > a")
|
||||
.attr("href")
|
||||
?.slice(1);
|
||||
const novelUrl = loadedCheerio(ele)
|
||||
.find("h3.truyen-title > a")
|
||||
.attr("href")
|
||||
?.slice(1);
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
path: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
@@ -47,25 +40,37 @@ class NovelFull implements Plugin.PluginBase {
|
||||
|
||||
return novels;
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
|
||||
const result = await fetchApi(url);
|
||||
async popularNovels(page: number, {filters}: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
let link = this.site;
|
||||
if (filters.genre.value !== '')
|
||||
link += `genre/${filters.genre.value}`;
|
||||
else
|
||||
link += filters.sort.value
|
||||
|
||||
link += `?page=${page}`;
|
||||
|
||||
const body = await fetchApi(link).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(this.site + novelPath);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
path: novelPath,
|
||||
name: loadedCheerio("div.book > img").attr("alt") || "Untitled",
|
||||
cover: this.site + loadedCheerio("div.book > img").attr("src"),
|
||||
summary: loadedCheerio("div.desc-text").text().trim(),
|
||||
status: loadedCheerio('h3:contains("Status")').next().text(),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio("div.book > img").attr("alt");
|
||||
|
||||
novel.cover = this.baseUrl + loadedCheerio("div.book > img").attr("src");
|
||||
|
||||
novel.summary = loadedCheerio("div.desc-text").text().trim();
|
||||
|
||||
novel.author = loadedCheerio('h3:contains("Author")')
|
||||
.parent()
|
||||
.contents()
|
||||
@@ -78,91 +83,118 @@ class NovelFull implements Plugin.PluginBase {
|
||||
.toArray()
|
||||
.join(",");
|
||||
|
||||
novel.status = loadedCheerio('h3:contains("Status")').next().text();
|
||||
|
||||
const novelId = loadedCheerio("#rating").attr("data-novel-id")!;
|
||||
const chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
async function getChapters(id: string, baseUrl: string) {
|
||||
const chapterListUrl =
|
||||
baseUrl + "ajax/chapter-option?novelId=" + id;
|
||||
const getChapters = async (id: string) => {
|
||||
const chapterListUrl = this.site + "ajax/chapter-option?novelId=" + id;
|
||||
|
||||
const data = await fetchApi(chapterListUrl);
|
||||
const chapterlist = await data.text();
|
||||
|
||||
loadedCheerio = parseHTML(chapterlist);
|
||||
|
||||
let chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio("select > option").each(function () {
|
||||
const chapterName = loadedCheerio(this).text();
|
||||
const releaseDate = null;
|
||||
const chapterUrl =
|
||||
baseUrl + loadedCheerio(this).attr("value")?.slice(1);
|
||||
const chapterUrl = loadedCheerio(this).attr("value")?.slice(1);
|
||||
if (!chapterUrl) return;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
path: chapterUrl,
|
||||
});
|
||||
});
|
||||
return chapter;
|
||||
}
|
||||
|
||||
novel.chapters = await getChapters(novelId, this.baseUrl);
|
||||
novel.chapters = await getChapters(novelId);
|
||||
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(chapterUrl);
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(this.site + chapterPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
loadedCheerio("#chapter-content div.ads").remove();
|
||||
let chapterText = loadedCheerio("#chapter-content").html() || '';
|
||||
const chapterText = loadedCheerio("#chapter-content").html() || '';
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const searchUrl = `${this.baseUrl}search?keyword=${searchTerm}`;
|
||||
|
||||
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
|
||||
const searchUrl = `${this.site}search?keyword=${searchTerm}&page=${page}`;
|
||||
|
||||
const result = await fetchApi(searchUrl);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".col-truyen-main .list-truyen .row").each((idx, ele) => {
|
||||
const novelName = loadedCheerio(ele)
|
||||
.find("h3.truyen-title > a")
|
||||
.text();
|
||||
|
||||
const novelCover =
|
||||
this.baseUrl + loadedCheerio(ele).find("img").attr("src")?.slice(1);
|
||||
|
||||
const novelUrl =
|
||||
this.baseUrl +
|
||||
loadedCheerio(ele)
|
||||
.find("h3.truyen-title > a")
|
||||
.attr("href")
|
||||
?.slice(1);
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url);
|
||||
}
|
||||
|
||||
filters = {
|
||||
sort: {
|
||||
value: "most-popular",
|
||||
label: "Sort by",
|
||||
options: [
|
||||
{ label: "Latest Release", value: "latest-release-novel" },
|
||||
{ label: "Hot Novel", value: "hot-novel" },
|
||||
{ label: "Completed Novel", value: "completed-novel" },
|
||||
{ label: "Most Popular", value: "most-popular" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
genre: {
|
||||
value: "",
|
||||
label: "Genres",
|
||||
options: [
|
||||
{ label: "None", value: "" },
|
||||
{ label: "Shounen", value: "Shounen" },
|
||||
{ label: "Harem", value: "Harem" },
|
||||
{ label: "Comedy", value: "Comedy" },
|
||||
{ label: "Martial Arts", value: "Martial+Arts" },
|
||||
{ label: "School Life", value: "School+Life" },
|
||||
{ label: "Mystery", value: "Mystery" },
|
||||
{ label: "Shoujo", value: "Shoujo" },
|
||||
{ label: "Romance", value: "Romance" },
|
||||
{ label: "Sci-fi", value: "Sci-fi" },
|
||||
{ label: "Gender Bender", value: "Gender+Bender" },
|
||||
{ label: "Mature", value: "Mature" },
|
||||
{ label: "Fantasy", value: "Fantasy" },
|
||||
{ label: "Horror", value: "Horror" },
|
||||
{ label: "Drama", value: "Drama" },
|
||||
{ label: "Tragedy", value: "Tragedy" },
|
||||
{ label: "Supernatural", value: "Supernatural" },
|
||||
{ label: "Ecchi", value: "Ecchi" },
|
||||
{ label: "Xuanhuan", value: "Xuanhuan" },
|
||||
{ label: "Adventure", value: "Adventure" },
|
||||
{ label: "Action", value: "Action" },
|
||||
{ label: "Psychological", value: "Psychological" },
|
||||
{ label: "Xianxia", value: "Xianxia" },
|
||||
{ label: "Wuxia", value: "Wuxia" },
|
||||
{ label: "Historical", value: "Historical" },
|
||||
{ label: "Slice of Life", value: "Slice+of+Life" },
|
||||
{ label: "Seinen", value: "Seinen" },
|
||||
{ label: "Lolicon", value: "Lolicon" },
|
||||
{ label: "Adult", value: "Adult" },
|
||||
{ label: "Josei", value: "Josei" },
|
||||
{ label: "Sports", value: "Sports" },
|
||||
{ label: "Smut", value: "Smut" },
|
||||
{ label: "Mecha", value: "Mecha" },
|
||||
{ label: "Yaoi", value: "Yaoi" },
|
||||
{ label: "Shounen Ai", value: "Shounen+Ai" },
|
||||
{ label: "History", value: "History" },
|
||||
{ label: "Martial", value: "Martial" }
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new NovelFull();
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { load, load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { Filters } from "@libs/filterInputs";
|
||||
import { defaultCover } from "@libs/defaultCover";
|
||||
|
||||
class NovelHall implements Plugin.PluginBase {
|
||||
id = "novelhall";
|
||||
name = "Novel Hall";
|
||||
version = "1.0.0";
|
||||
icon = "src/en/novelhall/icon.png";
|
||||
filters?: Filters | undefined;
|
||||
filters?: Filters | undefined; //TODO: Filters Requires hideOnSelect
|
||||
site = "https://novelhall.com/";
|
||||
baseUrl = this.site;
|
||||
|
||||
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}all2022-${pageNo}.html`;
|
||||
async popularNovels(page: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}all2022-${page}.html`;
|
||||
|
||||
const body = await fetchApi(url).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio("li.btm").each((idx, ele) => {
|
||||
const novelName = loadedCheerio(ele).text().trim()
|
||||
const novelUrl =
|
||||
this.baseUrl + loadedCheerio(ele).find("a").attr("href");
|
||||
const novelUrl = loadedCheerio(ele).find("a").attr("href");
|
||||
if(!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
url: novelUrl,
|
||||
cover: defaultCover,
|
||||
path: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
@@ -36,77 +37,66 @@ class NovelHall implements Plugin.PluginBase {
|
||||
|
||||
return novels;
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
|
||||
|
||||
const body = await fetchApi(url).then((r) => r.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
path: novelPath,
|
||||
name: loadedCheerio(".book-info > h1").text() || 'Untitled',
|
||||
cover: loadedCheerio('meta[property="og:image"]').attr('content'),
|
||||
summary: loadedCheerio(".intro").text().trim(),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio(".booknav2 > h1").text();
|
||||
|
||||
novel.cover = loadedCheerio('meta[property="og:image"]').attr(
|
||||
"content"
|
||||
);
|
||||
|
||||
novel.summary = loadedCheerio(".navtxt").text().trim();
|
||||
|
||||
novel.author = loadedCheerio('p:contains("Author")')
|
||||
loadedCheerio('.total').find('p').remove();
|
||||
novel.author = loadedCheerio('.total span:contains("Author")')
|
||||
.text()
|
||||
.replace("Author:", "")
|
||||
.trim();
|
||||
|
||||
novel.status = loadedCheerio('p:contains("Status")')
|
||||
novel.status = loadedCheerio('.total span:contains("Status")')
|
||||
.text()
|
||||
.replace("Status:", "")
|
||||
.replace("Active", "Ongoing")
|
||||
.trim();
|
||||
|
||||
novel.genres = loadedCheerio('p:contains("Genre")')
|
||||
.text()
|
||||
?.replace("Genre:", "")
|
||||
.trim();
|
||||
novel.genres = loadedCheerio('.total a')
|
||||
.map((a,ex) => loadedCheerio(ex).text())
|
||||
.toArray()
|
||||
.join(',');
|
||||
|
||||
let chapter: Plugin.ChapterItem[] = [];
|
||||
const chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio("#morelist ul > li").each((idx, ele) => {
|
||||
const chapterName = loadedCheerio(ele).find("a").text().trim();
|
||||
const releaseDate = null;
|
||||
const chapterUrl =
|
||||
this.baseUrl + loadedCheerio(ele).find("a").attr("href");
|
||||
const chapterUrl = loadedCheerio(ele).find("a").attr("href");
|
||||
if (!chapterUrl) return;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
path: chapterUrl,
|
||||
});
|
||||
});
|
||||
|
||||
novel.chapters = chapter;
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const body = await fetchApi(chapterUrl).then((r) => r.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const chapterText = loadedCheerio(".content").html() || '';
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}index.php?s=so&module=book&keyword=${searchTerm}`;
|
||||
|
||||
const url = `${this.site}index.php?s=so&module=book&keyword=${searchTerm}`;
|
||||
const body = await fetchApi(url).then((r) => r.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio("#article_list_content > li").each((idx, ele) => {
|
||||
const novelName = loadedCheerio(ele)
|
||||
@@ -115,13 +105,13 @@ class NovelHall implements Plugin.PluginBase {
|
||||
.replace(/\t+/g, "")
|
||||
.replace(/\n/g, " ");
|
||||
const novelCover = loadedCheerio(ele).find("img").attr("data-src");
|
||||
const novelUrl =
|
||||
this.baseUrl + loadedCheerio(ele).find("a").attr("href")?.slice(1);
|
||||
const novelUrl = loadedCheerio(ele).find("a").attr("href")?.slice(1);
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
path: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
|
||||
@@ -25,7 +25,7 @@ class NovelUpdates implements Plugin.PluginBase {
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
path: novelUrl.replace(this.site, ''),
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
@@ -40,7 +40,7 @@ class NovelUpdates implements Plugin.PluginBase {
|
||||
filters.novelType.value.length ||
|
||||
filters.genres.value.include?.length ||
|
||||
filters.genres.value.exclude?.length ||
|
||||
filters.storyStatus.value.length ){
|
||||
filters.storyStatus.value !== '' ){
|
||||
link += "series-finder/?sf=1"
|
||||
} else if (showLatestNovels){
|
||||
link += "latest-series/?st=1"
|
||||
@@ -72,8 +72,7 @@ class NovelUpdates implements Plugin.PluginBase {
|
||||
|
||||
link += '&pg=' + page;
|
||||
|
||||
const headers = new Headers();
|
||||
const body = await fetchApi(link, { headers }).then((result) =>
|
||||
const body = await fetchApi(link).then((result) =>
|
||||
result.text()
|
||||
);
|
||||
|
||||
@@ -81,24 +80,21 @@ class NovelUpdates implements Plugin.PluginBase {
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const url = this.site + novelPath;
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
path: novelPath,
|
||||
name: loadedCheerio(".seriestitlenu").text() || "Untitled",
|
||||
cover: loadedCheerio(".seriesimg > img").attr("src"),
|
||||
author: loadedCheerio("#showauthors").text().trim(),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio(".seriestitlenu").text();
|
||||
|
||||
novel.cover = loadedCheerio(".seriesimg > img").attr("src");
|
||||
|
||||
novel.author = loadedCheerio("#showauthors").text().trim();
|
||||
|
||||
novel.genres = loadedCheerio("#seriesgenre")
|
||||
.children("a")
|
||||
.map((i, el) => loadedCheerio(el).text())
|
||||
@@ -109,22 +105,22 @@ class NovelUpdates implements Plugin.PluginBase {
|
||||
? "Ongoing"
|
||||
: "Completed";
|
||||
|
||||
let type = loadedCheerio("#showtype").text().trim();
|
||||
const type = loadedCheerio("#showtype").text().trim();
|
||||
|
||||
let summary = loadedCheerio("#editdescription").text().trim();
|
||||
const summary = loadedCheerio("#editdescription").text().trim();
|
||||
|
||||
novel.summary = summary + `\n\nType: ${type}`;
|
||||
|
||||
let chapter: Plugin.ChapterItem[] = [];
|
||||
const chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
const novelId = loadedCheerio("input#mypostid").attr("value")!;
|
||||
|
||||
let formData = new FormData();
|
||||
const formData = new FormData();
|
||||
formData.append("action", "nd_getchapters");
|
||||
formData.append("mygrr", "0");
|
||||
formData.append("mypostid", novelId);
|
||||
|
||||
let link = `${this.site}wp-admin/admin-ajax.php`;
|
||||
const link = `${this.site}wp-admin/admin-ajax.php`;
|
||||
|
||||
const text = await fetchApi(
|
||||
link,
|
||||
@@ -136,17 +132,15 @@ class NovelUpdates implements Plugin.PluginBase {
|
||||
|
||||
loadedCheerio = parseHTML(text);
|
||||
|
||||
loadedCheerio("li.sp_li_chp").each(function () {
|
||||
const chapterName = loadedCheerio(this).text().trim();
|
||||
const releaseDate = null;
|
||||
loadedCheerio("li.sp_li_chp").each((i,el) => {
|
||||
const chapterName = loadedCheerio(el).text().trim();
|
||||
const chapterUrl =
|
||||
"https:" +
|
||||
loadedCheerio(this).find("a").first().next().attr("href");
|
||||
loadedCheerio(el).find("a").first().next().attr("href");
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
path: chapterUrl.replace(this.site, ''),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -154,6 +148,7 @@ class NovelUpdates implements Plugin.PluginBase {
|
||||
|
||||
return novel;
|
||||
}
|
||||
|
||||
getLocation(href: string) {
|
||||
var match = href.match(
|
||||
/^(https?:)\/\/(([^:/?#]*)(?::([0-9]+))?)([/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/
|
||||
@@ -161,11 +156,11 @@ class NovelUpdates implements Plugin.PluginBase {
|
||||
return match && `${match[1]}//${match[3]}`;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
let chapterText = "";
|
||||
|
||||
const result = await fetchApi(
|
||||
chapterUrl,
|
||||
this.site + chapterPath,
|
||||
);
|
||||
const body = await result.text();
|
||||
|
||||
@@ -175,29 +170,29 @@ class NovelUpdates implements Plugin.PluginBase {
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let isWuxiaWorld = chapterUrl.toLowerCase().includes("wuxiaworld");
|
||||
let isWuxiaWorld = result.url.toLowerCase().includes("wuxiaworld");
|
||||
|
||||
let isBlogspot = chapterUrl.toLowerCase().includes("blogspot");
|
||||
let isBlogspot = result.url.toLowerCase().includes("blogspot");
|
||||
|
||||
let isTumblr = chapterUrl.toLowerCase().includes("tumblr");
|
||||
let isTumblr = result.url.toLowerCase().includes("tumblr");
|
||||
|
||||
let isWattpad = chapterUrl.toLowerCase().includes("wattpad");
|
||||
let isWattpad = result.url.toLowerCase().includes("wattpad");
|
||||
|
||||
let isAnomalously = chapterUrl.toLowerCase().includes('anotivereads');
|
||||
let isAnomalously = result.url.toLowerCase().includes('anotivereads');
|
||||
|
||||
let isBlossomTranslation = chapterUrl
|
||||
let isBlossomTranslation = result.url
|
||||
.toLowerCase()
|
||||
.includes('blossomtranslation');
|
||||
|
||||
let isLightNovelsTls = chapterUrl
|
||||
let isLightNovelsTls = result.url
|
||||
.toLowerCase()
|
||||
.includes("lightnovelstranslations");
|
||||
|
||||
let isiNovelTranslation = chapterUrl
|
||||
let isiNovelTranslation = result.url
|
||||
.toLowerCase()
|
||||
.includes("inoveltranslation");
|
||||
|
||||
let isTravisTranslation = chapterUrl
|
||||
let isTravisTranslation = result.url
|
||||
.toLowerCase()
|
||||
.includes("travistranslations");
|
||||
|
||||
@@ -220,13 +215,13 @@ class NovelUpdates implements Plugin.PluginBase {
|
||||
.includes("wordpress");
|
||||
}
|
||||
|
||||
let isRainOfSnow = chapterUrl.toLowerCase().includes("rainofsnow");
|
||||
let isRainOfSnow = result.url.toLowerCase().includes("rainofsnow");
|
||||
|
||||
let isWebNovel = chapterUrl.toLowerCase().includes("webnovel");
|
||||
let isWebNovel = result.url.toLowerCase().includes("webnovel");
|
||||
|
||||
let isHostedNovel = chapterUrl.toLowerCase().includes("hostednovel");
|
||||
let isHostedNovel = result.url.toLowerCase().includes("hostednovel");
|
||||
|
||||
let isScribbleHub = chapterUrl.toLowerCase().includes("scribblehub");
|
||||
let isScribbleHub = result.url.toLowerCase().includes("scribblehub");
|
||||
|
||||
if (isWuxiaWorld) {
|
||||
chapterText = loadedCheerio("#chapter-content").html()!;
|
||||
@@ -254,7 +249,7 @@ class NovelUpdates implements Plugin.PluginBase {
|
||||
} else if (isAnomalously) {
|
||||
chapterText = loadedCheerio('#comic').html()!;
|
||||
} else if (isiNovelTranslation) {
|
||||
const link = 'https://api.' + chapterUrl.slice(8);
|
||||
const link = 'https://api.' + result.url.slice(8);
|
||||
const json = await fetchApi(link).then(r => r.json());
|
||||
chapterText = json.content.replace(/\n/g, '<br>');
|
||||
if (json.notes) {
|
||||
@@ -317,12 +312,13 @@ class NovelUpdates implements Plugin.PluginBase {
|
||||
*/
|
||||
chapterText = chapterText.replace(
|
||||
/href="\//g,
|
||||
`href="${this.getLocation(chapterUrl)}/`
|
||||
`href="${this.getLocation(result.url)}/`
|
||||
);
|
||||
}
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
|
||||
const url =`${this.site}page/${page}/?s=${searchTerm}&post_type=seriesplans`;
|
||||
const result = await fetchApi(url);
|
||||
@@ -376,7 +372,6 @@ class NovelUpdates implements Plugin.PluginBase {
|
||||
label: "Language",
|
||||
value: [],
|
||||
options: [
|
||||
{ label: "None", value: "" },
|
||||
{ label: "Chinese", value: "495" },
|
||||
{ label: "Filipino", value: "9181" },
|
||||
{ label: "Indonesian", value: "9179" },
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
|
||||
class PandaMTL implements Plugin.PluginBase {
|
||||
id = "pandamtl";
|
||||
name = "PandaMTL";
|
||||
icon = "src/en/wordpress/icon.png";
|
||||
site = "https://pandamtl.com/";
|
||||
version = "1.0.0";
|
||||
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = `${this.site}series/?page=${pageNo}`;
|
||||
|
||||
if (filters.genres.value.length)
|
||||
link += filters.genres.value.map((i) => `&genre[]=${i}`).join("");
|
||||
|
||||
if (filters.type.value.length)
|
||||
link += filters.type.value.map((i) => `&lang[]=${i}`).join("");
|
||||
|
||||
link += "&status=" + filters.status.value;
|
||||
link += "&order=" + filters.order.value;
|
||||
|
||||
const headers = new Headers();
|
||||
const body = await fetchApi(link, { headers }).then((result) =>
|
||||
result.text()
|
||||
);
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio("article.maindet").each(function () {
|
||||
const novelName = loadedCheerio(this).find("h2").text();
|
||||
let image = loadedCheerio(this).find("img");
|
||||
const novelCover = image.attr("data-src") || image.attr("src");
|
||||
const novelUrl = loadedCheerio(this).find("h2 a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(url, { headers });
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio("h1.entry-title").text();
|
||||
|
||||
novel.cover =
|
||||
loadedCheerio("img.wp-post-image").attr("data-src") ||
|
||||
loadedCheerio("img.wp-post-image").attr("src");
|
||||
|
||||
loadedCheerio(".serl:nth-child(3) > span").each(function () {
|
||||
const detailName = loadedCheerio(this).text().trim();
|
||||
const detail = loadedCheerio(this).next().text().trim();
|
||||
|
||||
switch (detailName) {
|
||||
case "Author":
|
||||
novel.author = detail;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
novel.status = loadedCheerio(".sertostat > span").attr("class");
|
||||
|
||||
novel.genres = loadedCheerio(".sertogenre")
|
||||
.children("a")
|
||||
.map((i, el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join(",");
|
||||
|
||||
novel.summary = loadedCheerio(".sersys")
|
||||
.find("br")
|
||||
.replaceWith("\n")
|
||||
.end()
|
||||
.text();
|
||||
|
||||
let chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio(".eplister")
|
||||
.find("li")
|
||||
.each(function () {
|
||||
const chapterName =
|
||||
loadedCheerio(this).find(".epl-num").text() +
|
||||
" - " +
|
||||
loadedCheerio(this).find(".epl-title").text();
|
||||
|
||||
const releaseDate = loadedCheerio(this)
|
||||
.find(".epl-date")
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
const chapterUrl = loadedCheerio(this).find("a").attr("href");
|
||||
|
||||
if (!chapterUrl) return;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
});
|
||||
});
|
||||
|
||||
novel.chapters = chapter.reverse();
|
||||
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(chapterUrl, { headers });
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let chapterText = loadedCheerio(".epcontent").html() || "";
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
pageNo?: number | undefined
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}?s=${searchTerm}`;
|
||||
const headers = new Headers();
|
||||
const result = await fetchApi(url, { headers });
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio("article.maindet").each(function () {
|
||||
const novelName = loadedCheerio(this).find("h2").text();
|
||||
let image = loadedCheerio(this).find("img");
|
||||
const novelCover = image.attr("data-src") || image.attr("src");
|
||||
const novelUrl = loadedCheerio(this).find("h2 a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
novels.push({
|
||||
name: novelName,
|
||||
url: novelUrl,
|
||||
cover: novelCover,
|
||||
});
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return await fetchFile(url);
|
||||
}
|
||||
|
||||
filters = {
|
||||
order: {
|
||||
label: "Sort By",
|
||||
value: "popular",
|
||||
options: [
|
||||
{ label: "Default", value: "" },
|
||||
{ label: "A-Z", value: "title" },
|
||||
{ label: "Z-A", value: "titlereverse" },
|
||||
{ label: "Latest Update", value: "update" },
|
||||
{ label: "Latest Added", value: "latest" },
|
||||
{ label: "Popular", value: "popular" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
status: {
|
||||
label: "Status",
|
||||
value: "",
|
||||
options: [
|
||||
{ label: "All", value: "" },
|
||||
{ label: "Ongoing", value: "ongoing" },
|
||||
{ label: "Hiatus", value: "hiatus" },
|
||||
{ label: "Completed", value: "completed" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
type: {
|
||||
value: [],
|
||||
label: "Type",
|
||||
options: [
|
||||
{ label: "Light Novel (KR)", value: "light-novel-kr" },
|
||||
{ label: "Web Novel", value: "web-novel" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
genres: {
|
||||
label: "Genres",
|
||||
value: [],
|
||||
options: [
|
||||
{ label: "Action", value: "action" },
|
||||
{ label: "Adult", value: "adult" },
|
||||
{ label: "Adventure", value: "adventure" },
|
||||
{ label: "Comedy", value: "comedy" },
|
||||
{ label: "Ecchi", value: "ecchi" },
|
||||
{ label: "Fantasy", value: "fantasy" },
|
||||
{ label: "Harem", value: "harem" },
|
||||
{ label: "Josei", value: "josei" },
|
||||
{ label: "Martial Arts", value: "martial-arts" },
|
||||
{ label: "Mature", value: "mature" },
|
||||
{ label: "Romance", value: "romance" },
|
||||
{ label: "School Life", value: "school-life" },
|
||||
{ label: "Sci-fi", value: "sci-fi" },
|
||||
{ label: "Seinen", value: "seinen" },
|
||||
{ label: "Slice of Life", value: "slice-of-life" },
|
||||
{ label: "Smut", value: "smut" },
|
||||
{ label: "Sports", value: "sports" },
|
||||
{ label: "Supernatural", value: "supernatural" },
|
||||
{ label: "Tragedy", value: "tragedy" },
|
||||
],
|
||||
type: FilterTypes.CheckboxGroup,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new PandaMTL();
|
||||
@@ -0,0 +1,189 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { isUrlAbsolute } from "@libs/isAbsoluteUrl";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { Filters, FilterTypes } from "@libs/filterInputs";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
class ReadLiteNovel implements Plugin.PluginBase {
|
||||
id = "rln.app";
|
||||
name = "ReadLiteNovel";
|
||||
version = "1.0.0";
|
||||
icon = "src/en/readlitenovel/icon.png";
|
||||
site = "https://rln.app";
|
||||
|
||||
parseAgoDate(date: string | undefined){ //parseMadaraDate
|
||||
if (date?.includes("ago")) {
|
||||
const dayJSDate = dayjs(new Date()); // today
|
||||
const timeAgo = date.match(/\d+/)?.[0] || "";
|
||||
const timeAgoInt = parseInt(timeAgo, 10);
|
||||
|
||||
if (!timeAgo) return null; // there is no number!
|
||||
|
||||
if (date.includes("hours ago") || date.includes("hour ago")) {
|
||||
dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours
|
||||
}
|
||||
|
||||
if (date.includes("days ago") || date.includes("day ago")) {
|
||||
dayJSDate.subtract(timeAgoInt, "days"); // go back N days
|
||||
}
|
||||
|
||||
if (date.includes("months ago") || date.includes("month ago")) {
|
||||
dayJSDate.subtract(timeAgoInt, "months"); // go back N months
|
||||
}
|
||||
|
||||
if (date.includes("years ago") || date.includes("year ago")) {
|
||||
dayJSDate.subtract(timeAgoInt, "years"); // go back N years
|
||||
}
|
||||
|
||||
return dayJSDate.toISOString();
|
||||
}
|
||||
return null; // there is no "ago" so give up
|
||||
};
|
||||
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const link = `${this.site}/ranking/${filters.order.value}/${page}`;
|
||||
const result = await fetchApi(link);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".category-items li").each((i,el) => {
|
||||
const novelUrl = loadedCheerio(el)
|
||||
.find(".category-name a")
|
||||
.attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
const novelName = loadedCheerio(el)
|
||||
.find(".category-name a")
|
||||
.text()
|
||||
.trim();
|
||||
let novelCover = loadedCheerio(el)
|
||||
.find(".category-img img")
|
||||
.attr("src");
|
||||
|
||||
if (novelCover && !isUrlAbsolute(novelCover)) {
|
||||
novelCover = this.site + novelCover;
|
||||
}
|
||||
|
||||
const novel = {
|
||||
path: novelUrl?.replace(this.site, ''),
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
};
|
||||
novels.push(novel);
|
||||
})
|
||||
return novels;
|
||||
};
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(this.site + novelPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: loadedCheerio(".novel-title").text() || "Untitled",
|
||||
cover: loadedCheerio(".novels-detail img").attr("src"),
|
||||
summary: loadedCheerio('.empty-box').text().trim(),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
loadedCheerio('.novels-detail-right li').each((i,el) => {
|
||||
const detailName = loadedCheerio(el).find('div:first').text();
|
||||
const detail = loadedCheerio(el).find('div:last');
|
||||
|
||||
switch(detailName){
|
||||
case "Status:":
|
||||
novel.status = detail.text();
|
||||
break;
|
||||
case "Genres:":
|
||||
novel.genres = detail.find('a')
|
||||
.map((i,el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join(', ') || detail.text();
|
||||
break;
|
||||
case "Author(s):":
|
||||
novel.author = detail.find('a')
|
||||
.map((i,el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join(', ') || detail.text();
|
||||
break;
|
||||
case "Translator:":
|
||||
novel.artist = detail.text();
|
||||
break;
|
||||
}
|
||||
})
|
||||
const chapter: Plugin.ChapterItem[] = [];
|
||||
loadedCheerio(".cm-tabs-content li").each((i,el) => {
|
||||
const chapterUrl = loadedCheerio(el).find("a").attr("href");
|
||||
if (!chapterUrl) return;
|
||||
const chapterName = loadedCheerio(el).find("a").text().trim();
|
||||
const releaseDate = this.parseAgoDate(
|
||||
loadedCheerio(el).find('svg').attr('data-bs-original-title')!
|
||||
);
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
path: chapterUrl?.replace(this.site, ''),
|
||||
releaseTime: releaseDate,
|
||||
})})
|
||||
|
||||
novel.chapters = chapter;
|
||||
return novel;
|
||||
};
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(this.site + chapterPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const chapterText = loadedCheerio("#chapterText").html() || "";
|
||||
|
||||
return chapterText;
|
||||
};
|
||||
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const url = this.site + `/search/autocomplete?dataType=json&query=${searchTerm}`;
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.json();
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
body.results.forEach((item: {link: string; original_title:string; image:string;}) =>
|
||||
novels.push({
|
||||
path: item.link,
|
||||
name: item.original_title,
|
||||
cover: item.image,
|
||||
})
|
||||
);
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url);
|
||||
}
|
||||
|
||||
filters = {
|
||||
order: {
|
||||
value: "top-rated",
|
||||
label: "Order by",
|
||||
options: [
|
||||
{ label: "MOST VIEWED", value: "most-viewed" },
|
||||
{ label: "TOP RATED", value: "top-rated" },
|
||||
{ label: "BOOKMARKS", value: "subscribers" },
|
||||
{ label: "NEW", value: "new" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new ReadLiteNovel();
|
||||
@@ -13,17 +13,17 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
|
||||
parseNovels(loadedCheerio: CheerioAPI){
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio('.search_main_box').each(function () {
|
||||
const novelName = loadedCheerio(this).find('.search_title > a').text();
|
||||
const novelCover = loadedCheerio(this).find('.search_img > img').attr('src');
|
||||
const novelUrl = loadedCheerio(this).find('.search_title > a').attr('href');
|
||||
loadedCheerio('.search_main_box').each((i,el) => {
|
||||
const novelName = loadedCheerio(el).find('.search_title > a').text();
|
||||
const novelCover = loadedCheerio(el).find('.search_img > img').attr('src');
|
||||
const novelUrl = loadedCheerio(el).find('.search_title > a').attr('href');
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
path: novelUrl.replace(this.site, ''),
|
||||
}
|
||||
novels.push(novel);
|
||||
});
|
||||
@@ -74,23 +74,20 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const result = await fetchApi(url);
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(this.site + novelPath);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
path: novelPath,
|
||||
name: loadedCheerio('.fic_title').text() || 'Untitled',
|
||||
cover: loadedCheerio('.fic_image > img').attr('src'),
|
||||
summary: loadedCheerio('.wi_fic_desc').text(),
|
||||
author: loadedCheerio('.auth_name_fic').text(),
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio('.fic_title').text();
|
||||
|
||||
novel.cover = loadedCheerio('.fic_image > img').attr('src');
|
||||
|
||||
novel.summary = loadedCheerio('.wi_fic_desc').text();
|
||||
|
||||
novel.genres = loadedCheerio('.fic_genre')
|
||||
.map((i, el) => loadedCheerio(el).text())
|
||||
@@ -103,13 +100,11 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
|
||||
.includes('Ongoing')
|
||||
? 'Ongoing'
|
||||
: 'Completed';
|
||||
|
||||
novel.author = loadedCheerio('.auth_name_fic').text();
|
||||
|
||||
let formData = new FormData();
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'wi_getreleases_pagination');
|
||||
formData.append('pagenum', '-1');
|
||||
formData.append('mypostid', novelUrl.split('/')[4]);
|
||||
formData.append('mypostid', novelPath.split('/')[1]);
|
||||
|
||||
const data = await fetchApi(
|
||||
`${this.site}wp-admin/admin-ajax.php`,
|
||||
@@ -130,7 +125,7 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
|
||||
const timeAgo = date.match(/\d+/)?.[0] || "";
|
||||
const timeAgoInt = parseInt(timeAgo, 10);
|
||||
|
||||
if (!timeAgo) return date; // there is no number!
|
||||
if (!timeAgo) return null; // there is no number!
|
||||
|
||||
if (date.includes("hours ago") || date.includes("hour ago")) {
|
||||
dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours
|
||||
@@ -149,16 +144,16 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
|
||||
return null;
|
||||
};
|
||||
|
||||
loadedCheerio('.toc_w').each(function () {
|
||||
const chapterName = loadedCheerio(this).find('.toc_a').text();
|
||||
const releaseDate = loadedCheerio(this).find('.fic_date_pub').text();
|
||||
const chapterUrl = loadedCheerio(this).find('a').attr('href');
|
||||
loadedCheerio('.toc_w').each((i,el) => {
|
||||
const chapterName = loadedCheerio(el).find('.toc_a').text();
|
||||
const releaseDate = loadedCheerio(el).find('.fic_date_pub').text();
|
||||
const chapterUrl = loadedCheerio(el).find('a').attr('href');
|
||||
|
||||
if (!chapterUrl) return;
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: parseISODate(releaseDate),
|
||||
url: chapterUrl,
|
||||
path: chapterUrl.replace(this.site, ''),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -167,13 +162,13 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(chapterUrl);
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(this.site + chapterPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let chapterText = loadedCheerio('div.chp_raw').html() || '';
|
||||
const chapterText = loadedCheerio('div.chp_raw').html() || '';
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
|
||||
class WTRLAB implements Plugin.PluginBase {
|
||||
id = "WTRLAB";
|
||||
name = "WTR-LAB";
|
||||
site = "https://wtr-lab.com/";
|
||||
version = "1.0.0";
|
||||
icon = "src/en/wtrlab/icon.png";
|
||||
sourceLang = "en/"
|
||||
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let link = this.site + this.sourceLang + "novel-list?";
|
||||
link += `orderBy=${filters.order.value}`;
|
||||
link += `&order=${filters.sort.value}`;
|
||||
link += `&filter=${filters.storyStatus.value}`;
|
||||
link += `&page=${page}`; //TODO Genre & Advance Searching Filter. Ez to implement, too much manual work, too lazy.
|
||||
|
||||
if(showLatestNovels){
|
||||
const response = await fetch('https://wtr-lab.com/api/home/recent', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ page: page })
|
||||
});
|
||||
|
||||
const recentNovel: JsonNovel = await response.json();
|
||||
|
||||
// Parse novels from JSON
|
||||
const novels: Plugin.NovelItem[] = recentNovel.data.map((datum: Datum) => ({
|
||||
name: datum.serie.data.title || "",
|
||||
cover: datum.serie.data.image,
|
||||
path: this.sourceLang + "serie-" + datum.serie.raw_id + "/" + datum.serie.slug || "",
|
||||
}));
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
else {
|
||||
const body = await fetchApi(link).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
//console.log(link);
|
||||
const novels: Plugin.NovelItem[] = loadedCheerio(".serie-item")
|
||||
.map((index, element) => ({
|
||||
name: loadedCheerio(element).find(".title-wrap > a").text().replace(loadedCheerio(element).find(".rawtitle").text(), "") || "",
|
||||
cover: loadedCheerio(element).find("img").attr("src"),
|
||||
path: loadedCheerio(element).find("a").attr("href") || "",
|
||||
}))
|
||||
.get()
|
||||
.filter((novel) => novel.name && novel.path);
|
||||
return novels;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(this.site + novelPath).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
//console.log(this.site + novelPath);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: loadedCheerio("h1.text-uppercase").text(),
|
||||
cover: loadedCheerio(".img-wrap > img").attr("src"),
|
||||
summary: loadedCheerio(".lead").text().trim(),
|
||||
};
|
||||
|
||||
novel.genres = loadedCheerio('td:contains("Genre")')
|
||||
.next()
|
||||
.find('a')
|
||||
.map((i,el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join(",");
|
||||
|
||||
novel.author = loadedCheerio('td:contains("Author")')
|
||||
.next()
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "");
|
||||
|
||||
novel.status = loadedCheerio('td:contains("Status")')
|
||||
.next()
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "");
|
||||
|
||||
|
||||
const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + "";
|
||||
const jsonData: NovelJson = JSON.parse(chapterJson);
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = jsonData.props.pageProps.serie.chapters.map((jsonChapter, chapterIndex) => ({
|
||||
name: "Chapter " + jsonChapter.slug + " " + jsonChapter.title,
|
||||
path: this.sourceLang + "serie-" +jsonData.props.pageProps.serie.serie_data.raw_id + "/" + jsonData.props.pageProps.serie.serie_data.slug + "/chapter-" + jsonChapter.slug, // Assuming 'slug' is the intended path
|
||||
releaseTime: jsonChapter.created_at.substring(0, 10),
|
||||
chapterNumber: chapterIndex + 1
|
||||
}));
|
||||
|
||||
novel.chapters = chapters;
|
||||
|
||||
return novel;
|
||||
}
|
||||
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const body = await fetchApi(this.site + chapterPath).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + "";
|
||||
const jsonData: NovelJson = JSON.parse(chapterJson);
|
||||
|
||||
//console.log(body);
|
||||
|
||||
const chapterContent = JSON.stringify(jsonData.props.pageProps.serie.chapter_data.data.body);
|
||||
//console.log(chapterContent);
|
||||
const parsedArray = JSON.parse(chapterContent);
|
||||
let htmlString = '';
|
||||
|
||||
for (const text of parsedArray) {
|
||||
htmlString += `<p>${text}</p>`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return htmlString;
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
const response = await fetch(this.site + "api/search", {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Referer: this.site + this.sourceLang,
|
||||
Origin: this.site,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ text: searchTerm}),
|
||||
});
|
||||
|
||||
const recentNovel: JsonNovel = await response.json();
|
||||
|
||||
|
||||
// Parse novels from JSON
|
||||
const novels: Plugin.NovelItem[] = recentNovel.data.map((datum: Datum) => ({
|
||||
name: datum.data.title || "",
|
||||
cover: datum.data.image,
|
||||
path: this.sourceLang + "serie-" + datum.raw_id + "/" + datum.slug || "",
|
||||
}));
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
|
||||
fetchImage = fetchFile;
|
||||
filters = {
|
||||
order: {
|
||||
value: "chapter",
|
||||
label: "Order by",
|
||||
options: [
|
||||
{ label: "View", value: "view" },
|
||||
{ label: "Name", value: "name" },
|
||||
{ label: "Addition Date", value: "date" },
|
||||
{ label: "Reader", value: "reader" },
|
||||
{ label: "Chapter", value: "chapter" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
sort: {
|
||||
value: "desc",
|
||||
label: "Sort by",
|
||||
options: [
|
||||
{ label: "Descending", value: "desc" },
|
||||
{ label: "Ascending", value: "asc" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
storyStatus: {
|
||||
value: "all",
|
||||
label: "Status",
|
||||
options: [
|
||||
{ label: "All", value: "all" },
|
||||
{ label: "Ongoing", value: "ongoing" },
|
||||
{ label: "Completed", value: "completed" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface NovelJson {
|
||||
props: Props;
|
||||
page: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
pageProps: PageProps;
|
||||
__N_SSP: boolean;
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
serie: Serie;
|
||||
server_time: Date;
|
||||
}
|
||||
|
||||
interface Serie {
|
||||
serie_data: SerieData;
|
||||
chapters: Chapter[];
|
||||
recommendation: SerieData[];
|
||||
chapter_data: ChapterData;
|
||||
id: number;
|
||||
raw_id: number;
|
||||
slug: string;
|
||||
data: Data;
|
||||
is_default: boolean;
|
||||
raw_type: string;
|
||||
}
|
||||
|
||||
interface Chapter {
|
||||
serie_id: number;
|
||||
id: number;
|
||||
slug: string;
|
||||
title: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
}
|
||||
interface ChapterData {
|
||||
data: ChapterContent;
|
||||
}
|
||||
interface ChapterContent {
|
||||
title: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
interface SerieData {
|
||||
serie_id?: number;
|
||||
recommendation_id?: number;
|
||||
score?: string;
|
||||
id: number;
|
||||
slug: string;
|
||||
search_text: string;
|
||||
status: number;
|
||||
data: Data;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
view: number;
|
||||
in_library: number;
|
||||
rating: number | null;
|
||||
chapter_count: number;
|
||||
power: number;
|
||||
total_rate: number;
|
||||
user_status: number;
|
||||
verified: boolean;
|
||||
from: null;
|
||||
raw_id: number;
|
||||
genres?: number[];
|
||||
}
|
||||
|
||||
interface Data {
|
||||
title: string;
|
||||
author: string;
|
||||
description: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
interface JsonNovel {
|
||||
success: boolean;
|
||||
data: Datum[];
|
||||
}
|
||||
interface Datum {
|
||||
serie: Serie;
|
||||
chapters: Chapter[];
|
||||
updated_at: Date;
|
||||
raw_id: number;
|
||||
slug: string;
|
||||
data: Data;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export default new WTRLAB();
|
||||
+224
-80
@@ -1,77 +1,155 @@
|
||||
import { CheerioAPI, load as parseHTML } from "cheerio";
|
||||
import { CheerioAPI, load } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { Filters, FilterTypes } from "@libs/filterInputs";
|
||||
import dayjs from "dayjs";
|
||||
import { defaultCover } from "@libs/defaultCover";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
|
||||
class ChireadsPlugin implements Plugin.PluginBase {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
site: string;
|
||||
version: string;
|
||||
id = "chireads";
|
||||
name = "Chireads";
|
||||
icon = "src/fr/chireads/icon.png";
|
||||
site = "https://chireads.com";
|
||||
version = "1.0.2";
|
||||
|
||||
async getCheerio(url: string): Promise<CheerioAPI> {
|
||||
const r = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }});
|
||||
const body = await r.text();
|
||||
const $ = load(body);
|
||||
return ($);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.id = "chireads.com";
|
||||
this.name = "Chireads";
|
||||
this.icon = "src/fr/chireads/icon.png";
|
||||
this.site = "https://chireads.com";
|
||||
this.version = "1.0.0";
|
||||
|
||||
}
|
||||
parseNovels(loadedCheerio: CheerioAPI) {
|
||||
return [];
|
||||
}
|
||||
async popularNovels(page: number, options: Plugin.PopularNovelsOptions): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/category/translatedtales/page/${page}/`;
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
const loadedCheerio = parseHTML(body);
|
||||
async popularNovels(pageNo: number, { filters, showLatestNovels }: Plugin.PopularNovelsOptions): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site;
|
||||
let tag: string = "all";
|
||||
if (showLatestNovels)
|
||||
url += "/category/translatedtales/page/" + pageNo;
|
||||
else {
|
||||
if (filters && typeof filters.tag.value === "string" && filters.tag.value !== "all")
|
||||
tag = filters.tag.value;
|
||||
if (tag !== "all")
|
||||
url += "/tag/" + tag + "/page/" + pageNo;
|
||||
else if (pageNo > 1)
|
||||
return [];
|
||||
}
|
||||
let $ = await this.getCheerio(url);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
let novel: Plugin.NovelItem;
|
||||
|
||||
loadedCheerio("#content li").each(function () {
|
||||
const novelName = loadedCheerio(this).find(".news-list-tit h5").text();
|
||||
const novelCover = loadedCheerio(this).find("img").attr("src");
|
||||
const novelUrl = loadedCheerio(this)
|
||||
.find(".news-list-tit h5 a")
|
||||
.attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = { name: novelName, cover: novelCover, url: novelUrl };
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
if (showLatestNovels || tag !== "all") {
|
||||
let loop = 1;
|
||||
if (showLatestNovels)
|
||||
loop = 2;
|
||||
for (let i = 0; i < loop; i++) {
|
||||
if (i === 1)
|
||||
$ = await this.getCheerio(this.site + "/category/original/page/" + pageNo);
|
||||
let romans = $(".romans-content li");
|
||||
if (!romans.length)
|
||||
romans = $("#content li");
|
||||
romans.each((i, elem) => {
|
||||
const novelName = $(elem).contents().find("div").first().text().trim();
|
||||
const novelCover = $(elem).find("div").first().find("img").attr("src");
|
||||
const novelUrl = $(elem).find("div").first().find("a").attr("href")
|
||||
|
||||
if (novelUrl){
|
||||
novel = { name: novelName, cover: novelCover, path: novelUrl.replace(this.site, "") };
|
||||
novels.push(novel);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
const populaire = $(":contains(\"Populaire\")").last().parent().next().find("li > div")
|
||||
if (populaire.length === 12) // pc
|
||||
{
|
||||
let novelCover : string | undefined;
|
||||
let novelName : string | undefined;
|
||||
let novelUrl : string | undefined;
|
||||
populaire.each((i, elem) => {
|
||||
if (i % 2 === 0)
|
||||
novelCover = $(elem).find("img").attr("src");
|
||||
else {
|
||||
novelName = $(elem).text().trim();
|
||||
novelUrl = $(elem).find("a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
novel = { name: novelName, cover: novelCover || defaultCover, path: novelUrl.replace(this.site, "") };
|
||||
|
||||
novels.push(novel);
|
||||
}
|
||||
});
|
||||
}
|
||||
else // mobile
|
||||
{
|
||||
const imgs = populaire.find("div.popular-list-img img");
|
||||
const txts = populaire.find("div.popular-list-name");
|
||||
|
||||
txts.each((i, elem) => {
|
||||
const novelName = $(elem).text().trim();
|
||||
const novelCover = $(imgs[i]).attr("src");
|
||||
const novelUrl = $(elem).find("a").attr("href");
|
||||
|
||||
if (novelUrl){
|
||||
novel = { name: novelName, cover: novelCover, path: novelUrl.replace(this.site, "") };
|
||||
novels.push(novel);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return novels;
|
||||
};
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const novel: Plugin.SourceNovel = { url };
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const novel: Plugin.SourceNovel = { path: novelPath, name: "Sans titre" };
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
let $ = await this.getCheerio(this.site + novelPath);
|
||||
|
||||
novel.name = loadedCheerio(".inform-title").text().trim();
|
||||
novel.cover = loadedCheerio(".inform-product img").attr("src");
|
||||
novel.summary = loadedCheerio(".inform-inform-txt").text().trim();
|
||||
novel.name = $(".inform-product-txt").first().text().trim() || $(".inform-title").text().trim();
|
||||
novel.cover = $(".inform-product img").attr("src") || $(".inform-product-img img").attr("src") || defaultCover;
|
||||
novel.summary = $(".inform-inform-txt").text().trim() || $(".inform-intr-txt").text().trim();
|
||||
|
||||
let infos = $("div.inform-product-txt > div.inform-intr-col").text().trim() || $("div.inform-inform-data > h6").text().trim();
|
||||
if (infos.includes("Auteur : "))
|
||||
novel.author = infos.substring(infos.indexOf("Auteur : ") + 9, infos.indexOf("Statut de Parution : ")).trim();
|
||||
else if (infos.includes("Fantrad : "))
|
||||
novel.author = infos.substring(infos.indexOf("Fantrad : ") + 10, infos.indexOf("Statut de Parution : ")).trim();
|
||||
else
|
||||
novel.author = "Inconnu";
|
||||
switch (infos.substring(infos.indexOf("Statut de Parution : ") + 21).toLowerCase()) {
|
||||
case "en pause":
|
||||
novel.status = NovelStatus.OnHiatus;
|
||||
break;
|
||||
case "complet":
|
||||
novel.status = NovelStatus.Completed;
|
||||
break;
|
||||
default:
|
||||
novel.status = NovelStatus.Ongoing;
|
||||
break;
|
||||
}
|
||||
|
||||
let chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio(".chapitre-table a").each(function () {
|
||||
const chapterName = loadedCheerio(this).text().trim();
|
||||
const releaseDate = null;
|
||||
const chapterUrl = loadedCheerio(this).attr("href");
|
||||
let chapterList = $(".chapitre-table a");
|
||||
if (!chapterList.length)
|
||||
{
|
||||
$("div.inform-annexe-list").first().remove();
|
||||
chapterList = $(".inform-annexe-list").find("a");
|
||||
}
|
||||
chapterList.each((i, elem) => {
|
||||
const chapterName = $(elem).text().trim();
|
||||
const chapterUrl = $(elem).attr("href");
|
||||
const releaseDate = dayjs(chapterUrl?.substring(chapterUrl.length - 11, chapterUrl.length - 1)).format("DD MMMM YYYY");
|
||||
|
||||
if (!chapterUrl) return;
|
||||
|
||||
chapters.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
});
|
||||
if (chapterUrl){
|
||||
chapters.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
path: chapterUrl.replace(this.site, ""),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
novel.chapters = chapters;
|
||||
@@ -80,42 +158,108 @@ class ChireadsPlugin implements Plugin.PluginBase {
|
||||
};
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(chapterUrl);
|
||||
const body = await result.text();
|
||||
const $ = await this.getCheerio(this.site + chapterUrl);
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const chapterText = loadedCheerio("#content").html() || "";
|
||||
const chapterText = $("#content").html() || "";
|
||||
|
||||
return chapterText;
|
||||
};
|
||||
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/search?x=0&y=0&name=${searchTerm}`;
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
if (pageNo !== 1)
|
||||
return [];
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio("#content li").each(function () {
|
||||
const novelName = loadedCheerio(this).find(".news-list-tit h5").text();
|
||||
const novelCover = loadedCheerio(this).find("img").attr("src");
|
||||
const novelUrl = loadedCheerio(this)
|
||||
.find(".news-list-tit h5 a")
|
||||
.attr("href");
|
||||
let i = 1;
|
||||
let finised = false;
|
||||
while ( !finised )
|
||||
{
|
||||
await this.popularNovels(i, { showLatestNovels: true, filters: undefined } ).then((res) => {
|
||||
if (res.length === 0)
|
||||
finised = true;
|
||||
novels.push(...res);
|
||||
});
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!novelUrl) return;
|
||||
novels = novels.filter((novel) =>
|
||||
novel.name
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.includes(
|
||||
searchTerm
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
)
|
||||
);
|
||||
|
||||
const novel = { name: novelName, cover: novelCover, url: novelUrl };
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
return novels;
|
||||
};
|
||||
async fetchImage(url: string): Promise<string | undefined>{
|
||||
return await fetchFile(url);
|
||||
}
|
||||
|
||||
fetchImage = fetchFile;
|
||||
|
||||
filters = {
|
||||
tag: {
|
||||
type: FilterTypes.Picker,
|
||||
label: "Tag",
|
||||
value: "all",
|
||||
options: [
|
||||
{ label: "Tous", value: "all" },
|
||||
{ label: "Arts martiaux", value: "arts-martiaux" },
|
||||
{ label: "De faible à fort", value: "de-faible-a-fort" },
|
||||
{ label: "Adapté en manhua", value: "adapte-en-manhua" },
|
||||
{ label: "Cultivation", value: "cultivation" },
|
||||
{ label: "Action", value: "action" },
|
||||
{ label: "Aventure", value: "aventure" },
|
||||
{ label: "Monstres", value: "monstres" },
|
||||
{ label: "Xuanhuan", value: "xuanhuan" },
|
||||
{ label: "Fantastique", value: "fantastique" },
|
||||
{ label: "Adapté en Animé", value: "adapte-en-anime" },
|
||||
{ label: "Alchimie", value: "alchimie" },
|
||||
{ label: "Éléments de jeux", value: "elements-de-jeux" },
|
||||
{ label: "Calme Protagoniste", value: "calme-protagoniste" },
|
||||
{ label: "Protagoniste intelligent", value: "protagoniste-intelligent" },
|
||||
{ label: "Polygamie", value: "polygamie" },
|
||||
{ label: "Belle femelle Lea", value: "belle-femelle-lea" },
|
||||
{ label: "Personnages arrogants", value: "personnages-arrogants" },
|
||||
{ label: "Système de niveau", value: "systeme-de-niveau" },
|
||||
{ label: "Cheat", value: "cheat" },
|
||||
{ label: "Protagoniste génie", value: "protagoniste-genie" },
|
||||
{ label: "Comédie", value: "comedie" },
|
||||
{ label: "Gamer", value: "gamer" },
|
||||
{ label: "Mariage", value: "mariage" },
|
||||
{ label: "seeking Protag", value: "seeking-protag" },
|
||||
{ label: "Romance précoce", value: "romance-precoce" },
|
||||
{ label: "Croissance accélérée", value: "croissance-acceleree" },
|
||||
{ label: "Artefacts", value: "artefacts" },
|
||||
{ label: "Intelligence artificielle", value: "intelligence-artificielle" },
|
||||
{ label: "Mariage arrangé", value: "mariage-arrange" },
|
||||
{ label: "Mature", value: "mature" },
|
||||
{ label: "Adulte", value: "adulte" },
|
||||
{ label: "Administrateur de système", value: "administrateur-de-systeme" },
|
||||
{ label: "Beau protagoniste", value: "beau-protagoniste" },
|
||||
{ label: "Protagoniste charismatique", value: "protagoniste-charismatique" },
|
||||
{ label: "Protagoniste masculin", value: "protagoniste-masculin" },
|
||||
{ label: "Démons", value: "demons" },
|
||||
{ label: "Reincarnation", value: "reincarnation" },
|
||||
{ label: "Académie", value: "academie" },
|
||||
{ label: "Cacher les vraies capacités", value: "cacher-les-vraies-capacites" },
|
||||
{ label: "Protagoniste surpuissant", value: "protagoniste-surpuissant" },
|
||||
{ label: "Joueur", value: "joueur" },
|
||||
{ label: "Protagoniste fort dès le départ", value: "protagoniste-fort-des-le-depart" },
|
||||
{ label: "Immortels", value: "immortels" },
|
||||
{ label: "Cultivation rapide", value: "cultivation-rapide" },
|
||||
{ label: "Harem", value: "harem" },
|
||||
{ label: "Assasins", value: "assasins" },
|
||||
{ label: "De pauvre à riche", value: "de-pauvre-a-riche" },
|
||||
{ label: "Système de classement de jeux", value: "systeme-de-classement-de-jeux" },
|
||||
{ label: "Capacités spéciales", value: "capacites-speciales" },
|
||||
{ label: "Vengeance", value: "vengeance" },
|
||||
],
|
||||
}
|
||||
} satisfies Filters;
|
||||
};
|
||||
|
||||
export default new ChireadsPlugin();
|
||||
+303
-149
@@ -1,180 +1,334 @@
|
||||
import { load } from "cheerio";
|
||||
import { load, CheerioAPI } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { Filters, FilterTypes } from "@libs/filterInputs";
|
||||
import { defaultCover } from "@libs/defaultCover";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
class NovelDeGlacePlugin implements Plugin.PluginBase{
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
site: string;
|
||||
version: string;
|
||||
id = "noveldeglace";
|
||||
name = "NovelDeGlace";
|
||||
icon = "src/fr/noveldeglace/icon.png";
|
||||
site = "https://noveldeglace.com/";
|
||||
version = "1.0.2";
|
||||
|
||||
async getCheerio(url: string): Promise<CheerioAPI | undefined> {
|
||||
const r = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }});
|
||||
if (!r.ok) return (undefined);
|
||||
const body = await r.text();
|
||||
const loadedCheerio = load(body);
|
||||
return (loadedCheerio);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.id = "NDG.com";
|
||||
this.name = "NovelDeGlace";
|
||||
this.icon = "src/fr/noveldeglace/icon.png";
|
||||
this.site = "https://noveldeglace.com/";
|
||||
this.version = "1.0.0";
|
||||
|
||||
}
|
||||
|
||||
async popularNovels(page: number): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + "roman";
|
||||
const result = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' },});
|
||||
const body = await result.text();
|
||||
let loadedCheerio = load(body);
|
||||
|
||||
parseNovels($: CheerioAPI, showLatestNovels: boolean | undefined): Plugin.NovelItem[] {
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio("article").each(function () {
|
||||
const novelName = loadedCheerio(this).find("h2").text().trim();
|
||||
const novelCover = loadedCheerio(this).find("img").attr("src");
|
||||
const novelUrl = loadedCheerio(this).find("h2 > a").attr("href");
|
||||
$("article").each((i, el) => {
|
||||
const cheerio = $(el);
|
||||
const novelName = cheerio.find("h2").text().trim();
|
||||
const novelCover = cheerio.find("img").attr("src");
|
||||
let novelUrl: string | undefined;
|
||||
if (showLatestNovels)
|
||||
novelUrl = cheerio.find("span.Roman > a").attr("href");
|
||||
else
|
||||
novelUrl = cheerio.find("h2 > a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
if (novelUrl){
|
||||
const novel:Plugin.NovelItem = {
|
||||
name: novelName,
|
||||
path: novelUrl.replace(this.site, ""),
|
||||
cover: novelCover,
|
||||
};
|
||||
novels.push(novel);
|
||||
}
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async popularNovels(pageNo: number, { filters, showLatestNovels }: Plugin.PopularNovelsOptions<typeof this.filters>): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site;
|
||||
if (showLatestNovels)
|
||||
url += "chapitre"
|
||||
else
|
||||
{
|
||||
let cat_gen: string = "all";
|
||||
if (filters && typeof filters.categorie_genre.value == "string")
|
||||
cat_gen = filters.categorie_genre.value;
|
||||
if (cat_gen != "all" && cat_gen != "categorie_roman" && cat_gen != "genre")
|
||||
{
|
||||
if (cat_gen[0] == "c")
|
||||
url += "categorie_roman/" + cat_gen.substring(2);
|
||||
else if (cat_gen[0] == "g")
|
||||
url += "genre/" + cat_gen.substring(2);
|
||||
}
|
||||
else if (pageNo > 1)
|
||||
return ([]); // when asking for all novels, there is only 1 page
|
||||
else
|
||||
url += "roman";
|
||||
}
|
||||
url += "/page/" + pageNo;
|
||||
let $ = await this.getCheerio(url);
|
||||
if (!$)
|
||||
return ([]);
|
||||
return this.parseNovels($, showLatestNovels);
|
||||
};
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const result = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }});
|
||||
const body = await result.text();
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const $ = await this.getCheerio(this.site + novelPath);
|
||||
if (!$)
|
||||
throw new Error("Failed to load page (open in web view)");
|
||||
|
||||
let novel: Plugin.SourceNovel = { path: novelPath, name: "Untitled" };
|
||||
|
||||
novel.name = ($("div.entry-content > div > strong")[0]
|
||||
.nextSibling as Text | null)
|
||||
?.nodeValue
|
||||
?.trim() || "Untitled";
|
||||
|
||||
novel.cover = $(".su-row > div > div > img")
|
||||
.attr("src") || defaultCover;
|
||||
|
||||
let novelInfos = $("div[data-title=Tomes] >")
|
||||
.toArray();
|
||||
novelInfos.pop();
|
||||
novelInfos.shift();
|
||||
novel.summary = $("div[data-title=Synopsis]")
|
||||
.text().trim() +
|
||||
"\n\n" + novelInfos.map((el) => $(el).text()).join("\n").trim();
|
||||
|
||||
novel.author = $("strong:contains('Auteur :')")
|
||||
.parent()
|
||||
.text()
|
||||
.replace("Auteur : ", "")
|
||||
.trim();
|
||||
|
||||
novel.artist = $("strong:contains('Illustrateur :')")
|
||||
.parent()
|
||||
.text()
|
||||
.replace("Illustrateur :", "")
|
||||
.trim();
|
||||
|
||||
const categorie = $(".categorie")
|
||||
.text()
|
||||
.replace("Catégorie :", "")
|
||||
.trim();
|
||||
const genres = $(".genre")
|
||||
.text()
|
||||
.replace("Genre :", "")
|
||||
.replace(/, /g, ",")
|
||||
.trim();
|
||||
if (categorie && categorie != "Autre")
|
||||
novel.genres = categorie;
|
||||
if (genres)
|
||||
novel.genres = novel.genres ? novel.genres + "," + genres : genres;
|
||||
|
||||
let status = $("strong:contains('Statut :')")
|
||||
.parent()
|
||||
.attr("class");
|
||||
switch (status) {
|
||||
case "type etat0":
|
||||
case "type etat1":
|
||||
novel.status = NovelStatus.Ongoing;
|
||||
break;
|
||||
case "type etat4":
|
||||
novel.status = NovelStatus.OnHiatus;
|
||||
break;
|
||||
case "type etat5":
|
||||
novel.status = NovelStatus.Completed;
|
||||
break;
|
||||
case "type etat6":
|
||||
novel.status = NovelStatus.Cancelled;
|
||||
break;
|
||||
default:
|
||||
novel.status = NovelStatus.Unknown;
|
||||
break;
|
||||
}
|
||||
|
||||
let novelChapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
const volumes = $("div[data-title=Tomes] > div")
|
||||
.last()
|
||||
.contents();
|
||||
const hasMultipleVolumes = volumes.length > 1;
|
||||
|
||||
let loadedCheerio = load(body);
|
||||
let chapterName = ""
|
||||
let site = this.site;
|
||||
const monthMapping: { [key: string]: string } = { janvier: "January", fevrier: "February", mars: "March", avril: "April", mai: "May", juin: "June", juillet: "July", aout: "August", septembre: "September", octobre: "October", novembre: "November", decembre: "December"};
|
||||
volumes.each((volumeIndex: number, el) => {
|
||||
if (hasMultipleVolumes)
|
||||
chapterName = "T." + (volumeIndex + 1) + " ";
|
||||
$(el)
|
||||
.find(".chpt")
|
||||
.each((chapterIndex: number, el) => {
|
||||
const cheerio = $(el);
|
||||
const newChapterName = chapterName + cheerio
|
||||
.find("a")
|
||||
.first()
|
||||
.text().trim() || "";
|
||||
if (!cheerio.find("i.fa").length) // no parts
|
||||
{
|
||||
const dateHtml = cheerio.html()?.substring(cheerio.html()?.indexOf("</a>") || 0) || "";
|
||||
let releaseDate = dateHtml?.substring(dateHtml.indexOf("(") + 1, dateHtml.indexOf(")")) || undefined;
|
||||
if (releaseDate)
|
||||
{
|
||||
try {
|
||||
const [day, month, year] = releaseDate.split(" ");
|
||||
releaseDate = dayjs(`${day} ${monthMapping[month.normalize("NFD").replace(/[\u0300-\u036f]/g, "")]} ${year}`, "DD MMMM YYYY").format("YYYY-MM-DD");
|
||||
} catch (e) {}
|
||||
}
|
||||
const chapterUrl = cheerio.find("a").attr("href");
|
||||
if (chapterUrl)
|
||||
{
|
||||
const chapter: Plugin.ChapterItem = {
|
||||
name: newChapterName,
|
||||
releaseTime: releaseDate,
|
||||
path: chapterUrl.replace(site, ""),
|
||||
chapterNumber: chapterIndex,
|
||||
};
|
||||
|
||||
novelChapters.push(chapter);
|
||||
}
|
||||
}
|
||||
else // has parts that needs to be added individually
|
||||
{
|
||||
const items = cheerio
|
||||
.find("i")
|
||||
.parent()
|
||||
.next()
|
||||
.html()
|
||||
?.split("</a>") || [];
|
||||
items?.shift();
|
||||
const dates: string[] = [];
|
||||
items?.forEach((item) => { // there is a date on every publish parts
|
||||
dates.push(item.substring(item.indexOf("(") + 1, item.indexOf(")")));
|
||||
});
|
||||
const hrefs: string[] = [];
|
||||
cheerio
|
||||
.find("i")
|
||||
.parent()
|
||||
.next()
|
||||
.find("a")
|
||||
.each(function () {
|
||||
hrefs.push(this["attribs"]["href"]);
|
||||
});
|
||||
if (dates.length == hrefs.length)
|
||||
dates.forEach((date, index) => {
|
||||
let releaseDate = date;
|
||||
try {
|
||||
const [day, month, year] = date.split(" ");
|
||||
releaseDate = dayjs(`${day} ${monthMapping[month.normalize("NFD").replace(/[\u0300-\u036f]/g, "")]} ${year}`, "DD MMMM YYYY").format("YYYY-MM-DD");
|
||||
} catch (e) {}
|
||||
const chapter: Plugin.ChapterItem = {
|
||||
name: newChapterName + " (" + (index + 1) + ")",
|
||||
releaseTime: releaseDate,
|
||||
path: hrefs[index].replace(site, ""),
|
||||
chapterNumber: chapterIndex + (index + 1) / 1000,
|
||||
};
|
||||
novelChapters.push(chapter);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
novel.chapters = novelChapters;
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
let novel: Plugin.SourceNovel = { url };
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const $ = await this.getCheerio(this.site + chapterPath);
|
||||
if (!$)
|
||||
throw new Error("Failed to load page (open in web view)");
|
||||
|
||||
novel.name = (loadedCheerio("div.entry-content > div > strong"
|
||||
)[0].nextSibling as Text | null)?.nodeValue?.trim() || '';
|
||||
|
||||
novel.cover = loadedCheerio(".su-row > div > div > img").attr("src");
|
||||
|
||||
novel.summary = loadedCheerio("div[data-title=Synopsis]").text();
|
||||
|
||||
novel.author = loadedCheerio("strong:contains('Auteur :')"
|
||||
).parent().text().replace("Auteur : ", "").trim();
|
||||
|
||||
// novel.artist = loadedCheerio("strong:contains('Illustrateur :')"
|
||||
// ).parent().text().replace("Illustrateur : ", "").trim();
|
||||
|
||||
const categorie = loadedCheerio(".categorie")
|
||||
.text()
|
||||
.replace("Catégorie :", "")
|
||||
.trim();
|
||||
const genres = loadedCheerio(".genre")
|
||||
.text()
|
||||
.replace("Genre :", "")
|
||||
.replace(/, /g, ",")
|
||||
.trim();
|
||||
if (categorie && categorie != "Autre")
|
||||
novel.genres = categorie;
|
||||
if (genres)
|
||||
novel.genres = novel.genres ? novel.genres + "," + genres : genres;
|
||||
|
||||
let status = loadedCheerio("strong:contains('Statut :')"
|
||||
).parent().attr("class");
|
||||
switch (status) {
|
||||
case "type etat0":
|
||||
novel.status = NovelStatus.Ongoing;
|
||||
break;
|
||||
case "type etat1":
|
||||
novel.status = NovelStatus.Ongoing;
|
||||
break;
|
||||
case "type etat4":
|
||||
novel.status = NovelStatus.OnHiatus;
|
||||
break;
|
||||
case "type etat5":
|
||||
novel.status = NovelStatus.Completed;
|
||||
break;
|
||||
case "type etat6":
|
||||
novel.status = NovelStatus.Cancelled;
|
||||
break;
|
||||
default:
|
||||
novel.status = NovelStatus.Unknown;
|
||||
break;
|
||||
}
|
||||
|
||||
let novelChapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
loadedCheerio(".chpt").each(function () {
|
||||
const chapterName = loadedCheerio(this).find("a").text().trim();
|
||||
const releaseDate = null;
|
||||
const chapterUrl = loadedCheerio(this).find("a").attr("href");
|
||||
|
||||
if (!chapterUrl) return;
|
||||
|
||||
const chapter = {
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
};
|
||||
|
||||
novelChapters.push(chapter);
|
||||
});
|
||||
|
||||
novel.chapters = novelChapters;
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const url = chapterUrl;
|
||||
const result = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }});
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = load(body);
|
||||
|
||||
loadedCheerio(".mistape_caption").remove();
|
||||
let chapterText = loadedCheerio(".chapter-content").html() || "Chapitre introuvable";
|
||||
$(".mistape_caption").remove();
|
||||
let chapterText = $(".chapter-content").html()
|
||||
|| $(".entry-content").html()
|
||||
|| "";
|
||||
return chapterText;
|
||||
};
|
||||
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
async searchNovels(searchTerm: string, num: number): Promise<Plugin.NovelItem[]> {
|
||||
if (num !== 1) return ([]); // only 1 page of results
|
||||
let url = this.site + "roman";
|
||||
const result = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }});
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = load(body);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio("article").each(function () {
|
||||
const novelName = loadedCheerio(this).find("h2").text().trim();
|
||||
const novelCover = loadedCheerio(this).find("img").attr("src");
|
||||
const novelUrl = loadedCheerio(this).find("h2 > a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
const $ = await this.getCheerio(url);
|
||||
if (!$)
|
||||
throw new Error("Failed to load page (open in web view)");
|
||||
|
||||
let novels = this.parseNovels($, false);
|
||||
|
||||
novels = novels.filter((novel) =>
|
||||
novel.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
novel.name
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.trim()
|
||||
.includes(
|
||||
searchTerm
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.trim()
|
||||
)
|
||||
);
|
||||
|
||||
return novels;
|
||||
};
|
||||
async fetchImage(url: string): Promise<string | undefined>{
|
||||
return await fetchFile(url);
|
||||
}
|
||||
|
||||
fetchImage = fetchFile;
|
||||
|
||||
filters = {
|
||||
categorie_genre: {
|
||||
type: FilterTypes.Picker,
|
||||
label: "Catégorie/Genre",
|
||||
value: "all",
|
||||
options: [
|
||||
{ label: "Tous", value: "all" },
|
||||
{ label: "═══CATÉGORIES═══", value: "categorie_roman" },
|
||||
{ label: "Seinen", value: "c_seinen" },
|
||||
{ label: "Shonen", value: "c_shonen" },
|
||||
{ label: "Original", value: "c_original" },
|
||||
{ label: "Yuri", value: "c_yuri" },
|
||||
{ label: "Autre", value: "c_autre" },
|
||||
{ label: "Fille", value: "c_fille" },
|
||||
{ label: "Roman pour Adulte", value: "c_roman-pour-adulte" },
|
||||
{ label: "Xuanhuan", value: "c_xuanhuan" },
|
||||
{ label: "Yaoi", value: "c_yaoi" },
|
||||
{ label: "═══GENRES═══", value: "genre" },
|
||||
{ label: "Action", value: "g_action" },
|
||||
{ label: "Aventure", value: "g_aventure" },
|
||||
{ label: "Comédie", value: "g_comedie" },
|
||||
{ label: "Drame", value: "g_drame" },
|
||||
{ label: "Fantastique", value: "g_fantastique" },
|
||||
{ label: "Harem", value: "g_harem" },
|
||||
{ label: "Psychologique", value: "g_psychologique" },
|
||||
{ label: "Romance", value: "g_romance" },
|
||||
{ label: "Ecchi", value: "g_ecchi" },
|
||||
{ label: "Mature", value: "g_mature" },
|
||||
{ label: "Surnaturel", value: "g_surnaturel" },
|
||||
{ label: "Vie scolaire", value: "g_vie-scolaire" },
|
||||
{ label: "Adulte", value: "g_adulte" },
|
||||
{ label: "Tragédie", value: "g_tragedie" },
|
||||
{ label: "Arts Martiaux", value: "g_arts-martiaux" },
|
||||
{ label: "Pas de harem", value: "g_pas-de-harem" },
|
||||
{ label: "Tranche de vie", value: "g_tranche-de-vie" },
|
||||
{ label: "Mecha", value: "g_mecha" },
|
||||
{ label: "Sci-fi", value: "g_sci-fi" },
|
||||
{ label: "Science-Fiction", value: "g_science-fiction" },
|
||||
{ label: "Anti-Héros", value: "g_anti-heros" },
|
||||
{ label: "Horreur", value: "g_horreur" },
|
||||
{ label: "Insectes", value: "g_insectes" },
|
||||
{ label: "Mystère", value: "g_mystere" },
|
||||
{ label: "Lolicon", value: "g_lolicon" },
|
||||
{ label: "Shoujo Ai", value: "g_shoujo-ai" },
|
||||
{ label: "Smut", value: "g_smut" },
|
||||
{ label: "Xuanhuan", value: "g_xuanhuan" },
|
||||
{ label: "Shotacon", value: "g_shotacon" },
|
||||
],
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new NovelDeGlacePlugin();
|
||||
+28
-22
@@ -14,7 +14,10 @@ class freedlit implements Plugin.PluginBase {
|
||||
|
||||
async popularNovels(
|
||||
page: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + "/get-books/all/list/" + page + "?sort=";
|
||||
url += showLatestNovels ? "recent" : filters?.sort?.value || "popular";
|
||||
@@ -38,27 +41,30 @@ class freedlit implements Plugin.PluginBase {
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio("#bookListBlock > div > div").each(function () {
|
||||
const name = loadedCheerio(this).find("div > h4 > a").text()?.trim();
|
||||
const cover = loadedCheerio(this)
|
||||
loadedCheerio("#bookListBlock > div > div").each((index, element) => {
|
||||
const name = loadedCheerio(element).find("div > h4 > a").text()?.trim();
|
||||
const cover = loadedCheerio(element)
|
||||
.find("div > a > img")
|
||||
.attr("src")
|
||||
?.trim();
|
||||
const url = loadedCheerio(this).find("div > h4 > a").attr("href")?.trim();
|
||||
const url = loadedCheerio(element)
|
||||
.find("div > h4 > a")
|
||||
.attr("href")
|
||||
?.trim();
|
||||
if (!name || !url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
novels.push({ name, cover, path: url.replace(this.site, "") });
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(novelUrl).then((res) => res.text());
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(this.site + novelPath).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
path: novelPath,
|
||||
name: loadedCheerio(".book-info h4").text(),
|
||||
cover: loadedCheerio(".book-cover > div > img").attr("src")?.trim(),
|
||||
summary: loadedCheerio("#nav-home").text()?.trim(),
|
||||
@@ -77,11 +83,11 @@ class freedlit implements Plugin.PluginBase {
|
||||
if (!name || !url) return;
|
||||
|
||||
const releaseDate = loadedCheerio(element).find('span[class="date"]').text();
|
||||
chapters.push({
|
||||
name,
|
||||
url,
|
||||
releaseTime: this.parseDate(releaseDate),
|
||||
chapterNumber: chapterIndex + 1
|
||||
chapters.push({
|
||||
name,
|
||||
path: url.replace(this.site, ""),
|
||||
releaseTime: this.parseDate(releaseDate),
|
||||
chapterNumber: chapterIndex + 1,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,8 +95,8 @@ class freedlit implements Plugin.PluginBase {
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const body = await fetchApi(chapterUrl).then((res) => res.text());
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const body = await fetchApi(this.site + chapterPath).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
loadedCheerio("div.mobile-block").remove();
|
||||
@@ -101,18 +107,18 @@ class freedlit implements Plugin.PluginBase {
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/search?query=${searchTerm}&type=all`;
|
||||
const url = this.site + "/search?query=" + searchTerm + "&type=all";
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio("#bookListBlock > div").each(function () {
|
||||
const name = loadedCheerio(this).find("h4 > a").text()?.trim();
|
||||
const cover = loadedCheerio(this).find("a > img").attr("src")?.trim();
|
||||
const url = loadedCheerio(this).find("h4 > a").attr("href")?.trim();
|
||||
loadedCheerio("#bookListBlock > div").each((index, element) => {
|
||||
const name = loadedCheerio(element).find("h4 > a").text()?.trim();
|
||||
const cover = loadedCheerio(element).find("a > img").attr("src")?.trim();
|
||||
const url = loadedCheerio(element).find("h4 > a").attr("href")?.trim();
|
||||
if (!name || !url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
novels.push({ name, cover, path: url.replace(this.site, "") });
|
||||
});
|
||||
|
||||
return novels;
|
||||
|
||||
@@ -53,14 +53,15 @@ class AuthorToday implements Plugin.PluginBase {
|
||||
cover: novel.coverUrl
|
||||
? "https://cm.author.today/content/" + novel.coverUrl
|
||||
: defaultCover,
|
||||
url: this.site + "/work/" + novel.id,
|
||||
path: "/work/" + novel.id,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const workID = novelUrl.split("/")[4];
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const workID = novelPath.split("/")[2];
|
||||
const result = await fetchApi(`${apiUrl}v1/work/${workID}/details`, {
|
||||
headers: {
|
||||
Authorization: token,
|
||||
@@ -69,7 +70,7 @@ class AuthorToday implements Plugin.PluginBase {
|
||||
|
||||
const book = (await result.json()) as responseBook;
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
path: novelPath,
|
||||
name: book.title,
|
||||
cover: book.coverUrl ? book.coverUrl.split("?")[0] : defaultCover,
|
||||
genres: book.tags?.join(", "),
|
||||
@@ -84,10 +85,13 @@ class AuthorToday implements Plugin.PluginBase {
|
||||
status: book.isFinished ? NovelStatus.Completed : NovelStatus.Ongoing,
|
||||
};
|
||||
|
||||
if (book.annotation) novel.summary += book.annotation + "\n";
|
||||
if (book.authorNotes) novel.summary += "Примечания автора:\n" + book.authorNotes;
|
||||
if (book.annotation) {
|
||||
novel.summary += book.annotation + "\n";
|
||||
}
|
||||
if (book.authorNotes) {
|
||||
novel.summary += "Примечания автора:\n" + book.authorNotes;
|
||||
}
|
||||
|
||||
// all chapters
|
||||
const chaptersRaw = await fetchApi(`${apiUrl}v1/work/${workID}/content`, {
|
||||
headers: {
|
||||
Authorization: token,
|
||||
@@ -96,12 +100,15 @@ class AuthorToday implements Plugin.PluginBase {
|
||||
|
||||
const chaptersJSON = (await chaptersRaw.json()) as ChaptersEntity[];
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
chaptersJSON.forEach((chapter, chapterIndex) => {
|
||||
if (chapter.isAvailable && !chapter.isDraft) {
|
||||
chapters.push({
|
||||
name: chapter.title || 'Глава ' + (chapterIndex + 1),
|
||||
url: `${apiUrl}v1/work/${workID}/chapter/${chapter.id}/text`,
|
||||
releaseTime: dayjs(chapter.publishTime || chapter.lastModificationTime).format("LLL"),
|
||||
name: chapter.title || "Глава " + (chapterIndex + 1),
|
||||
path: `v1/work/${workID}/chapter/${chapter.id}/text`,
|
||||
releaseTime: dayjs(
|
||||
chapter.publishTime || chapter.lastModificationTime,
|
||||
).format("LLL"),
|
||||
chapterNumber: (chapter.sortOrder || chapterIndex) + 1,
|
||||
});
|
||||
}
|
||||
@@ -111,8 +118,8 @@ class AuthorToday implements Plugin.PluginBase {
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(chapterUrl, {
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(apiUrl + chapterPath, {
|
||||
headers: {
|
||||
Authorization: token,
|
||||
},
|
||||
@@ -159,15 +166,14 @@ class AuthorToday implements Plugin.PluginBase {
|
||||
.text()
|
||||
.trim();
|
||||
let cover = loadedCheerio(element).find("img").attr("src");
|
||||
const url = loadedCheerio(element)
|
||||
const path = loadedCheerio(element)
|
||||
.find('div[class="book-title"] a')
|
||||
.attr("href")
|
||||
?.split("/")?.[2];
|
||||
.attr("href");
|
||||
|
||||
cover = cover ? cover.split("?")[0] : defaultCover;
|
||||
|
||||
if (!url) return;
|
||||
novels.push({ name, cover, url: this.site + "/work/" + url });
|
||||
if (!path) return;
|
||||
novels.push({ name, cover, path });
|
||||
});
|
||||
|
||||
return novels;
|
||||
@@ -193,7 +199,7 @@ class AuthorToday implements Plugin.PluginBase {
|
||||
label: "Жанры",
|
||||
value: "",
|
||||
options: [
|
||||
{ label: "Все", value:"" },
|
||||
{ label: "Все", value: "" },
|
||||
{ label: "Альтернативная история", value: "sf-history" },
|
||||
{ label: "Антиутопия", value: "dystopia" },
|
||||
{ label: "Бизнес-литература", value: "biznes-literatura" },
|
||||
|
||||
@@ -14,10 +14,15 @@ class Bookriver implements Plugin.PluginBase {
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + `/genre?page=${pageNo}&perPage=24&sortingType=`;
|
||||
url += showLatestNovels ? "last-update" : filters?.sort?.value || "bestseller";
|
||||
let url = this.site + "/genre?page=" + pageNo + "&perPage=24&sortingType=";
|
||||
url += showLatestNovels
|
||||
? "last-update"
|
||||
: filters?.sort?.value || "bestseller";
|
||||
|
||||
if (filters?.genres?.value?.length) {
|
||||
url += "&g=" + filters.genres.value.join(",");
|
||||
@@ -34,15 +39,15 @@ class Bookriver implements Plugin.PluginBase {
|
||||
novels.push({
|
||||
name: novel.name,
|
||||
cover: novel.coverImages[0].url,
|
||||
url: this.site + "/book/" + novel.slug,
|
||||
path: "/book/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(novelUrl).then((res) => res.text());
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(this.site + novelPath).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(result);
|
||||
|
||||
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
|
||||
@@ -50,8 +55,8 @@ class Bookriver implements Plugin.PluginBase {
|
||||
const book = json.props.pageProps.state.book?.bookPage;
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
name: book?.name,
|
||||
path: novelPath,
|
||||
name: book?.name || "",
|
||||
cover: book?.coverImages[0].url,
|
||||
summary: book?.annotation,
|
||||
author: book?.author?.name,
|
||||
@@ -67,7 +72,7 @@ class Bookriver implements Plugin.PluginBase {
|
||||
if (chapter.available) {
|
||||
chapters.push({
|
||||
name: chapter.name,
|
||||
url: this.site + "/reader/" + book?.slug + "/" + chapter.chapterId,
|
||||
path: "/reader/" + book?.slug + "/" + chapter.chapterId,
|
||||
releaseTime: dayjs(
|
||||
chapter.firstPublishedAt || chapter.createdAt || undefined,
|
||||
).format("LLL"),
|
||||
@@ -79,9 +84,9 @@ class Bookriver implements Plugin.PluginBase {
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const url = "https://api.bookriver.ru/api/v1/books/chapter/text/";
|
||||
const result = await fetchApi(url + chapterUrl.split("/").pop());
|
||||
const result = await fetchApi(url + chapterPath.split("/").pop());
|
||||
const json = (await result.json()) as responseChapter;
|
||||
|
||||
let chapterText = json.data.content || "Конец произведения";
|
||||
@@ -96,7 +101,9 @@ class Bookriver implements Plugin.PluginBase {
|
||||
searchTerm: string,
|
||||
pageNo: number | undefined = 1,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `https://api.bookriver.ru/api/v1/search/autocomplete?keyword=${searchTerm}&page=${pageNo}&perPage=10`;
|
||||
const url =
|
||||
"https://api.bookriver.ru/api/v1/search/autocomplete?keyword=" + searchTerm +
|
||||
"&page=" + pageNo + "&perPage=10";
|
||||
const result = await fetchApi(url);
|
||||
const json = (await result.json()) as responseSearch;
|
||||
|
||||
@@ -105,12 +112,13 @@ class Bookriver implements Plugin.PluginBase {
|
||||
novels.push({
|
||||
name: novel.name,
|
||||
cover: novel.coverImages[0].url,
|
||||
url: this.site + "/book/" + novel.slug,
|
||||
path: "/book/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
fetchImage = fetchFile;
|
||||
|
||||
filters = {
|
||||
|
||||
+18
-20
@@ -41,25 +41,24 @@ class ficbook implements Plugin.PluginBase {
|
||||
: defaultCover;
|
||||
if (!name || !url) return;
|
||||
|
||||
novels.push({ name, cover, url: this.site + url.replace(/\?.*/g, "") });
|
||||
novels.push({ name, cover, path: url.replace(/\?.*/g, "") });
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(novelUrl).then((res) => res.text());
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(this.site + novelPath).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(result);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
path: novelPath,
|
||||
name: (
|
||||
loadedCheerio('h1[itemprop="headline"]').text() ||
|
||||
loadedCheerio('h1[itemprop="name"]').text() ||
|
||||
""
|
||||
).trim(),
|
||||
};
|
||||
|
||||
novel.name = (
|
||||
loadedCheerio('h1[itemprop="headline"]').text() ||
|
||||
loadedCheerio('h1[itemprop="name"]').text()
|
||||
).trim();
|
||||
|
||||
novel.cover = loadedCheerio('meta[property="og:image"]').attr("content");
|
||||
novel.summary = loadedCheerio('div[itemprop="description"]').text().trim();
|
||||
novel.author = loadedCheerio('a[itemprop="author"]').text();
|
||||
@@ -91,7 +90,7 @@ class ficbook implements Plugin.PluginBase {
|
||||
const name = loadedCheerio(".title-area > h2").text();
|
||||
const releaseTime = loadedCheerio(".part-date > span").attr("title");
|
||||
|
||||
if (name) chapters.push({ name, url: novelUrl, releaseTime });
|
||||
if (name) chapters.push({ name, path: novelPath, releaseTime });
|
||||
} else {
|
||||
loadedCheerio("li.part").each((chapterIndex, element) => {
|
||||
const name = loadedCheerio(element).find("h3").text();
|
||||
@@ -99,22 +98,21 @@ class ficbook implements Plugin.PluginBase {
|
||||
if (!name || !url) return;
|
||||
|
||||
const releaseTime = loadedCheerio(element).find("div > span").attr("title");
|
||||
chapters.push({
|
||||
name,
|
||||
url: this.site + url,
|
||||
releaseTime,
|
||||
chapters.push({
|
||||
name,
|
||||
path: url.replace(this.site, ""),
|
||||
releaseTime,
|
||||
chapterNumber: chapterIndex + 1,
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(chapterUrl).then((res) => res.text());
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(this.site + chapterPath).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(result);
|
||||
let chapterText = "";
|
||||
|
||||
@@ -154,12 +152,12 @@ class ficbook implements Plugin.PluginBase {
|
||||
|
||||
json.data?.data?.forEach((novel) => {
|
||||
const name = novel.title.trim();
|
||||
const url = this.site + "/readfic/" + novel.slug;
|
||||
const path = "/readfic/" + novel.slug;
|
||||
const cover = novel.cover
|
||||
? "https://images.ficbook.net/fanfic-covers/" + novel.cover
|
||||
: defaultCover;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
novels.push({ name, cover, path });
|
||||
});
|
||||
|
||||
return novels;
|
||||
|
||||
+31
-28
@@ -14,7 +14,10 @@ class Jaomix implements Plugin.PluginBase {
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + "/?searchrn";
|
||||
|
||||
@@ -45,34 +48,33 @@ class Jaomix implements Plugin.PluginBase {
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio('div[class="block-home"] > div[class="one"]').each(
|
||||
function () {
|
||||
const name = loadedCheerio(this)
|
||||
loadedCheerio('div[class="block-home"] > div[class="one"]')
|
||||
.each((index, element) => {
|
||||
const name = loadedCheerio(element)
|
||||
.find('div[class="img-home"] > a')
|
||||
.attr("title");
|
||||
const cover = loadedCheerio(this)
|
||||
const cover = loadedCheerio(element)
|
||||
.find('div[class="img-home"] > a > img')
|
||||
.attr("src")
|
||||
?.replace("-150x150", "");
|
||||
const url = loadedCheerio(this)
|
||||
const url = loadedCheerio(element)
|
||||
.find('div[class="img-home"] > a')
|
||||
.attr("href");
|
||||
|
||||
if (!name || !url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
},
|
||||
);
|
||||
novels.push({ name, cover, path: url.replace(this.site, "") });
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(novelUrl).then((res) => res.text());
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(this.site + novelPath).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
path: novelPath,
|
||||
name: loadedCheerio('div[class="desc-book"] > h1').text().trim(),
|
||||
cover: loadedCheerio('div[class="img-book"] > img').attr("src"),
|
||||
summary: loadedCheerio('div[id="desc-tab"]').text().trim(),
|
||||
@@ -100,11 +102,11 @@ class Jaomix implements Plugin.PluginBase {
|
||||
if (!name || !url) return;
|
||||
|
||||
const releaseDate = loadedCheerio(element).find("time").text();
|
||||
chapters.push({
|
||||
name,
|
||||
url,
|
||||
releaseTime: this.parseDate(releaseDate),
|
||||
chapterNumber: totalChapters - chapterIndex
|
||||
chapters.push({
|
||||
name,
|
||||
path: url.replace(this.site, ""),
|
||||
releaseTime: this.parseDate(releaseDate),
|
||||
chapterNumber: totalChapters - chapterIndex,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,8 +114,8 @@ class Jaomix implements Plugin.PluginBase {
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const body = await fetchApi(chapterUrl).then((res) => res.text());
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const body = await fetchApi(this.site + chapterPath).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
loadedCheerio('div[class="adblock-service"]').remove();
|
||||
@@ -126,29 +128,30 @@ class Jaomix implements Plugin.PluginBase {
|
||||
searchTerm: string,
|
||||
pageNo: number | undefined = 1,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/?searchrn=${searchTerm}&but=Поиск по названию&sortby=upd&gpage=${pageNo}`;
|
||||
const url =
|
||||
this.site + "/?searchrn=" + searchTerm +
|
||||
"&but=Поиск по названию&sortby=upd&gpage=" + pageNo;
|
||||
const body = await fetchApi(url).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio('div[class="block-home"] > div[class="one"]').each(
|
||||
function () {
|
||||
const name = loadedCheerio(this)
|
||||
loadedCheerio('div[class="block-home"] > div[class="one"]')
|
||||
.each((index, element) => {
|
||||
const name = loadedCheerio(element)
|
||||
.find('div[class="img-home"] > a')
|
||||
.attr("title");
|
||||
const cover = loadedCheerio(this)
|
||||
const cover = loadedCheerio(element)
|
||||
.find('div[class="img-home"] > a > img')
|
||||
.attr("src")
|
||||
?.replace("-150x150", "");
|
||||
const url = loadedCheerio(this)
|
||||
const url = loadedCheerio(element)
|
||||
.find('div[class="img-home"] > a')
|
||||
.attr("href");
|
||||
|
||||
if (!name || !url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
},
|
||||
);
|
||||
novels.push({ name, cover, path: url.replace(this.site, "") });
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
+12
-12
@@ -9,7 +9,7 @@ class novelOvh implements Plugin.PluginBase {
|
||||
name = "НовелОВХ";
|
||||
site = "https://novel.ovh";
|
||||
version = "1.0.0";
|
||||
icon = "src/ru/novelOvh/icon.png";
|
||||
icon = "src/ru/novelovh/icon.png";
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
@@ -27,21 +27,21 @@ class novelOvh implements Plugin.PluginBase {
|
||||
novels.push({
|
||||
name: novel.name.ru,
|
||||
cover: novel.poster,
|
||||
url: this.site + "/novel/" + novel.slug,
|
||||
path: "/novel/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(
|
||||
novelUrl + "?_data=routes/reader/book/$slug/index",
|
||||
this.site + novelPath + "?_data=routes/reader/book/$slug/index",
|
||||
);
|
||||
const json = (await result.json()) as responseNovel;
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
path: novelPath,
|
||||
name: json.book.name.ru,
|
||||
cover: json.book.poster,
|
||||
genres: json.book.labels?.map?.((label) => label.name).join(","),
|
||||
@@ -67,10 +67,10 @@ class novelOvh implements Plugin.PluginBase {
|
||||
chapters.push({
|
||||
name:
|
||||
chapter.title ||
|
||||
"Том " + (chapter.volume || 0) + " " +
|
||||
(chapter.name || "Глава " +
|
||||
(chapter.number || json.chapters.length - chapterIndex)),
|
||||
url: novelUrl + "/" + chapter.id + "/0",
|
||||
"Том " + (chapter.volume || 0) + " " +
|
||||
(chapter.name || "Глава " +
|
||||
(chapter.number || json.chapters.length - chapterIndex)),
|
||||
path: novelPath + "/" + chapter.id + "/0",
|
||||
releaseTime: dayjs(chapter.createdAt).format("LLL"),
|
||||
chapterNumber: json.chapters.length - chapterIndex,
|
||||
}),
|
||||
@@ -80,9 +80,9 @@ class novelOvh implements Plugin.PluginBase {
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(
|
||||
"https://api.novel.ovh/v2/chapters/" + chapterUrl.split("/")[5],
|
||||
"https://api.novel.ovh/v2/chapters/" + chapterPath.split("/")[3],
|
||||
);
|
||||
const book = (await result.json()) as responseChapter;
|
||||
const image = Object.fromEntries(
|
||||
@@ -106,7 +106,7 @@ class novelOvh implements Plugin.PluginBase {
|
||||
novels.push({
|
||||
name: novel.name.ru,
|
||||
cover: novel.poster,
|
||||
url: this.site + "/novel/" + novel.slug,
|
||||
path: "/novel/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
+22
-19
@@ -6,6 +6,13 @@ import { NovelStatus } from "@libs/novelStatus";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const statusKey: { [key: string]: string } = {
|
||||
"active": NovelStatus.Ongoing,
|
||||
"completed": NovelStatus.Completed,
|
||||
"freezed": NovelStatus.OnHiatus,
|
||||
"unknown": NovelStatus.Unknown,
|
||||
};
|
||||
|
||||
class TL implements Plugin.PluginBase {
|
||||
id = "TL";
|
||||
name = "NovelTL";
|
||||
@@ -24,9 +31,8 @@ class TL implements Plugin.PluginBase {
|
||||
"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
referrer: this.site,
|
||||
Referer: this.site,
|
||||
body: JSON.stringify({
|
||||
operationName: "Projects",
|
||||
query:
|
||||
"query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title fullUrl covers{url}}}}",
|
||||
variables: {
|
||||
@@ -47,7 +53,7 @@ class TL implements Plugin.PluginBase {
|
||||
json.data?.projects?.content?.forEach((novel) =>
|
||||
novels.push({
|
||||
name: novel.title,
|
||||
url: "https://" + novel.fullUrl,
|
||||
path: novel.fullUrl,
|
||||
cover: novel?.covers?.[0]?.url
|
||||
? this.site + novel.covers[0].url
|
||||
: defaultCover,
|
||||
@@ -57,7 +63,7 @@ class TL implements Plugin.PluginBase {
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(this.site + "/api/site/v2/graphql", {
|
||||
method: "post",
|
||||
headers: {
|
||||
@@ -65,28 +71,25 @@ class TL implements Plugin.PluginBase {
|
||||
"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
referrer: this.site,
|
||||
Referer: this.site,
|
||||
body: JSON.stringify({
|
||||
operationName: "Book",
|
||||
query:
|
||||
'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus fullUrl covers{url}persons(langs:["ru","en","*"],roles:["author","illustrator"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{shortName chapters{title publishDate fullUrl published}}}}}}}',
|
||||
variables: {
|
||||
url: novelUrl,
|
||||
url: novelPath,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const json = (await result.json()) as response;
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
name: json.data.project?.title,
|
||||
path: novelPath,
|
||||
name: json.data.project?.title || "",
|
||||
cover: json.data.project?.covers?.[0]?.url
|
||||
? this.site + json.data.project.covers[0].url
|
||||
: defaultCover,
|
||||
summary: json.data.project?.annotation?.text,
|
||||
status:
|
||||
json.data.project?.translationStatus === "active"
|
||||
? NovelStatus.Ongoing
|
||||
: NovelStatus.Completed,
|
||||
statusKey[json.data.project?.translationStatus || "unknown"] || NovelStatus.Unknown,
|
||||
};
|
||||
|
||||
const genres = [json.data.project?.tags, json.data.project?.genres]
|
||||
@@ -119,9 +122,10 @@ class TL implements Plugin.PluginBase {
|
||||
name:
|
||||
(volume.shortName || "Том " + (volumeIndex + 1)) + " " +
|
||||
(chapter.title || "Глава " + (chapterIndex + 1)),
|
||||
url: "https://" + chapter.fullUrl,
|
||||
path: chapter.fullUrl,
|
||||
releaseTime: dayjs(chapter.publishDate).format("LLL"),
|
||||
chapterNumber: chapters.length + 1,
|
||||
page: work.title || "Основная серия",
|
||||
});
|
||||
}
|
||||
}),
|
||||
@@ -132,7 +136,7 @@ class TL implements Plugin.PluginBase {
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(this.site + "/api/site/v2/graphql", {
|
||||
method: "post",
|
||||
headers: {
|
||||
@@ -140,12 +144,12 @@ class TL implements Plugin.PluginBase {
|
||||
"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
referrer: this.site,
|
||||
Referer: this.site,
|
||||
body: JSON.stringify({
|
||||
query:
|
||||
"query($url:String){chapter(chapter:{fullUrl:$url}){text{text}}}",
|
||||
variables: {
|
||||
url: decodeURI(chapterUrl),
|
||||
url: decodeURI(chapterPath),
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -177,9 +181,8 @@ class TL implements Plugin.PluginBase {
|
||||
"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
referrer: this.site,
|
||||
Referer: this.site,
|
||||
body: JSON.stringify({
|
||||
operationName: "Projects",
|
||||
query:
|
||||
"query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title fullUrl covers{url}}}}",
|
||||
variables: {
|
||||
@@ -198,7 +201,7 @@ class TL implements Plugin.PluginBase {
|
||||
json?.data?.projects?.content?.forEach((novel) =>
|
||||
novels.push({
|
||||
name: novel.title,
|
||||
url: "https://" + novel.fullUrl,
|
||||
path: novel.fullUrl,
|
||||
cover: novel?.covers?.[0]?.url
|
||||
? this.site + novel.covers[0].url
|
||||
: defaultCover,
|
||||
|
||||
@@ -5,6 +5,12 @@ import { NovelStatus } from "@libs/novelStatus";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const statusKey: { [key: number]: string } = {
|
||||
1: NovelStatus.Ongoing,
|
||||
2: NovelStatus.Completed,
|
||||
3: NovelStatus.OnHiatus,
|
||||
};
|
||||
|
||||
class RNBH implements Plugin.PluginBase {
|
||||
id = "RNBH.org";
|
||||
name = "RanobeHub";
|
||||
@@ -14,10 +20,13 @@ class RNBH implements Plugin.PluginBase {
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + "/api/search?page=" + pageNo + "&sort=";
|
||||
url += showLatestNovels
|
||||
url += showLatestNovels
|
||||
? "last_chapter_at"
|
||||
: filters?.sort?.value || "computed_rating";
|
||||
url += "&status=" + (filters?.status?.value ? filters?.status?.value : "0");
|
||||
@@ -30,7 +39,9 @@ class RNBH implements Plugin.PluginBase {
|
||||
const includeTags = [
|
||||
filters.tags?.value?.include,
|
||||
filters.events?.value?.include,
|
||||
].flat().filter((t) => t);
|
||||
]
|
||||
.flat()
|
||||
.filter((t) => t);
|
||||
|
||||
if (includeTags.length) {
|
||||
url += "&tags:positive=" + includeTags.join(",");
|
||||
@@ -39,7 +50,9 @@ class RNBH implements Plugin.PluginBase {
|
||||
const excludeTags = [
|
||||
filters.tags?.value?.exclude,
|
||||
filters.events?.value?.exclude,
|
||||
].flat().filter((t) => t);
|
||||
]
|
||||
.flat()
|
||||
.filter((t) => t);
|
||||
|
||||
if (excludeTags.length) {
|
||||
url += "&tags:negative=" + excludeTags.join(",");
|
||||
@@ -54,29 +67,24 @@ class RNBH implements Plugin.PluginBase {
|
||||
novels.push({
|
||||
name: novel.names.rus || novel.names.eng || novel.names.original,
|
||||
cover: novel.poster.medium,
|
||||
url: novel.url,
|
||||
path: "/ranobe/" + novel.id,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const novelId = novelUrl
|
||||
.substring("https://ranobehub.org/ranobe/".length)
|
||||
.split("-")[0];
|
||||
const result = await fetchApi(this.site + "/api/ranobe/" + novelId);
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(this.site + "/api" + novelPath);
|
||||
const json = (await result.json()) as { data: responseNovel };
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
name: json.data.names?.rus || json.data.names.eng,
|
||||
path: novelPath,
|
||||
name: json.data.names.rus || json.data.names.eng || "",
|
||||
cover: json.data.posters.medium,
|
||||
summary: json.data.description,
|
||||
summary: json.data.description.trim(),
|
||||
author: json.data?.authors?.[0]?.name_eng || "",
|
||||
status: json.data.status.title.includes("процессе")
|
||||
? NovelStatus.Ongoing
|
||||
: NovelStatus.Completed,
|
||||
status: statusKey[json.data.status.id] || NovelStatus.Unknown,
|
||||
};
|
||||
|
||||
const tags = [json.data.tags.events, json.data.tags.genres]
|
||||
@@ -89,28 +97,33 @@ class RNBH implements Plugin.PluginBase {
|
||||
}
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
const chaptersRaw = await fetchApi(`${this.site}/api/ranobe/${novelId}/contents`);
|
||||
const chaptersRaw = await fetchApi(
|
||||
this.site + "/api" + novelPath + "/contents",
|
||||
);
|
||||
const chaptersJSON = (await chaptersRaw.json()) as {
|
||||
volumes: VolumesEntity[];
|
||||
};
|
||||
|
||||
chaptersJSON.volumes.forEach((volume) =>
|
||||
volume.chapters?.forEach((chapter) => {
|
||||
chapters.push({
|
||||
name: chapter.name,
|
||||
url: chapter.url,
|
||||
releaseTime: dayjs(parseInt(chapter.changed_at, 10) * 1000).format("LLL"),
|
||||
chapterNumber: chapters.length + 1,
|
||||
});
|
||||
}),
|
||||
volume.chapters?.forEach((chapter) =>
|
||||
chapters.push({
|
||||
name: chapter.name,
|
||||
path:
|
||||
volume.num && chapter.num
|
||||
? novelPath + "/" + volume.num + "/" + chapter.num
|
||||
: chapter.url.replace(this.site, ""),
|
||||
releaseTime: dayjs(parseInt(chapter.changed_at, 10) * 1000).format("LLL"),
|
||||
chapterNumber: chapters.length + 1,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(chapterUrl);
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(this.site + chapterPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
@@ -132,10 +145,7 @@ class RNBH implements Plugin.PluginBase {
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
//pageNo: number | undefined = 1,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/api/fulltext/global?query=${searchTerm}&take=10`;
|
||||
const result = await fetchApi(url);
|
||||
const data = (await result.json()) as responseSearch[];
|
||||
@@ -151,11 +161,7 @@ class RNBH implements Plugin.PluginBase {
|
||||
novel.name ||
|
||||
novel?.names?.original ||
|
||||
"",
|
||||
url:
|
||||
"https://ranobehub.org/ranobe/" +
|
||||
novel?.url?.match(
|
||||
/https:\/\/ranobehub\.org\/ranobe\/(.*?)\?utm_source=search_name&utm_medium=search&utm_campaign=search_using/,
|
||||
)?.[1],
|
||||
path: "/ranobe/" + novel.id,
|
||||
cover: novel?.image?.replace("/small", "/medium"),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -5,12 +5,12 @@ import { NovelStatus } from "@libs/novelStatus";
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const statusKey = [
|
||||
NovelStatus.Ongoing,
|
||||
NovelStatus.Completed,
|
||||
NovelStatus.OnHiatus,
|
||||
NovelStatus.Cancelled,
|
||||
];
|
||||
const statusKey: { [key: number]: string } = {
|
||||
1: NovelStatus.Ongoing,
|
||||
2: NovelStatus.Completed,
|
||||
3: NovelStatus.OnHiatus,
|
||||
4: NovelStatus.Cancelled,
|
||||
};
|
||||
|
||||
class RLIB implements Plugin.PluginBase {
|
||||
id = "RLIB";
|
||||
@@ -22,22 +22,26 @@ class RLIB implements Plugin.PluginBase {
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + "/manga-list?sort=";
|
||||
url += showLatestNovels ? "last_chapter_at" : filters?.sort?.value || "rate";
|
||||
url += "&dir=" + (filters?.order?.value || "desc");
|
||||
url += "&chapters[min]=" + (filters?.require_chapters?.value ? "1" : "0");
|
||||
|
||||
Object.entries(filters || {}).forEach(([type, { value }]: any) => {
|
||||
if (value instanceof Array && value.length) {
|
||||
url += "&" + type + "[]=" + value.join("&" + type + "[]=");
|
||||
}
|
||||
if (value?.include instanceof Array && value.include.length) {
|
||||
url += "&" + type + "[include][]=" +
|
||||
url += "&" + type + "[include][]=" +
|
||||
value.include.join("&" + type + "[include][]=");
|
||||
}
|
||||
}
|
||||
if (value?.exclude instanceof Array && value.exclude.length) {
|
||||
url += "&" + type + "[exclude][]=" +
|
||||
url += "&" + type + "[exclude][]=" +
|
||||
value.exclude.join("&" + type + "[exclude][]=");
|
||||
}
|
||||
});
|
||||
@@ -51,27 +55,25 @@ class RLIB implements Plugin.PluginBase {
|
||||
?.replace?.(/[^0-9]/g, "");
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio(".media-card-wrap").each(function () {
|
||||
const name = loadedCheerio(this).find(".media-card__title").text();
|
||||
const cover = loadedCheerio(this).find("a.media-card").attr("data-src");
|
||||
const url = loadedCheerio(this).find("a.media-card").attr("href");
|
||||
loadedCheerio(".media-card-wrap").each((index, element) => {
|
||||
const name = loadedCheerio(element).find(".media-card__title").text();
|
||||
const cover = loadedCheerio(element).find("a.media-card").attr("data-src");
|
||||
const url = loadedCheerio(element).find("a.media-card").attr("href");
|
||||
if (!url) return;
|
||||
novels.push({ name, cover, url });
|
||||
novels.push({ name, cover, path: url.replace(this.site, "") });
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(novelUrl).then((res) => res.text());
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const body = await fetchApi(this.site + novelPath).then((res) => res.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
path: novelPath,
|
||||
name: loadedCheerio(".media-name__main").text()?.trim?.() || "",
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio(".media-name__main").text().trim();
|
||||
|
||||
novel.cover = loadedCheerio(".container_responsive img").attr("src");
|
||||
|
||||
novel.summary = loadedCheerio(".media-description__text").text().trim();
|
||||
@@ -102,45 +104,75 @@ class RLIB implements Plugin.PluginBase {
|
||||
}
|
||||
});
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
const chaptersRaw = body.match(/window\.__DATA__ = ({.*?});/);
|
||||
if (chaptersRaw instanceof Array && chaptersRaw.length >= 2) {
|
||||
const chaptersJson: responseBook = JSON.parse(chaptersRaw[1]);
|
||||
|
||||
let chaptersRaw: any = body.match(
|
||||
/window.__DATA__ = [\s\S]*?window._SITE_COLOR_/gm,
|
||||
);
|
||||
if (!novel.name) {
|
||||
novel.name =
|
||||
chaptersJson.manga.rusName ||
|
||||
chaptersJson.manga.engName ||
|
||||
chaptersJson.manga.name;
|
||||
}
|
||||
novel.status = statusKey[chaptersJson.manga.status] || NovelStatus.Unknown;
|
||||
this.ui = chaptersJson?.user?.id;
|
||||
|
||||
chaptersRaw = chaptersRaw?.[0]
|
||||
?.replace("window.__DATA__ = ", "")
|
||||
?.replace("window._SITE_COLOR_", "")
|
||||
?.trim()
|
||||
?.slice(0, -1);
|
||||
if (!chaptersJson.chapters.list?.length) return novel;
|
||||
const totalChapters = chaptersJson.chapters.list.length;
|
||||
|
||||
const chaptersJson: responseBook = JSON.parse(chaptersRaw);
|
||||
const totalChapters = chaptersJson.chapters.list?.length || 0;
|
||||
novel.status = statusKey[chaptersJson.manga.status - 1] || NovelStatus.Unknown;
|
||||
this.ui = chaptersJson?.user?.id;
|
||||
const customPage: { [key: number]: string } = {};
|
||||
const customOrder: { [key: number]: number } = {};
|
||||
if (chaptersJson.chapters.branches?.length && chaptersJson.chapters.branches.length > 1) {
|
||||
//if the novel is being translated by more than one team
|
||||
chaptersJson.chapters.branches.forEach(({ teams, id }) => {
|
||||
if (teams?.length) {
|
||||
customPage[id || 0] =
|
||||
teams.find((team) => team.is_active)?.name || teams[0].name;
|
||||
}
|
||||
});
|
||||
//fixes the chapter's position.
|
||||
chaptersJson.chapters.list.forEach((chapter) => {
|
||||
chapter.index = customOrder[chapter.branch_id || 0] || 1;
|
||||
customOrder[chapter.branch_id || 0] =
|
||||
(customOrder[chapter.branch_id || 0] || 1) + 1;
|
||||
});
|
||||
};
|
||||
|
||||
chaptersJson.chapters?.list?.forEach((chapter, chapterIndex) =>
|
||||
chapters.push({
|
||||
name:
|
||||
"Том " + chapter.chapter_volume +
|
||||
" Глава " + chapter.chapter_number +
|
||||
(chapter.chapter_name ? " " + chapter.chapter_name.trim() : ""),
|
||||
url:
|
||||
this.site + "/" + chaptersJson.manga.slug +
|
||||
"/v" + chapter.chapter_volume + "/c" + chapter.chapter_number +
|
||||
"?bid=" + (chapter.branch_id || ""),
|
||||
releaseTime: dayjs(chapter.chapter_created_at).format("LLL"),
|
||||
chapterNumber: totalChapters - chapterIndex,
|
||||
}),
|
||||
);
|
||||
|
||||
novel.chapters = chapters.reverse();
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
chaptersJson.chapters.list.forEach((chapter, chapterIndex) =>
|
||||
chapters.push({
|
||||
name:
|
||||
"Том " + chapter.chapter_volume +
|
||||
" Глава " + chapter.chapter_number +
|
||||
(chapter.chapter_name ? " " + chapter.chapter_name.trim() : ""),
|
||||
path:
|
||||
"/" + chaptersJson.manga.slug +
|
||||
"/v" + chapter.chapter_volume +
|
||||
"/c" + chapter.chapter_number +
|
||||
"?bid=" + (chapter.branch_id || ""),
|
||||
releaseTime: dayjs(chapter.chapter_created_at).format("LLL"),
|
||||
chapterNumber:
|
||||
customOrder[chapter.branch_id || 0] - (chapter.index || 0) ||
|
||||
totalChapters - chapterIndex,
|
||||
page: customPage[chapter.branch_id || 0] || "Основной перевод",
|
||||
}),
|
||||
);
|
||||
novel.chapters =
|
||||
chaptersJson.chapters.branches?.length &&
|
||||
chaptersJson.chapters.branches.length > 1
|
||||
? chapters.sort((a, b) => {
|
||||
if ((a.page || 0) > (b.page || 0)) return 1;
|
||||
if ((a.page || 0) < (b.page || 0)) return -1;
|
||||
return (a.chapterNumber || 0) - (b.chapterNumber || 0);
|
||||
})
|
||||
: chapters.reverse();
|
||||
}
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(
|
||||
chapterUrl + (this.ui ? "&ui=" + this.ui : ""),
|
||||
this.site + chapterPath + (this.ui ? "&ui=" + this.ui : ""),
|
||||
).then((res) => res.text());
|
||||
|
||||
const loadedCheerio = parseHTML(result);
|
||||
@@ -160,21 +192,18 @@ class RLIB implements Plugin.PluginBase {
|
||||
return chapterText || "";
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
//pageNo: number | undefined = 1,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
const result = await fetchApi(
|
||||
`${this.site}/search?q=${searchTerm}&type=manga`,
|
||||
this.site + "/search?q=" + searchTerm + "&type=manga",
|
||||
);
|
||||
const body = (await result.json()) as Manga[];
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
body.forEach((novel) =>
|
||||
novels.push({
|
||||
name: novel?.rus_name || novel.name,
|
||||
cover: novel?.coverImage,
|
||||
url: novel?.href || this.site + "/" + novel.slug,
|
||||
name: novel.rus_name || novel.name,
|
||||
cover: novel.coverImage,
|
||||
path: "/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -396,6 +425,11 @@ class RLIB implements Plugin.PluginBase {
|
||||
],
|
||||
type: FilterTypes.ExcludableCheckboxGroup,
|
||||
},
|
||||
require_chapters: {
|
||||
label: "Только проекты с главами",
|
||||
value: true,
|
||||
type: FilterTypes.Switch,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
@@ -424,12 +458,13 @@ interface Manga {
|
||||
href?: string;
|
||||
}
|
||||
interface Chapters {
|
||||
list?: ListEntity[] | null;
|
||||
teams?: TeamsEntity[] | null;
|
||||
branches?: BranchesEntity[] | null;
|
||||
is_paid?: null[] | null;
|
||||
list?: ListEntity[];
|
||||
teams?: TeamsEntity[];
|
||||
branches?: BranchesEntity[];
|
||||
is_paid?: string[] | null;
|
||||
}
|
||||
interface ListEntity {
|
||||
index?: number; //crutch
|
||||
chapter_id: number;
|
||||
chapter_slug: string;
|
||||
chapter_name: string;
|
||||
|
||||
+13
-13
@@ -37,15 +37,15 @@ class RNRF implements Plugin.PluginBase {
|
||||
cover: novel?.verticalImage?.url
|
||||
? this.site + novel.verticalImage.url
|
||||
: defaultCover,
|
||||
url: this.site + "/" + novel.slug,
|
||||
path: "/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(novelUrl);
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(this.site + novelPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
@@ -53,8 +53,8 @@ class RNRF implements Plugin.PluginBase {
|
||||
const book = (JSON.parse(jsonRaw || "{}") as response).props.pageProps.book;
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
name: book?.title,
|
||||
path: novelPath,
|
||||
name: book?.title || "",
|
||||
cover: book?.verticalImage?.url
|
||||
? this.site + book.verticalImage.url
|
||||
: defaultCover,
|
||||
@@ -72,7 +72,7 @@ class RNRF implements Plugin.PluginBase {
|
||||
if (!chapter.isDonate || chapter.isUserPaid) {
|
||||
chapters.push({
|
||||
name: chapter.title,
|
||||
url: this.site + chapter.url,
|
||||
path: chapter.url,
|
||||
releaseTime: dayjs(chapter.publishedAt).format("LLL"),
|
||||
chapterNumber: book.chapters.length - chapterIndex,
|
||||
});
|
||||
@@ -83,8 +83,10 @@ class RNRF implements Plugin.PluginBase {
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(chapterUrl).then((res) => res.text());
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(this.site + chapterPath).then((res) =>
|
||||
res.text(),
|
||||
);
|
||||
|
||||
let loadedCheerio = parseHTML(result);
|
||||
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
|
||||
@@ -105,10 +107,7 @@ class RNRF implements Plugin.PluginBase {
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
//pageNo: number | undefined = 1,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/v3/books?filter[or][0][title][like]=${searchTerm}&filter[or][1][titleEn][like]=${searchTerm}&filter[or][2][fullTitle][like]=${searchTerm}&filter[status][]=active&filter[status][]=abandoned&filter[status][]=completed&expand=verticalImage`;
|
||||
const result = await fetchApi(url);
|
||||
const body = (await result.json()) as { items: Item[] };
|
||||
@@ -120,12 +119,13 @@ class RNRF implements Plugin.PluginBase {
|
||||
cover: novel?.verticalImage?.url
|
||||
? this.site + novel.verticalImage.url
|
||||
: defaultCover,
|
||||
url: this.site + "/" + novel.slug,
|
||||
path: "/" + novel.slug,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
fetchImage = fetchFile;
|
||||
|
||||
filters = {
|
||||
|
||||
+32
-35
@@ -4,32 +4,34 @@ import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
|
||||
class ReN implements Plugin.PluginBase {
|
||||
id = "ReN";
|
||||
name = "Renovels";
|
||||
icon = "src/ru/renovels/icon.png";
|
||||
site = "https://renovels.org";
|
||||
version = "1.0.0";
|
||||
icon = "src/ru/renovels/icon.png";
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
{
|
||||
showLatestNovels,
|
||||
filters,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.site + "/api/search/catalog/?count=30&ordering=";
|
||||
url += filters?.order?.value ? "-" : "";
|
||||
let url = this.site + "/api/search/catalog/?count=30&ordering=-";
|
||||
url += showLatestNovels ? "chapter_date" : filters?.sort?.value || "rating";
|
||||
|
||||
Object.entries(filters || {}).forEach(([type, { value }]: any) => {
|
||||
if (value instanceof Array && value.length) {
|
||||
url += "&" + type + "=" +
|
||||
value.join("&" + type + "=");
|
||||
url += "&" + type + "=" + value.join("&" + type + "=");
|
||||
}
|
||||
if (value?.include instanceof Array && value.include.length) {
|
||||
url += "&" + type + "=" +
|
||||
value.include.join("&" + type + "=");
|
||||
url += "&" + type + "=" + value.include.join("&" + type + "=");
|
||||
}
|
||||
if (value?.exclude instanceof Array && value.exclude.length) {
|
||||
url += "&exclude_" + type + "=" +
|
||||
url += "&exclude_" + type + "=" +
|
||||
value.exclude.join("&exclude_" + type + "=");
|
||||
}
|
||||
});
|
||||
@@ -42,26 +44,27 @@ class ReN implements Plugin.PluginBase {
|
||||
const novels: Plugin.NovelItem[] = body.content.map((novel) => ({
|
||||
name: novel.main_name || novel.secondary_name,
|
||||
cover: this.site + (novel.img.high || novel.img.mid || novel.img.low),
|
||||
url: this.site + "/novel/" + novel.dir,
|
||||
path: "/novel/" + novel.dir,
|
||||
}));
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const novelID = novelUrl.split("/")[4];
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const novelID = novelPath.split("/")[2];
|
||||
const result = await fetchApi(this.site + "/api/titles/" + novelID);
|
||||
const body = (await result.json()) as { content: responseNovel };
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
name: body.content.main_name || body.content.secondary_name || body.content.another_name,
|
||||
path: novelPath,
|
||||
name:
|
||||
body.content.main_name ||
|
||||
body.content.secondary_name ||
|
||||
body.content.another_name,
|
||||
summary: body.content.description,
|
||||
cover:
|
||||
this.site +
|
||||
(body.content.img.high ||
|
||||
body.content.img.mid ||
|
||||
body.content.img.low),
|
||||
(body.content.img.high || body.content.img.mid || body.content.img.low),
|
||||
status:
|
||||
body.content.status.name === "Продолжается"
|
||||
? NovelStatus.Ongoing
|
||||
@@ -77,15 +80,18 @@ class ReN implements Plugin.PluginBase {
|
||||
novel.genres = tags.join(",");
|
||||
}
|
||||
|
||||
const all = body.content.count_chapters / 100 + 1;
|
||||
const totalPages =
|
||||
(body.content.branches?.[0]?.count_chapters ||
|
||||
body.content.count_chapters) / 100;
|
||||
const branch_id = body.content.branches?.[0]?.id || body.content.id;
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
|
||||
for (let i = 0; i < all; i++) {
|
||||
for (let page = 0; page < totalPages; page++) {
|
||||
await delay(1000);
|
||||
const chapterResult = await fetchApi(
|
||||
this.site +
|
||||
"/api/titles/chapters/?branch_id=" + branch_id +
|
||||
"&count=100&page=" + (i + 1),
|
||||
"&count=100&page=" + (page + 1),
|
||||
);
|
||||
const volumes = (await chapterResult.json()) as {
|
||||
content: responseСhapters[];
|
||||
@@ -95,10 +101,10 @@ class ReN implements Plugin.PluginBase {
|
||||
if (!chapter.is_paid || chapter.is_bought) {
|
||||
chapters.push({
|
||||
name:
|
||||
"Том " + chapter.tome +
|
||||
" Глава " + chapter.chapter +
|
||||
"Том " + chapter.tome +
|
||||
" Глава " + chapter.chapter +
|
||||
(chapter.name ? " " + chapter.name.trim() : ""),
|
||||
url: `${this.site}/novel/${novelID}/${chapter.id}/`,
|
||||
path: "/novel/" + novelID + "/" + chapter.id,
|
||||
releaseTime: dayjs(chapter.upload_date).format("LLL"),
|
||||
chapterNumber: chapter.index,
|
||||
});
|
||||
@@ -110,8 +116,8 @@ class ReN implements Plugin.PluginBase {
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const url = this.site + "/api/titles/chapters/" + chapterUrl.split("/")[5];
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const url = this.site + "/api/titles/chapters/" + chapterPath.split("/")[3];
|
||||
const result = await fetchApi(url);
|
||||
const body = (await result.json()) as { content: responseСhapter };
|
||||
|
||||
@@ -131,7 +137,7 @@ class ReN implements Plugin.PluginBase {
|
||||
novels.push({
|
||||
name: novel.main_name || novel.secondary_name,
|
||||
cover: this.site + (novel.img.high || novel.img.mid || novel.img.low),
|
||||
url: this.site + "/novel/" + novel.dir,
|
||||
path: "/novel/" + novel.dir,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -154,15 +160,6 @@ class ReN implements Plugin.PluginBase {
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
order: {
|
||||
label: "Порядок",
|
||||
value: "-",
|
||||
options: [
|
||||
{ label: "По убыванию", value: "-" },
|
||||
{ label: "По возрастанию", value: "" },
|
||||
],
|
||||
type: FilterTypes.Picker,
|
||||
},
|
||||
genres: {
|
||||
label: "Жанры:",
|
||||
value: { include: [], exclude: [] },
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { CheerioAPI, load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { defaultCover } from "@libs/defaultCover";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
|
||||
class TuNovelaLigera implements Plugin.PluginBase {
|
||||
class TuNovelaLigera implements Plugin.PagePlugin {
|
||||
id = "tunovelaligera";
|
||||
name = "TuNovelaLigera";
|
||||
icon = "src/es/tunovelaligera/icon.png";
|
||||
site = "https://tunovelaligera.com";
|
||||
version = "1.0.0";
|
||||
|
||||
async sleep (ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{
|
||||
@@ -36,27 +39,51 @@ class TuNovelaLigera implements Plugin.PluginBase {
|
||||
const url = loadedCheerio(el).find(".h5 > a").attr("href");
|
||||
if (!url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
novels.push({ name, cover, path: url.replace(this.site, '') });
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
parseChapters(loadedCheerio: CheerioAPI): Plugin.ChapterItem[] {
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
loadedCheerio("#lcp_instance_0 li").each((i, el) => {
|
||||
const chapterName = loadedCheerio(el)
|
||||
.find("a")
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "")
|
||||
.trim();
|
||||
|
||||
const chapterUrl = loadedCheerio(el).find("a").attr("href");
|
||||
if(!chapterUrl) return;
|
||||
chapters.push({
|
||||
name: chapterName,
|
||||
path: chapterUrl.replace(this.site, ''),
|
||||
});
|
||||
});
|
||||
return chapters;
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & { totalPages: number; }> {
|
||||
const novelUrl = this.site + novelPath;
|
||||
const result = await fetchApi(novelUrl);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
let lastPage = 1;
|
||||
loadedCheerio('ul.lcp_paginator > li > a').each(function() {
|
||||
const page = Number(this.attribs['title']);
|
||||
if(page && page > lastPage) lastPage = page;
|
||||
});
|
||||
const novel: Plugin.SourceNovel & { totalPages: number; } = {
|
||||
path: novelPath,
|
||||
chapters: [],
|
||||
totalPages: lastPage,
|
||||
name: loadedCheerio(".post-title > h1").text().trim()
|
||||
};
|
||||
|
||||
loadedCheerio(".manga-title-badges").remove();
|
||||
|
||||
novel.name = loadedCheerio(".post-title > h1").text().trim();
|
||||
|
||||
let novelCover = loadedCheerio(".summary_image > a > img");
|
||||
|
||||
novel.cover =
|
||||
@@ -90,95 +117,43 @@ class TuNovelaLigera implements Plugin.PluginBase {
|
||||
|
||||
novel.summary = loadedCheerio("div.summary__content > p").text().trim();
|
||||
|
||||
let chapter: Plugin.ChapterItem[] = [];
|
||||
|
||||
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
let lastPage = 1;
|
||||
lastPage = +loadedCheerio(".lcp_paginator li:last").prev().text();
|
||||
|
||||
const getChapters = async () => {
|
||||
const n = novelUrl.split("/");
|
||||
const novelName = n[4];
|
||||
const formData = new FormData();
|
||||
formData.append("action", "madara_load_more");
|
||||
formData.append("page", "0");
|
||||
formData.append("template", "html/loop/content");
|
||||
formData.append("vars[category_name]", novelName);
|
||||
formData.append("vars[posts_per_page]", "10000");
|
||||
|
||||
const result = await fetchApi(this.site + '/wp-admin/admin-ajax.php', {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const text = await result.text();
|
||||
|
||||
loadedCheerio = parseHTML(text);
|
||||
|
||||
loadedCheerio(".heading").each((i, el) => {
|
||||
const chapterName = loadedCheerio(el)
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "")
|
||||
.trim();
|
||||
const releaseDate = null;
|
||||
const chapterUrl = loadedCheerio(el).find("a").attr("href") || "";
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
url: chapterUrl,
|
||||
releaseTime: releaseDate,
|
||||
});
|
||||
});
|
||||
return chapter.reverse();
|
||||
};
|
||||
|
||||
const getPageChapters = async () => {
|
||||
for (let i = 1; i <= lastPage; i++) {
|
||||
const chaptersUrl = `${novelUrl}?lcp_page0=${i}`;
|
||||
const result = await fetchApi(chaptersUrl);
|
||||
const chaptersHTML = await result.text();
|
||||
|
||||
let chapterCheerio = parseHTML(chaptersHTML);
|
||||
|
||||
chapterCheerio("#lcp_instance_0 li").each((i, el) => {
|
||||
const chapterName = chapterCheerio(el)
|
||||
.find("a")
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "")
|
||||
.trim();
|
||||
|
||||
const releaseDate = chapterCheerio(el).find("span").text().trim();
|
||||
|
||||
const chapterUrl = chapterCheerio(el).find("a").attr("href") || "";
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
});
|
||||
});
|
||||
await delay(1000);
|
||||
}
|
||||
return chapter.reverse();
|
||||
};
|
||||
|
||||
novel.chapters = await getChapters();
|
||||
|
||||
if (!novel.chapters.length) {
|
||||
await delay(1000);
|
||||
novel.chapters = await getPageChapters();
|
||||
}
|
||||
novel.chapters = this.parseChapters(loadedCheerio);
|
||||
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(chapterUrl);
|
||||
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
||||
const novelUrl = this.site + novelPath;
|
||||
const pageUrl = this.site + novelPath + '?lcp_page0=' + page;
|
||||
const result = await fetchApi(novelUrl);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
const latestChapterEle = loadedCheerio('#lcp_instance_0 li').first();
|
||||
const latestChapterUrl = loadedCheerio(latestChapterEle).find("a").attr("href");
|
||||
const latestChapterName = loadedCheerio(latestChapterEle).find("a")
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "")
|
||||
.trim();
|
||||
const latestChapter: Plugin.ChapterItem | undefined = latestChapterUrl ? {
|
||||
path: latestChapterUrl.replace(this.site, ''),
|
||||
name: latestChapterName
|
||||
} : undefined
|
||||
await this.sleep(1000);
|
||||
const pageText = await fetchApi(pageUrl).then(res => res.text());
|
||||
const chapters = this.parseChapters(parseHTML(pageText));
|
||||
return {
|
||||
chapters,
|
||||
latestChapter
|
||||
}
|
||||
}
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetchApi(this.site + chapterPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
loadedCheerio("#hola_siguiente").next().find("div").remove();
|
||||
const chapterText = loadedCheerio("#hola_siguiente").next().html() || "";
|
||||
const chapterText = loadedCheerio(".c-blog-post.post").html() || "";
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
@@ -201,7 +176,7 @@ class TuNovelaLigera implements Plugin.PluginBase {
|
||||
const url = loadedCheerio(el).find(".h4 > a").attr("href");
|
||||
if (!url) return;
|
||||
|
||||
novels.push({ name, cover, url });
|
||||
novels.push({ name, cover, path: url.replace(this.site, '') });
|
||||
});
|
||||
|
||||
return novels;
|
||||
|
||||
@@ -10,8 +10,6 @@ class Smakolykytl implements Plugin.PluginBase {
|
||||
version = "1.0.0";
|
||||
icon = "src/uk/smakolykytl/icon.png";
|
||||
|
||||
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
|
||||
@@ -28,23 +26,23 @@ class Smakolykytl implements Plugin.PluginBase {
|
||||
novels.push({
|
||||
name: novel.title,
|
||||
cover: novel.image.url,
|
||||
url: this.site + "titles/" + novel.id,
|
||||
path: "titles/" + novel.id,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const id = novelUrl.split("/").pop();
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const id = novelPath.split("/").pop();
|
||||
const result = await fetchApi(
|
||||
"https://api.smakolykytl.site/api/user/projects/" + id,
|
||||
);
|
||||
const book = (await result.json()) as response;
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
name: book?.project?.title,
|
||||
path: novelPath,
|
||||
name: book?.project?.title || "",
|
||||
cover: book?.project?.image?.url,
|
||||
summary: book?.project?.description,
|
||||
author: book?.project?.author,
|
||||
@@ -67,22 +65,22 @@ class Smakolykytl implements Plugin.PluginBase {
|
||||
);
|
||||
const data = (await res.json()) as response;
|
||||
data?.books?.forEach((volume) =>
|
||||
volume?.chapters?.forEach((chapter) =>
|
||||
chapters.push({
|
||||
name: volume.title + " " + chapter.title,
|
||||
url: this.site + "read/" + chapter.id,
|
||||
releaseTime: dayjs(chapter.modifiedAt).format("LLL"),
|
||||
chapterNumber: chapters.length + 1,
|
||||
}),
|
||||
),
|
||||
volume?.chapters?.forEach((chapter) =>
|
||||
chapters.push({
|
||||
name: volume.title + " " + chapter.title,
|
||||
path: "read/" + chapter.id,
|
||||
releaseTime: dayjs(chapter.modifiedAt).format("LLL"),
|
||||
chapterNumber: chapters.length + 1,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const id = chapterUrl.split("/").pop();
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const id = chapterPath.split("/").pop();
|
||||
const result = await fetchApi(
|
||||
"https://api.smakolykytl.site/api/user/chapters/" + id,
|
||||
);
|
||||
@@ -93,26 +91,22 @@ class Smakolykytl implements Plugin.PluginBase {
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
//pageNo: number | undefined = 1,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const result = await fetchApi(
|
||||
"https://api.smakolykytl.site/api/user/projects",
|
||||
);
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
const result = await fetchApi("https://api.smakolykytl.site/api/user/projects");
|
||||
const json = (await result.json()) as response;
|
||||
const searchTitle = searchTerm.toLowerCase();
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
|
||||
json?.projects
|
||||
?.filter(
|
||||
(novel) =>
|
||||
novel.title.includes(searchTerm) || String(novel.id) === searchTerm,
|
||||
({ title, id }) =>
|
||||
title.toLowerCase().includes(searchTitle) || String(id) == searchTerm,
|
||||
)
|
||||
?.forEach((novel) =>
|
||||
novels.push({
|
||||
name: novel.title,
|
||||
cover: novel.image.url,
|
||||
url: this.site + "titles/" + novel.id,
|
||||
path: "titles/" + novel.id,
|
||||
}),
|
||||
);
|
||||
return novels;
|
||||
|
||||
@@ -18,10 +18,6 @@ class HakoPlugin implements Plugin.PluginBase {
|
||||
.find("div.thumb_attr.series-title > a")
|
||||
.attr("href");
|
||||
|
||||
if (url && !isUrlAbsolute(url)) {
|
||||
url = this.site + url;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
const name = loadedCheerio(ele)
|
||||
.find(".series-title")
|
||||
@@ -35,7 +31,7 @@ class HakoPlugin implements Plugin.PluginBase {
|
||||
cover = this.site + cover;
|
||||
}
|
||||
|
||||
const novel = { name, url, cover };
|
||||
const novel = { name, path: url.replace(this.site, ''), cover };
|
||||
|
||||
novels.push(novel);
|
||||
}
|
||||
@@ -64,11 +60,12 @@ class HakoPlugin implements Plugin.PluginBase {
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url: novelUrl,
|
||||
path: novelPath,
|
||||
name: 'Không có tiêu đề',
|
||||
};
|
||||
const result = await fetch(novelUrl);
|
||||
const result = await fetch(this.site + novelPath);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
@@ -121,39 +118,46 @@ class HakoPlugin implements Plugin.PluginBase {
|
||||
novel.status = NovelStatus.Unknown;
|
||||
}
|
||||
|
||||
let num = 0, part = 1;
|
||||
const chapters: Plugin.ChapterItem[] = loadedCheerio('.list-chapters li').toArray().map((ele) => {
|
||||
let chapterUrl = this.site + loadedCheerio(ele).find("a").attr("href");
|
||||
const chapterName = loadedCheerio(ele)
|
||||
.find(".chapter-name")
|
||||
.text()
|
||||
.trim();
|
||||
let chapterNumber = Number(chapterName.match(/Chương\s*(\d+)/i)?.[1]);
|
||||
if(chapterNumber){
|
||||
num = chapterNumber;
|
||||
part = 1;
|
||||
}else{
|
||||
chapterNumber = num + part / 10;
|
||||
part ++;
|
||||
}
|
||||
const chapterTime = loadedCheerio(ele)
|
||||
.find(".chapter-time")
|
||||
.text()
|
||||
.split('/')
|
||||
.map(x => Number(x));
|
||||
return {
|
||||
url: chapterUrl || '',
|
||||
name: chapterName,
|
||||
releaseTime: new Date(chapterTime[2], chapterTime[1], chapterTime[0]).toISOString(),
|
||||
chapterNumber: chapterNumber,
|
||||
};
|
||||
}).filter(c => c.url);
|
||||
|
||||
novel.chapters = chapters;
|
||||
novel.chapters = [];
|
||||
loadedCheerio('.volume-list').each((idx, ele) => {
|
||||
const customPage = loadedCheerio(ele).find('.sect-title').text().trim();
|
||||
let num = 0, part = 1;
|
||||
loadedCheerio(ele).find('.list-chapters > li').each((idx, chapterEle) => {
|
||||
const path = loadedCheerio(chapterEle).find("a").attr("href");
|
||||
const chapterName = loadedCheerio(chapterEle)
|
||||
.find(".chapter-name")
|
||||
.text()
|
||||
.trim();
|
||||
let chapterNumber = Number(chapterName.match(/Chương\s*(\d+)/i)?.[1]);
|
||||
if(chapterNumber){
|
||||
num = chapterNumber;
|
||||
part = 1;
|
||||
}else{
|
||||
chapterNumber = num + part / 10;
|
||||
part ++;
|
||||
}
|
||||
const chapterTime = loadedCheerio(chapterEle)
|
||||
.find(".chapter-time")
|
||||
.text()
|
||||
.split('/')
|
||||
.map(x => Number(x));
|
||||
if(path){
|
||||
novel.chapters?.push(
|
||||
{
|
||||
page: customPage,
|
||||
path,
|
||||
name: chapterName,
|
||||
releaseTime: new Date(chapterTime[2], chapterTime[1], chapterTime[0]).toISOString(),
|
||||
chapterNumber: chapterNumber,
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetch(chapterUrl);
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetch(this.site + chapterPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
|
||||
import { CheerioAPI, load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
|
||||
class TruyenConect implements Plugin.PluginBase {
|
||||
id = "truyenconect";
|
||||
name = "Truyen Conect";
|
||||
icon = "src/vi/truyenconect/icon.png";
|
||||
site = "https://truyenconect.com";
|
||||
version = "1.0.0";
|
||||
parseNovels(loadedCheerio: CheerioAPI, selector: string){
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio(selector).each(function () {
|
||||
const url = loadedCheerio(this).find('a').attr('href');
|
||||
if(url){
|
||||
novels.push({
|
||||
name: loadedCheerio(this).find('img').attr('alt') || '',
|
||||
url,
|
||||
cover: loadedCheerio(this).find('img').attr('data-src'),
|
||||
})
|
||||
}
|
||||
});
|
||||
return novels;
|
||||
}
|
||||
async popularNovels(pageNo: number, {filters, showLatestNovels}: Plugin.PopularNovelsOptions<typeof this.filters>): Promise<Plugin.NovelItem[]> {
|
||||
let link = this.site;
|
||||
let selector = '.c-page__content > .grid9.block .item-thumb.c-image-hover';
|
||||
|
||||
if(showLatestNovels){
|
||||
selector = '.c-page__content .page-content-listing.item-big_thumbnail .item-thumb.c-image-hover';
|
||||
}
|
||||
|
||||
if(filters.category.value){
|
||||
link += '/' + filters.category.value;
|
||||
selector = 'table.manga-shortcodes.manga-chapters-listing td[width="10%"]';
|
||||
if(filters.category.value === 'the-loai'){
|
||||
selector = '.item-thumb.hover-details.c-image-hover'
|
||||
link += '/' + filters.genre.value;
|
||||
}
|
||||
link += '?page=' + pageNo;
|
||||
}
|
||||
|
||||
const body = await fetch(link).then(r => r.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio, selector);
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio(".post-title > h1").text().trim();
|
||||
|
||||
novel.cover = loadedCheerio(".summary_image > a > img").attr(
|
||||
"data-src"
|
||||
);
|
||||
|
||||
loadedCheerio(".post-content_item").each(function () {
|
||||
const detailName = loadedCheerio(this)
|
||||
.find(".summary-heading > h5")
|
||||
.text()
|
||||
.trim();
|
||||
const detail = loadedCheerio(this).find(".summary-content").html();
|
||||
|
||||
if (!detail) return;
|
||||
|
||||
switch (detailName) {
|
||||
case "Genre":
|
||||
novel.genres = loadedCheerio(detail)
|
||||
.children("a")
|
||||
.map((i, el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join(",");
|
||||
break;
|
||||
case "Author":
|
||||
novel.author = loadedCheerio(detail)
|
||||
.children("a")
|
||||
.map((i, el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join(", ");
|
||||
break;
|
||||
case "Novel":
|
||||
novel.status = detail?.trim().replace(/G/g, "g");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
loadedCheerio(
|
||||
".description-summary > div.summary__content > div"
|
||||
).remove();
|
||||
|
||||
novel.summary = loadedCheerio(
|
||||
".description-summary > div.summary__content"
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
let chapter: Plugin.ChapterItem[] = [];
|
||||
loadedCheerio(".wp-manga-chapter").each(function () {
|
||||
const chapterName = loadedCheerio(this)
|
||||
.find("a")
|
||||
.text()
|
||||
.replace(/[\t\n]/g, "")
|
||||
.trim();
|
||||
const releaseDate = loadedCheerio(this).find("span").text().trim();
|
||||
const chapterUrl = loadedCheerio(this).find("a").attr("href");
|
||||
|
||||
if (!chapterUrl) return;
|
||||
|
||||
chapter.push({
|
||||
name: chapterName,
|
||||
releaseTime: releaseDate,
|
||||
url: chapterUrl,
|
||||
});
|
||||
});
|
||||
|
||||
novel.chapters = chapter.reverse();
|
||||
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(chapterUrl);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
loadedCheerio("img").removeAttr("srcset");
|
||||
let chapterText = loadedCheerio(".reading-content").html() || '';
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}?s=${searchTerm}&post_type=wp-manga`;
|
||||
const result = await fetchApi(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".c-tabs-item__content").each(function () {
|
||||
const novelName = loadedCheerio(this).find(".h4 > a").text();
|
||||
const novelCover = loadedCheerio(this).find("img").attr("data-src");
|
||||
const novelUrl = loadedCheerio(this).find(".h4 > a").attr("href");
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
novels.push({
|
||||
name: novelName,
|
||||
url: novelUrl,
|
||||
cover: novelCover,
|
||||
});
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url);
|
||||
}
|
||||
filters = {
|
||||
category: {
|
||||
label: "Lọc theo",
|
||||
value: '',
|
||||
type: FilterTypes.Picker,
|
||||
options: [
|
||||
{label: '', value: ''},
|
||||
{label: 'Thể loại', value: 'the-loai'},
|
||||
{label: 'Truyện dịch', value: 'truyen-dich'},
|
||||
{label: 'Truyện convert', value: 'convert'}
|
||||
]
|
||||
},
|
||||
genre: {
|
||||
label: 'Thể loại',
|
||||
value: 'action',
|
||||
type: FilterTypes.Picker,
|
||||
options: [
|
||||
{label: 'Action', value: 'action'},
|
||||
{label: 'Adult', value: 'adult'},
|
||||
{label: 'Adventure', value: 'adventure'},
|
||||
{label: 'Chinese novel', value: 'chinese-novel'},
|
||||
{label: 'Chuyển Sinh', value: 'chuyen-sinh'},
|
||||
{label: 'English Novel', value: 'english-novel'},
|
||||
{label: 'Harem', value: 'harem'},
|
||||
{label: 'Ecchi', value: 'ecchi'},
|
||||
{label: 'Fantasy', value: 'fantasy'},
|
||||
{label: 'Drama', value: 'drama'},
|
||||
{label: 'Game', value: 'game'},
|
||||
{label: 'Tiên hiệp', value: 'tien-hiep'},
|
||||
{label: 'Kiếm Hiệp', value: 'kiem-hiep'},
|
||||
{label: 'Ngôn Tình', value: 'ngon-tinh'},
|
||||
{label: 'Isekai', value: 'isekai'},
|
||||
{label: 'Lịch Sử', value: 'lich-su'},
|
||||
{label: 'Web Novel', value: 'web-novel'},
|
||||
{label: 'Xuyên không', value: 'xuyen-khong'},
|
||||
{label: 'Trọng sinh', value: 'trong-sinh'},
|
||||
{label: 'Trinh thám', value: 'trinh-tham'},
|
||||
{label: 'Dị giới', value: 'di-gioi'},
|
||||
{label: 'Huyền ảo', value: 'huyen-ao'},
|
||||
{label: 'Sắc Hiệp', value: 'sac-hiep'},
|
||||
{label: 'Dị năng', value: 'di-nang'},
|
||||
{label: 'Linh dị', value: 'linh-di'},
|
||||
{label: 'Đô thị', value: 'do-thi'},
|
||||
{label: 'Comedy', value: 'comedy'},
|
||||
{label: 'School Life', value: 'school-life'},
|
||||
{label: 'Romance', value: 'romance'},
|
||||
{label: 'Martial-arts', value: 'martial-arts'},
|
||||
{label: 'Light Novel', value: 'light-novel'},
|
||||
{label: 'Huyền huyễn', value: 'huyen-huyen'},
|
||||
{label: 'Kỳ Huyễn', value: 'ky-huyen'},
|
||||
{label: 'Khoa Huyễn', value: 'khoa-huyen'},
|
||||
{label: 'Võng Du', value: 'vong-du'},
|
||||
{label: 'Đồng Nhân', value: 'dong-nhan'},
|
||||
|
||||
]
|
||||
}
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new TruyenConect();
|
||||
@@ -0,0 +1,299 @@
|
||||
|
||||
import { CheerioAPI, load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { FilterTypes, Filters } from "@libs/filterInputs";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
|
||||
interface VolumeData {
|
||||
story: string;
|
||||
navigation: string;
|
||||
value: string
|
||||
}
|
||||
|
||||
interface ChapterSelect {
|
||||
chap_selector: string;
|
||||
eps_selector: boolean | string;
|
||||
}
|
||||
|
||||
interface VolumeList {
|
||||
chapters: Plugin.ChapterItem[],
|
||||
volumes?: VolumeData[]
|
||||
}
|
||||
|
||||
class TruyenConect implements Plugin.PagePlugin {
|
||||
id = "truyenconect";
|
||||
name = "Truyen Conect";
|
||||
icon = "src/vi/truyenconect/icon.png";
|
||||
site = "https://truyenconect.com";
|
||||
version = "1.0.0";
|
||||
async sleep (ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
parseNovels(loadedCheerio: CheerioAPI, selector: string){
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio(selector).each((idx, ele) => {
|
||||
const url = loadedCheerio(ele).find('a').attr('href');
|
||||
if(url){
|
||||
novels.push({
|
||||
name: loadedCheerio(ele).find('img').attr('alt') || '',
|
||||
path: url.replace(this.site, ''),
|
||||
cover: loadedCheerio(ele).find('img').attr('data-src'),
|
||||
})
|
||||
}
|
||||
});
|
||||
return novels;
|
||||
}
|
||||
async popularNovels(pageNo: number, {filters, showLatestNovels}: Plugin.PopularNovelsOptions<typeof this.filters>): Promise<Plugin.NovelItem[]> {
|
||||
let link = this.site;
|
||||
let selector = '.c-page__content > .grid9.block .item-thumb.c-image-hover';
|
||||
|
||||
if(showLatestNovels){
|
||||
selector = '.c-page__content .page-content-listing.item-big_thumbnail .item-thumb.c-image-hover';
|
||||
}else if(filters.category.value){
|
||||
link += '/' + filters.category.value;
|
||||
selector = 'table.manga-shortcodes.manga-chapters-listing td[width="10%"]';
|
||||
if(filters.category.value === 'the-loai'){
|
||||
selector = '.item-thumb.hover-details.c-image-hover'
|
||||
link += '/' + filters.genre.value;
|
||||
}
|
||||
link += '?page=' + pageNo;
|
||||
}
|
||||
|
||||
const body = await fetch(link).then(r => r.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio, selector);
|
||||
}
|
||||
parseChapters(loadedCheerio: CheerioAPI) {
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
loadedCheerio('option').each((idx, ele) => {
|
||||
let url = ele.attribs['value'];
|
||||
if(!url) return;
|
||||
const chapterId = url.match(/\/(\d+)-/)?.[1];
|
||||
if(chapterId){
|
||||
url = url.replace(chapterId+'-', '') + '-' + chapterId;
|
||||
}
|
||||
let num = url.match(/chuong-(\d+)/)?.[1];
|
||||
chapters.push({
|
||||
path: url.replace(this.site, ''),
|
||||
name: loadedCheerio(ele).text().trim(),
|
||||
chapterNumber: Number(num) || undefined,
|
||||
})
|
||||
})
|
||||
return chapters.reverse();
|
||||
}
|
||||
parseVolumes(loadedCheerio: CheerioAPI) {
|
||||
const volumes: VolumeData[] = [];
|
||||
loadedCheerio('option').each((idx, ele) => {
|
||||
volumes.push({
|
||||
story: ele.attribs['data-story'],
|
||||
navigation: ele.attribs['data-navigation'],
|
||||
value: ele.attribs['value'],
|
||||
})
|
||||
})
|
||||
return volumes;
|
||||
}
|
||||
async getVolumes (firstChapterUrl: string) {
|
||||
const chapterId = firstChapterUrl.match(/-(\d+)$/)?.[1];
|
||||
if(!chapterId) throw new Error('Không tìm thấy chương');
|
||||
const url = this.site + '/truyen/get-chap-selector?chap=' + chapterId;
|
||||
const data: ChapterSelect = await fetch(url).then(r => r.json());
|
||||
const volumeList: VolumeList = {
|
||||
chapters: this.parseChapters(parseHTML(data.chap_selector))
|
||||
}
|
||||
if(data.eps_selector){
|
||||
volumeList.volumes = this.parseVolumes(parseHTML(data.eps_selector as string))
|
||||
}
|
||||
return volumeList
|
||||
}
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & { totalPages: number; }> {
|
||||
const url = this.site + novelPath;
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel & { totalPages: number; }= {
|
||||
name: loadedCheerio(".post-title > h1").text().trim(),
|
||||
path: novelPath,
|
||||
chapters: [],
|
||||
totalPages: 1
|
||||
};
|
||||
|
||||
novel.cover = loadedCheerio(".summary_image > a > img").attr(
|
||||
"data-src"
|
||||
);
|
||||
|
||||
loadedCheerio(".post-content_item").each(function () {
|
||||
const detailName = loadedCheerio(this)
|
||||
.find(".summary-heading > h5")
|
||||
.text()
|
||||
.trim();
|
||||
const detail = loadedCheerio(this).find(".summary-content").html();
|
||||
|
||||
if (!detail) return;
|
||||
|
||||
switch (detailName) {
|
||||
case "Thể loại":
|
||||
novel.genres = loadedCheerio(detail)
|
||||
.children("a")
|
||||
.map((i, el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join(",");
|
||||
break;
|
||||
case "Tác giả":
|
||||
novel.author = loadedCheerio(detail).text().trim();
|
||||
break;
|
||||
case "Hoạ sĩ":
|
||||
novel.artist = loadedCheerio(detail).text().trim();
|
||||
break;
|
||||
case "Trạng thái":
|
||||
const status = detail.trim();
|
||||
if(status === 'Full') novel.status = NovelStatus.Completed;
|
||||
else if(status === 'Tạm ngưng') novel.status = NovelStatus.OnHiatus;
|
||||
else if(status === 'Đang tiến hành') novel.status = NovelStatus.Ongoing
|
||||
else novel.status = NovelStatus.Unknown;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
loadedCheerio(
|
||||
".description-summary > div.summary__content > div"
|
||||
).remove();
|
||||
|
||||
novel.summary = loadedCheerio(
|
||||
".description-summary > div.summary__content"
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
const firstChapLink = loadedCheerio('#init-links > a').first().attr('href');
|
||||
if(!firstChapLink) throw new Error('Không tìm thấy truyện');
|
||||
await this.sleep(1000);
|
||||
const volumeList = await this.getVolumes(firstChapLink);
|
||||
if(volumeList.volumes) {
|
||||
novel.totalPages = volumeList.volumes.length;
|
||||
novel.chapters = volumeList.chapters
|
||||
}else{
|
||||
novel.totalPages = 1;
|
||||
novel.chapters = volumeList.chapters;
|
||||
}
|
||||
return novel;
|
||||
}
|
||||
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
||||
const url = this.site + novelPath;
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
let loadedCheerio = parseHTML(body);
|
||||
const firstChapLink = loadedCheerio('#init-links > a').first().attr('href');
|
||||
if(!firstChapLink) throw new Error('Không tìm thấy truyện');
|
||||
await this.sleep(1000);
|
||||
const volumeList = await this.getVolumes(firstChapLink);
|
||||
const volumeIndex = Number(page) - 1;
|
||||
if(!volumeList.volumes) throw new Error("Không tìm thấy truyện");
|
||||
if(volumeIndex >= volumeList.volumes.length) throw new Error("Không tìm thấy volume");
|
||||
const volume = volumeList.volumes[volumeIndex];
|
||||
const query = `dataEpisodes=${volume.value}&datastory=${volume.story}&dataNavigation=${encodeURIComponent(volume.navigation)}`;
|
||||
const chaptersUrl = `${this.site}/truyen/getreadingchapAction?${query}`;
|
||||
await this.sleep(1000);
|
||||
const res: {err: number, html: string} = await fetch(chaptersUrl).then(r => r.json());
|
||||
if(res.err) throw new Error(res.html);
|
||||
return {
|
||||
chapters: this.parseChapters(parseHTML(res.html))
|
||||
}
|
||||
}
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetch(this.site + chapterPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
let chapterText = loadedCheerio(".reading-content").html() || '';
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}?key=${searchTerm}&page=${pageNo}`;
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".tab-thumb.c-image-hover > a").each((idx, ele) => {
|
||||
const novelName = ele.attribs['title'];
|
||||
const novelCover = loadedCheerio(ele).find('img').first().attr('src');
|
||||
const novelUrl = ele.attribs['href'];
|
||||
|
||||
if (!novelUrl) return;
|
||||
|
||||
novels.push({
|
||||
name: novelName,
|
||||
path: novelUrl.replace(this.site, ''),
|
||||
cover: novelCover,
|
||||
});
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url);
|
||||
}
|
||||
filters = {
|
||||
category: {
|
||||
label: "Lọc theo",
|
||||
value: '',
|
||||
type: FilterTypes.Picker,
|
||||
options: [
|
||||
{label: '', value: ''},
|
||||
{label: 'Thể loại', value: 'the-loai'},
|
||||
{label: 'Truyện dịch', value: 'truyen-dich'},
|
||||
{label: 'Truyện convert', value: 'convert'}
|
||||
]
|
||||
},
|
||||
genre: {
|
||||
label: 'Thể loại',
|
||||
value: 'action',
|
||||
type: FilterTypes.Picker,
|
||||
options: [
|
||||
{label: 'Action', value: 'action'},
|
||||
{label: 'Adult', value: 'adult'},
|
||||
{label: 'Adventure', value: 'adventure'},
|
||||
{label: 'Chinese novel', value: 'chinese-novel'},
|
||||
{label: 'Chuyển Sinh', value: 'chuyen-sinh'},
|
||||
{label: 'English Novel', value: 'english-novel'},
|
||||
{label: 'Harem', value: 'harem'},
|
||||
{label: 'Ecchi', value: 'ecchi'},
|
||||
{label: 'Fantasy', value: 'fantasy'},
|
||||
{label: 'Drama', value: 'drama'},
|
||||
{label: 'Game', value: 'game'},
|
||||
{label: 'Tiên hiệp', value: 'tien-hiep'},
|
||||
{label: 'Kiếm Hiệp', value: 'kiem-hiep'},
|
||||
{label: 'Ngôn Tình', value: 'ngon-tinh'},
|
||||
{label: 'Isekai', value: 'isekai'},
|
||||
{label: 'Lịch Sử', value: 'lich-su'},
|
||||
{label: 'Web Novel', value: 'web-novel'},
|
||||
{label: 'Xuyên không', value: 'xuyen-khong'},
|
||||
{label: 'Trọng sinh', value: 'trong-sinh'},
|
||||
{label: 'Trinh thám', value: 'trinh-tham'},
|
||||
{label: 'Dị giới', value: 'di-gioi'},
|
||||
{label: 'Huyền ảo', value: 'huyen-ao'},
|
||||
{label: 'Sắc Hiệp', value: 'sac-hiep'},
|
||||
{label: 'Dị năng', value: 'di-nang'},
|
||||
{label: 'Linh dị', value: 'linh-di'},
|
||||
{label: 'Đô thị', value: 'do-thi'},
|
||||
{label: 'Comedy', value: 'comedy'},
|
||||
{label: 'School Life', value: 'school-life'},
|
||||
{label: 'Romance', value: 'romance'},
|
||||
{label: 'Martial-arts', value: 'martial-arts'},
|
||||
{label: 'Light Novel', value: 'light-novel'},
|
||||
{label: 'Huyền huyễn', value: 'huyen-huyen'},
|
||||
{label: 'Kỳ Huyễn', value: 'ky-huyen'},
|
||||
{label: 'Khoa Huyễn', value: 'khoa-huyen'},
|
||||
{label: 'Võng Du', value: 'vong-du'},
|
||||
{label: 'Đồng Nhân', value: 'dong-nhan'},
|
||||
]
|
||||
}
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new TruyenConect();
|
||||
@@ -1,164 +0,0 @@
|
||||
import { load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { Filters } from "@libs/filterInputs";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
|
||||
|
||||
class LightNovelVN implements Plugin.PluginBase {
|
||||
id = "lightnovel.vn";
|
||||
name = "Light Novel VN";
|
||||
version = "1.0.0";
|
||||
icon = "src/vi/lightnovelvn/icon.png";
|
||||
filters?: Filters | undefined;
|
||||
site = "https://lightnovel.vn/";
|
||||
baseUrl = this.site;
|
||||
headers = {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}truyen-hot-ds?page=${pageNo}`;
|
||||
const body = await fetch(url).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".flex.flex-col[itemtype='https://schema.org/Book']").each((idx, ele) => {
|
||||
const novelName = loadedCheerio(ele).find('h3[itemprop="name"] > a').text().trim();
|
||||
const img = loadedCheerio(ele).find('noscript').html();
|
||||
const novelCover = img?.match(/srcSet="([^\s]+)/)?.[1];
|
||||
const novelUrl = this.site + loadedCheerio(ele).find('h3[itemprop="name"] > a').attr('href')?.substring(1);
|
||||
|
||||
const novel = {
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
const body = await fetch(url).then((r) => r.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio('h1[itemprop="name"]').text().trim();
|
||||
|
||||
novel.cover = loadedCheerio('header div:nth-child(2) img').attr('srcset')?.split(/\s+/)[0]
|
||||
|
||||
const genres: string[] = [];
|
||||
loadedCheerio('a[itemprop="genre"]').each(function() {
|
||||
genres.push(loadedCheerio(this).text());
|
||||
})
|
||||
novel.genres = genres.join(',');
|
||||
|
||||
novel.status = loadedCheerio('span.font-bold.text-size22:last').text();
|
||||
if(novel.status === 'Đang ra'){
|
||||
novel.status = NovelStatus.Ongoing;
|
||||
}else if(novel.status === 'Hoàn thành'){
|
||||
novel.status = NovelStatus.Completed;
|
||||
}else {
|
||||
novel.status = NovelStatus.Unknown;
|
||||
}
|
||||
novel.author = loadedCheerio('a[itemprop="author"] > span').text();
|
||||
|
||||
novel.summary = loadedCheerio('#bookIntro').text().replace(/\s+/g, ' ');
|
||||
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
await delay(1000);
|
||||
const chapterListUrl = novelUrl + '/chapters?chorder=desc';
|
||||
|
||||
const chapterListBody = await fetchApi(chapterListUrl).then(r => r.text());
|
||||
const loadedChapterList = parseHTML(chapterListBody);
|
||||
|
||||
|
||||
const lastNumber = parseInt(
|
||||
loadedChapterList('ul.chapter-list > li:nth-child(1)').attr('data-chapterno') ?? '0'
|
||||
);
|
||||
const lastChapterUrl = loadedChapterList('ul.chapter-list > li:nth-child(1) > a').attr('href');
|
||||
|
||||
if(lastChapterUrl){
|
||||
const chapterUrlPrefix = this.baseUrl + lastChapterUrl.split('/').slice(1, -1).join('/');
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
for(let i = 1; i <= lastNumber; i++){
|
||||
chapters.push({
|
||||
chapterNumber: i,
|
||||
name: 'Chapter ' + i,
|
||||
url: chapterUrlPrefix + '/chapter-' + i,
|
||||
})
|
||||
}
|
||||
novel.chapters = chapters;
|
||||
}
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const body = await fetchApi(chapterUrl).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let chapterText = loadedCheerio("#chapter-container").html() || '';
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.baseUrl}lnsearchlive`;
|
||||
const link = `${this.baseUrl}search`;
|
||||
const response = await fetchApi(link).then((r) => r.text());
|
||||
const token = parseHTML(response);
|
||||
let verifytoken = token("#novelSearchForm > input").attr("value");
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append("inputContent", searchTerm);
|
||||
|
||||
const body = await fetchApi(
|
||||
url,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { LNRequestVerifyToken: verifytoken! },
|
||||
body: formData,
|
||||
},
|
||||
).then((r) => r.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
let novels: Plugin.NovelItem[] = [];
|
||||
|
||||
let results = JSON.parse(loadedCheerio("body").text());
|
||||
|
||||
loadedCheerio = parseHTML(results.resultview);
|
||||
|
||||
loadedCheerio(".novel-item").each((idx, ele) => {
|
||||
const novelName = loadedCheerio(ele)
|
||||
.find("h4.novel-title")
|
||||
.text()
|
||||
.trim();
|
||||
const novelCover = loadedCheerio(ele).find("img").attr("src");
|
||||
const novelUrl =
|
||||
this.baseUrl + loadedCheerio(ele).find("a").attr("href")?.substring(1);
|
||||
|
||||
novels.push({
|
||||
name: novelName,
|
||||
url: novelUrl,
|
||||
cover: novelCover,
|
||||
});
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url, {headers: this.headers});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new LightNovelVN();
|
||||
@@ -0,0 +1,161 @@
|
||||
import { CheerioAPI, load as parseHTML } from "cheerio";
|
||||
import { fetchApi, fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { Filters } from "@libs/filterInputs";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { NovelItem } from "../../test_web/static/js";
|
||||
|
||||
interface SearchedNovel {
|
||||
name: string;
|
||||
slug: string;
|
||||
coverUrl: string;
|
||||
}
|
||||
interface SearchedResult {
|
||||
data?: SearchedNovel[]
|
||||
}
|
||||
|
||||
class LightNovelVN implements Plugin.PagePlugin {
|
||||
id = "lightnovel.vn";
|
||||
name = "Light Novel VN";
|
||||
version = "1.0.0";
|
||||
icon = "src/vi/lightnovelvn/icon.png";
|
||||
filters?: Filters | undefined;
|
||||
site = "https://lightnovel.vn";
|
||||
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/truyen-hot-ds?page=${pageNo}`;
|
||||
const body = await fetch(url).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
loadedCheerio(".flex.flex-col[itemtype='https://schema.org/Book']").each((idx, ele) => {
|
||||
const novelName = loadedCheerio(ele).find('h3[itemprop="name"] > a').text().trim();
|
||||
const img = loadedCheerio(ele).find('noscript').html();
|
||||
const novelCover = img?.match(/srcSet="([^\s]+)/)?.[1];
|
||||
const novelUrl = loadedCheerio(ele).find('h3[itemprop="name"] > a').attr('href');
|
||||
if(novelUrl)
|
||||
novels.push({
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
path: novelUrl.replace(this.site, ''),
|
||||
});
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
parseChapters(loadedCheerio: CheerioAPI){
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
loadedCheerio('ul.chapter-list > li').each((idx, ele) => {
|
||||
const chNum = Number(loadedCheerio(ele).find('div').first().text());
|
||||
const chapterUrl = loadedCheerio(ele).find('a').attr('href');
|
||||
const name = loadedCheerio(ele).find('a').text().trim();
|
||||
if(chapterUrl){
|
||||
chapters.push({
|
||||
path: chapterUrl.replace(this.site, ''),
|
||||
name,
|
||||
chapterNumber: chNum
|
||||
})
|
||||
}
|
||||
})
|
||||
return chapters;
|
||||
}
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & { totalPages: number; }> {
|
||||
const url = this.site + novelPath;
|
||||
const body = await fetch(url).then((r) => r.text());
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel & { totalPages: number; } = {
|
||||
path: novelPath,
|
||||
chapters: [],
|
||||
name: 'Không có tiêu đề',
|
||||
totalPages: 1
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio('h1[itemprop="name"]').text().trim();
|
||||
|
||||
novel.cover = loadedCheerio('header div:nth-child(2) img').attr('srcset')?.split(/\s+/)[0]
|
||||
|
||||
const genres: string[] = [];
|
||||
loadedCheerio('a[itemprop="genre"]').each(function() {
|
||||
genres.push(loadedCheerio(this).text());
|
||||
})
|
||||
novel.genres = genres.join(',');
|
||||
|
||||
novel.status = loadedCheerio('span.font-bold.text-size22:last').text();
|
||||
if(novel.status === 'Đang ra'){
|
||||
novel.status = NovelStatus.Ongoing;
|
||||
}else if(novel.status === 'Hoàn thành'){
|
||||
novel.status = NovelStatus.Completed;
|
||||
}else {
|
||||
novel.status = NovelStatus.Unknown;
|
||||
}
|
||||
novel.author = loadedCheerio('a[itemprop="author"] > span').text();
|
||||
|
||||
novel.summary = loadedCheerio('#bookIntro').text().replace(/\s+/g, ' ');
|
||||
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
await delay(1000);
|
||||
const chapterListUrl = url + '/danh-sach-chuong';
|
||||
|
||||
const chapterListBody = await fetchApi(chapterListUrl).then(r => r.text());
|
||||
const loadedChapterList = parseHTML(chapterListBody);
|
||||
novel.chapters = this.parseChapters(loadedChapterList);
|
||||
loadedChapterList('nav[aria-label="Pagination"] a').each((index, ele) => {
|
||||
const href = ele.attribs['href'];
|
||||
if(href){
|
||||
const page = Number(href.match(/\?page=(\d+)/)?.[1]);
|
||||
if(page && page > novel.totalPages){
|
||||
novel.totalPages = page;
|
||||
}
|
||||
}
|
||||
})
|
||||
return novel;
|
||||
}
|
||||
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
||||
const url = `${this.site}${novelPath}/danh-sach-chuong?page=${page}`;
|
||||
const chapterListBody = await fetchApi(url).then(r => r.text());
|
||||
const chapters = this.parseChapters(parseHTML(chapterListBody));
|
||||
return {
|
||||
chapters
|
||||
}
|
||||
}
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let chapterText = loadedCheerio("div.chapter-content").html() || '';
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
if(pageNo > 1) return [];
|
||||
const url = `${this.site}/api/book-search`;
|
||||
let formData = new FormData();
|
||||
formData.append("keyword", searchTerm);
|
||||
|
||||
const result: SearchedResult = await fetchApi(
|
||||
url,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
},
|
||||
).then((r) => r.json());
|
||||
|
||||
const novels: NovelItem[] = result.data?.map(item => {
|
||||
return {
|
||||
name: item.name,
|
||||
path: '/truyen/' + item.slug,
|
||||
cover: (this.site + item.coverUrl).replace('default.jpg', '150.jpg?w=256&q=')
|
||||
}
|
||||
}) || [];
|
||||
return novels;
|
||||
}
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new LightNovelVN();
|
||||
@@ -0,0 +1,108 @@
|
||||
import { fetchFile } from "@libs/fetch";
|
||||
import { Filters } from "@libs/filterInputs";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { CheerioAPI, load as parseHTML } from "cheerio";
|
||||
|
||||
class Nettruyen implements Plugin.PagePlugin {
|
||||
id = 'nettruyen';
|
||||
name = 'Nettruyen';
|
||||
icon = 'src/vi/nettruyen/icon.png';
|
||||
site = 'https://nettruyen.com.vn';
|
||||
version = '1.0.0';
|
||||
parseNovels(loadedCheerio: CheerioAPI){
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio('#listchuong > ul > li > a.thumb').each((idx, ele) => {
|
||||
const path = ele.attribs['href'];
|
||||
if(!path) return;
|
||||
const name = ele.attribs['title'];
|
||||
const cover = this.site + '/' + loadedCheerio(ele).find('img').first().attr('src');
|
||||
novels.push({
|
||||
path: '/' + path,
|
||||
name,
|
||||
cover
|
||||
})
|
||||
})
|
||||
return novels;
|
||||
}
|
||||
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/xem-nhieu/trang-${pageNo}.html`;
|
||||
const body = await fetch(url).then(r => r.text());
|
||||
return this.parseNovels(parseHTML(body));
|
||||
}
|
||||
parseChapters(loadedCheerio: CheerioAPI){
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
loadedCheerio('ul.list-unstyled > li > a').each((idx, ele) => {
|
||||
const path = ele.attribs['href'];
|
||||
if(!path) return;
|
||||
const name = ele.attribs['title'];
|
||||
chapters.push({
|
||||
path: '/' + path,
|
||||
name,
|
||||
chapterNumber: Number(name.match(/Chương (\d+)/)?.[1])
|
||||
})
|
||||
})
|
||||
return chapters;
|
||||
}
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & { totalPages: number; }> {
|
||||
const url = this.site + novelPath;
|
||||
const body = await fetch(url).then(r => r.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const novel: Plugin.SourceNovel & { totalPages: number; } = {
|
||||
path: novelPath,
|
||||
name: loadedCheerio('.gioithieutruyen.profile-info > h1').text().trim(),
|
||||
chapters: [],
|
||||
totalPages: Number(loadedCheerio('.phantrang > span:nth-child(2)').text()) || 1,
|
||||
}
|
||||
novel.cover = this.site + '/' + loadedCheerio('.img-thumb > img').attr('src');
|
||||
loadedCheerio('.thongtintruyen.note.note-info').each((idx, ele) => {
|
||||
const strong = loadedCheerio(ele).find('strong').text().trim();
|
||||
switch(strong){
|
||||
case 'Tác giả':
|
||||
novel.author = loadedCheerio(ele).text().replace(/Tác giả\s+\n?:/, '')
|
||||
break;
|
||||
case 'Trạng thái':
|
||||
const text = loadedCheerio(ele).text();
|
||||
if(text.includes('Đang ra')){
|
||||
novel.status = NovelStatus.Ongoing;
|
||||
}else if(text.includes('Hoàn thành')){
|
||||
novel.status = NovelStatus.Completed;
|
||||
}else {
|
||||
novel.status = NovelStatus.Unknown;
|
||||
}
|
||||
break;
|
||||
case 'Thể loại':
|
||||
novel.genres = loadedCheerio('a > span').toArray().map((gEle) => loadedCheerio(gEle).text()).join(',');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
novel.summary = loadedCheerio('.gioithieutruyen > .gioithieutruyen').text().trim()
|
||||
novel.chapters = this.parseChapters(loadedCheerio);
|
||||
return novel;
|
||||
}
|
||||
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
||||
const id = novelPath.match(/-(\d+)\//)?.[1];
|
||||
if(!id) throw new Error("Cant parse page");
|
||||
const url = `${this.site}/ajax.chuong.php?id=${id}&page=${page}&url=${novelPath.replace(/\//g,'')}&loai=truyendich`;
|
||||
const body = await fetch(url).then(r => r.text());
|
||||
const chapters = this.parseChapters(parseHTML(body));
|
||||
return {
|
||||
chapters
|
||||
}
|
||||
}
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const url = this.site + chapterPath;
|
||||
const body = await fetch(url).then(r => r.text());
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return loadedCheerio('#noidungchap').html() || '';
|
||||
}
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url);
|
||||
}
|
||||
}
|
||||
export default new Nettruyen();
|
||||
@@ -0,0 +1,147 @@
|
||||
import { CheerioAPI, load as parseHTML } from "cheerio";
|
||||
import { fetchFile } from "@libs/fetch";
|
||||
import { Plugin } from "@typings/plugin";
|
||||
import { Filters } from "@libs/filterInputs";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
|
||||
class TruyenFull implements Plugin.PagePlugin {
|
||||
id = "truyenchu";
|
||||
name = "Truyện Chữ";
|
||||
icon = "src/vi/truyenchu/icon.png";
|
||||
site = "https://truyenchu.vn";
|
||||
version = "1.0.0";
|
||||
|
||||
parseNovels(loadedCheerio: CheerioAPI){
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
loadedCheerio(".list-truyen .row").each((idx, ele) => {
|
||||
const novelName = loadedCheerio(ele)
|
||||
.find("h3.truyen-title > a")
|
||||
.text();
|
||||
|
||||
const novelCover = this.site + loadedCheerio(ele).find("div[data-classname='cover']").attr("data-image");
|
||||
|
||||
const novelUrl = loadedCheerio(ele)
|
||||
.find("h3.truyen-title > a")
|
||||
.attr("href");
|
||||
if(novelUrl){
|
||||
novels.push({
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
path: novelUrl.replace(this.site, '')
|
||||
});
|
||||
}
|
||||
});
|
||||
return novels;
|
||||
}
|
||||
parseChapters(html: string){
|
||||
const listChapterHTML =
|
||||
html.match(/("list_chapter":\s?\\{0}"(.+)\\{0}"),/)?.[1];
|
||||
if(!listChapterHTML) throw new Error("Không tải được chương");
|
||||
const listChapter = JSON.parse(`{${listChapterHTML}}`);
|
||||
const loadedChapterList = parseHTML(listChapter.list_chapter);
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
loadedChapterList('ul > li > a').each((idx, ele) => {
|
||||
const path = ele.attribs['href'].replace(this.site, '');
|
||||
if(path){
|
||||
chapters.push({
|
||||
name: ele.attribs['title'],
|
||||
path,
|
||||
chapterNumber: Number(path.match(/\/chuong-(\d+)/)?.[1])
|
||||
})
|
||||
}
|
||||
});
|
||||
return chapters;
|
||||
}
|
||||
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/danh-sach/truyen-hot?page=${pageNo}`;
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
|
||||
const url = this.site + novelPath;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
let lastPage = 1;
|
||||
const regex = new RegExp(`${novelPath}\\?page=\\d+`, 'g');
|
||||
body.match(regex)?.forEach(v => {
|
||||
const page = Number(v.match(/\?page=(\d+)/)?.[1]);
|
||||
if(page && page > lastPage){
|
||||
lastPage = page;
|
||||
}
|
||||
});
|
||||
const novel: Plugin.SourceNovel & {totalPages: number} = {
|
||||
path: novelPath,
|
||||
name: loadedCheerio("div.book > img").attr("alt") || 'Không có tiêu đề',
|
||||
chapters: [],
|
||||
totalPages: lastPage,
|
||||
};
|
||||
|
||||
novel.cover = this.site + loadedCheerio("div.book > img").attr("src");
|
||||
|
||||
novel.summary = loadedCheerio("div.desc-text").text().trim();
|
||||
|
||||
novel.author = loadedCheerio('h3:contains("Tác giả:")')
|
||||
.parent()
|
||||
.contents()
|
||||
.text()
|
||||
.replace("Tác giả:", "");
|
||||
|
||||
novel.genres = loadedCheerio('h3:contains("Thể loại")')
|
||||
.siblings()
|
||||
.map((i, el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join(",");
|
||||
|
||||
novel.status = loadedCheerio('h3:contains("Trạng thái")').next().text();
|
||||
if(novel.status === "Full"){
|
||||
novel.status = NovelStatus.Completed;
|
||||
}else if(novel.status === 'Đang ra'){
|
||||
novel.status = NovelStatus.Ongoing;
|
||||
}else{
|
||||
novel.status = NovelStatus.Unknown;
|
||||
}
|
||||
novel.chapters = this.parseChapters(body);
|
||||
return novel;
|
||||
}
|
||||
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
||||
const url = `${this.site}${novelPath}?page=${page}`;
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const chapters = this.parseChapters(body);
|
||||
return {
|
||||
chapters
|
||||
}
|
||||
}
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetch(this.site + chapterPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
let chapterText = (loadedCheerio(".chapter-title").html() || '') + (loadedCheerio('#chapter-c').html() || '');
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||
const searchUrl = `${this.site}/tim-kiem?tukhoa=${searchTerm}&page=${pageNo}`;
|
||||
|
||||
const result = await fetch(searchUrl);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
return this.parseNovels(loadedCheerio)
|
||||
}
|
||||
async fetchImage(url: string): Promise<string | undefined> {
|
||||
return fetchFile(url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new TruyenFull();
|
||||
@@ -4,7 +4,7 @@ import { Plugin } from "@typings/plugin";
|
||||
import { Filters } from "@libs/filterInputs";
|
||||
import { NovelStatus } from "@libs/novelStatus";
|
||||
|
||||
class TruyenFull implements Plugin.PluginBase {
|
||||
class TruyenFull implements Plugin.PagePlugin {
|
||||
id = "truyenfull";
|
||||
name = "Truyện Full";
|
||||
icon = "src/vi/truyenfull/icon.png";
|
||||
@@ -27,13 +27,22 @@ class TruyenFull implements Plugin.PluginBase {
|
||||
novels.push({
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
url: novelUrl
|
||||
path: novelUrl.replace(this.site, '')
|
||||
});
|
||||
}
|
||||
});
|
||||
return novels;
|
||||
}
|
||||
|
||||
parseChapters(loadedCheerio: CheerioAPI): Plugin.ChapterItem[]{
|
||||
return loadedCheerio('ul.list-chapter > li > a').toArray().map((ele) => {
|
||||
const path = ele.attribs['href'].replace(this.site, '');
|
||||
return {
|
||||
name: loadedCheerio(ele).text().trim(),
|
||||
path,
|
||||
chapterNumber: Number(path.match(/\/chuong-(\d+)\//)?.[1])
|
||||
}
|
||||
})
|
||||
}
|
||||
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
|
||||
const url = `${this.site}/danh-sach/truyen-hot/trang-${pageNo}/`;
|
||||
|
||||
@@ -44,21 +53,26 @@ class TruyenFull implements Plugin.PluginBase {
|
||||
|
||||
return this.parseNovels(loadedCheerio);
|
||||
}
|
||||
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const url = novelUrl;
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
|
||||
const url = this.site + novelPath;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = parseHTML(body);
|
||||
let lastPage = 1;
|
||||
loadedCheerio('ul.pagination.pagination-sm > li > a').each(function() {
|
||||
const page = Number(this.attribs['href'].match(/\/trang-(\d+)\//)?.[1]);
|
||||
if(page && page > lastPage) lastPage = page;
|
||||
});
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
url,
|
||||
const novel: Plugin.SourceNovel & {totalPages: number} = {
|
||||
path: novelPath,
|
||||
name: loadedCheerio("div.book > img").attr("alt") || 'Không có tiêu đề',
|
||||
chapters: [],
|
||||
totalPages: lastPage,
|
||||
};
|
||||
|
||||
novel.name = loadedCheerio("div.book > img").attr("alt");
|
||||
|
||||
novel.cover = loadedCheerio("div.book > img").attr("src");
|
||||
|
||||
novel.summary = loadedCheerio("div.desc-text").text().trim();
|
||||
@@ -83,30 +97,22 @@ class TruyenFull implements Plugin.PluginBase {
|
||||
}else{
|
||||
novel.status = NovelStatus.Unknown;
|
||||
}
|
||||
|
||||
let lastPage = 1;
|
||||
loadedCheerio('ul.pagination.pagination-sm > li').each(function() {
|
||||
const page = Number(loadedCheerio(this).text());
|
||||
if(page && page > lastPage) lastPage = page;
|
||||
});
|
||||
|
||||
const lastPageUrl = novelUrl + 'trang-' + lastPage + '/';
|
||||
const res = await fetch(lastPageUrl).then(res => res.text());
|
||||
loadedCheerio = parseHTML(res);
|
||||
const lastChapterUrl = loadedCheerio('ul.list-chapter > li:last-child > a').attr('href');
|
||||
const lastChapter = Number(lastChapterUrl?.match(/\/chuong-(\d+)\//)?.[1]) || 1;
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
for(let i = 1; i <= lastChapter; i++){
|
||||
chapters.push({
|
||||
name: 'Chương ' + i,
|
||||
url: novelUrl + 'chuong-'+ i + '/',
|
||||
})
|
||||
}
|
||||
novel.chapters = chapters;
|
||||
novel.chapters = this.parseChapters(loadedCheerio);
|
||||
return novel;
|
||||
}
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetch(chapterUrl);
|
||||
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
||||
const url = `${this.site}${novelPath}trang-${page}/#list-chapter`;
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
const chapters = this.parseChapters(loadedCheerio);
|
||||
return {
|
||||
chapters
|
||||
}
|
||||
}
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const result = await fetch(this.site + chapterPath);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = parseHTML(body);
|
||||
|
||||
Reference in New Issue
Block a user