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:
nyagami
2024-02-25 14:52:37 +07:00
committed by GitHub
parent 2b9c8f7d2a
commit d818d1758f
147 changed files with 10278 additions and 5758 deletions
-170
View File
@@ -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;
}
-168
View File
@@ -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
View File
@@ -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();
+24 -38
View File
@@ -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
View File
@@ -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();
+22 -32
View File
@@ -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);
+53 -78
View File
@@ -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);
+42 -43
View File
@@ -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;
}
-127
View File
@@ -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 });
};
+27 -21
View File
@@ -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;
}
-247
View File
@@ -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();
+142 -69
View File
@@ -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();
+142 -69
View File
@@ -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();
-180
View File
@@ -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
View File
@@ -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
View File
@@ -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();
+38 -58
View File
@@ -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);
}
-327
View File
@@ -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
View File
@@ -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();
+171
View File
@@ -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();
-167
View File
@@ -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
View File
@@ -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();
+38 -48
View File
@@ -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);
+37 -42
View File
@@ -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" },
-239
View File
@@ -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();
+189
View File
@@ -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();
+23 -28
View File
@@ -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;
}
+286
View File
@@ -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();