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,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();
|
||||
Reference in New Issue
Block a user