From 5f9a067ab98fe67f72590e0fc80e0513fa2271c2 Mon Sep 17 00:00:00 2001
From: nyagami
Date: Sat, 23 Mar 2024 22:00:29 +0700
Subject: [PATCH] Prettier (#852)
* add prettier
* lint-staged
* prettier
* upgrade husky
---
.husky/.gitignore | 1 +
.husky/pre-commit | 4 +
.prettierignore | 2 +
.prettierrc.js | 10 +
docs/plugin-template.ts | 180 +-
index.ts | 282 +-
libs/defaultCover.ts | 2 +-
libs/fetch.ts | 225 +-
libs/filterInputs.ts | 100 +-
libs/isAbsoluteUrl.ts | 42 +-
libs/languages.ts | 30 +-
libs/novelStatus.ts | 16 +-
package-lock.json | 1204 +-
package.json | 11 +-
plugins/chinese/69shu.ts | 374 +-
plugins/chinese/69xinshu.ts | 357 +-
plugins/chinese/linovelib.ts | 1030 +-
plugins/english/NovelOnline.ts | 377 +-
plugins/english/StorySeedling.ts | 252 +-
plugins/english/allnovelfull.ts | 383 +-
plugins/english/bestlightnovel.ts | 555 +-
plugins/english/comrademao.broken.ts | 312 +-
plugins/english/divinedaolibrary.ts | 251 +-
plugins/english/earlynovel.ts | 381 +-
plugins/english/faqwikius.ts | 264 +-
plugins/english/freewebnovel.ts | 95 +-
plugins/english/lightnovelpub.ts | 443 +-
plugins/english/lightnovelworld.ts | 441 +-
plugins/english/lnmtl.ts | 456 +-
plugins/english/mtlnovel.ts | 344 +-
plugins/english/mtlreader.ts | 163 +-
plugins/english/novelbin.ts | 422 +-
plugins/english/novelbuddy.ts | 305 +-
plugins/english/novelfull.ts | 353 +-
plugins/english/novelhall.ts | 203 +-
plugins/english/novelupdates.ts | 851 +-
plugins/english/pawread.ts | 386 +-
plugins/english/ranobes.ts | 314 +-
plugins/english/readlitenovel.ts | 357 +-
plugins/english/royalroad.ts | 300 +-
plugins/english/scribblehub.ts | 541 +-
plugins/english/wtrlab.ts | 321 +-
plugins/english/wuxiaworld.ts | 448 +-
plugins/french/chireads.ts | 572 +-
plugins/french/noveldeglace.ts | 644 +-
plugins/indonesian/indowebnovel.ts | 367 +-
plugins/indonesian/novelringan.ts | 441 +-
plugins/indonesian/sakuranovel.ts | 416 +-
plugins/japanese/Syosetu.ts | 526 +-
plugins/korean/Agitoon.ts | 107 +-
plugins/polish/novelki.ts | 438 +-
plugins/portuguese/lightnovelbrasil.broken.ts | 815 +-
plugins/russian/LitSpace.ts | 307 +-
plugins/russian/authortoday.ts | 312 +-
plugins/russian/bookriver.ts | 193 +-
plugins/russian/ficbook.ts | 4978 +-
plugins/russian/jaomix.ts | 309 +-
plugins/russian/neobook.ts | 197 +-
plugins/russian/novelOvh.ts | 119 +-
plugins/russian/novelTL.ts | 1432 +-
plugins/russian/ranobehub.ts | 1981 +-
plugins/russian/ranobelib.ts | 493 +-
plugins/russian/ranoberf.ts | 86 +-
plugins/russian/renovels.ts | 1590 +-
plugins/russian/zelluloza.ts | 485 +-
plugins/spanish/hasutl.broken.ts | 227 +-
plugins/spanish/novelasligera.broken.ts | 329 +-
plugins/spanish/novelawuxia.broken.ts | 321 +-
plugins/spanish/oasistranslations.broken.ts | 255 +-
plugins/spanish/skynovels.broken.ts | 178 +-
plugins/spanish/tunovelaligera.ts | 255 +-
plugins/spanish/yukitls.broken.ts | 329 +-
plugins/turkish/epiknovel.broken.ts | 271 +-
plugins/turkish/lshnovel.broken.ts | 609 +-
plugins/ukrainian/smakolykytl.ts | 100 +-
plugins/vietnamese/LNHako.ts | 478 +-
plugins/vietnamese/Truyenconect.ts | 556 +-
plugins/vietnamese/lightnovelvn.ts | 307 +-
plugins/vietnamese/nettruyen.ts | 228 +-
plugins/vietnamese/truyenchu.ts | 272 +-
plugins/vietnamese/truyenfull.ts | 255 +-
scripts/json_plugins.ts | 72 +-
scripts/multisrc/generate.ts | 88 +-
.../multisrc/hotnovelpub/filter_refresh.ts | 48 +-
scripts/multisrc/hotnovelpub/generator.ts | 31 +-
scripts/multisrc/hotnovelpub/template.ts | 66 +-
scripts/multisrc/ifreedom/generator.ts | 20 +-
scripts/multisrc/ifreedom/template.ts | 131 +-
scripts/multisrc/lightnovelwp/generator.ts | 25 +-
scripts/multisrc/lightnovelwp/get_filters.ts | 196 +-
scripts/multisrc/lightnovelwp/template.ts | 229 +-
scripts/multisrc/madara/generator.ts | 51 +-
scripts/multisrc/madara/get_filters.ts | 339 +-
scripts/multisrc/madara/template.ts | 290 +-
scripts/multisrc/readwn/generator.ts | 33 +-
scripts/multisrc/readwn/template.ts | 142 +-
scripts/multisrc/rulate/generator.ts | 26 +-
scripts/multisrc/rulate/template.ts | 152 +-
scripts/terser.ts | 4 +-
test_web/api/plugins.ts | 138 +-
test_web/static/js/accordion.js | 182 +-
test_web/static/js/bootstrap.min.js | 5043 +-
.../static/js/html-sanitize-nonbrowser.js | 116 +-
test_web/static/js/html-sanitize.js | 26623 +++++-----
test_web/static/js/index.js | 3279 +-
test_web/static/js/jquery.min.js | 5595 +-
test_web/static/js/less.js | 8776 +++-
test_web/static/js/mdb5.js | 42938 +++++++++-------
test_web/static/js/types.d.ts | 10 +-
test_web/static/md5Themes/themes.js | 28 +-
tswatch.ts | 36 +-
types/plugin.ts | 208 +-
types/types.ts | 4 +-
113 files changed, 79244 insertions(+), 50841 deletions(-)
create mode 100644 .husky/.gitignore
create mode 100644 .husky/pre-commit
create mode 100644 .prettierignore
create mode 100644 .prettierrc.js
diff --git a/.husky/.gitignore b/.husky/.gitignore
new file mode 100644
index 0000000..31354ec
--- /dev/null
+++ b/.husky/.gitignore
@@ -0,0 +1 @@
+_
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 0000000..36af219
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npx lint-staged
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..7a3d9f0
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,2 @@
+node_modules
+.js
\ No newline at end of file
diff --git a/.prettierrc.js b/.prettierrc.js
new file mode 100644
index 0000000..d161a9d
--- /dev/null
+++ b/.prettierrc.js
@@ -0,0 +1,10 @@
+module.exports = {
+ tabWidth: 2,
+ bracketSpacing: true,
+ useTabs: false,
+ bracketSameLine: false,
+ singleQuote: true,
+ trailingComma: 'all',
+ arrowParens: 'avoid',
+ quoteProps: 'preserve',
+};
diff --git a/docs/plugin-template.ts b/docs/plugin-template.ts
index 2f63dc1..cfdc283 100644
--- a/docs/plugin-template.ts
+++ b/docs/plugin-template.ts
@@ -1,90 +1,90 @@
-import { fetchFile } from "@libs/fetch";
-import { Plugin } from "@typings/plugin";
-import { Filters } from "@libs/filterInputs";
-import { load as loadCheerio } from "cheerio";
-import { defaultCover } from "@libs/defaultCover";
-import { NovelStatus } from "@libs/novelStatus";
-// import { isUrlAbsolute } from "@libs/isAbsoluteUrl";
-// import { parseMadaraDate } from "@libs/parseMadaraDate";
-
-class TemplatePlugin implements Plugin.PluginBase {
- id = "";
- name = "";
- icon = "";
- site = "https://example.com";
- version = "1.0.0";
- filters: Filters | undefined = undefined;
-
- async popularNovels(
- pageNo: number,
- {
- showLatestNovels,
- filters,
- }: Plugin.PopularNovelsOptions
- ): Promise {
- const novels: Plugin.NovelItem[] = [];
-
- /** Add your fetching code here */
- novels.push({
- name: "Novel1",
- path: "/novels/1",
- cover: defaultCover,
- });
- return novels;
- }
- async parseNovel(novelPath: string): Promise {
- const novel: Plugin.SourceNovel = {
- path: novelPath,
- name: 'Untitled'
- };
-
- // TODO: get here data from the site and
- // un-comment and fill-in the relevant fields
-
- // novel.name = "";
- // novel.artist = "";
- // novel.author = "";
- novel.cover = defaultCover;
- // novel.genres = "";
- // novel.status = NovelStatus.Completed;
- // novel.summary = "";
-
- let chapters: Plugin.ChapterItem[] = [];
-
- // TODO: here parse the chapter list
-
- // TODO: add each chapter to the list using
- const chapter: Plugin.ChapterItem = {
- name: "",
- path: "",
- releaseTime: "",
- chapterNumber: 0,
- };
- chapters.push(chapter);
-
- novel.chapters = chapters;
- return novel;
- }
- async parseChapter(chapterPath: string): Promise {
- // parse chapter text here
- const chapterText = "";
- return chapterText;
- }
- async searchNovels(
- searchTerm: string,
- pageNo: number
- ): Promise {
- let novels: Plugin.NovelItem[] = [];
-
- // get novels using the search term
-
- return novels;
- }
- async fetchImage(url: string): Promise {
- // if your plugin has images and they won't load
- // this is the function to fiddle with
- return fetchFile(url);
- }
-}
-
-export default new TemplatePlugin();
+import { fetchFile } from '@libs/fetch';
+import { Plugin } from '@typings/plugin';
+import { Filters } from '@libs/filterInputs';
+import { load as loadCheerio } from 'cheerio';
+import { defaultCover } from '@libs/defaultCover';
+import { NovelStatus } from '@libs/novelStatus';
+// import { isUrlAbsolute } from "@libs/isAbsoluteUrl";
+// import { parseMadaraDate } from "@libs/parseMadaraDate";
+
+class TemplatePlugin implements Plugin.PluginBase {
+ id = '';
+ name = '';
+ icon = '';
+ site = 'https://example.com';
+ version = '1.0.0';
+ filters: Filters | undefined = undefined;
+
+ async popularNovels(
+ pageNo: number,
+ {
+ showLatestNovels,
+ filters,
+ }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ const novels: Plugin.NovelItem[] = [];
+
+ /** Add your fetching code here */
+ novels.push({
+ name: 'Novel1',
+ path: '/novels/1',
+ cover: defaultCover,
+ });
+ return novels;
+ }
+ async parseNovel(novelPath: string): Promise {
+ const novel: Plugin.SourceNovel = {
+ path: novelPath,
+ name: 'Untitled',
+ };
+
+ // TODO: get here data from the site and
+ // un-comment and fill-in the relevant fields
+
+ // novel.name = "";
+ // novel.artist = "";
+ // novel.author = "";
+ novel.cover = defaultCover;
+ // novel.genres = "";
+ // novel.status = NovelStatus.Completed;
+ // novel.summary = "";
+
+ let chapters: Plugin.ChapterItem[] = [];
+
+ // TODO: here parse the chapter list
+
+ // TODO: add each chapter to the list using
+ const chapter: Plugin.ChapterItem = {
+ name: '',
+ path: '',
+ releaseTime: '',
+ chapterNumber: 0,
+ };
+ chapters.push(chapter);
+
+ novel.chapters = chapters;
+ return novel;
+ }
+ async parseChapter(chapterPath: string): Promise {
+ // parse chapter text here
+ const chapterText = '';
+ return chapterText;
+ }
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ let novels: Plugin.NovelItem[] = [];
+
+ // get novels using the search term
+
+ return novels;
+ }
+ async fetchImage(url: string): Promise {
+ // if your plugin has images and they won't load
+ // this is the function to fiddle with
+ return fetchFile(url);
+ }
+}
+
+export default new TemplatePlugin();
diff --git a/index.ts b/index.ts
index 6133cef..782cb38 100644
--- a/index.ts
+++ b/index.ts
@@ -1,181 +1,175 @@
-require("module-alias/register");
+require('module-alias/register');
-import path from "path";
-import dayjs from "dayjs";
-import express from "express";
-import bodyParser from "body-parser";
-import * as pluginApi from "./test_web/api/plugins";
-import localizedFormat from "dayjs/plugin/localizedFormat";
+import path from 'path';
+import dayjs from 'dayjs';
+import express from 'express';
+import bodyParser from 'body-parser';
+import * as pluginApi from './test_web/api/plugins';
+import localizedFormat from 'dayjs/plugin/localizedFormat';
const app = express();
const port = Number(process.argv[2]);
if (!port) {
- process.exit(0);
+ process.exit(0);
}
-const host = "localhost";
+const host = 'localhost';
-const dirname = path.join(__dirname, "..");
+const dirname = path.join(__dirname, '..');
export const getHeaders = () => {
- return pluginApi.fetchHeaders;
+ return pluginApi.fetchHeaders;
};
app.use((req, res, next) => {
- const xHeaders = req.headers["x-custom-headers"];
- if (xHeaders && typeof xHeaders === "string") {
- for (const [headerName, headerValue] of Object.entries(
- pluginApi.fetchHeaders
- )) {
- if (
- typeof headerName === "string" &&
- typeof headerValue === "string"
- )
- delete pluginApi.fetchHeaders[headerName];
- }
- const headers = JSON.parse(xHeaders);
- for (const [headerName, headerValue] of Object.entries(headers)) {
- if (
- typeof headerName === "string" &&
- typeof headerValue === "string"
- )
- pluginApi.fetchHeaders[headerName] = headerValue;
- }
+ const xHeaders = req.headers['x-custom-headers'];
+ if (xHeaders && typeof xHeaders === 'string') {
+ for (const [headerName, headerValue] of Object.entries(
+ pluginApi.fetchHeaders,
+ )) {
+ if (typeof headerName === 'string' && typeof headerValue === 'string')
+ delete pluginApi.fetchHeaders[headerName];
}
- if (pluginApi.fetchHeaders.userAgent)
- console.log("Custom headers: ", pluginApi.fetchHeaders);
- next();
+ const headers = JSON.parse(xHeaders);
+ for (const [headerName, headerValue] of Object.entries(headers)) {
+ if (typeof headerName === 'string' && typeof headerValue === 'string')
+ pluginApi.fetchHeaders[headerName] = headerValue;
+ }
+ }
+ if (pluginApi.fetchHeaders.userAgent)
+ console.log('Custom headers: ', pluginApi.fetchHeaders);
+ next();
});
app.use(bodyParser.json());
-app.use("/static", express.static(path.join(dirname, "test_web", "static")));
-app.use("/icons", express.static(path.join(dirname, "icons")));
-app.get("/all_plugins", (req, res) => {
- const allPlugins = pluginApi.all_plugins();
- res.json(allPlugins);
+app.use('/static', express.static(path.join(dirname, 'test_web', 'static')));
+app.use('/icons', express.static(path.join(dirname, 'icons')));
+app.get('/all_plugins', (req, res) => {
+ const allPlugins = pluginApi.all_plugins();
+ res.json(allPlugins);
});
-app.post("/filters", async (req, res) => {
- const filters = await pluginApi.getFilter(req.body["pluginRequirePath"]);
- res.json(filters || []);
+app.post('/filters', async (req, res) => {
+ const filters = await pluginApi.getFilter(req.body['pluginRequirePath']);
+ res.json(filters || []);
});
-app.post("/popularNovels/", async (req, res) => {
- const requirePath = req.body["pluginRequirePath"];
- const showLatestNovels = req.body["showLatestNovels"] || false;
- const defaultFilters = await pluginApi.getFilter(requirePath);
- const filters = showLatestNovels
- ? defaultFilters
- : req.body["filters"] || defaultFilters;
- const page = parseInt(req.body["page"]);
- try {
- const novels = await pluginApi.popularNovels(requirePath, page || 1, {
- showLatestNovels,
- filters,
- });
- res.json(novels);
- } catch (err: unknown) {
- res.json({ error: String(err) });
- }
+app.post('/popularNovels/', async (req, res) => {
+ const requirePath = req.body['pluginRequirePath'];
+ const showLatestNovels = req.body['showLatestNovels'] || false;
+ const defaultFilters = await pluginApi.getFilter(requirePath);
+ const filters = showLatestNovels
+ ? defaultFilters
+ : req.body['filters'] || defaultFilters;
+ const page = parseInt(req.body['page']);
+ try {
+ const novels = await pluginApi.popularNovels(requirePath, page || 1, {
+ showLatestNovels,
+ filters,
+ });
+ res.json(novels);
+ } catch (err: unknown) {
+ res.json({ error: String(err) });
+ }
});
-app.post("/searchNovels/", async (req, res) => {
- const page = parseInt(req.body["page"]) || 1;
- try {
- const novels = await pluginApi.searchNovels(
- req.body["pluginRequirePath"],
- page,
- req.body["searchTerm"]
- );
- res.json(novels);
- } catch (err: unknown) {
- res.json({ error: String(err) });
- }
+app.post('/searchNovels/', async (req, res) => {
+ const page = parseInt(req.body['page']) || 1;
+ try {
+ const novels = await pluginApi.searchNovels(
+ req.body['pluginRequirePath'],
+ page,
+ req.body['searchTerm'],
+ );
+ res.json(novels);
+ } catch (err: unknown) {
+ res.json({ error: String(err) });
+ }
});
-app.post("/parseNovel/", async (req, res) => {
- try {
- const sourceNovel = await pluginApi.parseNovel(
- req.body["pluginRequirePath"],
- req.body["novelPath"]
- );
- res.json(sourceNovel);
- } catch (err: unknown) {
- res.json({ error: String(err) });
- }
+app.post('/parseNovel/', async (req, res) => {
+ try {
+ const sourceNovel = await pluginApi.parseNovel(
+ req.body['pluginRequirePath'],
+ req.body['novelPath'],
+ );
+ res.json(sourceNovel);
+ } catch (err: unknown) {
+ res.json({ error: String(err) });
+ }
});
-app.post("/hasParsePage", async (req, res) => {
- try {
- const parsePage = await pluginApi.hasParsePage(
- req.body["pluginRequirePath"]
- )
- res.json({ hasParsePage: parsePage !== undefined });
- } catch (err: unknown) {
- res.json({ hasParsePage: false });
- }
+app.post('/hasParsePage', async (req, res) => {
+ try {
+ const parsePage = await pluginApi.hasParsePage(
+ req.body['pluginRequirePath'],
+ );
+ res.json({ hasParsePage: parsePage !== undefined });
+ } catch (err: unknown) {
+ res.json({ hasParsePage: false });
+ }
});
-app.post("/parsePage/", async (req, res) => {
- try {
- const sourcePage = await pluginApi.parsePage(
- req.body["pluginRequirePath"],
- req.body["novelPath"],
- req.body["page"]
- );
- res.json(sourcePage);
- } catch (err: unknown) {
- res.json({ error: String(err) });
- }
+app.post('/parsePage/', async (req, res) => {
+ try {
+ const sourcePage = await pluginApi.parsePage(
+ req.body['pluginRequirePath'],
+ req.body['novelPath'],
+ req.body['page'],
+ );
+ res.json(sourcePage);
+ } catch (err: unknown) {
+ res.json({ error: String(err) });
+ }
});
-app.post("/parseChapter/", async (req, res) => {
- try {
- const chapterText = await pluginApi.parseChapter(
- req.body["pluginRequirePath"],
- req.body["chapterPath"]
- );
- res.send(chapterText);
- } catch (err: unknown) {
- res.json({ error: String(err) });
- }
+app.post('/parseChapter/', async (req, res) => {
+ try {
+ const chapterText = await pluginApi.parseChapter(
+ req.body['pluginRequirePath'],
+ req.body['chapterPath'],
+ );
+ res.send(chapterText);
+ } catch (err: unknown) {
+ res.json({ error: String(err) });
+ }
});
-app.post("/fetchImage/", async (req, res) => {
- try {
- const base64 = await pluginApi.fetchImage(
- req.body["pluginRequirePath"],
- req.body["url"]
- );
- res.send(base64);
- } catch (err: unknown) {
- res.json({ error: String(err) });
- }
+app.post('/fetchImage/', async (req, res) => {
+ try {
+ const base64 = await pluginApi.fetchImage(
+ req.body['pluginRequirePath'],
+ req.body['url'],
+ );
+ res.send(base64);
+ } catch (err: unknown) {
+ res.json({ error: String(err) });
+ }
});
-app.post("/resolveUrl/", async (req, res) => {
- try {
- const fullURL = await pluginApi.resolveUrl(
- req.body["pluginRequirePath"],
- req.body["path"],
- req.body["isNovel"],
- );
- res.send(fullURL);
- } catch (err: unknown) {
- res.json({ error: String(err) });
- }
+app.post('/resolveUrl/', async (req, res) => {
+ try {
+ const fullURL = await pluginApi.resolveUrl(
+ req.body['pluginRequirePath'],
+ req.body['path'],
+ req.body['isNovel'],
+ );
+ res.send(fullURL);
+ } catch (err: unknown) {
+ res.json({ error: String(err) });
+ }
});
-app.get("/", (req, res) => {
- res.sendFile(path.join(dirname, "test_web", "index.html"));
+app.get('/', (req, res) => {
+ res.sendFile(path.join(dirname, 'test_web', 'index.html'));
});
app.listen(port, host, () => {
- console.log(`Testing plugins web listening on http://localhost:${port}`);
+ console.log(`Testing plugins web listening on http://localhost:${port}`);
});
//Dayjs localization
-const language = Intl?.DateTimeFormat()?.resolvedOptions()?.locale || "en";
+const language = Intl?.DateTimeFormat()?.resolvedOptions()?.locale || 'en';
-require("dayjs/locale/ar");
-require("dayjs/locale/de");
-require("dayjs/locale/es");
-require("dayjs/locale/it");
-require("dayjs/locale/pt");
-require("dayjs/locale/ru");
-require("dayjs/locale/tr");
-require("dayjs/locale/uk");
-require("dayjs/locale/zh");
+require('dayjs/locale/ar');
+require('dayjs/locale/de');
+require('dayjs/locale/es');
+require('dayjs/locale/it');
+require('dayjs/locale/pt');
+require('dayjs/locale/ru');
+require('dayjs/locale/tr');
+require('dayjs/locale/uk');
+require('dayjs/locale/zh');
dayjs.locale(language);
dayjs.extend(localizedFormat);
diff --git a/libs/defaultCover.ts b/libs/defaultCover.ts
index 2fe0459..bbc5754 100644
--- a/libs/defaultCover.ts
+++ b/libs/defaultCover.ts
@@ -1,2 +1,2 @@
export const defaultCover =
- "https://github.com/LNReader/lnreader-sources/blob/main/icons/src/coverNotAvailable.jpg?raw=true";
+ 'https://github.com/LNReader/lnreader-sources/blob/main/icons/src/coverNotAvailable.jpg?raw=true';
diff --git a/libs/fetch.ts b/libs/fetch.ts
index b92bbdf..8e4675a 100644
--- a/libs/fetch.ts
+++ b/libs/fetch.ts
@@ -1,53 +1,53 @@
import { parse as parseProto } from 'protobufjs';
type FetchInit = {
- headers?: Record | Headers;
- method?: string;
- body?: FormData | string;
- [x: string]:
- | string
- | Record
- | undefined
- | FormData
- | Headers;
+ headers?: Record | Headers;
+ method?: string;
+ body?: FormData | string;
+ [x: string]:
+ | string
+ | Record
+ | undefined
+ | FormData
+ | Headers;
};
const makeInit = async (init?: FetchInit) => {
- let defaultHeaders: Record = {
- 'Connection': 'keep-alive',
- 'Accept': '*/*',
- 'Accept-Language': '*',
- 'Sec-Fetch-Mode': 'cors',
- 'Accept-Encoding': 'gzip, deflate',
- 'Cache-Control': 'max-age=0',
+ let defaultHeaders: Record = {
+ 'Connection': 'keep-alive',
+ 'Accept': '*/*',
+ 'Accept-Language': '*',
+ 'Sec-Fetch-Mode': 'cors',
+ 'Accept-Encoding': 'gzip, deflate',
+ 'Cache-Control': 'max-age=0',
+ };
+ try {
+ const { getHeaders } = await import('../index.js');
+ defaultHeaders = {
+ ...defaultHeaders,
+ ...getHeaders(),
};
- try {
- const { getHeaders } = await import("../index.js");
- defaultHeaders = {
- ...defaultHeaders,
- ...getHeaders()
- };
- } catch {
- // nothing to do
- }
- if (init?.headers) {
- if (init.headers instanceof Headers) {
- for (const [name, value] of Object.entries(defaultHeaders)) {
- if (!init.headers.get(name)) init.headers.set(name, value);
- }
- } else {
- init.headers = {
- ...defaultHeaders,
- ...init.headers,
- };
- }
+ } catch {
+ // nothing to do
+ }
+ if (init?.headers) {
+ if (init.headers instanceof Headers) {
+ for (const [name, value] of Object.entries(defaultHeaders)) {
+ if (!init.headers.get(name)) init.headers.set(name, value);
+ }
} else {
- init = {
- ...init,
- headers: defaultHeaders,
- };
+ init.headers = {
+ ...defaultHeaders,
+ ...init.headers,
+ };
}
- return init;
+ } else {
+ init = {
+ ...init,
+ headers: defaultHeaders,
+ };
+ }
+ return init;
};
/**
@@ -57,10 +57,10 @@ const makeInit = async (init?: FetchInit) => {
* @returns response as normal fetch
*/
export async function fetchApi(url: string, init?: FetchInit) {
- init = await makeInit(init);
- // console.log(init.headers);
- // console.log(url, init);
- return await fetch(url, init as RequestInit);
+ init = await makeInit(init);
+ // console.log(init.headers);
+ // console.log(url, init);
+ return await fetch(url, init as RequestInit);
}
/**
@@ -71,16 +71,16 @@ export async function fetchApi(url: string, init?: FetchInit) {
* @example fetchFile('https://avatars.githubusercontent.com/u/81222734?s=48&v=4');
*/
export const fetchFile = async function (url: string, init?: FetchInit) {
- init = await makeInit(init);
- console.log(url, init);
- try {
- const res = await fetch(url, init as RequestInit);
- if (!res.ok) return "";
- const arrayBuffer = await res.arrayBuffer();
- return Buffer.from(arrayBuffer).toString("base64");
- } catch (e) {
- return "";
- }
+ init = await makeInit(init);
+ console.log(url, init);
+ try {
+ const res = await fetch(url, init as RequestInit);
+ if (!res.ok) return '';
+ const arrayBuffer = await res.arrayBuffer();
+ return Buffer.from(arrayBuffer).toString('base64');
+ } catch (e) {
+ return '';
+ }
};
/**
@@ -92,67 +92,74 @@ export const fetchFile = async function (url: string, init?: FetchInit) {
* @example fetchText('https://github.com/LNReader/lnreader', {}, 'gbk');
*/
export const fetchText = async function (
- url: string,
- init?: FetchInit,
- encoding?: string
+ url: string,
+ init?: FetchInit,
+ encoding?: string,
): Promise {
- init = await makeInit(init);
- console.log(url, init);
- try {
- const res = await fetch(url, init as RequestInit);
- if (!res.ok) return "";
- const arrayBuffer = await res.arrayBuffer();
- const decoder = new TextDecoder(encoding);
- return decoder.decode(arrayBuffer);
- } catch (e) {
- return "";
- }
+ init = await makeInit(init);
+ console.log(url, init);
+ try {
+ const res = await fetch(url, init as RequestInit);
+ if (!res.ok) return '';
+ const arrayBuffer = await res.arrayBuffer();
+ const decoder = new TextDecoder(encoding);
+ return decoder.decode(arrayBuffer);
+ } catch (e) {
+ return '';
+ }
};
interface ProtoRequestInit {
- // merged .proto file
- proto: string;
- requestType: string;
- requestData?: any;
- responseType: string;
+ // merged .proto file
+ proto: string;
+ requestType: string;
+ requestData?: any;
+ responseType: string;
}
const BYTE_MARK = BigInt((1 << 8) - 1);
-export const fetchProto = async function (
- protoInit: ProtoRequestInit,
- url: string,
- init?: FetchInit,
+export const fetchProto = async function (
+ protoInit: ProtoRequestInit,
+ url: string,
+ init?: FetchInit,
) {
- const protoRoot = parseProto(protoInit.proto).root;
- const RequestMessge = protoRoot.lookupType(protoInit.requestType);
- if (RequestMessge.verify(protoInit.requestData)){
- throw new Error('Invalid Proto');
- }
- // encode request data
- const encodedrequest = RequestMessge.encode(protoInit.requestData).finish();
- const requestLength = BigInt(encodedrequest.length);
- const headers = new Uint8Array(
- Array(5).fill(0).map((v, idx) => {
- if(idx === 0) return 0;
- return Number((requestLength >> BigInt(8 * (5 - idx - 1))) & BYTE_MARK);
- })
- );
- init = await makeInit(init);
- const bodyArray = new Uint8Array(headers.length + encodedrequest.length)
- bodyArray.set(headers, 0)
- bodyArray.set(encodedrequest, headers.length)
- return fetch(url, {
- method: 'POST',
- ...init,
- body: bodyArray
- } as RequestInit
- ).then(r => r.arrayBuffer())
+ const protoRoot = parseProto(protoInit.proto).root;
+ const RequestMessge = protoRoot.lookupType(protoInit.requestType);
+ if (RequestMessge.verify(protoInit.requestData)) {
+ throw new Error('Invalid Proto');
+ }
+ // encode request data
+ const encodedrequest = RequestMessge.encode(protoInit.requestData).finish();
+ const requestLength = BigInt(encodedrequest.length);
+ const headers = new Uint8Array(
+ Array(5)
+ .fill(0)
+ .map((v, idx) => {
+ if (idx === 0) return 0;
+ return Number((requestLength >> BigInt(8 * (5 - idx - 1))) & BYTE_MARK);
+ }),
+ );
+ init = await makeInit(init);
+ const bodyArray = new Uint8Array(headers.length + encodedrequest.length);
+ bodyArray.set(headers, 0);
+ bodyArray.set(encodedrequest, headers.length);
+ return fetch(url, {
+ method: 'POST',
+ ...init,
+ body: bodyArray,
+ } as RequestInit)
+ .then(r => r.arrayBuffer())
.then(arr => {
- // decode response data
- const payload = new Uint8Array(arr);
- const length = Number(BigInt(payload[1] << 24) | BigInt(payload[2] << 16) | BigInt(payload[3] << 8) | BigInt(payload[4]));
- const ResponseMessage = protoRoot.lookupType(protoInit.responseType);
- return ResponseMessage.decode(payload.slice(5, 5 + length));
- }) as ReturnType
-}
+ // decode response data
+ const payload = new Uint8Array(arr);
+ const length = Number(
+ BigInt(payload[1] << 24) |
+ BigInt(payload[2] << 16) |
+ BigInt(payload[3] << 8) |
+ BigInt(payload[4]),
+ );
+ const ResponseMessage = protoRoot.lookupType(protoInit.responseType);
+ return ResponseMessage.decode(payload.slice(5, 5 + length));
+ }) as ReturnType;
+};
diff --git a/libs/filterInputs.ts b/libs/filterInputs.ts
index e1580bf..7023987 100644
--- a/libs/filterInputs.ts
+++ b/libs/filterInputs.ts
@@ -1,78 +1,78 @@
export interface FilterOption {
- readonly label: string;
- readonly value: string;
+ readonly label: string;
+ readonly value: string;
}
export enum FilterTypes {
- TextInput = "Text",
- Picker = "Picker",
- CheckboxGroup = "Checkbox",
- Switch = "Switch",
- ExcludableCheckboxGroup = "XCheckbox",
+ TextInput = 'Text',
+ Picker = 'Picker',
+ CheckboxGroup = 'Checkbox',
+ Switch = 'Switch',
+ ExcludableCheckboxGroup = 'XCheckbox',
}
type SwitchFilter = {
- type: FilterTypes.Switch;
- /** Default value */
- value: boolean;
+ type: FilterTypes.Switch;
+ /** Default value */
+ value: boolean;
};
type TextFilter = {
- type: FilterTypes.TextInput;
- /** Default value */
- value: string;
+ type: FilterTypes.TextInput;
+ /** Default value */
+ value: string;
};
type CheckboxFilter = {
- type: FilterTypes.CheckboxGroup;
- options: readonly FilterOption[];
- /** Default value */
- value: string[];
+ type: FilterTypes.CheckboxGroup;
+ options: readonly FilterOption[];
+ /** Default value */
+ value: string[];
};
type PickerFilter = {
- type: FilterTypes.Picker;
- options: readonly FilterOption[];
- /** Default value */
- value: string;
+ type: FilterTypes.Picker;
+ options: readonly FilterOption[];
+ /** Default value */
+ value: string;
};
type ExcludableCheckboxFilter = {
- type: FilterTypes.ExcludableCheckboxGroup;
- options: readonly FilterOption[];
- value: { include?: string[]; exclude?: string[] };
+ type: FilterTypes.ExcludableCheckboxGroup;
+ options: readonly FilterOption[];
+ value: { include?: string[]; exclude?: string[] };
};
export type Filters = Record<
- string,
- | { label: string } & (
- | PickerFilter
- | CheckboxFilter
- | TextFilter
- | SwitchFilter
- | ExcludableCheckboxFilter
- )
+ string,
+ { label: string } & (
+ | PickerFilter
+ | CheckboxFilter
+ | TextFilter
+ | SwitchFilter
+ | ExcludableCheckboxFilter
+ )
>;
export type FilterToValues<
- T extends Record | undefined
+ T extends Record | undefined,
> = T extends undefined
- ? undefined
- : {
- [K in keyof T]: Omit<
- { type: NonNullable[K]["type"] } & Filters[string],
- "label" | "options"
- >;
- };
+ ? undefined
+ : {
+ [K in keyof T]: Omit<
+ { type: NonNullable[K]['type'] } & Filters[string],
+ 'label' | 'options'
+ >;
+ };
export type ValueOfFilter =
- T extends FilterTypes.CheckboxGroup
- ? CheckboxFilter["value"]
- : T extends FilterTypes.Picker
- ? PickerFilter["value"]
- : T extends FilterTypes.Switch
- ? SwitchFilter["value"]
+ T extends FilterTypes.CheckboxGroup
+ ? CheckboxFilter['value']
+ : T extends FilterTypes.Picker
+ ? PickerFilter['value']
+ : T extends FilterTypes.Switch
+ ? SwitchFilter['value']
: T extends FilterTypes.TextInput
- ? TextFilter["value"]
- : T extends FilterTypes.ExcludableCheckboxGroup
- ? ExcludableCheckboxFilter["value"]
- : never;
+ ? TextFilter['value']
+ : T extends FilterTypes.ExcludableCheckboxGroup
+ ? ExcludableCheckboxFilter['value']
+ : never;
diff --git a/libs/isAbsoluteUrl.ts b/libs/isAbsoluteUrl.ts
index d082f6e..5f47249 100644
--- a/libs/isAbsoluteUrl.ts
+++ b/libs/isAbsoluteUrl.ts
@@ -1,23 +1,23 @@
export const isUrlAbsolute = (url: string) => {
- if (url) {
- if (url.indexOf("//") === 0) {
- return true;
- } // URL is protocol-relative (= absolute)
- if (url.indexOf("://") === -1) {
- return false;
- } // URL has no protocol (= relative)
- if (url.indexOf(".") === -1) {
- return false;
- } // URL does not contain a dot, i.e. no TLD (= relative, possibly REST)
- if (url.indexOf("/") === -1) {
- return false;
- } // URL does not contain a single slash (= relative)
- if (url.indexOf(":") > url.indexOf("/")) {
- return false;
- } // The first colon comes after the first slash (= relative)
- if (url.indexOf("://") < url.indexOf(".")) {
- return true;
- } // Protocol is defined before first dot (= absolute)
- }
- return false; // Anything else must be relative
+ if (url) {
+ if (url.indexOf('//') === 0) {
+ return true;
+ } // URL is protocol-relative (= absolute)
+ if (url.indexOf('://') === -1) {
+ return false;
+ } // URL has no protocol (= relative)
+ if (url.indexOf('.') === -1) {
+ return false;
+ } // URL does not contain a dot, i.e. no TLD (= relative, possibly REST)
+ if (url.indexOf('/') === -1) {
+ return false;
+ } // URL does not contain a single slash (= relative)
+ if (url.indexOf(':') > url.indexOf('/')) {
+ return false;
+ } // The first colon comes after the first slash (= relative)
+ if (url.indexOf('://') < url.indexOf('.')) {
+ return true;
+ } // Protocol is defined before first dot (= absolute)
+ }
+ return false; // Anything else must be relative
};
diff --git a/libs/languages.ts b/libs/languages.ts
index abc6e63..e94d447 100644
--- a/libs/languages.ts
+++ b/libs/languages.ts
@@ -1,17 +1,17 @@
export const languages = {
- Arabic: "العربية",
- Chinese: "中文, 汉语, 漢語",
- English: "English",
- French: "Français",
- Indonesian: "Bahasa Indonesia",
- Japanese: "日本語",
- Korean: "조선말, 한국어",
- Polish: "Polski",
- Portuguese: "Português",
- Russian: "Русский",
- Spanish: "Español",
- Thai: "ไทย",
- Turkish: "Türkçe",
- Ukrainian: "Українська",
- Vietnamese: "Tiếng Việt",
+ Arabic: 'العربية',
+ Chinese: '中文, 汉语, 漢語',
+ English: 'English',
+ French: 'Français',
+ Indonesian: 'Bahasa Indonesia',
+ Japanese: '日本語',
+ Korean: '조선말, 한국어',
+ Polish: 'Polski',
+ Portuguese: 'Português',
+ Russian: 'Русский',
+ Spanish: 'Español',
+ Thai: 'ไทย',
+ Turkish: 'Türkçe',
+ Ukrainian: 'Українська',
+ Vietnamese: 'Tiếng Việt',
} as const;
diff --git a/libs/novelStatus.ts b/libs/novelStatus.ts
index 280a647..b638dd3 100644
--- a/libs/novelStatus.ts
+++ b/libs/novelStatus.ts
@@ -1,9 +1,9 @@
export const NovelStatus = {
- Unknown: "Unknown",
- Ongoing: "Ongoing",
- Completed: "Completed",
- Licensed: "Licensed",
- PublishingFinished: "Publishing Finished",
- Cancelled: "Cancelled",
- OnHiatus: "On Hiatus",
-} as const;
\ No newline at end of file
+ Unknown: 'Unknown',
+ Ongoing: 'Ongoing',
+ Completed: 'Completed',
+ Licensed: 'Licensed',
+ PublishingFinished: 'Publishing Finished',
+ Cancelled: 'Cancelled',
+ OnHiatus: 'On Hiatus',
+} as const;
diff --git a/package-lock.json b/package-lock.json
index 8c02ee9..8c04ee8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,7 +30,9 @@
"@types/express": "^4.17.21",
"@types/jquery": "^3.5.29",
"@types/node": "^20.11.19",
- "pre-commit": "^1.2.2",
+ "husky": "^9.0.11",
+ "lint-staged": "^15.2.2",
+ "prettier": "3.2.5",
"tsc-watch": "^6.0.4"
}
},
@@ -367,6 +369,45 @@
"node": ">=0.4.0"
}
},
+ "node_modules/ansi-escapes": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz",
+ "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+ "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -781,6 +822,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/chalk": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+ "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+ "dev": true,
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
"node_modules/cheerio": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
@@ -867,6 +920,43 @@
"node": ">=0.10"
}
},
+ "node_modules/cli-cursor": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
+ "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
+ "dev": true,
+ "dependencies": {
+ "restore-cursor": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
+ "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
+ "dev": true,
+ "dependencies": {
+ "slice-ansi": "^5.0.0",
+ "string-width": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true
+ },
"node_modules/combine-source-map": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz",
@@ -1012,17 +1102,6 @@
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
},
- "node_modules/cross-spawn": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
- "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==",
- "dev": true,
- "dependencies": {
- "lru-cache": "^4.0.1",
- "shebang-command": "^1.2.0",
- "which": "^1.2.9"
- }
- },
"node_modules/crypto-browserify": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@@ -1331,6 +1410,12 @@
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
+ "node_modules/emoji-regex": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
+ "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
+ "dev": true
+ },
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@@ -1454,6 +1539,12 @@
"through": "~2.3.1"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "dev": true
+ },
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -1471,6 +1562,79 @@
"safe-buffer": "^5.1.1"
}
},
+ "node_modules/execa": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
+ "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^8.0.1",
+ "human-signals": "^5.0.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-final-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=16.17"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/execa/node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/execa/node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/execa/node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/execa/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
@@ -1719,6 +1883,18 @@
"resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz",
"integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ=="
},
+ "node_modules/get-east-asian-width": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz",
+ "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
@@ -1733,6 +1909,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-stream": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
+ "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
+ "dev": true,
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -1959,6 +2147,30 @@
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
"integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg=="
},
+ "node_modules/human-signals": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
+ "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=16.17.0"
+ }
+ },
+ "node_modules/husky": {
+ "version": "9.0.11",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz",
+ "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==",
+ "dev": true,
+ "bin": {
+ "husky": "bin.mjs"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/typicode"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -2120,6 +2332,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
+ "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-generator-function": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
@@ -2166,6 +2390,18 @@
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="
},
+ "node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-typed-array": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
@@ -2262,26 +2498,151 @@
"node": ">=0.10.0"
}
},
+ "node_modules/lilconfig": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz",
+ "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/lint-staged": {
+ "version": "15.2.2",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.2.tgz",
+ "integrity": "sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "5.3.0",
+ "commander": "11.1.0",
+ "debug": "4.3.4",
+ "execa": "8.0.1",
+ "lilconfig": "3.0.0",
+ "listr2": "8.0.1",
+ "micromatch": "4.0.5",
+ "pidtree": "0.6.0",
+ "string-argv": "0.3.2",
+ "yaml": "2.3.4"
+ },
+ "bin": {
+ "lint-staged": "bin/lint-staged.js"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/lint-staged"
+ }
+ },
+ "node_modules/lint-staged/node_modules/commander": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/lint-staged/node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/lint-staged/node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/listr2": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.1.tgz",
+ "integrity": "sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==",
+ "dev": true,
+ "dependencies": {
+ "cli-truncate": "^4.0.0",
+ "colorette": "^2.0.20",
+ "eventemitter3": "^5.0.1",
+ "log-update": "^6.0.0",
+ "rfdc": "^1.3.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/lodash.memoize": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz",
"integrity": "sha512-eDn9kqrAmVUC1wmZvlQ6Uhde44n+tXpqPrN8olQJbttgh0oKclk+SF54P47VEGE9CEiMeRwAP8BaM7UHvBkz2A=="
},
+ "node_modules/log-update": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz",
+ "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==",
+ "dev": true,
+ "dependencies": {
+ "ansi-escapes": "^6.2.0",
+ "cli-cursor": "^4.0.0",
+ "slice-ansi": "^7.0.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/is-fullwidth-code-point": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz",
+ "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==",
+ "dev": true,
+ "dependencies": {
+ "get-east-asian-width": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/slice-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz",
+ "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
"node_modules/long": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
},
- "node_modules/lru-cache": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
- "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
- "dev": true,
- "dependencies": {
- "pseudomap": "^1.0.2",
- "yallist": "^2.1.2"
- }
- },
"node_modules/lru-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
@@ -2352,6 +2713,12 @@
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -2427,6 +2794,18 @@
"node": ">= 0.6"
}
},
+ "node_modules/mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -2584,6 +2963,33 @@
"node": ">=0.10.0"
}
},
+ "node_modules/npm-run-path": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
+ "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -2647,20 +3053,26 @@
"wrappy": "1"
}
},
+ "node_modules/onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/os-browserify": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
"integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A=="
},
- "node_modules/os-shim": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz",
- "integrity": "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==",
- "dev": true,
- "engines": {
- "node": ">= 0.4.0"
- }
- },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@@ -2818,6 +3230,18 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pidtree": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
+ "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
+ "dev": true,
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
@@ -2862,16 +3286,19 @@
"node": "^10 || ^12 || >=14"
}
},
- "node_modules/pre-commit": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz",
- "integrity": "sha512-qokTiqxD6GjODy5ETAIgzsRgnBWWQHQH2ghy86PU7mIn/wuWeTwF3otyNQZxWBwVn8XNr8Tdzj/QfUXpH+gRZA==",
+ "node_modules/prettier": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
+ "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": true,
- "hasInstallScript": true,
- "dependencies": {
- "cross-spawn": "^5.0.1",
- "spawn-sync": "^1.0.15",
- "which": "1.2.x"
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/process": {
@@ -2943,12 +3370,6 @@
"node": ">= 0.10"
}
},
- "node_modules/pseudomap": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
- "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==",
- "dev": true
- },
"node_modules/public-encrypt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
@@ -3111,6 +3532,52 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/restore-cursor": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
+ "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
+ "dev": true,
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -3120,6 +3587,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/rfdc": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz",
+ "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==",
+ "dev": true
+ },
"node_modules/ripemd160": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
@@ -3292,27 +3765,6 @@
"fast-safe-stringify": "^2.0.7"
}
},
- "node_modules/shebang-command": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
- "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
- "dev": true,
- "dependencies": {
- "shebang-regex": "^1.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/shebang-regex": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
- "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/shell-quote": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
@@ -3334,6 +3786,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
@@ -3361,6 +3825,22 @@
"node": ">=8"
}
},
+ "node_modules/slice-ansi": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
+ "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^6.0.0",
+ "is-fullwidth-code-point": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -3394,17 +3874,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/spawn-sync": {
- "version": "1.0.15",
- "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz",
- "integrity": "sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==",
- "dev": true,
- "hasInstallScript": true,
- "dependencies": {
- "concat-stream": "^1.4.7",
- "os-shim": "^0.1.2"
- }
- },
"node_modules/split": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
@@ -3520,6 +3989,50 @@
"node": ">=0.6.19"
}
},
+ "node_modules/string-width": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz",
+ "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/subarg": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz",
@@ -3766,6 +4279,18 @@
"resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
"integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
},
+ "node_modules/type-fest": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
+ "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -3902,18 +4427,6 @@
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="
},
- "node_modules/which": {
- "version": "1.2.14",
- "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz",
- "integrity": "sha512-16uPglFkRPzgiUXYMi1Jf8Z5EzN1iB4V0ZtMXcHZnwsBtQhhHeCqoWw7tsUY42hJGNDWtUsVLTjakIa5BgAxCw==",
- "dev": true,
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "which": "bin/which"
- }
- },
"node_modules/which-typed-array": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
@@ -3932,6 +4445,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/wrap-ansi": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
+ "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -3945,11 +4475,14 @@
"node": ">=0.4"
}
},
- "node_modules/yallist": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
- "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
- "dev": true
+ "node_modules/yaml": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz",
+ "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 14"
+ }
},
"node_modules/yn": {
"version": "3.1.1",
@@ -4254,6 +4787,27 @@
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA=="
},
+ "ansi-escapes": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz",
+ "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^3.0.0"
+ }
+ },
+ "ansi-regex": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+ "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true
+ },
"anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -4613,6 +5167,12 @@
"set-function-length": "^1.1.1"
}
},
+ "chalk": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+ "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+ "dev": true
+ },
"cheerio": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
@@ -4676,6 +5236,31 @@
"timers-ext": "^0.1.7"
}
},
+ "cli-cursor": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
+ "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
+ "dev": true,
+ "requires": {
+ "restore-cursor": "^4.0.0"
+ }
+ },
+ "cli-truncate": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
+ "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
+ "dev": true,
+ "requires": {
+ "slice-ansi": "^5.0.0",
+ "string-width": "^7.0.0"
+ }
+ },
+ "colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true
+ },
"combine-source-map": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz",
@@ -4805,17 +5390,6 @@
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
},
- "cross-spawn": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
- "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==",
- "dev": true,
- "requires": {
- "lru-cache": "^4.0.1",
- "shebang-command": "^1.2.0",
- "which": "^1.2.9"
- }
- },
"crypto-browserify": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@@ -5057,6 +5631,12 @@
}
}
},
+ "emoji-regex": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
+ "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
+ "dev": true
+ },
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@@ -5155,6 +5735,12 @@
"through": "~2.3.1"
}
},
+ "eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "dev": true
+ },
"events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -5169,6 +5755,60 @@
"safe-buffer": "^5.1.1"
}
},
+ "execa": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
+ "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^8.0.1",
+ "human-signals": "^5.0.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-final-newline": "^3.0.0"
+ },
+ "dependencies": {
+ "cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ }
+ },
+ "shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^3.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ }
+ }
+ },
"express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
@@ -5382,6 +6022,12 @@
"resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz",
"integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ=="
},
+ "get-east-asian-width": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz",
+ "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==",
+ "dev": true
+ },
"get-intrinsic": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
@@ -5393,6 +6039,12 @@
"hasown": "^2.0.0"
}
},
+ "get-stream": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
+ "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
+ "dev": true
+ },
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -5557,6 +6209,18 @@
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
"integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg=="
},
+ "human-signals": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
+ "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
+ "dev": true
+ },
+ "husky": {
+ "version": "9.0.11",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz",
+ "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==",
+ "dev": true
+ },
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -5665,6 +6329,12 @@
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
},
+ "is-fullwidth-code-point": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
+ "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
+ "dev": true
+ },
"is-generator-function": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
@@ -5696,6 +6366,12 @@
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="
},
+ "is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true
+ },
"is-typed-array": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
@@ -5768,26 +6444,111 @@
}
}
},
+ "lilconfig": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz",
+ "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==",
+ "dev": true
+ },
+ "lint-staged": {
+ "version": "15.2.2",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.2.tgz",
+ "integrity": "sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==",
+ "dev": true,
+ "requires": {
+ "chalk": "5.3.0",
+ "commander": "11.1.0",
+ "debug": "4.3.4",
+ "execa": "8.0.1",
+ "lilconfig": "3.0.0",
+ "listr2": "8.0.1",
+ "micromatch": "4.0.5",
+ "pidtree": "0.6.0",
+ "string-argv": "0.3.2",
+ "yaml": "2.3.4"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
+ "dev": true
+ },
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ }
+ }
+ },
+ "listr2": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.1.tgz",
+ "integrity": "sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==",
+ "dev": true,
+ "requires": {
+ "cli-truncate": "^4.0.0",
+ "colorette": "^2.0.20",
+ "eventemitter3": "^5.0.1",
+ "log-update": "^6.0.0",
+ "rfdc": "^1.3.0",
+ "wrap-ansi": "^9.0.0"
+ }
+ },
"lodash.memoize": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz",
"integrity": "sha512-eDn9kqrAmVUC1wmZvlQ6Uhde44n+tXpqPrN8olQJbttgh0oKclk+SF54P47VEGE9CEiMeRwAP8BaM7UHvBkz2A=="
},
+ "log-update": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz",
+ "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==",
+ "dev": true,
+ "requires": {
+ "ansi-escapes": "^6.2.0",
+ "cli-cursor": "^4.0.0",
+ "slice-ansi": "^7.0.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "dependencies": {
+ "is-fullwidth-code-point": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz",
+ "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==",
+ "dev": true,
+ "requires": {
+ "get-east-asian-width": "^1.0.0"
+ }
+ },
+ "slice-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz",
+ "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^5.0.0"
+ }
+ }
+ }
+ },
"long": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
},
- "lru-cache": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
- "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
- "dev": true,
- "requires": {
- "pseudomap": "^1.0.2",
- "yallist": "^2.1.2"
- }
- },
"lru-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
@@ -5852,6 +6613,12 @@
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
+ "merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
"merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -5905,6 +6672,12 @@
"mime-db": "1.52.0"
}
},
+ "mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "dev": true
+ },
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -6018,6 +6791,23 @@
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
},
+ "npm-run-path": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
+ "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
+ "dev": true,
+ "requires": {
+ "path-key": "^4.0.0"
+ },
+ "dependencies": {
+ "path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true
+ }
+ }
+ },
"nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -6063,17 +6853,20 @@
"wrappy": "1"
}
},
+ "onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^4.0.0"
+ }
+ },
"os-browserify": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
"integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A=="
},
- "os-shim": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz",
- "integrity": "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==",
- "dev": true
- },
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@@ -6198,6 +6991,12 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
},
+ "pidtree": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
+ "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
+ "dev": true
+ },
"pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
@@ -6222,16 +7021,11 @@
"source-map-js": "^1.0.2"
}
},
- "pre-commit": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz",
- "integrity": "sha512-qokTiqxD6GjODy5ETAIgzsRgnBWWQHQH2ghy86PU7mIn/wuWeTwF3otyNQZxWBwVn8XNr8Tdzj/QfUXpH+gRZA==",
- "dev": true,
- "requires": {
- "cross-spawn": "^5.0.1",
- "spawn-sync": "^1.0.15",
- "which": "1.2.x"
- }
+ "prettier": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
+ "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
+ "dev": true
},
"process": {
"version": "0.11.10",
@@ -6286,12 +7080,6 @@
"event-stream": "=3.3.4"
}
},
- "pseudomap": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
- "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==",
- "dev": true
- },
"public-encrypt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
@@ -6420,11 +7208,50 @@
"supports-preserve-symlinks-flag": "^1.0.0"
}
},
+ "restore-cursor": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
+ "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
+ "dev": true,
+ "requires": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "dependencies": {
+ "mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true
+ },
+ "onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^2.1.0"
+ }
+ },
+ "signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ }
+ }
+ },
"reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="
},
+ "rfdc": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz",
+ "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==",
+ "dev": true
+ },
"ripemd160": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
@@ -6558,21 +7385,6 @@
"fast-safe-stringify": "^2.0.7"
}
},
- "shebang-command": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
- "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
- "dev": true,
- "requires": {
- "shebang-regex": "^1.0.0"
- }
- },
- "shebang-regex": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
- "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
- "dev": true
- },
"shell-quote": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
@@ -6588,6 +7400,12 @@
"object-inspect": "^1.9.0"
}
},
+ "signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true
+ },
"simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
@@ -6598,6 +7416,16 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="
},
+ "slice-ansi": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
+ "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^6.0.0",
+ "is-fullwidth-code-point": "^4.0.0"
+ }
+ },
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -6624,16 +7452,6 @@
}
}
},
- "spawn-sync": {
- "version": "1.0.15",
- "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz",
- "integrity": "sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==",
- "dev": true,
- "requires": {
- "concat-stream": "^1.4.7",
- "os-shim": "^0.1.2"
- }
- },
"split": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
@@ -6740,6 +7558,32 @@
"integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
"dev": true
},
+ "string-width": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz",
+ "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^6.0.1"
+ }
+ },
+ "strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true
+ },
"subarg": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz",
@@ -6920,6 +7764,12 @@
"resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
"integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
},
+ "type-fest": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
+ "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
+ "dev": true
+ },
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -7030,15 +7880,6 @@
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="
},
- "which": {
- "version": "1.2.14",
- "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz",
- "integrity": "sha512-16uPglFkRPzgiUXYMi1Jf8Z5EzN1iB4V0ZtMXcHZnwsBtQhhHeCqoWw7tsUY42hJGNDWtUsVLTjakIa5BgAxCw==",
- "dev": true,
- "requires": {
- "isexe": "^2.0.0"
- }
- },
"which-typed-array": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
@@ -7051,6 +7892,17 @@
"has-tostringtag": "^1.0.0"
}
},
+ "wrap-ansi": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
+ "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ }
+ },
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -7061,10 +7913,10 @@
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
- "yallist": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
- "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
+ "yaml": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz",
+ "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==",
"dev": true
},
"yn": {
diff --git a/package.json b/package.json
index 774c90d..02d823c 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,9 @@
"clearMultisrc": "find . -type f -wholename \"*plugins*\\[*\\]*.[t,j]s\" -delete",
"less": "npx less ./test_web/static/css/index.less ./test_web/static/css/index.css",
"host-linux": "chmod +x ./host.sh && ./host.sh",
- "host-windows": "powershell ./host.ps1"
+ "host-windows": "powershell ./host.ps1",
+ "prettier": "prettier --write \"./**/*.{js,ts}\"",
+ "prepare": "husky install"
},
"author": "LNReader",
"license": "MIT",
@@ -44,7 +46,12 @@
"@types/express": "^4.17.21",
"@types/jquery": "^3.5.29",
"@types/node": "^20.11.19",
- "pre-commit": "^1.2.2",
+ "husky": "^9.0.11",
+ "lint-staged": "^15.2.2",
+ "prettier": "3.2.5",
"tsc-watch": "^6.0.4"
+ },
+ "lint-staged": {
+ "**/*.{js,ts}": "prettier --write"
}
}
diff --git a/plugins/chinese/69shu.ts b/plugins/chinese/69shu.ts
index 0668063..1d1e885 100644
--- a/plugins/chinese/69shu.ts
+++ b/plugins/chinese/69shu.ts
@@ -1,219 +1,223 @@
-import { load as parseHTML } from "cheerio";
-import { fetchText, fetchFile } from "@libs/fetch";
-import { FilterTypes, Filters } from "@libs/filterInputs";
-import { Plugin } from "@typings/plugin";
-import { NovelStatus } from "@libs/novelStatus";
+import { load as parseHTML } from 'cheerio';
+import { fetchText, fetchFile } from '@libs/fetch';
+import { FilterTypes, Filters } from '@libs/filterInputs';
+import { Plugin } from '@typings/plugin';
+import { NovelStatus } from '@libs/novelStatus';
class Shu69 implements Plugin.PluginBase {
- id = "69shu";
- name = "69书吧";
- icon = "src/cn/69shu/icon.png";
- site = "https://www.69shu.xyz";
- version = "0.2.0";
+ id = '69shu';
+ name = '69书吧';
+ icon = 'src/cn/69shu/icon.png';
+ site = 'https://www.69shu.xyz';
+ version = '0.2.0';
- async popularNovels(
- pageNo: number,
- {
- showLatestNovels,
- filters,
- }: Plugin.PopularNovelsOptions
- ): Promise {
- let url: string;
- if (showLatestNovels) {
- url = `${this.site}/rank/lastupdate/${pageNo}.html`;
- } else if (filters.sort.value === 'none') {
- url = `${this.site}/rank/${filters.rank.value}/${pageNo}.html`;
- } else {
- url = `${this.site}/sort/${filters.sort.value}/${pageNo}.html`;
- }
-
- const body = await fetchText(url);
- if (body === '') throw Error('无法获取小说列表,请检查网络');
-
- const loadedCheerio = parseHTML(body);
-
- const novels: Plugin.NovelItem[] = [];
-
- loadedCheerio('div.book-coverlist').each((i, el) => {
- let url = loadedCheerio(el).find('a.cover').attr('href');
-
- const novelName = loadedCheerio(el).find('h4.name').text().trim();
- const novelCover = loadedCheerio(el).find('a.cover > img').attr('src');
- if (!url) return;
-
- const novel = {
- name: novelName,
- cover: novelCover,
- path: url,
- };
-
- novels.push(novel);
- });
-
- return novels;
+ async popularNovels(
+ pageNo: number,
+ {
+ showLatestNovels,
+ filters,
+ }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ let url: string;
+ if (showLatestNovels) {
+ url = `${this.site}/rank/lastupdate/${pageNo}.html`;
+ } else if (filters.sort.value === 'none') {
+ url = `${this.site}/rank/${filters.rank.value}/${pageNo}.html`;
+ } else {
+ url = `${this.site}/sort/${filters.sort.value}/${pageNo}.html`;
}
- async parseNovel(novelPath: string): Promise {
- const url = this.site + novelPath;
+ const body = await fetchText(url);
+ if (body === '') throw Error('无法获取小说列表,请检查网络');
- const body = await fetchText(url);
- if (body === '') throw Error('无法获取小说内容,请检查网络');
+ const loadedCheerio = parseHTML(body);
- let loadedCheerio = parseHTML(body);
+ const novels: Plugin.NovelItem[] = [];
- const novel: Plugin.SourceNovel = {
- path: novelPath,
- chapters: [],
- name: loadedCheerio('h1').text().trim()
- };
+ loadedCheerio('div.book-coverlist').each((i, el) => {
+ let url = loadedCheerio(el).find('a.cover').attr('href');
- novel.cover = loadedCheerio('div.cover > img').attr('src');
+ const novelName = loadedCheerio(el).find('h4.name').text().trim();
+ const novelCover = loadedCheerio(el).find('a.cover > img').attr('src');
+ if (!url) return;
- novel.summary = loadedCheerio('#bookIntro').text().trim();
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: url,
+ };
- const bookInfo = loadedCheerio('div.caption-bookinfo > p')
+ novels.push(novel);
+ });
- novel.author = bookInfo.find("a").attr('title');
+ return novels;
+ }
- novel.artist = undefined;
+ async parseNovel(novelPath: string): Promise {
+ const url = this.site + novelPath;
- novel.status = bookInfo.text().includes('连载') ? NovelStatus.Ongoing : NovelStatus.Completed;
+ const body = await fetchText(url);
+ if (body === '') throw Error('无法获取小说内容,请检查网络');
- novel.genres = '';
+ let loadedCheerio = parseHTML(body);
- // Table of Content is on a different page than the summary page
- let chapters: Plugin.ChapterItem[] = [];
-
- const allUrl = loadedCheerio('dd.all > a').attr('href')
- if (allUrl) {
- const chaptersUrl = this.site + allUrl;
- const chaptersBody = await fetchText(chaptersUrl);
-
- const chaptersLoadedCheerio = parseHTML(chaptersBody);
-
- chaptersLoadedCheerio('dd').each((i, el) => {
- const chapterUrl = chaptersLoadedCheerio(el).find('a').attr('href');
- const chapterName = chaptersLoadedCheerio(el).find('a').text().trim();
- if(chapterUrl){
- chapters.push({
- name: chapterName,
- path: chapterUrl,
- });
- }
- });
- } else {
- loadedCheerio('div.panel.hidden-xs > dl.panel-chapterlist:nth-child(2) > dd').each((i, el) => {
- const chapterUrl = loadedCheerio(el).find('a').attr('href');
- const chapterName = loadedCheerio(el).find('a').text().trim();
- if(chapterUrl){
- chapters.push({
- name: chapterName,
- path: chapterUrl,
- });
- }
- });
- }
-
- novel.chapters = chapters;
-
- return novel;
- }
-
- async parseChapter(chapterPath: string): Promise {
- const body = await fetchText(this.site + chapterPath);
-
- const loadedCheerio = parseHTML(body);
-
- const chapterText = loadedCheerio('#chaptercontent p')
- .map((i, el) => loadedCheerio(el).text())
- .get()
- // remove empty lines and 69shu ads
- .map((line: string) => line.trim())
- .filter((line: string) => line !== '' && !line.includes('69书吧'))
- .map((line: string) => `${line}
`)
- .join('\n');
-
- return chapterText;
+ const novel: Plugin.SourceNovel = {
+ path: novelPath,
+ chapters: [],
+ name: loadedCheerio('h1').text().trim(),
};
- async searchNovels(
- searchTerm: string,
- pageNo: number
- ): Promise {
- if (pageNo > 1) return [];
+ novel.cover = loadedCheerio('div.cover > img').attr('src');
- const searchUrl = `${this.site}/search`;
- const formData = new FormData();
- formData.append('searchkey', searchTerm);
+ novel.summary = loadedCheerio('#bookIntro').text().trim();
- const body = await fetchText(searchUrl, {
- method: 'post',
- body: formData,
- });
- if (body === '') throw Error('无法获取搜索结果,请检查网络');
+ const bookInfo = loadedCheerio('div.caption-bookinfo > p');
- let loadedCheerio = parseHTML(body);
+ novel.author = bookInfo.find('a').attr('title');
- const novels: Plugin.NovelItem[] = [];
+ novel.artist = undefined;
- loadedCheerio('div.book-coverlist').each((i, el) => {
- let url = loadedCheerio(el).find('a.cover').attr('href');
+ novel.status = bookInfo.text().includes('连载')
+ ? NovelStatus.Ongoing
+ : NovelStatus.Completed;
- const novelName = loadedCheerio(el).find('h4.name').text().trim();
- const novelCover = loadedCheerio(el).find('a.cover > img').attr('src');
+ novel.genres = '';
- if (!url) return;
+ // Table of Content is on a different page than the summary page
+ let chapters: Plugin.ChapterItem[] = [];
- const novel = {
- name: novelName,
- cover: novelCover,
- path: url,
- };
+ const allUrl = loadedCheerio('dd.all > a').attr('href');
+ if (allUrl) {
+ const chaptersUrl = this.site + allUrl;
+ const chaptersBody = await fetchText(chaptersUrl);
- novels.push(novel);
- });
+ const chaptersLoadedCheerio = parseHTML(chaptersBody);
- return novels;
+ chaptersLoadedCheerio('dd').each((i, el) => {
+ const chapterUrl = chaptersLoadedCheerio(el).find('a').attr('href');
+ const chapterName = chaptersLoadedCheerio(el).find('a').text().trim();
+ if (chapterUrl) {
+ chapters.push({
+ name: chapterName,
+ path: chapterUrl,
+ });
+ }
+ });
+ } else {
+ loadedCheerio(
+ 'div.panel.hidden-xs > dl.panel-chapterlist:nth-child(2) > dd',
+ ).each((i, el) => {
+ const chapterUrl = loadedCheerio(el).find('a').attr('href');
+ const chapterName = loadedCheerio(el).find('a').text().trim();
+ if (chapterUrl) {
+ chapters.push({
+ name: chapterName,
+ path: chapterUrl,
+ });
+ }
+ });
}
- fetchImage = fetchFile;
+ novel.chapters = chapters;
- filters = {
- rank: {
- label: "排行榜",
- value: "allvisit",
- options: [
- { label: "总排行榜", value: "allvisit" },
- { label: "月排行榜", value: "monthvisit" },
- { label: "周排行榜", value: "weekvisit" },
- { label: "日排行榜", value: "dayvisit" },
- { label: "收藏榜", value: "goodnum" },
- { label: "字数榜", value: "words" },
- { label: "推荐榜", value: "allvote" },
- { label: "新书榜", value: "postdate" },
- { label: "更新榜", value: "lastupdate" },
- ],
- type: FilterTypes.Picker,
- },
- sort: {
- label: "分类",
- value: "none",
- options: [
- { label: "无", value: "none" },
- { label: "全部", value: "all" },
- { label: "玄幻", value: "xuanhuan" },
- { label: "仙侠", value: "xianxia" },
- { label: "都市", value: "dushi" },
- { label: "历史", value: "lishi" },
- { label: "游戏", value: "youxi" },
- { label: "科幻", value: "kehuan" },
- { label: "灵异", value: "kongbu" },
- { label: "言情", value: "nvsheng" },
- { label: "其它", value: "qita" },
- ],
- type: FilterTypes.Picker,
- }
- } satisfies Filters;
+ return novel;
+ }
+
+ async parseChapter(chapterPath: string): Promise {
+ const body = await fetchText(this.site + chapterPath);
+
+ const loadedCheerio = parseHTML(body);
+
+ const chapterText = loadedCheerio('#chaptercontent p')
+ .map((i, el) => loadedCheerio(el).text())
+ .get()
+ // remove empty lines and 69shu ads
+ .map((line: string) => line.trim())
+ .filter((line: string) => line !== '' && !line.includes('69书吧'))
+ .map((line: string) => `${line}
`)
+ .join('\n');
+
+ return chapterText;
+ }
+
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ if (pageNo > 1) return [];
+
+ const searchUrl = `${this.site}/search`;
+ const formData = new FormData();
+ formData.append('searchkey', searchTerm);
+
+ const body = await fetchText(searchUrl, {
+ method: 'post',
+ body: formData,
+ });
+ if (body === '') throw Error('无法获取搜索结果,请检查网络');
+
+ let loadedCheerio = parseHTML(body);
+
+ const novels: Plugin.NovelItem[] = [];
+
+ loadedCheerio('div.book-coverlist').each((i, el) => {
+ let url = loadedCheerio(el).find('a.cover').attr('href');
+
+ const novelName = loadedCheerio(el).find('h4.name').text().trim();
+ const novelCover = loadedCheerio(el).find('a.cover > img').attr('src');
+
+ if (!url) return;
+
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: url,
+ };
+
+ novels.push(novel);
+ });
+
+ return novels;
+ }
+
+ fetchImage = fetchFile;
+
+ filters = {
+ rank: {
+ label: '排行榜',
+ value: 'allvisit',
+ options: [
+ { label: '总排行榜', value: 'allvisit' },
+ { label: '月排行榜', value: 'monthvisit' },
+ { label: '周排行榜', value: 'weekvisit' },
+ { label: '日排行榜', value: 'dayvisit' },
+ { label: '收藏榜', value: 'goodnum' },
+ { label: '字数榜', value: 'words' },
+ { label: '推荐榜', value: 'allvote' },
+ { label: '新书榜', value: 'postdate' },
+ { label: '更新榜', value: 'lastupdate' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ sort: {
+ label: '分类',
+ value: 'none',
+ options: [
+ { label: '无', value: 'none' },
+ { label: '全部', value: 'all' },
+ { label: '玄幻', value: 'xuanhuan' },
+ { label: '仙侠', value: 'xianxia' },
+ { label: '都市', value: 'dushi' },
+ { label: '历史', value: 'lishi' },
+ { label: '游戏', value: 'youxi' },
+ { label: '科幻', value: 'kehuan' },
+ { label: '灵异', value: 'kongbu' },
+ { label: '言情', value: 'nvsheng' },
+ { label: '其它', value: 'qita' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ } satisfies Filters;
}
export default new Shu69();
diff --git a/plugins/chinese/69xinshu.ts b/plugins/chinese/69xinshu.ts
index 078fc13..2e410cf 100644
--- a/plugins/chinese/69xinshu.ts
+++ b/plugins/chinese/69xinshu.ts
@@ -1,199 +1,210 @@
-import { load as parseHTML } from "cheerio";
-import { fetchText, fetchFile } from "@libs/fetch";
-import { FilterTypes, Filters } from "@libs/filterInputs";
-import { Plugin } from "@typings/plugin";
-import { encode } from "urlencode";
-import { NovelStatus } from "@libs/novelStatus";
+import { load as parseHTML } from 'cheerio';
+import { fetchText, fetchFile } from '@libs/fetch';
+import { FilterTypes, Filters } from '@libs/filterInputs';
+import { Plugin } from '@typings/plugin';
+import { encode } from 'urlencode';
+import { NovelStatus } from '@libs/novelStatus';
class XinShu69 implements Plugin.PluginBase {
- id = "69xinshu";
- name = "69书吧";
- icon = "src/cn/69xinshu/icon.png";
- site = "https://www.69shu.pro";
- version = "0.1.1";
+ id = '69xinshu';
+ name = '69书吧';
+ icon = 'src/cn/69xinshu/icon.png';
+ site = 'https://www.69shu.pro';
+ version = '0.1.1';
- async popularNovels(
- pageNo: number,
- { filters }: Plugin.PopularNovelsOptions
- ): Promise {
- let url: string;
- if (pageNo === 1) {
- url = `${this.site}/novels/class/${filters.class.value}.htm`;
- } else {
- url = `${this.site}/ajax_novels/class/${filters.class.value}/${pageNo}.htm`;
- }
-
- const body = await fetchText(url, {}, 'gbk');
- if (body === '') throw Error('无法获取小说列表,请检查网络');
-
- const loadedCheerio = parseHTML(body);
-
- const novels: Plugin.NovelItem[] = [];
-
- const novelsList = pageNo === 1 ? loadedCheerio('div.newbox > ul > li') : loadedCheerio('li');
- novelsList.each((i, e) => {
- const novelUrl = loadedCheerio(e).find('li > div > h3 > a:nth-child(2)').attr('href');
-
- if (novelUrl) {
- const novelName = loadedCheerio(e).find('li > div > h3 > a:nth-child(2)').text();
- const novelCover = loadedCheerio(e)
- .find('li > a > img')
- .attr('data-src');
-
- const novel = {
- name: novelName,
- cover: novelCover,
- path: novelUrl.replace(this.site, ''),
- };
-
- novels.push(novel);
- }
- });
-
- return novels;
+ async popularNovels(
+ pageNo: number,
+ { filters }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ let url: string;
+ if (pageNo === 1) {
+ url = `${this.site}/novels/class/${filters.class.value}.htm`;
+ } else {
+ url = `${this.site}/ajax_novels/class/${filters.class.value}/${pageNo}.htm`;
}
- async parseNovel(novelPath: string): Promise {
- const url = this.site + novelPath;
+ const body = await fetchText(url, {}, 'gbk');
+ if (body === '') throw Error('无法获取小说列表,请检查网络');
- const body = await fetchText(url, {}, 'gbk');
- if (body === '') throw Error('无法获取小说内容,请检查网络');
+ const loadedCheerio = parseHTML(body);
- let loadedCheerio = parseHTML(body);
+ const novels: Plugin.NovelItem[] = [];
- const novel: Plugin.SourceNovel = {
- path: novelPath,
- chapters: [],
- name: loadedCheerio('h1 > a').text()
+ const novelsList =
+ pageNo === 1
+ ? loadedCheerio('div.newbox > ul > li')
+ : loadedCheerio('li');
+ novelsList.each((i, e) => {
+ const novelUrl = loadedCheerio(e)
+ .find('li > div > h3 > a:nth-child(2)')
+ .attr('href');
+
+ if (novelUrl) {
+ const novelName = loadedCheerio(e)
+ .find('li > div > h3 > a:nth-child(2)')
+ .text();
+ const novelCover = loadedCheerio(e)
+ .find('li > a > img')
+ .attr('data-src');
+
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl.replace(this.site, ''),
};
- novel.cover = loadedCheerio('div.bookimg2 > img').attr(
- 'src',
- );
+ novels.push(novel);
+ }
+ });
- novel.summary = loadedCheerio('div.navtxt').text().trim();
+ return novels;
+ }
- const bookInfo = loadedCheerio('div.booknav2').text()
+ async parseNovel(novelPath: string): Promise {
+ const url = this.site + novelPath;
- const authorMatch = bookInfo.match(/作者:(.*)/);
- if (authorMatch) {
- novel.author = authorMatch[1];
- }
+ const body = await fetchText(url, {}, 'gbk');
+ if (body === '') throw Error('无法获取小说内容,请检查网络');
- novel.artist = undefined;
+ let loadedCheerio = parseHTML(body);
- novel.status = bookInfo.includes('连载') ? NovelStatus.Ongoing : NovelStatus.Completed;
-
- const genresMatch = bookInfo.match(/分类:(.*)/);
- if (genresMatch) {
- novel.genres = genresMatch[1];
- }
-
- // Table of Content is on a different page than the summary page
- let chapters: Plugin.ChapterItem[] = [];
-
- const chaptersUrl = loadedCheerio('a.more-btn').attr('href');
- if (chaptersUrl) {
- const chaptersBody = await fetchText(chaptersUrl, {}, 'gbk');
-
- const chaptersLoadedCheerio = parseHTML(chaptersBody);
-
- chaptersLoadedCheerio('li').each((i, e) => {
- const chapterUrl = chaptersLoadedCheerio(e).find('a').attr('href');
-
- if (chapterUrl?.startsWith("https://")) {
- const chapterName = chaptersLoadedCheerio(e).find('a').text().trim();
-
- chapters.push({
- name: chapterName,
- path: chapterUrl.replace(this.site, ''),
- });
- }
- });
- }
-
- novel.chapters = chapters;
-
- return novel;
- }
-
- async parseChapter(chapterPath: string): Promise {
- const body = await fetchText(this.site + chapterPath, {}, 'gbk');
-
- const loadedCheerio = parseHTML(body);
-
- const chapterText = (loadedCheerio('div.txtnav').prop('innerText') ?? '')
- .split('\n')
- // remove empty lines
- .map((line: string) => line.trim())
- .filter((line: string) => line !== '')
- // remove the first two lines which are the chapter name and author name
- .slice(2)
- .map((line: string) => `${line}
`)
- .join('\n');
-
- return chapterText;
+ const novel: Plugin.SourceNovel = {
+ path: novelPath,
+ chapters: [],
+ name: loadedCheerio('h1 > a').text(),
};
- async searchNovels(
- searchTerm: string,
- pageNo: number
- ): Promise {
- if (pageNo > 1) return [];
+ novel.cover = loadedCheerio('div.bookimg2 > img').attr('src');
- const searchUrl = `${this.site}/modules/article/search.php`;
- const formData = new FormData();
- formData.append('searchkey', encode(searchTerm, 'gbk'));
- formData.append('searchtype', 'all');
+ novel.summary = loadedCheerio('div.navtxt').text().trim();
- const body = await fetchText(searchUrl, { method: 'post', body: formData, }, 'gbk');
- if (body === '') throw Error('无法获取搜索结果,请检查网络');
+ const bookInfo = loadedCheerio('div.booknav2').text();
- let loadedCheerio = parseHTML(body);
-
- const novels: Plugin.NovelItem[] = [];
-
- loadedCheerio('div.newbox > ul > li').each((i, e) => {
- const novelUrl = loadedCheerio(e).find('a').attr('href')!;
- const novelName = loadedCheerio(e).find('div.newnav > h3').text();
- const novelCover = loadedCheerio(e).find('img').attr('data-src');
-
- const novel = {
- name: novelName,
- path: novelUrl.replace(this.site, ''),
- cover: novelCover,
- };
-
- novels.push(novel);
- });
-
- return novels;
+ const authorMatch = bookInfo.match(/作者:(.*)/);
+ if (authorMatch) {
+ novel.author = authorMatch[1];
}
- fetchImage = fetchFile;
+ novel.artist = undefined;
- filters = {
- class: {
- label: "分类",
- value: "0",
- options: [
- { label: "全部分类", value: "0" },
- { label: "言情小说", value: "3" },
- { label: "玄幻魔法", value: "1" },
- { label: "修真武侠", value: "2" },
- { label: "穿越时空", value: "11" },
- { label: "都市小说", value: "9" },
- { label: "历史军事", value: "4" },
- { label: "游戏竞技", value: "5" },
- { label: "科幻空间", value: "6" },
- { label: "悬疑惊悚", value: "7" },
- { label: "同人小说", value: "8" },
- { label: "官场职场", value: "10" },
- { label: "青春校园", value: "12" },
- ],
- type: FilterTypes.Picker,
- },
- } satisfies Filters;
+ novel.status = bookInfo.includes('连载')
+ ? NovelStatus.Ongoing
+ : NovelStatus.Completed;
+
+ const genresMatch = bookInfo.match(/分类:(.*)/);
+ if (genresMatch) {
+ novel.genres = genresMatch[1];
+ }
+
+ // Table of Content is on a different page than the summary page
+ let chapters: Plugin.ChapterItem[] = [];
+
+ const chaptersUrl = loadedCheerio('a.more-btn').attr('href');
+ if (chaptersUrl) {
+ const chaptersBody = await fetchText(chaptersUrl, {}, 'gbk');
+
+ const chaptersLoadedCheerio = parseHTML(chaptersBody);
+
+ chaptersLoadedCheerio('li').each((i, e) => {
+ const chapterUrl = chaptersLoadedCheerio(e).find('a').attr('href');
+
+ if (chapterUrl?.startsWith('https://')) {
+ const chapterName = chaptersLoadedCheerio(e).find('a').text().trim();
+
+ chapters.push({
+ name: chapterName,
+ path: chapterUrl.replace(this.site, ''),
+ });
+ }
+ });
+ }
+
+ novel.chapters = chapters;
+
+ return novel;
+ }
+
+ async parseChapter(chapterPath: string): Promise {
+ const body = await fetchText(this.site + chapterPath, {}, 'gbk');
+
+ const loadedCheerio = parseHTML(body);
+
+ const chapterText = (loadedCheerio('div.txtnav').prop('innerText') ?? '')
+ .split('\n')
+ // remove empty lines
+ .map((line: string) => line.trim())
+ .filter((line: string) => line !== '')
+ // remove the first two lines which are the chapter name and author name
+ .slice(2)
+ .map((line: string) => `${line}
`)
+ .join('\n');
+
+ return chapterText;
+ }
+
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ if (pageNo > 1) return [];
+
+ const searchUrl = `${this.site}/modules/article/search.php`;
+ const formData = new FormData();
+ formData.append('searchkey', encode(searchTerm, 'gbk'));
+ formData.append('searchtype', 'all');
+
+ const body = await fetchText(
+ searchUrl,
+ { method: 'post', body: formData },
+ 'gbk',
+ );
+ if (body === '') throw Error('无法获取搜索结果,请检查网络');
+
+ let loadedCheerio = parseHTML(body);
+
+ const novels: Plugin.NovelItem[] = [];
+
+ loadedCheerio('div.newbox > ul > li').each((i, e) => {
+ const novelUrl = loadedCheerio(e).find('a').attr('href')!;
+ const novelName = loadedCheerio(e).find('div.newnav > h3').text();
+ const novelCover = loadedCheerio(e).find('img').attr('data-src');
+
+ const novel = {
+ name: novelName,
+ path: novelUrl.replace(this.site, ''),
+ cover: novelCover,
+ };
+
+ novels.push(novel);
+ });
+
+ return novels;
+ }
+
+ fetchImage = fetchFile;
+
+ filters = {
+ class: {
+ label: '分类',
+ value: '0',
+ options: [
+ { label: '全部分类', value: '0' },
+ { label: '言情小说', value: '3' },
+ { label: '玄幻魔法', value: '1' },
+ { label: '修真武侠', value: '2' },
+ { label: '穿越时空', value: '11' },
+ { label: '都市小说', value: '9' },
+ { label: '历史军事', value: '4' },
+ { label: '游戏竞技', value: '5' },
+ { label: '科幻空间', value: '6' },
+ { label: '悬疑惊悚', value: '7' },
+ { label: '同人小说', value: '8' },
+ { label: '官场职场', value: '10' },
+ { label: '青春校园', value: '12' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ } satisfies Filters;
}
export default new XinShu69();
diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts
index ac38281..368d59c 100644
--- a/plugins/chinese/linovelib.ts
+++ b/plugins/chinese/linovelib.ts
@@ -1,561 +1,551 @@
-import { CheerioAPI, load as parseHTML } from "cheerio";
-import { fetchText, fetchFile } from "@libs/fetch";
-import { FilterTypes, Filters } from "@libs/filterInputs";
-import { Plugin } from "@typings/plugin";
-import { NovelStatus } from "@libs/novelStatus";
+import { CheerioAPI, load as parseHTML } from 'cheerio';
+import { fetchText, fetchFile } from '@libs/fetch';
+import { FilterTypes, Filters } from '@libs/filterInputs';
+import { Plugin } from '@typings/plugin';
+import { NovelStatus } from '@libs/novelStatus';
class Linovelib implements Plugin.PluginBase {
- id = "linovelib";
- name = "Linovelib";
- icon = "src/cn/linovelib/icon.png";
- site = "https://www.bilinovel.com";
- version = "1.1.1";
+ id = 'linovelib';
+ name = 'Linovelib';
+ icon = 'src/cn/linovelib/icon.png';
+ site = 'https://www.bilinovel.com';
+ version = '1.1.1';
- async popularNovels(
- pageNo: number,
- {
- showLatestNovels,
- filters,
- }: Plugin.PopularNovelsOptions
- ): Promise {
- const rank = showLatestNovels ? 'lastupdate' : filters.rank.value;
- const url = `${this.site}/top/${rank}/${pageNo}.html`;
+ async popularNovels(
+ pageNo: number,
+ {
+ showLatestNovels,
+ filters,
+ }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ const rank = showLatestNovels ? 'lastupdate' : filters.rank.value;
+ const url = `${this.site}/top/${rank}/${pageNo}.html`;
- const body = await fetchText(url);
- if (body === '') throw Error('无法获取小说列表,请检查网络');
+ const body = await fetchText(url);
+ if (body === '') throw Error('无法获取小说列表,请检查网络');
- const loadedCheerio = parseHTML(body);
+ const loadedCheerio = parseHTML(body);
- const novels: Plugin.NovelItem[] = [];
+ const novels: Plugin.NovelItem[] = [];
- loadedCheerio(".module-rank-booklist .book-layout").each((i, el) => {
- let url = loadedCheerio(el).attr("href");
+ loadedCheerio('.module-rank-booklist .book-layout').each((i, el) => {
+ let url = loadedCheerio(el).attr('href');
- const novelName = loadedCheerio(el).find(".book-title").text();
- const novelCover = loadedCheerio(el)
- .find("div.book-cover > img")
- .attr("data-src");
- if (!url) return;
+ const novelName = loadedCheerio(el).find('.book-title').text();
+ const novelCover = loadedCheerio(el)
+ .find('div.book-cover > img')
+ .attr('data-src');
+ if (!url) return;
- const novel = {
- name: novelName,
- cover: novelCover,
- path: url,
- };
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: url,
+ };
- novels.push(novel);
+ novels.push(novel);
+ });
+
+ return novels;
+ }
+
+ async parseNovel(novelPath: string): Promise {
+ const url = this.site + novelPath;
+
+ const body = await fetchText(url);
+ if (body === '') throw Error('无法获取小说内容,请检查网络');
+
+ let loadedCheerio = parseHTML(body);
+
+ const novel: Plugin.SourceNovel = {
+ path: novelPath,
+ chapters: [],
+ name: loadedCheerio('#bookDetailWrapper .book-title').text(),
+ };
+
+ novel.cover = loadedCheerio('#bookDetailWrapper img.book-cover').attr(
+ 'src',
+ );
+
+ novel.summary = loadedCheerio('#bookSummary content').text();
+
+ novel.author = loadedCheerio('#bookDetailWrapper .book-rand-a a').text();
+
+ const meta = loadedCheerio('#bookDetailWrapper .book-meta').text();
+ novel.status = meta.includes('完结')
+ ? NovelStatus.Completed
+ : NovelStatus.Ongoing;
+
+ novel.genres = loadedCheerio('.tag-small.red')
+ .children('a')
+ .map((i, el) => loadedCheerio(el).text())
+ .toArray()
+ .join(',');
+
+ // Table of Content is on a different page than the summary page
+ let chapter: Plugin.ChapterItem[] = [];
+
+ const idPattern = /\/(\d+)\.html/;
+ const novelId = url.match(idPattern)?.[1];
+
+ const chaptersUrl = this.site + loadedCheerio('#btnReadBook').attr('href');
+ const chaptersBody = await fetchText(chaptersUrl);
+
+ const chaptersLoadedCheerio = parseHTML(chaptersBody);
+
+ let volumeName: string, chapterId: number;
+
+ chaptersLoadedCheerio('#volumes .chapter-li:not(.volume-cover)').each(
+ (i, el) => {
+ if (chaptersLoadedCheerio(el).hasClass('chapter-bar')) {
+ volumeName = chaptersLoadedCheerio(el).text();
+ return;
+ } else {
+ const urlPart = chaptersLoadedCheerio(el)
+ .find('.chapter-li-a')
+ .attr('href');
+ const chapterIdMatch = urlPart?.match(idPattern);
+
+ // Sometimes the href attribute does not contain the url, but javascript:cid(0).
+ // Increment the previous chapter ID should result in the right URL
+ if (chapterIdMatch) {
+ chapterId = +chapterIdMatch[1];
+ } else {
+ chapterId++;
+ }
+ }
+
+ const chapterUrl = `/novel/${novelId}/${chapterId}.html`;
+ const chapterName =
+ volumeName +
+ ' — ' +
+ chaptersLoadedCheerio(el).find('.chapter-index').text().trim();
+ const releaseDate = null;
+
+ if (!chapterId) return;
+
+ chapter.push({
+ name: chapterName,
+ releaseTime: releaseDate,
+ path: chapterUrl,
+ });
+ },
+ );
+
+ novel.chapters = chapter;
+
+ return novel;
+ }
+
+ async parseChapter(chapterPath: string): Promise {
+ let chapterName,
+ chapterText = '',
+ hasNextPage,
+ pageHasNextPage,
+ pageText = '';
+ let pageNumber = 1;
+
+ /*
+ * TODO: Maybe there are other ways to get the translation table
+ * It is embed and encrypted inside readtool.js
+ * UPDATE: Decrypted, see skillgg
+ */
+ // const mapping_dict = {
+ // '“': '「',
+ // '’': '』',
+ // '': '是',
+ // '': '不',
+ // '': '好',
+ // '': '个',
+ // '': '开',
+ // '': '样',
+ // '': '想',
+ // '': '说',
+ // '': '年',
+ // '': '那',
+ // '': '她',
+ // '': '美',
+ // '': '自',
+ // '': '家',
+ // '': '而',
+ // '': '去',
+ // '': '都',
+ // '': '于',
+ // '': '舔',
+ // '': '他',
+ // '': '只',
+ // '': '看',
+ // '': '来',
+ // '': '用',
+ // '': '道',
+ // '': '得',
+ // '': '乳',
+ // '': '茎',
+ // '': '肉',
+ // '': '胸',
+ // '': '淫',
+ // '': '性',
+ // '': '骚',
+ // '”': '」',
+ // '': '的',
+ // '': '当',
+ // '': '人',
+ // '': '有',
+ // '': '上',
+ // '': '到',
+ // '': '地',
+ // '': '中',
+ // '': '生',
+ // '': '着',
+ // '': '和',
+ // '': '起',
+ // '': '交',
+ // '': '以',
+ // '': '可',
+ // '': '过',
+ // '': '能',
+ // '': '多',
+ // '': '心',
+ // '': '小',
+ // '': '成',
+ // '': '了',
+ // '': '把',
+ // '': '发',
+ // '': '第',
+ // '': '子',
+ // '': '事',
+ // '': '阴',
+ // '': '欲',
+ // '': '里',
+ // '': '私',
+ // '': '臀',
+ // '': '脱',
+ // '': '唇',
+ // '‘': '『',
+ // '': '一',
+ // '': '我',
+ // '': '在',
+ // '': '这',
+ // '': '们',
+ // '': '时',
+ // '': '为',
+ // '': '你',
+ // '': '国',
+ // '': '就',
+ // '': '要',
+ // '': '也',
+ // '': '后',
+ // '': '没',
+ // '': '下',
+ // '': '天',
+ // '': '对',
+ // '': '然',
+ // '': '学',
+ // '': '之',
+ // '': '出',
+ // '': '没',
+ // '': '如',
+ // '': '还',
+ // '': '大',
+ // '': '作',
+ // '': '种',
+ // '': '液',
+ // '': '呻',
+ // '': '射',
+ // '': '穴',
+ // '': '么',
+ // '': '裸',
+ // };
+ const skillgg: Record = {
+ '\u201c': '\u300c',
+ '\u201d': '\u300d',
+ '\u2018': '\u300e',
+ '\u2019': '\u300f',
+ '\ue82c': '\u7684',
+ '\ue852': '\u4e00',
+ '\ue82d': '\u662f',
+ '\ue819': '\u4e86',
+ '\ue856': '\u6211',
+ '\ue857': '\u4e0d',
+ '\ue816': '\u4eba',
+ '\ue83c': '\u5728',
+ '\ue830': '\u4ed6',
+ '\ue82e': '\u6709',
+ '\ue836': '\u8fd9',
+ '\ue859': '\u4e2a',
+ '\ue80a': '\u4e0a',
+ '\ue855': '\u4eec',
+ '\ue842': '\u6765',
+ '\ue858': '\u5230',
+ '\ue80b': '\u65f6',
+ '\ue81f': '\u5927',
+ '\ue84a': '\u5730',
+ '\ue853': '\u4e3a',
+ '\ue81e': '\u5b50',
+ '\ue822': '\u4e2d',
+ '\ue813': '\u4f60',
+ '\ue85b': '\u8bf4',
+ '\ue807': '\u751f',
+ '\ue818': '\u56fd',
+ '\ue810': '\u5e74',
+ '\ue812': '\u7740',
+ '\ue851': '\u5c31',
+ '\ue801': '\u90a3',
+ '\ue80c': '\u548c',
+ '\ue815': '\u8981',
+ '\ue84c': '\u5979',
+ '\ue840': '\u51fa',
+ '\ue848': '\u4e5f',
+ '\ue835': '\u5f97',
+ '\ue800': '\u91cc',
+ '\ue826': '\u540e',
+ '\ue863': '\u81ea',
+ '\ue861': '\u4ee5',
+ '\ue854': '\u4f1a',
+ '\ue827': '\u5bb6',
+ '\ue83b': '\u53ef',
+ '\ue85d': '\u4e0b',
+ '\ue84d': '\u800c',
+ '\ue862': '\u8fc7',
+ '\ue81c': '\u5929',
+ '\ue81d': '\u53bb',
+ '\ue860': '\u80fd',
+ '\ue843': '\u5bf9',
+ '\ue82f': '\u5c0f',
+ '\ue802': '\u591a',
+ '\ue831': '\u7136',
+ '\ue84b': '\u4e8e',
+ '\ue837': '\u5fc3',
+ '\ue829': '\u5b66',
+ '\ue85e': '\u4e48',
+ '\ue83a': '\u4e4b',
+ '\ue832': '\u90fd',
+ '\ue808': '\u597d',
+ '\ue841': '\u770b',
+ '\ue821': '\u8d77',
+ '\ue845': '\u53d1',
+ '\ue803': '\u5f53',
+ '\ue828': '\u6ca1',
+ '\ue81b': '\u6210',
+ '\ue83e': '\u53ea',
+ '\ue820': '\u5982',
+ '\ue84e': '\u4e8b',
+ '\ue85a': '\u628a',
+ '\ue806': '\u8fd8',
+ '\ue83f': '\u7528',
+ '\ue833': '\u7b2c',
+ '\ue811': '\u6837',
+ '\ue804': '\u9053',
+ '\ue814': '\u60f3',
+ '\ue80f': '\u4f5c',
+ '\ue84f': '\u79cd',
+ '\ue80e': '\u5f00',
+ '\ue823': '\u7f8e',
+ '\ue849': '\u4e73',
+ '\ue805': '\u9634',
+ '\ue809': '\u6db2',
+ '\ue81a': '\u830e',
+ '\ue844': '\u6b32',
+ '\ue847': '\u547b',
+ '\ue850': '\u8089',
+ '\ue824': '\u4ea4',
+ '\ue85f': '\u6027',
+ '\ue817': '\u80f8',
+ '\ue85c': '\u79c1',
+ '\ue838': '\u7a74',
+ '\ue82a': '\u6deb',
+ '\ue83d': '\u81c0',
+ '\ue82b': '\u8214',
+ '\ue80d': '\u5c04',
+ '\ue839': '\u8131',
+ '\ue834': '\u88f8',
+ '\ue846': '\u9a9a',
+ '\ue825': '\u5507',
+ };
+ const addPage = async (pageCheerio: CheerioAPI) => {
+ const formatPage = async () => {
+ // Remove JS
+ pageCheerio('#acontentz .adsbygoogle').remove();
+ pageCheerio('#acontentz script').remove();
+
+ // Load lazyloaded images
+ pageCheerio('#acontentz img.imagecontent').each((i, el) => {
+ // Sometimes images are either in data-src or src
+ const imgSrc =
+ pageCheerio(el).attr('data-src') || pageCheerio(el).attr('src');
+ if (imgSrc) {
+ // Clean up img element
+ pageCheerio(el)
+ .attr('src', imgSrc)
+ .removeAttr('data-src')
+ .removeClass('lazyload');
+ }
});
- return novels;
- }
+ // Recover the original character
+ pageText = pageCheerio('#acontentz').html() || '';
+ pageText = pageText.replace(/./g, char => skillgg[char] || char);
- async parseNovel(novelPath: string): Promise {
- const url = this.site + novelPath;
+ return Promise.resolve();
+ };
- const body = await fetchText(url);
- if (body === '') throw Error('无法获取小说内容,请检查网络');
+ await formatPage();
+ chapterName =
+ pageCheerio('#atitle + h3').text() +
+ ' — ' +
+ pageCheerio('#atitle').text();
+ if (chapterText === '') {
+ chapterText = '' + chapterName + '
';
+ }
+ chapterText += pageText;
+ };
- let loadedCheerio = parseHTML(body);
+ const loadPage = async (url: string) => {
+ const headers = {
+ 'Accept':
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
+ 'Accept-Language':
+ 'zh-CN,zh;q=0.9,zh-TW;q=0.8,zh-HK;q=0.7,en;q=0.6,en-GB;q=0.5,en-US;q=0.4',
+ 'Cache-Control': 'no-cache',
+ };
- const novel: Plugin.SourceNovel = {
- path: novelPath,
- chapters: [],
- name: loadedCheerio("#bookDetailWrapper .book-title").text()
- };
+ const body = await fetchText(url, { headers });
+ const pageCheerio = parseHTML(body);
+ await addPage(pageCheerio);
+ pageHasNextPage =
+ pageCheerio('#footlink a:last').text() === '下一页' ? true : false;
+ return { pageCheerio, pageHasNextPage };
+ };
- novel.cover = loadedCheerio("#bookDetailWrapper img.book-cover").attr(
- "src"
- );
+ let url = this.site + chapterPath;
+ do {
+ const page = await loadPage(url);
+ hasNextPage = page.pageHasNextPage;
+ if (hasNextPage === true) {
+ pageNumber++;
+ url = url.replace(/\.html/gi, `_${pageNumber}` + '.html');
+ }
+ } while (hasNextPage === true);
- novel.summary = loadedCheerio("#bookSummary content").text();
+ return chapterText;
+ }
- novel.author = loadedCheerio(
- "#bookDetailWrapper .book-rand-a a"
- ).text();
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ const url = `${this.site}/search/${encodeURI(searchTerm)}_${pageNo}.html`;
- const meta = loadedCheerio("#bookDetailWrapper .book-meta").text()
- novel.status = meta.includes("完结") ? NovelStatus.Completed : NovelStatus.Ongoing;
+ const body = await fetchText(url);
+ if (body === '') throw Error('无法获取搜索结果,请检查网络');
- novel.genres = loadedCheerio(".tag-small.red")
- .children("a")
- .map((i, el) => loadedCheerio(el).text())
- .toArray()
- .join(",");
+ const pageCheerio = parseHTML(body);
- // Table of Content is on a different page than the summary page
- let chapter: Plugin.ChapterItem[] = [];
+ const novels: Plugin.NovelItem[] = [];
- const idPattern = /\/(\d+)\.html/;
- const novelId = url.match(idPattern)?.[1];
+ // const addPage = async (pageCheerio: CheerioAPI, redirect: string) => {
+ // const loadSearchResults = () => {
+ // pageCheerio(".book-ol .book-layout").each((i, el) => {
+ // let nUrl = pageCheerio(el).attr("href");
- const chaptersUrl =
- this.site + loadedCheerio("#btnReadBook").attr("href");
- const chaptersBody = await fetchText(chaptersUrl);
+ // const novelName = pageCheerio(el)
+ // .find(".book-title")
+ // .text();
+ // const novelCover = pageCheerio(el)
+ // .find("div.book-cover > img")
+ // .attr("data-src");
+ // const novelUrl = this.site + nUrl;
- const chaptersLoadedCheerio = parseHTML(chaptersBody);
+ // if (!nUrl) return;
- let volumeName: string, chapterId: number;
+ // novels.push({
+ // name: novelName,
+ // url: novelUrl,
+ // cover: novelCover,
+ // });
+ // });
+ // };
- chaptersLoadedCheerio("#volumes .chapter-li:not(.volume-cover)").each((i, el) => {
- if (chaptersLoadedCheerio(el).hasClass("chapter-bar")) {
- volumeName = chaptersLoadedCheerio(el).text();
- return;
- } else {
- const urlPart = chaptersLoadedCheerio(el)
- .find(".chapter-li-a")
- .attr("href");
- const chapterIdMatch = urlPart?.match(idPattern);
+ // const novelResults = pageCheerio(".book-ol a.book-layout");
+ // if (novelResults.length === 0) {
+ // } else {
+ // loadSearchResults();
+ // }
- // Sometimes the href attribute does not contain the url, but javascript:cid(0).
- // Increment the previous chapter ID should result in the right URL
- if (chapterIdMatch) {
- chapterId = +chapterIdMatch[1];
- } else {
- chapterId++;
- }
- }
+ // if (redirect.length) {
+ // novels.length = 0;
+ // const novelName = pageCheerio(
+ // "#bookDetailWrapper .book-title"
+ // ).text();
- const chapterUrl = `/novel/${novelId}/${chapterId}.html`;
- const chapterName =
- volumeName +
- " — " +
- chaptersLoadedCheerio(el).find(".chapter-index").text().trim();
- const releaseDate = null;
+ // const novelCover = pageCheerio(
+ // "#bookDetailWrapper div.book-cover > img"
+ // ).attr("src");
+ // const novelUrl =
+ // this.site +
+ // pageCheerio("#btnReadBook").attr("href")?.slice(0, -8) +
+ // ".html";
+ // novels.push({
+ // name: novelName,
+ // url: novelUrl,
+ // cover: novelCover,
+ // });
+ // }
+ // };
- if (!chapterId) return;
+ // NOTE: don't know redirect is for what, comment out for now
- chapter.push({
- name: chapterName,
- releaseTime: releaseDate,
- path: chapterUrl,
- });
- });
+ // const redirect = pageCheerio("div.book-layout").text();
+ // await addPage(pageCheerio, redirect);
- novel.chapters = chapter;
+ pageCheerio('.book-ol .book-layout').each((i, el) => {
+ let nUrl = pageCheerio(el).attr('href');
- return novel;
- }
+ const novelName = pageCheerio(el).find('.book-title').text();
+ const novelCover = pageCheerio(el)
+ .find('div.book-cover > img')
+ .attr('data-src');
- async parseChapter(chapterPath: string): Promise {
- let chapterName,
- chapterText = "",
- hasNextPage,
- pageHasNextPage,
- pageText = "";
- let pageNumber = 1;
+ if (!nUrl) return;
- /*
- * TODO: Maybe there are other ways to get the translation table
- * It is embed and encrypted inside readtool.js
- * UPDATE: Decrypted, see skillgg
- */
- // const mapping_dict = {
- // '“': '「',
- // '’': '』',
- // '': '是',
- // '': '不',
- // '': '好',
- // '': '个',
- // '': '开',
- // '': '样',
- // '': '想',
- // '': '说',
- // '': '年',
- // '': '那',
- // '': '她',
- // '': '美',
- // '': '自',
- // '': '家',
- // '': '而',
- // '': '去',
- // '': '都',
- // '': '于',
- // '': '舔',
- // '': '他',
- // '': '只',
- // '': '看',
- // '': '来',
- // '': '用',
- // '': '道',
- // '': '得',
- // '': '乳',
- // '': '茎',
- // '': '肉',
- // '': '胸',
- // '': '淫',
- // '': '性',
- // '': '骚',
- // '”': '」',
- // '': '的',
- // '': '当',
- // '': '人',
- // '': '有',
- // '': '上',
- // '': '到',
- // '': '地',
- // '': '中',
- // '': '生',
- // '': '着',
- // '': '和',
- // '': '起',
- // '': '交',
- // '': '以',
- // '': '可',
- // '': '过',
- // '': '能',
- // '': '多',
- // '': '心',
- // '': '小',
- // '': '成',
- // '': '了',
- // '': '把',
- // '': '发',
- // '': '第',
- // '': '子',
- // '': '事',
- // '': '阴',
- // '': '欲',
- // '': '里',
- // '': '私',
- // '': '臀',
- // '': '脱',
- // '': '唇',
- // '‘': '『',
- // '': '一',
- // '': '我',
- // '': '在',
- // '': '这',
- // '': '们',
- // '': '时',
- // '': '为',
- // '': '你',
- // '': '国',
- // '': '就',
- // '': '要',
- // '': '也',
- // '': '后',
- // '': '没',
- // '': '下',
- // '': '天',
- // '': '对',
- // '': '然',
- // '': '学',
- // '': '之',
- // '': '出',
- // '': '没',
- // '': '如',
- // '': '还',
- // '': '大',
- // '': '作',
- // '': '种',
- // '': '液',
- // '': '呻',
- // '': '射',
- // '': '穴',
- // '': '么',
- // '': '裸',
- // };
- const skillgg: Record = {
- "\u201c": "\u300c",
- "\u201d": "\u300d",
- "\u2018": "\u300e",
- "\u2019": "\u300f",
- "\ue82c": "\u7684",
- "\ue852": "\u4e00",
- "\ue82d": "\u662f",
- "\ue819": "\u4e86",
- "\ue856": "\u6211",
- "\ue857": "\u4e0d",
- "\ue816": "\u4eba",
- "\ue83c": "\u5728",
- "\ue830": "\u4ed6",
- "\ue82e": "\u6709",
- "\ue836": "\u8fd9",
- "\ue859": "\u4e2a",
- "\ue80a": "\u4e0a",
- "\ue855": "\u4eec",
- "\ue842": "\u6765",
- "\ue858": "\u5230",
- "\ue80b": "\u65f6",
- "\ue81f": "\u5927",
- "\ue84a": "\u5730",
- "\ue853": "\u4e3a",
- "\ue81e": "\u5b50",
- "\ue822": "\u4e2d",
- "\ue813": "\u4f60",
- "\ue85b": "\u8bf4",
- "\ue807": "\u751f",
- "\ue818": "\u56fd",
- "\ue810": "\u5e74",
- "\ue812": "\u7740",
- "\ue851": "\u5c31",
- "\ue801": "\u90a3",
- "\ue80c": "\u548c",
- "\ue815": "\u8981",
- "\ue84c": "\u5979",
- "\ue840": "\u51fa",
- "\ue848": "\u4e5f",
- "\ue835": "\u5f97",
- "\ue800": "\u91cc",
- "\ue826": "\u540e",
- "\ue863": "\u81ea",
- "\ue861": "\u4ee5",
- "\ue854": "\u4f1a",
- "\ue827": "\u5bb6",
- "\ue83b": "\u53ef",
- "\ue85d": "\u4e0b",
- "\ue84d": "\u800c",
- "\ue862": "\u8fc7",
- "\ue81c": "\u5929",
- "\ue81d": "\u53bb",
- "\ue860": "\u80fd",
- "\ue843": "\u5bf9",
- "\ue82f": "\u5c0f",
- "\ue802": "\u591a",
- "\ue831": "\u7136",
- "\ue84b": "\u4e8e",
- "\ue837": "\u5fc3",
- "\ue829": "\u5b66",
- "\ue85e": "\u4e48",
- "\ue83a": "\u4e4b",
- "\ue832": "\u90fd",
- "\ue808": "\u597d",
- "\ue841": "\u770b",
- "\ue821": "\u8d77",
- "\ue845": "\u53d1",
- "\ue803": "\u5f53",
- "\ue828": "\u6ca1",
- "\ue81b": "\u6210",
- "\ue83e": "\u53ea",
- "\ue820": "\u5982",
- "\ue84e": "\u4e8b",
- "\ue85a": "\u628a",
- "\ue806": "\u8fd8",
- "\ue83f": "\u7528",
- "\ue833": "\u7b2c",
- "\ue811": "\u6837",
- "\ue804": "\u9053",
- "\ue814": "\u60f3",
- "\ue80f": "\u4f5c",
- "\ue84f": "\u79cd",
- "\ue80e": "\u5f00",
- "\ue823": "\u7f8e",
- "\ue849": "\u4e73",
- "\ue805": "\u9634",
- "\ue809": "\u6db2",
- "\ue81a": "\u830e",
- "\ue844": "\u6b32",
- "\ue847": "\u547b",
- "\ue850": "\u8089",
- "\ue824": "\u4ea4",
- "\ue85f": "\u6027",
- "\ue817": "\u80f8",
- "\ue85c": "\u79c1",
- "\ue838": "\u7a74",
- "\ue82a": "\u6deb",
- "\ue83d": "\u81c0",
- "\ue82b": "\u8214",
- "\ue80d": "\u5c04",
- "\ue839": "\u8131",
- "\ue834": "\u88f8",
- "\ue846": "\u9a9a",
- "\ue825": "\u5507",
- };
- const addPage = async (pageCheerio: CheerioAPI) => {
- const formatPage = async () => {
- // Remove JS
- pageCheerio("#acontentz .adsbygoogle").remove();
- pageCheerio("#acontentz script").remove();
+ novels.push({
+ name: novelName,
+ path: nUrl,
+ cover: novelCover,
+ });
+ });
- // Load lazyloaded images
- pageCheerio("#acontentz img.imagecontent").each((i, el) => {
- // Sometimes images are either in data-src or src
- const imgSrc =
- pageCheerio(el).attr("data-src") ||
- pageCheerio(el).attr("src");
- if (imgSrc) {
- // Clean up img element
- pageCheerio(el)
- .attr("src", imgSrc)
- .removeAttr("data-src")
- .removeClass("lazyload");
- }
- });
+ return novels;
+ }
- // Recover the original character
- pageText = pageCheerio("#acontentz").html() || "";
- pageText = pageText.replace(
- /./g,
- (char) => skillgg[char] || char
- );
+ async fetchImage(url: string): Promise {
+ const headers = new Headers({
+ 'Referer': `${this.site}/`,
+ });
+ return await fetchFile(url, { headers });
+ }
- return Promise.resolve();
- };
-
- await formatPage();
- chapterName =
- pageCheerio("#atitle + h3").text() +
- " — " +
- pageCheerio("#atitle").text();
- if (chapterText === "") {
- chapterText = "" + chapterName + "
";
- }
- chapterText += pageText;
- };
-
- const loadPage = async (url: string) => {
- const headers = {
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
- "Accept-Language": "zh-CN,zh;q=0.9,zh-TW;q=0.8,zh-HK;q=0.7,en;q=0.6,en-GB;q=0.5,en-US;q=0.4",
- "Cache-Control": "no-cache",
- }
-
- const body = await fetchText(url, { headers });
- const pageCheerio = parseHTML(body);
- await addPage(pageCheerio);
- pageHasNextPage =
- pageCheerio("#footlink a:last").text() === "下一页"
- ? true
- : false;
- return { pageCheerio, pageHasNextPage };
- };
-
- let url = this.site + chapterPath;
- do {
- const page = await loadPage(url);
- hasNextPage = page.pageHasNextPage;
- if (hasNextPage === true) {
- pageNumber++;
- url = url.replace(
- /\.html/gi,
- `_${pageNumber}` + ".html"
- );
- }
- } while (hasNextPage === true);
-
- return chapterText;
- }
-
- async searchNovels(
- searchTerm: string,
- pageNo: number
- ): Promise {
- const url = `${this.site}/search/${encodeURI(searchTerm)}_${pageNo}.html`;
-
- const body = await fetchText(url);
- if (body === '') throw Error('无法获取搜索结果,请检查网络');
-
- const pageCheerio = parseHTML(body);
-
- const novels: Plugin.NovelItem[] = [];
-
- // const addPage = async (pageCheerio: CheerioAPI, redirect: string) => {
- // const loadSearchResults = () => {
- // pageCheerio(".book-ol .book-layout").each((i, el) => {
- // let nUrl = pageCheerio(el).attr("href");
-
- // const novelName = pageCheerio(el)
- // .find(".book-title")
- // .text();
- // const novelCover = pageCheerio(el)
- // .find("div.book-cover > img")
- // .attr("data-src");
- // const novelUrl = this.site + nUrl;
-
- // if (!nUrl) return;
-
- // novels.push({
- // name: novelName,
- // url: novelUrl,
- // cover: novelCover,
- // });
- // });
- // };
-
- // const novelResults = pageCheerio(".book-ol a.book-layout");
- // if (novelResults.length === 0) {
- // } else {
- // loadSearchResults();
- // }
-
- // if (redirect.length) {
- // novels.length = 0;
- // const novelName = pageCheerio(
- // "#bookDetailWrapper .book-title"
- // ).text();
-
- // const novelCover = pageCheerio(
- // "#bookDetailWrapper div.book-cover > img"
- // ).attr("src");
- // const novelUrl =
- // this.site +
- // pageCheerio("#btnReadBook").attr("href")?.slice(0, -8) +
- // ".html";
- // novels.push({
- // name: novelName,
- // url: novelUrl,
- // cover: novelCover,
- // });
- // }
- // };
-
- // NOTE: don't know redirect is for what, comment out for now
-
- // const redirect = pageCheerio("div.book-layout").text();
- // await addPage(pageCheerio, redirect);
-
- pageCheerio(".book-ol .book-layout").each((i, el) => {
- let nUrl = pageCheerio(el).attr("href");
-
- const novelName = pageCheerio(el)
- .find(".book-title")
- .text();
- const novelCover = pageCheerio(el)
- .find("div.book-cover > img")
- .attr("data-src");
-
- if (!nUrl) return;
-
- novels.push({
- name: novelName,
- path: nUrl,
- cover: novelCover,
- });
- });
-
- return novels;
- }
-
- async fetchImage(url: string): Promise {
- const headers = new Headers(
- {
- "Referer": `${this.site}/`,
- }
- )
- return await fetchFile(url, { headers });
- }
-
- filters = {
- rank: {
- label: "排行榜",
- value: "monthvisit",
- options: [
- { label: "月点击榜", value: "monthvisit" },
- { label: "周点击榜", value: "weekvisit" },
- { label: "月推荐榜", value: "monthvote" },
- { label: "周推荐榜", value: "weekvote" },
- { label: "月鲜花榜", value: "monthflower" },
- { label: "周鲜花榜", value: "weekflower" },
- { label: "月鸡蛋榜", value: "monthegg" },
- { label: "周鸡蛋榜", value: "weekegg" },
- { label: "最近更新", value: "lastupdate" },
- { label: "最新入库", value: "postdate" },
- { label: "收藏榜", value: "goodnum" },
- { label: "新书榜", value: "newhot" },
- ],
- type: FilterTypes.Picker,
- },
- } satisfies Filters;
+ filters = {
+ rank: {
+ label: '排行榜',
+ value: 'monthvisit',
+ options: [
+ { label: '月点击榜', value: 'monthvisit' },
+ { label: '周点击榜', value: 'weekvisit' },
+ { label: '月推荐榜', value: 'monthvote' },
+ { label: '周推荐榜', value: 'weekvote' },
+ { label: '月鲜花榜', value: 'monthflower' },
+ { label: '周鲜花榜', value: 'weekflower' },
+ { label: '月鸡蛋榜', value: 'monthegg' },
+ { label: '周鸡蛋榜', value: 'weekegg' },
+ { label: '最近更新', value: 'lastupdate' },
+ { label: '最新入库', value: 'postdate' },
+ { label: '收藏榜', value: 'goodnum' },
+ { label: '新书榜', value: 'newhot' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ } satisfies Filters;
}
export default new Linovelib();
diff --git a/plugins/english/NovelOnline.ts b/plugins/english/NovelOnline.ts
index fbdf410..fe48e5d 100644
--- a/plugins/english/NovelOnline.ts
+++ b/plugins/english/NovelOnline.ts
@@ -1,204 +1,209 @@
-import { load as parseHTML } from "cheerio";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { Plugin } from "@typings/plugin";
-import { Filters, FilterTypes } from "@libs/filterInputs";
-import qs from "qs";
+import { load as parseHTML } from 'cheerio';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { Plugin } from '@typings/plugin';
+import { Filters, FilterTypes } from '@libs/filterInputs';
+import qs from 'qs';
class NovelsOnline implements Plugin.PluginBase {
- id = "NO.net";
- name = "novelsOnline";
- site = "https://novelsonline.net";
- icon = "src/en/novelsonline/icon.png";
- version = "1.0.0";
+ id = 'NO.net';
+ name = 'novelsOnline';
+ site = 'https://novelsonline.net';
+ icon = 'src/en/novelsonline/icon.png';
+ version = '1.0.0';
- async popularNovels(
- page: number,
- { filters }: Plugin.PopularNovelsOptions
- ): Promise {
- 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}`
+ async popularNovels(
+ page: number,
+ { filters }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ let link = this.site;
+ if (filters.genre.value !== '') link += `/category/${filters.genre.value}/`;
+ else link += `/top-novel/`;
- const body = await fetchApi(link).then((r) => r.text());
+ link += page;
+ link += `?change_type=${filters.sort.value}`;
- 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;
+ const body = await fetchApi(link).then(r => r.text());
- novel.push({
- name: novelName,
- cover: novelCover,
- path: novelUrl.replace(this.site, ''),
- })
- })
- return novel;
- }
+ const loadedCheerio = parseHTML(body);
+ const novel: Plugin.NovelItem[] = [];
- async parseNovel(novelPath: string): Promise {
- const result = await fetchApi(this.site + novelPath).then((res) => res.text());
- let $ = parseHTML(result);
+ 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;
- const novel: Plugin.SourceNovel = {
- path: novelPath,
- name: $("h1").text() || 'Untitled',
- cover: $(".novel-cover").find("a > img").attr("src"),
- chapters: [],
- };
+ novel.push({
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl.replace(this.site, ''),
+ });
+ });
+ return novel;
+ }
- $('.novel-detail-item').each((i,el) => {
- let detailName = $(el).find('h6').text();
- let detail = $(el).find('.novel-detail-body');
+ async parseNovel(novelPath: string): Promise {
+ const result = await fetchApi(this.site + novelPath).then(res =>
+ res.text(),
+ );
+ let $ = parseHTML(result);
- 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;
- }
- })
+ const novel: Plugin.SourceNovel = {
+ path: novelPath,
+ name: $('h1').text() || 'Untitled',
+ cover: $('.novel-cover').find('a > img').attr('src'),
+ chapters: [],
+ };
- novel.chapters = $("ul.chapter-chs > li > a")
- .map((_, el) => {
- const chapterUrl = $(el).attr("href");
- const chapterName = $(el).text();
+ $('.novel-detail-item').each((i, el) => {
+ let detailName = $(el).find('h6').text();
+ let detail = $(el).find('.novel-detail-body');
- return {
- name: chapterName,
- path: chapterUrl?.replace(this.site, ''),
- } as Plugin.ChapterItem;
- })
- .get();
-
- return novel;
- }
-
- async parseChapter(chapterPath: string): Promise {
- 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 {
- const result = await fetchApi(
- "https://novelsonline.net/sResults.php",
- {
- headers: {
- Accept: "*/*",
- "Accept-Language": "pl,en-US;q=0.7,en;q=0.3",
- "Content-Type":
- "application/x-www-form-urlencoded; charset=UTF-8",
- },
- method: "POST",
- body: qs.stringify({ q: searchTerm }),
- },
- ).then((res) => res.text());
-
- const $ = parseHTML(result);
-
- const headers = $("li");
- return headers
- .map((i, h) => {
- const novelName = $(h).text();
- const novelUrl = $(h).find("a").attr("href");
- const novelCover = $(h).find("img").attr("src");
-
- if (!novelUrl) return
-
- return {
- name: novelName,
- cover: novelCover,
- path: novelUrl.replace(this.site, ''),
- };
- })
+ switch (detailName) {
+ case 'Description':
+ novel.summary = detail.text();
+ break;
+ case 'Genre':
+ novel.genres = detail
+ .find('li')
+ .map((_, el) => $(el).text())
.get()
- .filter((sr) => sr !== null)
- }
+ .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;
+ }
+ });
- async fetchImage(url: string): Promise {
- return fetchFile(url);
- }
+ novel.chapters = $('ul.chapter-chs > li > a')
+ .map((_, el) => {
+ const chapterUrl = $(el).attr('href');
+ const chapterName = $(el).text();
- 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;
+ return {
+ name: chapterName,
+ path: chapterUrl?.replace(this.site, ''),
+ } as Plugin.ChapterItem;
+ })
+ .get();
+
+ return novel;
+ }
+
+ async parseChapter(chapterPath: string): Promise {
+ 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 {
+ const result = await fetchApi('https://novelsonline.net/sResults.php', {
+ headers: {
+ Accept: '*/*',
+ 'Accept-Language': 'pl,en-US;q=0.7,en;q=0.3',
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
+ },
+ method: 'POST',
+ body: qs.stringify({ q: searchTerm }),
+ }).then(res => res.text());
+
+ const $ = parseHTML(result);
+
+ const headers = $('li');
+ return headers
+ .map((i, h) => {
+ const novelName = $(h).text();
+ const novelUrl = $(h).find('a').attr('href');
+ const novelCover = $(h).find('img').attr('src');
+
+ if (!novelUrl) return;
+
+ return {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl.replace(this.site, ''),
+ };
+ })
+ .get()
+ .filter(sr => sr !== null);
+ }
+
+ async fetchImage(url: string): Promise {
+ 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();
diff --git a/plugins/english/StorySeedling.ts b/plugins/english/StorySeedling.ts
index 60ae400..9dd258f 100644
--- a/plugins/english/StorySeedling.ts
+++ b/plugins/english/StorySeedling.ts
@@ -1,140 +1,164 @@
-import { Plugin } from "@typings/plugin";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { load as parseHTML } from "cheerio";
-import { defaultCover } from "@libs/defaultCover";
+import { Plugin } from '@typings/plugin';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { load as parseHTML } from 'cheerio';
+import { defaultCover } from '@libs/defaultCover';
class StorySeedlingPlugin implements Plugin.PluginBase {
- id = "storyseedling";
- name = "StorySeedling";
- icon = "src/en/storyseedling/icon.png";
- site = "https://storyseedling.com/";
- version = "1.0.0";
+ id = 'storyseedling';
+ name = 'StorySeedling';
+ icon = 'src/en/storyseedling/icon.png';
+ site = 'https://storyseedling.com/';
+ version = '1.0.0';
- async popularNovels(pageNo: number): Promise {
- let novels: Plugin.NovelItem[] = [];
- let url = "https://storyseedling.com/ajax";
+ async popularNovels(pageNo: number): Promise {
+ let novels: Plugin.NovelItem[] = [];
+ let url = 'https://storyseedling.com/ajax';
- const postUrl = `https://storyseedling.com/browse/`;
- const body = await fetchApi(postUrl).then((r) => r.text());
+ const postUrl = `https://storyseedling.com/browse/`;
+ const body = await fetchApi(postUrl).then(r => r.text());
- const loadedCheerio = parseHTML(body);
+ const loadedCheerio = parseHTML(body);
- var postValue = loadedCheerio("div[ax-load][x-data]").attr("x-data")?.replace("browse('","").replace("')", "") as string;
+ var postValue = loadedCheerio('div[ax-load][x-data]')
+ .attr('x-data')
+ ?.replace("browse('", '')
+ .replace("')", '') as string;
- var data = new FormData();
- data.set("search", "");
- data.set("orderBy", "recent");
- data.set("curpage", pageNo.toString());
- data.set("post", postValue);
- data.set("action", "fetch_browse");
+ var data = new FormData();
+ data.set('search', '');
+ data.set('orderBy', 'recent');
+ data.set('curpage', pageNo.toString());
+ data.set('post', postValue);
+ data.set('action', 'fetch_browse');
- var response = await(await fetchApi(url, {body: data, method: "POST"})).json();
+ var response = await (
+ await fetchApi(url, { body: data, method: 'POST' })
+ ).json();
- response.data.posts.forEach((element: any) => {
- const novel = {
- name: element.title,
- cover: element.thumbnail,
- path: element.permalink.replace(this.site, ""),
- };
- novels.push(novel);
- });
+ response.data.posts.forEach((element: any) => {
+ const novel = {
+ name: element.title,
+ cover: element.thumbnail,
+ path: element.permalink.replace(this.site, ''),
+ };
+ novels.push(novel);
+ });
- return novels;
+ return novels;
+ }
+
+ async parseNovel(novelPath: string): Promise {
+ var site = this.site;
+ const novel: Plugin.SourceNovel = {
+ path: novelPath,
+ name: '',
+ };
+
+ const body = await fetchApi(this.site + novelPath).then(r => r.text());
+
+ const loadedCheerio = parseHTML(body);
+
+ novel.name = loadedCheerio('h1').text().trim();
+
+ //novel.artist = "";
+ //novel.author = "";
+ novel.cover = loadedCheerio(
+ 'img[x-ref="art"].w-full.rounded.shadow-md',
+ ).attr('src');
+ if (!novel.cover) {
+ novel.cover = defaultCover;
}
+ let genres: string[] = [];
+ loadedCheerio(
+ 'section[x-data="{ tab: location.hash.substr(1) || \'chapters\' }"].relative > div > div > div.flex.flex-wrap > a',
+ ).each(function () {
+ genres.push(loadedCheerio(this).text().trim());
+ });
+ novel.genres = genres.join(', ');
+ // novel.status = NovelStatus.Completed;
+ novel.summary = loadedCheerio('div.mb-4.order-2:not(.lg\\:grid-in-buttons)')
+ .text()
+ .trim()
+ .replace(/(\r\n|\n|\r)/gm, '');
- async parseNovel(novelPath: string): Promise {
- var site = this.site;
- const novel: Plugin.SourceNovel = {
- path: novelPath,
- name: ""
- };
+ let chapters: Plugin.ChapterItem[] = [];
- const body = await fetchApi(this.site+novelPath).then((r) => r.text());
+ loadedCheerio(
+ 'div.grid.w-full.grid-cols-1.gap-4.md\\:grid-cols-2 > a',
+ ).each(function () {
+ if (loadedCheerio(this).find('> div').length == 2) {
+ return;
+ }
+ let name = loadedCheerio(this).find('.truncate').text().trim();
+ let url = loadedCheerio(this).attr('href') as string;
+ let releaseTime = loadedCheerio(this)
+ .find('div > div > small')
+ .text()
+ .trim();
+ let chapterNumber = name.split('-')[0].trim().split(' ')[1];
+ const chapter: Plugin.ChapterItem = {
+ name: name,
+ path: url.replace(site, ''),
+ releaseTime: releaseTime,
+ chapterNumber: parseInt(chapterNumber),
+ };
+ chapters.push(chapter);
+ });
+ novel.chapters = chapters;
+ return novel;
+ }
+ async parseChapter(chapterPath: string): Promise {
+ const body = await fetchApi(this.site + chapterPath).then(r => r.text());
+ const loadedCheerio = parseHTML(body);
+ let t = loadedCheerio('div.justify-center > div.mb-4');
+ let chapterText = t.html() || '';
- const loadedCheerio = parseHTML(body);
-
- novel.name = loadedCheerio("h1").text().trim();
+ return chapterText;
+ }
- //novel.artist = "";
- //novel.author = "";
- novel.cover = loadedCheerio('img[x-ref="art"].w-full.rounded.shadow-md').attr("src");
- if (!novel.cover) {
- novel.cover = defaultCover;
- }
- let genres:string[] = [];
- loadedCheerio('section[x-data="{ tab: location.hash.substr(1) || \'chapters\' }"].relative > div > div > div.flex.flex-wrap > a').each(function(){
- genres.push(loadedCheerio(this).text().trim());
- });
- novel.genres = genres.join(", ");
- // novel.status = NovelStatus.Completed;
- novel.summary = loadedCheerio("div.mb-4.order-2:not(.lg\\:grid-in-buttons)").text().trim().replace(/(\r\n|\n|\r)/gm, "");
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ let novels: Plugin.NovelItem[] = [];
+ let url = 'https://storyseedling.com/ajax';
- let chapters: Plugin.ChapterItem[] = [];
+ const postUrl = `https://storyseedling.com/browse/`;
+ const body = await fetchApi(postUrl).then(r => r.text());
- loadedCheerio("div.grid.w-full.grid-cols-1.gap-4.md\\:grid-cols-2 > a").each(function() {
- if(loadedCheerio(this).find("> div").length == 2){
- return;
- }
- let name = loadedCheerio(this).find(".truncate").text().trim();
- let url = loadedCheerio(this).attr("href") as string;
- let releaseTime = loadedCheerio(this).find("div > div > small").text().trim();
- let chapterNumber = name.split("-")[0].trim().split(" ")[1];
- const chapter: Plugin.ChapterItem = {
- name: name,
- path: url.replace(site, ""),
- releaseTime: releaseTime,
- chapterNumber: parseInt(chapterNumber),
- };
- chapters.push(chapter);
- });
- novel.chapters = chapters;
- return novel;
- }
- async parseChapter(chapterPath: string): Promise {
- const body = await fetchApi(this.site+chapterPath).then((r) => r.text());
+ const loadedCheerio = parseHTML(body);
- const loadedCheerio = parseHTML(body);
- let t = loadedCheerio("div.justify-center > div.mb-4");
- let chapterText = t.html() || '';
+ var postValue = loadedCheerio('div[ax-load][x-data]')
+ .attr('x-data')
+ ?.replace("browse('", '')
+ .replace("')", '') as string;
- return chapterText;
- }
-
- async searchNovels(searchTerm: string, pageNo: number): Promise {
- let novels: Plugin.NovelItem[] = [];
- let url = "https://storyseedling.com/ajax";
+ var data = new FormData();
+ data.set('search', searchTerm);
+ data.set('orderBy', 'recent');
+ data.set('curpage', pageNo.toString());
+ data.set('post', postValue);
+ data.set('action', 'fetch_browse');
- const postUrl = `https://storyseedling.com/browse/`;
- const body = await fetchApi(postUrl).then((r) => r.text());
+ var response = await (
+ await fetchApi(url, { body: data, method: 'POST' })
+ ).json();
- const loadedCheerio = parseHTML(body);
+ response.data.posts.forEach((element: any) => {
+ const novel = {
+ name: element.title,
+ cover: element.thumbnail,
+ path: element.permalink.replace(this.site, ''),
+ };
+ novels.push(novel);
+ });
- var postValue = loadedCheerio("div[ax-load][x-data]").attr("x-data")?.replace("browse('","").replace("')", "") as string;
-
- var data = new FormData();
- data.set("search", searchTerm);
- data.set("orderBy", "recent");
- data.set("curpage", pageNo.toString());
- data.set("post", postValue);
- data.set("action", "fetch_browse");
-
- var response = await(await fetchApi(url, {body: data, method: "POST"})).json();
-
- response.data.posts.forEach((element: any) => {
- const novel = {
- name: element.title,
- cover: element.thumbnail,
- path: element.permalink.replace(this.site, ""),
- };
- novels.push(novel);
- });
-
- return novels;
- }
- async fetchImage(url: string): Promise {
- return fetchFile(url);
- }
+ return novels;
+ }
+ async fetchImage(url: string): Promise {
+ return fetchFile(url);
+ }
}
export default new StorySeedlingPlugin();
diff --git a/plugins/english/allnovelfull.ts b/plugins/english/allnovelfull.ts
index 2c341f8..2981af3 100644
--- a/plugins/english/allnovelfull.ts
+++ b/plugins/english/allnovelfull.ts
@@ -1,215 +1,214 @@
-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 { isUrlAbsolute } from "@libs/isAbsoluteUrl";
+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 { isUrlAbsolute } from '@libs/isAbsoluteUrl';
class AllNovelFullPlugin implements Plugin.PluginBase {
- id = "anf.net";
- name = "AllNovelFull";
- icon = "src/en/allnovelfull/icon.png";
- site = "https://allnovelfull.net";
- version = "1.0.0";
+ id = 'anf.net';
+ name = 'AllNovelFull';
+ icon = 'src/en/allnovelfull/icon.png';
+ site = 'https://allnovelfull.net';
+ version = '1.0.0';
- parseNovels(loadedCheerio: CheerioAPI) {
- const novels: Plugin.NovelItem[] = [];
+ parseNovels(loadedCheerio: CheerioAPI) {
+ 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;
+ 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();
+ const novelName = loadedCheerio(el).find('h3.truyen-title > a').text();
- let novelCover = loadedCheerio(el).find("img.cover").attr("src");
- if (novelCover && !isUrlAbsolute(novelCover)){
- novelCover = this.site + novelCover;
- }
+ let novelCover = loadedCheerio(el).find('img.cover').attr('src');
+ if (novelCover && !isUrlAbsolute(novelCover)) {
+ novelCover = this.site + novelCover;
+ }
- const novel = {
- path: novelUrl,
- name: novelName,
- cover: novelCover,
- };
- novels.push(novel);
+ const novel = {
+ path: novelUrl,
+ name: novelName,
+ cover: novelCover,
+ };
+ novels.push(novel);
+ });
+ return novels;
+ }
+
+ async popularNovels(
+ pageNo: number,
+ { filters }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ let link = this.site;
+
+ if (filters.genres.value.length) link += filters.genres.value;
+ else link += filters.order.value;
+
+ link += `?page=${pageNo}`;
+
+ const result = await fetchApi(link);
+ const body = await result.text();
+
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
+
+ async parseNovel(novelPath: string): Promise {
+ const url = this.site + novelPath;
+ const result = await fetchApi(url);
+ const body = await result.text();
+
+ let loadedCheerio = parseHTML(body);
+
+ const novel: Plugin.SourceNovel = {
+ path: novelPath,
+ name: loadedCheerio('.book > img').attr('alt') || 'Untitled',
+ cover: this.site + loadedCheerio('.book > img').attr('src'),
+ summary: loadedCheerio('.desc-text').text().trim(),
+ chapters: [],
+ };
+
+ 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(',');
+
+ switch (detailName) {
+ case 'Author:':
+ novel.author = detail;
+ break;
+ case 'Status:':
+ novel.status = detail;
+ break;
+ case 'Genre:':
+ novel.genres = detail;
+ break;
+ }
+ });
+
+ const novelId = loadedCheerio('#rating').attr('data-novel-id');
+
+ const getChapters = async (id: string) => {
+ const chaptersUrl = this.site + '/ajax/chapter-option?novelId=' + id;
+
+ const data = await fetchApi(chaptersUrl);
+ const chapters = await data.text();
+
+ loadedCheerio = parseHTML(chapters);
+
+ const chapter: Plugin.ChapterItem[] = [];
+
+ loadedCheerio('select > option').each((i, el) => {
+ const chapterName = loadedCheerio(el).text();
+ const chapterUrl = loadedCheerio(el).attr('value');
+
+ if (!chapterUrl) return;
+
+ chapter.push({
+ name: chapterName,
+ path: chapterUrl,
});
- return novels;
+ });
+
+ return chapter;
};
- async popularNovels(
- pageNo: number,
- { filters }: Plugin.PopularNovelsOptions
- ): Promise {
- let link = this.site;
-
- if (filters.genres.value.length)
- link += filters.genres.value;
- else
- link += filters.order.value;
-
- link += `?page=${pageNo}`;
-
- const result = await fetchApi(link);
- const body = await result.text();
-
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
+ if (novelId) {
+ novel.chapters = await getChapters(novelId);
}
- async parseNovel(novelPath: string): Promise {
- const url = this.site + novelPath;
- const result = await fetchApi(url);
- const body = await result.text();
+ return novel;
+ }
- let loadedCheerio = parseHTML(body);
+ async parseChapter(chapterPath: string): Promise {
+ const result = await fetchApi(this.site + chapterPath);
+ const body = await result.text();
- const novel: Plugin.SourceNovel = {
- path: novelPath,
- name: loadedCheerio(".book > img").attr("alt") || 'Untitled',
- cover: this.site + loadedCheerio(".book > img").attr("src"),
- summary: loadedCheerio(".desc-text").text().trim(),
- chapters: [],
- };
+ const loadedCheerio = parseHTML(body);
- 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(',');
+ const chapterText = loadedCheerio('#chapter-content').html() || '';
- switch (detailName) {
- case 'Author:':
- novel.author = detail;
- break;
- case 'Status:':
- novel.status = detail;
- break;
- case 'Genre:':
- novel.genres = detail;
- break;
- }});
+ return chapterText;
+ }
- const novelId = loadedCheerio("#rating").attr("data-novel-id");
+ async searchNovels(
+ searchTerm: string,
+ page: number,
+ ): Promise {
+ const url = `${this.site}/search?keyword=${searchTerm}&page=${page}`;
+ const result = await fetchApi(url);
+ const body = await result.text();
- const getChapters = async (id: string) => {
- const chaptersUrl = this.site + "/ajax/chapter-option?novelId=" + id;
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
- const data = await fetchApi(chaptersUrl);
- const chapters = await data.text();
+ async fetchImage(url: string): Promise {
+ return await fetchFile(url);
+ }
- loadedCheerio = parseHTML(chapters);
-
- const chapter: Plugin.ChapterItem[] = [];
-
- loadedCheerio("select > option").each((i,el) => {
- const chapterName = loadedCheerio(el).text();
- const chapterUrl = loadedCheerio(el).attr("value");
-
- if (!chapterUrl ) return;
-
- chapter.push({
- name: chapterName,
- path: chapterUrl,
- });
- });
-
- return chapter;
- }
-
- if (novelId) {
- novel.chapters = await getChapters(novelId);
- }
-
- return novel;
- };
-
- async parseChapter(chapterPath: string): Promise {
- const result = await fetchApi(this.site + chapterPath);
- const body = await result.text();
-
- const loadedCheerio = parseHTML(body);
-
- const chapterText = loadedCheerio("#chapter-content").html() || "";
-
- return chapterText;
- };
-
- async searchNovels(
- searchTerm: string,
- page: number
- ): Promise {
- const url = `${this.site}/search?keyword=${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 {
- return await fetchFile(url);
- }
-
- filters = {
- order: {
- value: "/most-popular",
- label: "Order 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,
- },
- genres: {
- value: "",
- label: "Genre",
- options: [
- { label: "None", value: "" },
- { label: "Shounen", value: "/genre/Shounen" },
- { label: "Harem", value: "/genre/Harem" },
- { label: "Comedy", value: "/genre/Comedy" },
- { label: "Martial Arts", value: "/genre/Martial+Arts" },
- { label: "School Life", value: "/genre/School+Life" },
- { label: "Mystery", value: "/genre/Mystery" },
- { label: "Shoujo", value: "/genre/Shoujo" },
- { label: "Romance", value: "/genre/Romance" },
- { label: "Sci-fi", value: "/genre/Sci-fi" },
- { label: "Gender Bender", value: "/genre/Gender+Bender" },
- { label: "Mature", value: "/genre/Mature" },
- { label: "Fantasy", value: "/genre/Fantasy" },
- { label: "Horror", value: "/genre/Horror" },
- { label: "Drama", value: "/genre/Drama" },
- { label: "Tragedy", value: "/genre/Tragedy" },
- { label: "Supernatural", value: "/genre/Supernatural" },
- { label: "Ecchi", value: "/genre/Ecchi" },
- { label: "Xuanhuan", value: "/genre/Xuanhuan" },
- { label: "Adventure", value: "/genre/Adventure" },
- { label: "Action", value: "/genre/Action" },
- { label: "Psychological", value: "/genre/Psychological" },
- { label: "Xianxia", value: "/genre/Xianxia" },
- { label: "Wuxia", value: "/genre/Wuxia" },
- { label: "Historical", value: "/genre/Historical" },
- { label: "Slice of Life", value: "/genre/Slice+of+Life" },
- { label: "Seinen", value: "/genre/Seinen" },
- { label: "Lolicon", value: "/genre/Lolicon" },
- { label: "Adult", value: "/genre/Adult" },
- { label: "Josei", value: "/genre/Josei" },
- { label: "Sports", value: "/genre/Sports" },
- { label: "Smut", value: "/genre/Smut" },
- { label: "Mecha", value: "/genre/Mecha" },
- { label: "Yaoi", value: "/genre/Yaoi" },
- { label: "Shounen Ai", value: "/genre/Shounen+Ai" },
- { label: "Magical Realism", value: "/genre/Magical+Realism" },
- { label: "Video Games", value: "/genre/Video+Games" },
- ],
- type: FilterTypes.Picker,
- },
- } satisfies Filters;
+ filters = {
+ order: {
+ value: '/most-popular',
+ label: 'Order 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,
+ },
+ genres: {
+ value: '',
+ label: 'Genre',
+ options: [
+ { label: 'None', value: '' },
+ { label: 'Shounen', value: '/genre/Shounen' },
+ { label: 'Harem', value: '/genre/Harem' },
+ { label: 'Comedy', value: '/genre/Comedy' },
+ { label: 'Martial Arts', value: '/genre/Martial+Arts' },
+ { label: 'School Life', value: '/genre/School+Life' },
+ { label: 'Mystery', value: '/genre/Mystery' },
+ { label: 'Shoujo', value: '/genre/Shoujo' },
+ { label: 'Romance', value: '/genre/Romance' },
+ { label: 'Sci-fi', value: '/genre/Sci-fi' },
+ { label: 'Gender Bender', value: '/genre/Gender+Bender' },
+ { label: 'Mature', value: '/genre/Mature' },
+ { label: 'Fantasy', value: '/genre/Fantasy' },
+ { label: 'Horror', value: '/genre/Horror' },
+ { label: 'Drama', value: '/genre/Drama' },
+ { label: 'Tragedy', value: '/genre/Tragedy' },
+ { label: 'Supernatural', value: '/genre/Supernatural' },
+ { label: 'Ecchi', value: '/genre/Ecchi' },
+ { label: 'Xuanhuan', value: '/genre/Xuanhuan' },
+ { label: 'Adventure', value: '/genre/Adventure' },
+ { label: 'Action', value: '/genre/Action' },
+ { label: 'Psychological', value: '/genre/Psychological' },
+ { label: 'Xianxia', value: '/genre/Xianxia' },
+ { label: 'Wuxia', value: '/genre/Wuxia' },
+ { label: 'Historical', value: '/genre/Historical' },
+ { label: 'Slice of Life', value: '/genre/Slice+of+Life' },
+ { label: 'Seinen', value: '/genre/Seinen' },
+ { label: 'Lolicon', value: '/genre/Lolicon' },
+ { label: 'Adult', value: '/genre/Adult' },
+ { label: 'Josei', value: '/genre/Josei' },
+ { label: 'Sports', value: '/genre/Sports' },
+ { label: 'Smut', value: '/genre/Smut' },
+ { label: 'Mecha', value: '/genre/Mecha' },
+ { label: 'Yaoi', value: '/genre/Yaoi' },
+ { label: 'Shounen Ai', value: '/genre/Shounen+Ai' },
+ { label: 'Magical Realism', value: '/genre/Magical+Realism' },
+ { label: 'Video Games', value: '/genre/Video+Games' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ } satisfies Filters;
}
export default new AllNovelFullPlugin();
diff --git a/plugins/english/bestlightnovel.ts b/plugins/english/bestlightnovel.ts
index 8ba7f0e..cab25bb 100644
--- a/plugins/english/bestlightnovel.ts
+++ b/plugins/english/bestlightnovel.ts
@@ -1,279 +1,276 @@
-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";
- name = "BestLightNovel";
- icon = "src/en/bestlightnovel/icon.png";
- site = "https://bestlightnovel.com/";
- version = "1.0.0";
-
- 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,
- path: novelUrl.replace(this.site, ''),
- cover: novelCover,
- };
-
- novels.push(novel);
- });
-
- return novels;
- }
-
- async popularNovels(
- page: number,
- { filters }: Plugin.PopularNovelsOptions
- ): Promise {
- 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());
- // TODO: Cloudflare protection or other error
- return [];
- }
- const body = await result.text();
-
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
-
- async parseNovel(novelPath: string): Promise {
- 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;
- }
- })
-
- const chapter: Plugin.ChapterItem[] = [];
-
- 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();
-
- 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,
- releaseTime: new Date(year, month, day).toISOString(),
- path: chapterUrl.replace(this.site, ''),
- });
- });
-
- novel.chapters = chapter.reverse();
-
- return novel;
- }
-
- async parseChapter(chapterPath: string): Promise {
- 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,
- page: number
- ): Promise {
- const url = `${this.site}search_novels/${searchTerm}?page=${page}`;
-
- const result = await fetch(url);
- const body = await result.text();
-
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
-
- async fetchImage(url: string): Promise {
- 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();
+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';
+ name = 'BestLightNovel';
+ icon = 'src/en/bestlightnovel/icon.png';
+ site = 'https://bestlightnovel.com/';
+ version = '1.0.0';
+
+ 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,
+ path: novelUrl.replace(this.site, ''),
+ cover: novelCover,
+ };
+
+ novels.push(novel);
+ });
+
+ return novels;
+ }
+
+ async popularNovels(
+ page: number,
+ { filters }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ 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());
+ // TODO: Cloudflare protection or other error
+ return [];
+ }
+ const body = await result.text();
+
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
+
+ async parseNovel(novelPath: string): Promise {
+ 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;
+ }
+ });
+
+ const chapter: Plugin.ChapterItem[] = [];
+
+ 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();
+
+ 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,
+ releaseTime: new Date(year, month, day).toISOString(),
+ path: chapterUrl.replace(this.site, ''),
+ });
+ });
+
+ novel.chapters = chapter.reverse();
+
+ return novel;
+ }
+
+ async parseChapter(chapterPath: string): Promise {
+ 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,
+ page: number,
+ ): Promise {
+ const url = `${this.site}search_novels/${searchTerm}?page=${page}`;
+
+ const result = await fetch(url);
+ const body = await result.text();
+
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
+
+ async fetchImage(url: string): Promise {
+ 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();
diff --git a/plugins/english/comrademao.broken.ts b/plugins/english/comrademao.broken.ts
index cc8ef35..4826097 100644
--- a/plugins/english/comrademao.broken.ts
+++ b/plugins/english/comrademao.broken.ts
@@ -1,184 +1,180 @@
-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";
+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 ComradeMaoPlugin implements Plugin.PluginBase {
- id = "comrademao";
- name = "Comrade Mao";
- site = "https://comrademao.com/";
- version = "1.0.0";
- icon = "src/en/comrademao/icon.png";
+ id = 'comrademao';
+ name = 'Comrade Mao';
+ site = 'https://comrademao.com/';
+ version = '1.0.0';
+ icon = 'src/en/comrademao/icon.png';
- parseNovels(loadedCheerio: CheerioAPI) {
- const novels: Plugin.NovelItem[] = [];
+ parseNovels(loadedCheerio: CheerioAPI) {
+ const novels: Plugin.NovelItem[] = [];
- loadedCheerio(".listupd")
- .find("div.bs")
- .each(function () {
- const novelName = loadedCheerio(this).find(".tt").text().trim();
- const novelCover = loadedCheerio(this).find("img").attr("src");
- const novelUrl = loadedCheerio(this).find("a").attr("href");
+ loadedCheerio('.listupd')
+ .find('div.bs')
+ .each(function () {
+ const novelName = loadedCheerio(this).find('.tt').text().trim();
+ const novelCover = loadedCheerio(this).find('img').attr('src');
+ const novelUrl = loadedCheerio(this).find('a').attr('href');
- if (!novelUrl) return;
+ if (!novelUrl) return;
- const novel = {
- name: novelName,
- cover: novelCover,
- url: novelUrl,
- };
- novels.push(novel);
- });
- return novels;
- }
-
- async popularNovels(
- pageNo: number,
- { filters }: Plugin.PopularNovelsOptions,
- ): Promise {
- let link = this.site;
-
- link += filters.category.value;
-
- link += "page/" + pageNo + "/?post_type=novel";
-
- const body = await fetchApi(link).then((result) => result.text());
-
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
-
- async parseNovelAndChapters(novelUrl: string): Promise {
- const body = await fetchApi(novelUrl).then((result) => result.text());
-
- let loadedCheerio = parseHTML(body);
-
- const novel: Plugin.SourceNovel = {
- url: novelUrl,
- chapters: [],
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ url: novelUrl,
};
+ novels.push(novel);
+ });
+ return novels;
+ }
- novel.name = loadedCheerio(".entry-title").text().trim();
+ async popularNovels(
+ pageNo: number,
+ { filters }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ let link = this.site;
- novel.cover = loadedCheerio("div.thumbook > div > img").attr("src");
+ link += filters.category.value;
- loadedCheerio(".infox .wd-full").each((i, el) => {
- const detailName = loadedCheerio(el).find("b").text();
- const detail = loadedCheerio(el).find("span").text();
+ link += 'page/' + pageNo + '/?post_type=novel';
- switch (detailName) {
- case "Publisher: ":
- novel.author = detail.trim();
- break;
- case "Status: ":
- novel.status = detail.trim();
- break;
- case "Genres:":
- novel.genres = detail.trim().replace(/ , /g, ",");
- break;
- case "Description: ":
- novel.summary = detail.trim();
- break;
+ const body = await fetchApi(link).then(result => result.text());
+
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
+
+ async parseNovelAndChapters(novelUrl: string): Promise {
+ const body = await fetchApi(novelUrl).then(result => result.text());
+
+ let loadedCheerio = parseHTML(body);
+
+ const novel: Plugin.SourceNovel = {
+ url: novelUrl,
+ chapters: [],
+ };
+
+ novel.name = loadedCheerio('.entry-title').text().trim();
+
+ novel.cover = loadedCheerio('div.thumbook > div > img').attr('src');
+
+ loadedCheerio('.infox .wd-full').each((i, el) => {
+ const detailName = loadedCheerio(el).find('b').text();
+ const detail = loadedCheerio(el).find('span').text();
+
+ switch (detailName) {
+ case 'Publisher: ':
+ novel.author = detail.trim();
+ break;
+ case 'Status: ':
+ novel.status = detail.trim();
+ break;
+ case 'Genres:':
+ novel.genres = detail.trim().replace(/ , /g, ',');
+ break;
+ case 'Description: ':
+ novel.summary = detail.trim();
+ break;
+ }
+ });
+
+ const chapter: Plugin.ChapterItem[] = [];
+
+ loadedCheerio('#chapterlist')
+ .find('li')
+ .each(function () {
+ const chapterUrl = loadedCheerio(this).find('a').attr('href');
+ if (!chapterUrl) return;
+
+ const chapterName = loadedCheerio(this).find('.chapternum').text();
+ let releaseTime = loadedCheerio(this).find('.chapterdate').text();
+
+ if (releaseTime?.includes?.('ago')) {
+ const timeAgo = releaseTime.match(/\d+/)?.[0] || '0';
+ const timeAgoInt = parseInt(timeAgo, 10);
+
+ if (timeAgoInt) {
+ const dayJSDate = dayjs(); // today
+ if (
+ releaseTime.includes('hours ago') ||
+ releaseTime.includes('hour ago')
+ ) {
+ dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours
}
+
+ if (
+ releaseTime.includes('days ago') ||
+ releaseTime.includes('day ago')
+ ) {
+ dayJSDate.subtract(timeAgoInt, 'days'); // go back N days
+ }
+
+ if (
+ releaseTime.includes('months ago') ||
+ releaseTime.includes('month ago')
+ ) {
+ dayJSDate.subtract(timeAgoInt, 'months'); // go back N months
+ }
+
+ releaseTime = dayJSDate.format('LL');
+ }
+ }
+ chapter.push({
+ name: chapterName,
+ url: chapterUrl,
+ releaseTime,
});
+ });
- const chapter: Plugin.ChapterItem[] = [];
+ novel.chapters = chapter.reverse();
- loadedCheerio("#chapterlist")
- .find("li")
- .each(function () {
- const chapterUrl = loadedCheerio(this).find("a").attr("href");
- if (!chapterUrl) return;
+ return novel;
+ }
- const chapterName = loadedCheerio(this)
- .find(".chapternum")
- .text();
- let releaseTime = loadedCheerio(this)
- .find(".chapterdate")
- .text();
+ async parseChapter(chapterUrl: string): Promise {
+ const body = await fetchApi(chapterUrl).then(result => result.text());
+ const loadedCheerio = parseHTML(body);
- if (releaseTime?.includes?.("ago")) {
- const timeAgo = releaseTime.match(/\d+/)?.[0] || "0";
- const timeAgoInt = parseInt(timeAgo, 10);
+ const chapterText = loadedCheerio('#chaptercontent').html()?.trim() || '';
- if (timeAgoInt) {
- const dayJSDate = dayjs(); // today
- if (
- releaseTime.includes("hours ago") ||
- releaseTime.includes("hour ago")
- ) {
- dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours
- }
+ return chapterText;
+ }
- if (
- releaseTime.includes("days ago") ||
- releaseTime.includes("day ago")
- ) {
- dayJSDate.subtract(timeAgoInt, "days"); // go back N days
- }
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ const url = `${this.site}page/${pageNo}/?s=${searchTerm}&post_type=novel`;
- if (
- releaseTime.includes("months ago") ||
- releaseTime.includes("month ago")
- ) {
- dayJSDate.subtract(timeAgoInt, "months"); // go back N months
- }
+ const body = await fetchApi(url).then(result => result.text());
- releaseTime = dayJSDate.format("LL");
- }
- }
- chapter.push({
- name: chapterName,
- url: chapterUrl,
- releaseTime,
- });
- });
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
- novel.chapters = chapter.reverse();
+ async fetchImage(url: string): Promise {
+ return await fetchFile(url);
+ }
- return novel;
- }
-
- async parseChapter(chapterUrl: string): Promise {
- const body = await fetchApi(chapterUrl).then((result) => result.text());
- const loadedCheerio = parseHTML(body);
-
- const chapterText = loadedCheerio("#chaptercontent").html()?.trim() || "";
-
- return chapterText;
- }
-
- async searchNovels(
- searchTerm: string,
- pageNo: number,
- ): Promise {
- const url = `${this.site}page/${pageNo}/?s=${searchTerm}&post_type=novel`;
-
- const body = await fetchApi(url).then((result) => result.text());
-
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
-
- async fetchImage(url: string): Promise {
- return await fetchFile(url);
- }
-
- filters = {
- category: {
- value: "",
- label: "Type",
- options: [
- { label: "None", value: "" },
- { label: "Chinese", value: "/mtype/chinese/" },
- { label: "Japanese", value: "/mtype/japanese/" },
- { label: "Korean", value: "/mtype/korean/" },
- { label: "Manhua", value: "/mtype/manhua/" },
- ],
- type: FilterTypes.Picker,
- },
- } satisfies Filters;
+ filters = {
+ category: {
+ value: '',
+ label: 'Type',
+ options: [
+ { label: 'None', value: '' },
+ { label: 'Chinese', value: '/mtype/chinese/' },
+ { label: 'Japanese', value: '/mtype/japanese/' },
+ { label: 'Korean', value: '/mtype/korean/' },
+ { label: 'Manhua', value: '/mtype/manhua/' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ } satisfies Filters;
}
export default new ComradeMaoPlugin();
diff --git a/plugins/english/divinedaolibrary.ts b/plugins/english/divinedaolibrary.ts
index 3a0fab5..b80a3da 100644
--- a/plugins/english/divinedaolibrary.ts
+++ b/plugins/english/divinedaolibrary.ts
@@ -1,138 +1,137 @@
-import { CheerioAPI, load as parseHTML } from "cheerio";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { Plugin } from "@typings/plugin";
-import { defaultCover } from "@libs/defaultCover";
+import { CheerioAPI, load as parseHTML } from 'cheerio';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { Plugin } from '@typings/plugin';
+import { defaultCover } from '@libs/defaultCover';
class DDLPlugin implements Plugin.PluginBase {
- id = "DDL.com";
- name = "Divine Dao Library";
- site = "https://www.divinedaolibrary.com/";
- version = "1.0.1";
- icon = "src/en/divinedaolibrary/icon.png";
- filters?: undefined;
+ id = 'DDL.com';
+ name = 'Divine Dao Library';
+ site = 'https://www.divinedaolibrary.com/';
+ version = '1.0.1';
+ icon = 'src/en/divinedaolibrary/icon.png';
+ filters?: undefined;
- parseNovels(loadedCheerio: CheerioAPI, searchTerm?: string){
- let novels: Plugin.NovelItem[] = [];
+ parseNovels(loadedCheerio: CheerioAPI, searchTerm?: string) {
+ let novels: Plugin.NovelItem[] = [];
- loadedCheerio("#main")
- .find("li")
- .each((i,el) => {
- const novelName = loadedCheerio(el).find("a").text();
- const novelCover = defaultCover;
- const novelUrl = loadedCheerio(el).find("a").attr("href");
-
- if (!novelUrl) return;
-
- const novel = {
- name: novelName,
- cover: novelCover,
- path: novelUrl.replace(this.site, ''),
- };
-
- novels.push(novel);
- });
-
- if (searchTerm) {
- novels = novels.filter((novel) =>
- novel.name.toLowerCase().includes(searchTerm.toLowerCase())
- );
- }
- return novels;
- };
+ loadedCheerio('#main')
+ .find('li')
+ .each((i, el) => {
+ const novelName = loadedCheerio(el).find('a').text();
+ const novelCover = defaultCover;
+ const novelUrl = loadedCheerio(el).find('a').attr('href');
- async popularNovels(
- pageNo: number,
- options: Plugin.PopularNovelsOptions
- ): Promise {
- let link = this.site + "novels";
+ if (!novelUrl) return;
- const body = await fetchApi(link).then((res) => res.text());
-
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
-
- async parseNovel(novelPath: string): Promise {
- const result = await fetchApi(this.site + novelPath);
- const body = await result.text();
-
- let loadedCheerio = parseHTML(body);
-
- const novel: Plugin.SourceNovel = {
- path: novelPath,
- name: loadedCheerio("h1.entry-title").text().trim() || "Untitled",
- cover:
- loadedCheerio(".entry-content").find("img").attr("data-ezsrc") ||
- defaultCover,
- chapters: [],
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl.replace(this.site, ''),
};
- novel.summary = loadedCheerio("#main > article > div > p:nth-child(6)")
- .text()
- .trim();
+ novels.push(novel);
+ });
- novel.author = loadedCheerio("#main > article > div > h3:nth-child(2)")
- .text()
- .replace(/Author:/g, "")
- .trim();
-
- const chapter: Plugin.ChapterItem[] = [];
-
- loadedCheerio("#main")
- .find("li > span > a")
- .each((i, el) => {
- const chapterName = loadedCheerio(el).text().trim();
- const chapterUrl = loadedCheerio(el).attr("href");
-
- if (!chapterUrl) return;
-
- chapter.push({
- name: chapterName,
- path: chapterUrl.replace(this.site, ''),
- });
- });
-
- novel.chapters = chapter;
-
- return novel;
- };
-
- async parseChapter(chapterPath: string): Promise {
- const result = await fetchApi(this.site + chapterPath);
- const body = await result.text();
-
- const loadedCheerio = parseHTML(body);
-
- const chapterName = loadedCheerio(".entry-title").text().trim();
-
- let chapterText = loadedCheerio(".entry-content").html();
-
- if (!chapterText) {
- chapterText = loadedCheerio(".page-header").html();
- }
-
- chapterText = `${chapterName}
` + chapterText;
-
- return chapterText;
- };
-
- async searchNovels(
- searchTerm: string,
- pageNo: number
- ): Promise {
- let url = this.site + "novels";
-
- const result = await fetchApi(url);
- const body = await result.text();
-
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio, searchTerm);
- };
-
-
- async fetchImage(url: string): Promise {
- return await fetchFile(url);
+ if (searchTerm) {
+ novels = novels.filter(novel =>
+ novel.name.toLowerCase().includes(searchTerm.toLowerCase()),
+ );
}
+ return novels;
+ }
+
+ async popularNovels(
+ pageNo: number,
+ options: Plugin.PopularNovelsOptions,
+ ): Promise {
+ let link = this.site + 'novels';
+
+ const body = await fetchApi(link).then(res => res.text());
+
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
+
+ async parseNovel(novelPath: string): Promise {
+ const result = await fetchApi(this.site + novelPath);
+ const body = await result.text();
+
+ let loadedCheerio = parseHTML(body);
+
+ const novel: Plugin.SourceNovel = {
+ path: novelPath,
+ name: loadedCheerio('h1.entry-title').text().trim() || 'Untitled',
+ cover:
+ loadedCheerio('.entry-content').find('img').attr('data-ezsrc') ||
+ defaultCover,
+ chapters: [],
+ };
+
+ novel.summary = loadedCheerio('#main > article > div > p:nth-child(6)')
+ .text()
+ .trim();
+
+ novel.author = loadedCheerio('#main > article > div > h3:nth-child(2)')
+ .text()
+ .replace(/Author:/g, '')
+ .trim();
+
+ const chapter: Plugin.ChapterItem[] = [];
+
+ loadedCheerio('#main')
+ .find('li > span > a')
+ .each((i, el) => {
+ const chapterName = loadedCheerio(el).text().trim();
+ const chapterUrl = loadedCheerio(el).attr('href');
+
+ if (!chapterUrl) return;
+
+ chapter.push({
+ name: chapterName,
+ path: chapterUrl.replace(this.site, ''),
+ });
+ });
+
+ novel.chapters = chapter;
+
+ return novel;
+ }
+
+ async parseChapter(chapterPath: string): Promise {
+ const result = await fetchApi(this.site + chapterPath);
+ const body = await result.text();
+
+ const loadedCheerio = parseHTML(body);
+
+ const chapterName = loadedCheerio('.entry-title').text().trim();
+
+ let chapterText = loadedCheerio('.entry-content').html();
+
+ if (!chapterText) {
+ chapterText = loadedCheerio('.page-header').html();
+ }
+
+ chapterText = `${chapterName}
` + chapterText;
+
+ return chapterText;
+ }
+
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ let url = this.site + 'novels';
+
+ const result = await fetchApi(url);
+ const body = await result.text();
+
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio, searchTerm);
+ }
+
+ async fetchImage(url: string): Promise {
+ return await fetchFile(url);
+ }
}
-export default new DDLPlugin();
\ No newline at end of file
+export default new DDLPlugin();
diff --git a/plugins/english/earlynovel.ts b/plugins/english/earlynovel.ts
index c7f52b5..caa1a6b 100644
--- a/plugins/english/earlynovel.ts
+++ b/plugins/english/earlynovel.ts
@@ -1,213 +1,206 @@
-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 { CheerioAPI, load as parseHTML } from 'cheerio';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { FilterTypes, Filters } from '@libs/filterInputs';
+import { Plugin } from '@typings/plugin';
class EarlyNovelPlugin implements Plugin.PagePlugin {
- id = "earlynovel";
- name = "Early Novel";
- version = "1.0.0";
- icon = "multisrc/madara/latestnovel.png";
- site = "https://earlynovel.net/";
+ id = 'earlynovel';
+ name = 'Early Novel';
+ version = '1.0.0';
+ icon = 'multisrc/madara/latestnovel.png';
+ site = 'https://earlynovel.net/';
- parseNovels(loadedCheerio: CheerioAPI){
- const novels: Plugin.NovelItem[] = [];
+ parseNovels(loadedCheerio: CheerioAPI) {
+ 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");
- const novelName = loadedCheerio(el)
- .find("h3.truyen-title > a")
- .text();
- const novelCover = loadedCheerio(el).find('.lazyimg').attr("data-image");
-
- if (!novelUrl) return;
- novels.push({
- path: novelUrl,
- name: novelName,
- cover: novelCover,
- });
- }
- );
- return novels;
+ loadedCheerio('.col-truyen-main > .list-truyen > .row').each((i, el) => {
+ 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('.lazyimg').attr('data-image');
+
+ if (!novelUrl) return;
+ novels.push({
+ 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,
+ ): Promise {
+ let link = this.site;
+
+ if (filters.genres.value.length) link += filters.genres.value;
+ else link += filters.order.value;
+
+ link += `?page=${pageNo}`;
+
+ const body = await fetchApi(link).then(res => res.text());
+
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
+
+ async parseNovel(
+ novelPath: string,
+ ): Promise {
+ 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 & { 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,
};
- 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;
+ loadedCheerio('.info > div > h3').each(function () {
+ let detailName = loadedCheerio(this).text();
+ let detail = loadedCheerio(this)
+ .siblings()
+ .map((i, el) => loadedCheerio(el).text())
+ .toArray()
+ .join(',');
- chapter.push({
- name: chapterName,
- path: chapterUrl
- })});
- return chapter;
- }
+ switch (detailName) {
+ case 'Author:':
+ novel.author = detail;
+ break;
+ case 'Status:':
+ novel.status = detail;
+ break;
+ case 'Genre:':
+ novel.genres = detail;
+ break;
+ }
+ });
- async popularNovels(
- pageNo: number,
- { filters }: Plugin.PopularNovelsOptions
- ): Promise {
- let link = this.site;
+ novel.chapters = this.parseChapters(loadedCheerio);
+ return novel;
+ }
- if (filters.genres.value.length)
- link += filters.genres.value;
- else
- link += filters.order.value;
+ async parsePage(novelPath: string, page: string): Promise {
+ 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 };
+ }
- link += `?page=${pageNo}`;
+ async parseChapter(chapterPath: string): Promise {
+ const result = await fetchApi(this.site + chapterPath);
+ const body = await result.text();
- const body = await fetchApi(link).then((res) => res.text());
+ const loadedCheerio = parseHTML(body);
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
+ const chapterText = loadedCheerio('#chapter-c').html() || '';
- async parseNovel(novelPath: string): Promise {
- const result = await fetchApi(this.site + novelPath);
- const body = await result.text();
+ return chapterText;
+ }
- 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");
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ const url = `${this.site}search?keyword=${searchTerm}`;
+ const result = await fetchApi(url);
+ const body = await result.text();
- 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
- };
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
- loadedCheerio(".info > div > h3").each(function () {
- let detailName = loadedCheerio(this).text();
- let detail = loadedCheerio(this)
- .siblings()
- .map((i, el) => loadedCheerio(el).text())
- .toArray()
- .join(",");
+ async fetchImage(url: string): Promise {
+ return await fetchFile(url);
+ }
- switch (detailName) {
- case "Author:":
- novel.author = detail;
- break;
- case "Status:":
- novel.status = detail;
- break;
- case "Genre:":
- novel.genres = detail;
- break;
- }
- });
-
- novel.chapters = this.parseChapters(loadedCheerio);
- return novel;
- }
-
- async parsePage(novelPath: string, page: string): Promise {
- 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 {
- const result = await fetchApi(this.site + chapterPath);
- const body = await result.text();
-
- const loadedCheerio = parseHTML(body);
-
- const chapterText = loadedCheerio("#chapter-c").html() || "";
-
- return chapterText;
- };
-
- async searchNovels(
- searchTerm: string,
- pageNo: number
- ): Promise {
- const url = `${this.site}search?keyword=${searchTerm}`;
- const result = await fetchApi(url);
- const body = await result.text();
-
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- };
-
- async fetchImage(url: string): Promise {
- return await fetchFile(url);
- }
-
- filters = {
- order: {
- value: "/most-popular",
- label: "Order 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,
- },
- genres: {
- value: "",
- label: "Genre",
- options: [
- { label: "None", value: "" },
- { label: "Action", value: "/genre/action-1" },
- { label: "Adult", value: "/genre/adult-2" },
- { label: "Adventure", value: "/genre/adventure-3" },
- { label: "Comedy", value: "/genre/comedy-4" },
- { label: "Drama", value: "/genre/drama-5" },
- { label: "Ecchi", value: "/genre/ecchi-6" },
- { label: "Fantasy", value: "/genre/fantasy-7" },
- { label: "Gender Bender", value: "/genre/gender-bender-8" },
- { label: "Harem", value: "/genre/harem-9" },
- { label: "Historical", value: "/genre/historical-10" },
- { label: "Horror", value: "/genre/horror-11" },
- { label: "Josei", value: "/genre/josei-12" },
- { label: "Martial Arts", value: "/genre/martial-arts-13" },
- { label: "Mature", value: "/genre/mature-14" },
- { label: "Mecha", value: "/genre/mecha-15" },
- { label: "Mystery", value: "/genre/mystery-16" },
- { label: "Psychological", value: "/genre/psychological-17" },
- { label: "Romance", value: "/genre/romance-18" },
- { label: "School Life", value: "/genre/school-life-19" },
- { label: "Sci-fi", value: "/genre/sci-fi-20" },
- { label: "Seinen", value: "/genre/seinen-21" },
- { label: "Shoujo", value: "/genre/shoujo-22" },
- { label: "Shoujo Ai", value: "/genre/shoujo-ai-23" },
- { label: "Shounen", value: "/genre/shounen-24" },
- { label: "Shounen Ai", value: "/genre/shounen-ai-25" },
- { label: "Slice of Life", value: "/genre/slice-of-life-26" },
- { label: "Smut", value: "/genre/smut-27" },
- { label: "Sports", value: "/genre/sports-28" },
- { label: "Supernatural", value: "/genre/supernatural-29" },
- { label: "Tragedy", value: "/genre/tragedy-30" },
- { label: "Wuxia", value: "/genre/wuxia-31" },
- { label: "Xianxia", value: "/genre/xianxia-32" },
- { label: "Xuanhuan", value: "/genre/xuanhuan-33" },
- { label: "Yaoi", value: "/genre/yaoi-34" },
- { label: "Yuri", value: "/genre/yuri-35" },
- { label: "Video Games", value: "/genre/video-games-36" },
- { label: "Magical Realism", value: "/genre/magical-realism-37" },
- ],
- type: FilterTypes.Picker,
- },
- } satisfies Filters;
+ filters = {
+ order: {
+ value: '/most-popular',
+ label: 'Order 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,
+ },
+ genres: {
+ value: '',
+ label: 'Genre',
+ options: [
+ { label: 'None', value: '' },
+ { label: 'Action', value: '/genre/action-1' },
+ { label: 'Adult', value: '/genre/adult-2' },
+ { label: 'Adventure', value: '/genre/adventure-3' },
+ { label: 'Comedy', value: '/genre/comedy-4' },
+ { label: 'Drama', value: '/genre/drama-5' },
+ { label: 'Ecchi', value: '/genre/ecchi-6' },
+ { label: 'Fantasy', value: '/genre/fantasy-7' },
+ { label: 'Gender Bender', value: '/genre/gender-bender-8' },
+ { label: 'Harem', value: '/genre/harem-9' },
+ { label: 'Historical', value: '/genre/historical-10' },
+ { label: 'Horror', value: '/genre/horror-11' },
+ { label: 'Josei', value: '/genre/josei-12' },
+ { label: 'Martial Arts', value: '/genre/martial-arts-13' },
+ { label: 'Mature', value: '/genre/mature-14' },
+ { label: 'Mecha', value: '/genre/mecha-15' },
+ { label: 'Mystery', value: '/genre/mystery-16' },
+ { label: 'Psychological', value: '/genre/psychological-17' },
+ { label: 'Romance', value: '/genre/romance-18' },
+ { label: 'School Life', value: '/genre/school-life-19' },
+ { label: 'Sci-fi', value: '/genre/sci-fi-20' },
+ { label: 'Seinen', value: '/genre/seinen-21' },
+ { label: 'Shoujo', value: '/genre/shoujo-22' },
+ { label: 'Shoujo Ai', value: '/genre/shoujo-ai-23' },
+ { label: 'Shounen', value: '/genre/shounen-24' },
+ { label: 'Shounen Ai', value: '/genre/shounen-ai-25' },
+ { label: 'Slice of Life', value: '/genre/slice-of-life-26' },
+ { label: 'Smut', value: '/genre/smut-27' },
+ { label: 'Sports', value: '/genre/sports-28' },
+ { label: 'Supernatural', value: '/genre/supernatural-29' },
+ { label: 'Tragedy', value: '/genre/tragedy-30' },
+ { label: 'Wuxia', value: '/genre/wuxia-31' },
+ { label: 'Xianxia', value: '/genre/xianxia-32' },
+ { label: 'Xuanhuan', value: '/genre/xuanhuan-33' },
+ { label: 'Yaoi', value: '/genre/yaoi-34' },
+ { label: 'Yuri', value: '/genre/yuri-35' },
+ { label: 'Video Games', value: '/genre/video-games-36' },
+ { label: 'Magical Realism', value: '/genre/magical-realism-37' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ } satisfies Filters;
}
-export default new EarlyNovelPlugin();
\ No newline at end of file
+export default new EarlyNovelPlugin();
diff --git a/plugins/english/faqwikius.ts b/plugins/english/faqwikius.ts
index 92f79d5..9eb5be0 100644
--- a/plugins/english/faqwikius.ts
+++ b/plugins/english/faqwikius.ts
@@ -1,142 +1,168 @@
-import { Plugin } from "@typings/plugin";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { CheerioAPI, load as parseHTML } from "cheerio";
-import { NovelStatus } from "@libs/novelStatus";
+import { Plugin } from '@typings/plugin';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { CheerioAPI, load as parseHTML } from 'cheerio';
+import { NovelStatus } from '@libs/novelStatus';
class FaqWikiUs implements Plugin.PluginBase {
- id = "FWK.US";
- name = "Faq Wiki";
- site = "https://faqwiki.us/";
- version = "1.1.1";
- icon = "src/en/faqwikius/icon.png";
+ id = 'FWK.US';
+ name = 'Faq Wiki';
+ site = 'https://faqwiki.us/';
+ version = '1.1.1';
+ icon = 'src/en/faqwikius/icon.png';
- parseNovels(loadedCheerio:CheerioAPI, searchTerm?: string) {
- let novels: Plugin.NovelItem[] = [];
-
- loadedCheerio("figure.wp-block-image").each((index, element) => {
- const name = loadedCheerio(element).text();
- let cover = loadedCheerio(element).find("img").attr("data-ezsrc");
+ parseNovels(loadedCheerio: CheerioAPI, searchTerm?: string) {
+ let novels: Plugin.NovelItem[] = [];
- // Remove the appended query string
- if (cover) {
- cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic!
- } else {
- cover = loadedCheerio(element).find("img").attr("src");
- }
+ loadedCheerio('figure.wp-block-image').each((index, element) => {
+ const name = loadedCheerio(element).text();
+ let cover = loadedCheerio(element).find('img').attr('data-ezsrc');
- const path = loadedCheerio(element).find("a").attr("href")?.replace('tp:', 'tps:')?.slice(this.site.length);
- if (!path) return;
+ // Remove the appended query string
+ if (cover) {
+ cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic!
+ } else {
+ cover = loadedCheerio(element).find('img').attr('src');
+ }
- novels.push({ name, cover, path });
- })
+ const path = loadedCheerio(element)
+ .find('a')
+ .attr('href')
+ ?.replace('tp:', 'tps:')
+ ?.slice(this.site.length);
+ if (!path) return;
- if (searchTerm) {
- novels = novels.filter((novel) =>
- novel.name.toLowerCase().includes(searchTerm.toLowerCase())
- );
+ novels.push({ name, cover, path });
+ });
+
+ if (searchTerm) {
+ novels = novels.filter(novel =>
+ novel.name.toLowerCase().includes(searchTerm.toLowerCase()),
+ );
+ }
+
+ return novels;
+ }
+
+ async popularNovels(
+ page: number,
+ { showLatestNovels }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ const body = await fetchApi(this.site).then(res => res.text());
+ const loadedCheerio = parseHTML(body);
+
+ return this.parseNovels(loadedCheerio);
+ }
+
+ async parseNovel(path: string): Promise {
+ const body = await fetchApi(this.site + path).then(res => res.text());
+ const loadedCheerio = parseHTML(body);
+
+ const novel: Plugin.SourceNovel = {
+ path,
+ name: '',
+ };
+
+ novel.name = loadedCheerio('.entry-title')
+ .text()
+ .replace('Novel – All Chapters', '')
+ .trim();
+
+ novel.cover = loadedCheerio('.wp-block-image')
+ .find('img')
+ .attr('data-ezsrc')
+ ?.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic!
+
+ const status = loadedCheerio(
+ "#lcp_instance_0 +:icontains('complete')",
+ ).text();
+ novel.status = status ? NovelStatus.Completed : NovelStatus.Ongoing;
+
+ const div = loadedCheerio('.book-review-block__meta-item-value');
+
+ div.html(div.html()?.replace(/(?<=>)([^<]+)(?=
)/g, '$1
')!);
+
+ loadedCheerio('.book-review-block__meta-item-value strong').each(
+ (i, el) => {
+ const key = loadedCheerio(el)
+ .text()
+ .replace(':', '')
+ .trim()
+ .toLowerCase();
+ const value = loadedCheerio(el).next().text().replace(':', '').trim();
+
+ switch (key) {
+ case 'description':
+ novel.summary = value;
+ break;
+ case 'author(s)':
+ novel.author = value;
+ break;
+ case 'genre':
+ novel.genres = value.split(' ').join(',');
}
+ },
+ );
- return novels;
- }
+ const chapters: Plugin.ChapterItem[] = loadedCheerio(
+ '.lcp_catlist > li > a',
+ )
+ .map((chapterIndex, element) => ({
+ name: loadedCheerio(element)
+ .text()
+ .replace(novel.name + '', '')
+ .replace('Novel' + '', '')
+ .trim(),
+ path: '' + loadedCheerio(element).attr('href')?.slice(this.site.length),
+ chapterNumber: chapterIndex + 1,
+ }))
+ .get();
- async popularNovels(
- page: number,
- { showLatestNovels }: Plugin.PopularNovelsOptions,
- ): Promise {
-
- const body = await fetchApi(this.site).then((res) => res.text());
- const loadedCheerio = parseHTML(body);
+ novel.chapters = chapters;
+ return novel;
+ }
- return this.parseNovels(loadedCheerio);
- }
+ async parseChapter(chapterPath: string): Promise {
+ const body = await fetchApi(this.site + chapterPath).then(res =>
+ res.text(),
+ );
+ const loadedCheerio = parseHTML(body);
+ loadedCheerio('span').remove();
- async parseNovel(path: string): Promise {
- const body = await fetchApi(this.site + path).then((res) => res.text());
- const loadedCheerio = parseHTML(body);
+ const chapterParagraphs = loadedCheerio('.entry-content p');
- const novel: Plugin.SourceNovel = {
- path,
- name: ""
- };
+ let chapterContent; // Variable to store the result
- novel.name = loadedCheerio(".entry-title").text().replace("Novel – All Chapters", "").trim();
-
- novel.cover = loadedCheerio(".wp-block-image")
- .find("img")
- .attr("data-ezsrc")
- ?.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic!
-
- const status = loadedCheerio("#lcp_instance_0 +:icontains('complete')").text();
- novel.status = (status ? NovelStatus.Completed : NovelStatus.Ongoing)
-
- const div = loadedCheerio(".book-review-block__meta-item-value");
-
- div.html(div.html()?.replace(/(?<=>)([^<]+)(?=
)/g, '$1
')!);
-
- loadedCheerio(".book-review-block__meta-item-value strong").each((i,el) => {
- const key = loadedCheerio(el).text().replace(':', '').trim().toLowerCase();
- const value = loadedCheerio(el).next().text().replace(':', '').trim();
-
- switch(key){
- case "description":
- novel.summary = value;
- break;
- case "author(s)":
- novel.author = value;
- break;
- case "genre":
- novel.genres = value.split(' ').join(',');
- }
+ if (chapterParagraphs.length < 5) {
+ //some chapter in this site store their whole text in 1-4 ,
+ chapterContent = chapterParagraphs
+ .map((index, element) => {
+ const text = loadedCheerio(element).html();
+ return text;
})
-
- const chapters: Plugin.ChapterItem[] = loadedCheerio(".lcp_catlist > li > a")
- .map((chapterIndex, element) => ({
- name: loadedCheerio(element).text().replace(novel.name + "", "").replace("Novel" + "", "").trim(),
- path: ""+loadedCheerio(element).attr("href")?.slice(this.site.length),
- chapterNumber: chapterIndex + 1,
- }))
- .get();
-
- novel.chapters = chapters;
- return novel;
+ .get()
+ .join('\n\n');
+ } else {
+ // Multiple paragraphs case
+ chapterContent = chapterParagraphs
+ .map((index, element) => {
+ const text = loadedCheerio(element).text().trim();
+ return `
${text}
`;
+ })
+ .get()
+ .join('\n\n');
}
- async parseChapter(chapterPath: string): Promise {
- const body = await fetchApi(this.site + chapterPath).then((res) => res.text());
- const loadedCheerio = parseHTML(body);
- loadedCheerio("span").remove();
+ return chapterContent;
+ }
- const chapterParagraphs = loadedCheerio(".entry-content p");
+ async searchNovels(searchTerm: string): Promise {
+ const body = await fetchApi(this.site).then(res => res.text());
+ const loadedCheerio = parseHTML(body);
- let chapterContent; // Variable to store the result
+ return this.parseNovels(loadedCheerio, searchTerm);
+ }
- if (chapterParagraphs.length < 5) { //some chapter in this site store their whole text in 1-4 ,
- chapterContent = chapterParagraphs.map((index, element) => {
- const text = loadedCheerio(element).html()
- return text;
- }).get().join('\n\n');
- }
-
- else {
- // Multiple paragraphs case
- chapterContent = chapterParagraphs.map((index, element) => {
- const text = loadedCheerio(element).text().trim();
- return `
${text}
`;
- }).get().join('\n\n');
- }
-
- return chapterContent;
- }
-
-
- async searchNovels(searchTerm: string): Promise {
- const body = await fetchApi(this.site).then((res) => res.text());
- const loadedCheerio = parseHTML(body);
-
- return this.parseNovels(loadedCheerio, searchTerm);
- }
-
- fetchImage = fetchFile;
+ fetchImage = fetchFile;
}
export default new FaqWikiUs();
diff --git a/plugins/english/freewebnovel.ts b/plugins/english/freewebnovel.ts
index cd184eb..9fa214f 100644
--- a/plugins/english/freewebnovel.ts
+++ b/plugins/english/freewebnovel.ts
@@ -1,73 +1,78 @@
-import { Plugin } from "@typings/plugin";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { NovelStatus } from "@libs/novelStatus";
-import { load as parseHTML } from "cheerio";
-import qs from "qs";
+import { Plugin } from '@typings/plugin';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { NovelStatus } from '@libs/novelStatus';
+import { load as parseHTML } from 'cheerio';
+import qs from 'qs';
class FreeWebNovel implements Plugin.PluginBase {
- id = "FWN.com";
- name = "Free Web Novel";
- site = "https://freewebnovel.com";
- version = "1.0.0";
- icon = "src/en/freewebnovel/icon.png";
+ id = 'FWN.com';
+ name = 'Free Web Novel';
+ site = 'https://freewebnovel.com';
+ version = '1.0.0';
+ icon = 'src/en/freewebnovel/icon.png';
async popularNovels(
page: number,
{ showLatestNovels }: Plugin.PopularNovelsOptions,
): Promise {
const sort = showLatestNovels
- ? "/latest-release-novels/"
- : "/completed-novels/";
+ ? '/latest-release-novels/'
+ : '/completed-novels/';
- const body = await fetchApi(this.site + sort + page).then((res) => res.text());
+ const body = await fetchApi(this.site + sort + page).then(res =>
+ res.text(),
+ );
const loadedCheerio = parseHTML(body);
- const novels: Plugin.NovelItem[] = loadedCheerio(".li-row")
+ const novels: Plugin.NovelItem[] = loadedCheerio('.li-row')
.map((index, element) => ({
- name: loadedCheerio(element).find(".tit").text() || "",
- cover: loadedCheerio(element).find("img").attr("src"),
- path: loadedCheerio(element).find("h3 > a").attr("href") || "",
+ name: loadedCheerio(element).find('.tit').text() || '',
+ cover: loadedCheerio(element).find('img').attr('src'),
+ path: loadedCheerio(element).find('h3 > a').attr('href') || '',
}))
.get()
- .filter((novel) => novel.name && novel.path);
+ .filter(novel => novel.name && novel.path);
return novels;
}
async parseNovel(novelPath: string): Promise {
- const body = await fetchApi(this.site + novelPath).then((res) => res.text());
+ const body = await fetchApi(this.site + novelPath).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
- name: loadedCheerio("h1.tit").text(),
- cover: loadedCheerio(".pic > img").attr("src"),
- summary: loadedCheerio(".inner").text().trim(),
+ name: loadedCheerio('h1.tit').text(),
+ cover: loadedCheerio('.pic > img').attr('src'),
+ summary: loadedCheerio('.inner').text().trim(),
};
- novel.genres = loadedCheerio("[title=Genre]")
+ novel.genres = loadedCheerio('[title=Genre]')
.next()
.text()
- .replace(/[\t\n]/g, "");
+ .replace(/[\t\n]/g, '');
- novel.author = loadedCheerio("[title=Author]")
+ novel.author = loadedCheerio('[title=Author]')
.next()
.text()
- .replace(/[\t\n]/g, "");
+ .replace(/[\t\n]/g, '');
- novel.status = loadedCheerio("[title=Status]")
+ novel.status = loadedCheerio('[title=Status]')
.next()
.text()
- .replace(/[\t\n]/g, "");
+ .replace(/[\t\n]/g, '');
- const chapters: Plugin.ChapterItem[] = loadedCheerio("#idData > li > a")
+ const chapters: Plugin.ChapterItem[] = loadedCheerio('#idData > li > a')
.map((chapterIndex, element) => ({
name:
- loadedCheerio(element).attr("title") ||
- "Chapter " + (chapterIndex + 1),
+ loadedCheerio(element).attr('title') ||
+ 'Chapter ' + (chapterIndex + 1),
path:
- loadedCheerio(element).attr("href") ||
- novelPath.replace(".html", "/chapter-" + (chapterIndex + 1) + ".html"),
+ loadedCheerio(element).attr('href') ||
+ novelPath.replace(
+ '.html',
+ '/chapter-' + (chapterIndex + 1) + '.html',
+ ),
releaseTime: null,
chapterNumber: chapterIndex + 1,
}))
@@ -78,33 +83,35 @@ class FreeWebNovel implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise {
- const body = await fetchApi(this.site + chapterPath).then((res) => res.text());
+ const body = await fetchApi(this.site + chapterPath).then(res =>
+ res.text(),
+ );
const loadedCheerio = parseHTML(body);
- const chapterText = loadedCheerio("div.txt").html() || "";
+ const chapterText = loadedCheerio('div.txt').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string): Promise {
- const body = await fetchApi(this.site + "/search/", {
+ const body = await fetchApi(this.site + '/search/', {
headers: {
- "Content-Type": "application/x-www-form-urlencoded",
+ 'Content-Type': 'application/x-www-form-urlencoded',
Referer: this.site,
Origin: this.site,
},
- method: "POST",
+ method: 'POST',
body: qs.stringify({ searchkey: searchTerm }),
- }).then((res) => res.text());
+ }).then(res => res.text());
const loadedCheerio = parseHTML(body);
- const novels: Plugin.NovelItem[] = loadedCheerio(".li-row > .li > .con")
+ const novels: Plugin.NovelItem[] = loadedCheerio('.li-row > .li > .con')
.map((index, element) => ({
- name: loadedCheerio(element).find(".tit").text() || "",
- cover: loadedCheerio(element).find(".pic > a > img").attr("src"),
- path: loadedCheerio(element).find("h3 > a").attr("href") || "",
+ name: loadedCheerio(element).find('.tit').text() || '',
+ cover: loadedCheerio(element).find('.pic > a > img').attr('src'),
+ path: loadedCheerio(element).find('h3 > a').attr('href') || '',
}))
.get()
- .filter((novel) => novel.name && novel.path);
+ .filter(novel => novel.name && novel.path);
return novels;
}
diff --git a/plugins/english/lightnovelpub.ts b/plugins/english/lightnovelpub.ts
index cd7de8c..58932fd 100644
--- a/plugins/english/lightnovelpub.ts
+++ b/plugins/english/lightnovelpub.ts
@@ -1,247 +1,248 @@
-import { load as parseHTML } from "cheerio";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { Plugin } from "@typings/plugin";
-import { Filters, FilterTypes } from "@libs/filterInputs";
-import dayjs from "dayjs";
+import { load as parseHTML } from 'cheerio';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { Plugin } from '@typings/plugin';
+import { Filters, FilterTypes } from '@libs/filterInputs';
+import dayjs from 'dayjs';
class LightNovelPub implements Plugin.PagePlugin {
- id = "lightnovelpub";
- name = "LightNovelPub";
- version = "1.0.1";
- icon = "src/en/lightnovelpub/icon.png";
- site = "https://www.lightnovelpub.com/";
- headers = {
- Accept: "application/json",
- "Content-Type": "application/json",
+ id = 'lightnovelpub';
+ name = 'LightNovelPub';
+ version = '1.0.1';
+ icon = 'src/en/lightnovelpub/icon.png';
+ site = 'https://www.lightnovelpub.com/';
+ headers = {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ };
+
+ async popularNovels(
+ page: number,
+ { filters }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ 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);
+
+ const novels: Plugin.NovelItem[] = [];
+
+ loadedCheerio('.novel-item.ads').remove();
+
+ loadedCheerio('.novel-item').each((idx, ele) => {
+ const novelName = loadedCheerio(ele).find('.novel-title').text().trim();
+ const novelCover = loadedCheerio(ele).find('img').attr('data-src');
+ const novelUrl = loadedCheerio(ele)
+ .find('.novel-title > a')
+ .attr('href')
+ ?.substring(1);
+
+ if (!novelUrl) return;
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl,
+ };
+
+ novels.push(novel);
+ });
+
+ return novels;
+ }
+
+ async parseNovel(
+ novelPath: string,
+ ): Promise {
+ const body = await fetchApi(this.site + novelPath).then(r => r.text());
+
+ 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: [],
};
- async popularNovels(
- page: number,
- { filters }: Plugin.PopularNovelsOptions
- ): Promise {
- let link = `${this.site}browse/`;
- link += `${filters.genres.value}/`;
- link += `${filters.order.value}/`;
- link += `${filters.status.value}/`;
- link += page;
+ novel.genres = loadedCheerio('.categories ul li')
+ .map((a, ex) => loadedCheerio(ex).text().trim())
+ .toArray()
+ .join(',');
- const body = await fetchApi(link).then((r) => r.text());
+ return novel;
+ }
- const loadedCheerio = parseHTML(body);
+ async parsePage(novelPath: string, page: string): Promise {
+ 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 novels: Plugin.NovelItem[] = [];
+ const releaseDate = loadedCheerio(this)
+ .find('.chapter-update')
+ .attr('datetime');
- loadedCheerio(".novel-item.ads").remove();
+ const chapterUrl = loadedCheerio(this)
+ .find('a')
+ .attr('href')
+ ?.substring(1);
+ if (!chapterUrl) return;
- loadedCheerio(".novel-item").each((idx, ele) => {
- const novelName = loadedCheerio(ele)
- .find(".novel-title")
- .text()
- .trim();
- const novelCover = loadedCheerio(ele).find("img").attr("data-src");
- const novelUrl =
- loadedCheerio(ele)
- .find(".novel-title > a")
- .attr("href")
- ?.substring(1);
+ chapter.push({
+ name: chapterName,
+ path: chapterUrl,
+ releaseTime: dayjs(releaseDate).toISOString(),
+ });
+ });
+ const chapters = chapter;
+ return { chapters };
+ }
- if (!novelUrl) return;
- const novel = {
- name: novelName,
- cover: novelCover,
- path: novelUrl,
- };
+ async parseChapter(chapterPath: string): Promise {
+ const body = await fetchApi(this.site + chapterPath).then(r => r.text());
- novels.push(novel);
- });
+ const loadedCheerio = parseHTML(body);
- return novels;
- }
+ let chapterText = loadedCheerio('#chapter-container').html() || '';
- async parseNovel(novelPath: string): Promise {
- const body = await fetchApi(this.site + novelPath).then((r) => r.text());
+ return chapterText;
+ }
- const loadedCheerio = parseHTML(body);
- const totalChapters = parseInt(loadedCheerio(".header-stats span:first strong").text(), 10);
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ 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');
- 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: [],
- };
+ let formData = new FormData();
+ formData.append('inputContent', searchTerm);
- novel.genres = loadedCheerio('.categories ul li')
- .map((a, ex) => loadedCheerio(ex).text().trim())
- .toArray()
- .join(',');
+ const body = await fetchApi(url, {
+ method: 'POST',
+ headers: { LNRequestVerifyToken: verifytoken! },
+ body: formData,
+ }).then(r => r.text());
- return novel;
- }
+ let loadedCheerio = parseHTML(body);
- async parsePage(novelPath: string, page: string): Promise {
- 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();
+ let novels: Plugin.NovelItem[] = [];
- const releaseDate = loadedCheerio(this)
- .find('.chapter-update')
- .attr('datetime');
+ let results = JSON.parse(loadedCheerio('body').text());
- const chapterUrl = loadedCheerio(this).find('a').attr('href')?.substring(1);
- if (!chapterUrl) return;
+ loadedCheerio = parseHTML(results.resultview);
- chapter.push({
- name: chapterName,
- path: chapterUrl,
- releaseTime: dayjs(releaseDate).toISOString(),
- })
- });
- const chapters = chapter;
- return { chapters };
- }
+ loadedCheerio('.novel-item').each((idx, ele) => {
+ const novelName = loadedCheerio(ele).find('h4.novel-title').text().trim();
+ const novelCover = loadedCheerio(ele).find('img').attr('src');
+ const novelUrl = loadedCheerio(ele).find('a').attr('href')?.substring(1);
+ if (!novelUrl) return;
+ novels.push({
+ name: novelName,
+ path: novelUrl,
+ cover: novelCover,
+ });
+ });
- async parseChapter(chapterPath: string): Promise {
- const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
+ return novels;
+ }
- const loadedCheerio = parseHTML(body);
+ async fetchImage(url: string): Promise {
+ return fetchFile(url, { headers: this.headers });
+ }
- let chapterText = loadedCheerio("#chapter-container").html() || '';
-
- return chapterText;
- }
-
- async searchNovels(searchTerm: string, pageNo: number): Promise {
- 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");
-
- let formData = new FormData();
- formData.append("inputContent", searchTerm);
-
- const body = await fetchApi(
- url,
- {
- method: "POST",
- headers: { LNRequestVerifyToken: verifytoken! },
- body: formData,
- },
- ).then((r) => r.text());
-
- let loadedCheerio = parseHTML(body);
-
- let novels: Plugin.NovelItem[] = [];
-
- let results = JSON.parse(loadedCheerio("body").text());
-
- loadedCheerio = parseHTML(results.resultview);
-
- loadedCheerio(".novel-item").each((idx, ele) => {
- const novelName = loadedCheerio(ele)
- .find("h4.novel-title")
- .text()
- .trim();
- const novelCover = loadedCheerio(ele).find("img").attr("src");
- const novelUrl = loadedCheerio(ele).find("a").attr("href")?.substring(1);
- if (!novelUrl) return;
- novels.push({
- name: novelName,
- path: novelUrl,
- cover: novelCover,
- });
- });
-
- return novels;
- }
-
- async fetchImage(url: string): Promise {
- 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;
+ 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();
\ No newline at end of file
+export default new LightNovelPub();
diff --git a/plugins/english/lightnovelworld.ts b/plugins/english/lightnovelworld.ts
index 55e4226..51a4717 100644
--- a/plugins/english/lightnovelworld.ts
+++ b/plugins/english/lightnovelworld.ts
@@ -1,247 +1,248 @@
-import { load as parseHTML } from "cheerio";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { Plugin } from "@typings/plugin";
-import { Filters, FilterTypes } from "@libs/filterInputs";
-import dayjs from "dayjs";
+import { load as parseHTML } from 'cheerio';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { Plugin } from '@typings/plugin';
+import { Filters, FilterTypes } from '@libs/filterInputs';
+import dayjs from 'dayjs';
class LightNovelWorld implements Plugin.PagePlugin {
- id = "lightnovelworld";
- name = "LightNovelWorld";
- site = "https://www.webnovelworld.org/";
- version = "1.0.1";
- icon = "src/en/lightnovelworld/icon.png";
- headers = {
- Accept: "application/json",
- "Content-Type": "application/json",
+ id = 'lightnovelworld';
+ name = 'LightNovelWorld';
+ site = 'https://www.webnovelworld.org/';
+ version = '1.0.1';
+ icon = 'src/en/lightnovelworld/icon.png';
+ headers = {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ };
+
+ async popularNovels(
+ page: number,
+ { filters }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ 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);
+
+ const novels: Plugin.NovelItem[] = [];
+
+ loadedCheerio('.novel-item.ads').remove();
+
+ loadedCheerio('.novel-item').each((idx, ele) => {
+ const novelName = loadedCheerio(ele).find('.novel-title').text().trim();
+ const novelCover = loadedCheerio(ele).find('img').attr('data-src');
+ const novelUrl = loadedCheerio(ele)
+ .find('.novel-title > a')
+ .attr('href')
+ ?.substring(1);
+
+ if (!novelUrl) return;
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl,
+ };
+
+ novels.push(novel);
+ });
+
+ return novels;
+ }
+
+ async parseNovel(
+ novelPath: string,
+ ): Promise {
+ const body = await fetchApi(this.site + novelPath).then(r => r.text());
+
+ 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: [],
};
- async popularNovels(
- page: number,
- { filters }: Plugin.PopularNovelsOptions
- ): Promise {
- let link = `${this.site}browse/`;
- link += `${filters.genres.value}/`;
- link += `${filters.order.value}/`;
- link += `${filters.status.value}/`;
- link += page;
+ novel.genres = loadedCheerio('.categories ul li')
+ .map((a, ex) => loadedCheerio(ex).text().trim())
+ .toArray()
+ .join(',');
- const body = await fetchApi(link).then((r) => r.text());
+ return novel;
+ }
- const loadedCheerio = parseHTML(body);
+ async parsePage(novelPath: string, page: string): Promise {
+ 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 novels: Plugin.NovelItem[] = [];
+ const releaseDate = loadedCheerio(this)
+ .find('.chapter-update')
+ .attr('datetime');
- loadedCheerio(".novel-item.ads").remove();
+ const chapterUrl = loadedCheerio(this)
+ .find('a')
+ .attr('href')
+ ?.substring(1);
+ if (!chapterUrl) return;
- loadedCheerio(".novel-item").each((idx, ele) => {
- const novelName = loadedCheerio(ele)
- .find(".novel-title")
- .text()
- .trim();
- const novelCover = loadedCheerio(ele).find("img").attr("data-src");
- const novelUrl =
- loadedCheerio(ele)
- .find(".novel-title > a")
- .attr("href")
- ?.substring(1);
+ chapter.push({
+ name: chapterName,
+ path: chapterUrl,
+ releaseTime: dayjs(releaseDate).toISOString(),
+ });
+ });
+ const chapters = chapter;
+ return { chapters };
+ }
- if (!novelUrl) return;
- const novel = {
- name: novelName,
- cover: novelCover,
- path: novelUrl,
- };
+ async parseChapter(chapterPath: string): Promise {
+ const body = await fetchApi(this.site + chapterPath).then(r => r.text());
- novels.push(novel);
- });
+ const loadedCheerio = parseHTML(body);
- return novels;
- }
+ let chapterText = loadedCheerio('#chapter-container').html() || '';
- async parseNovel(novelPath: string): Promise {
- const body = await fetchApi(this.site + novelPath).then((r) => r.text());
+ return chapterText;
+ }
- const loadedCheerio = parseHTML(body);
- const totalChapters = parseInt(loadedCheerio(".header-stats span:first strong").text(), 10);
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ 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');
- 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: [],
- };
+ let formData = new FormData();
+ formData.append('inputContent', searchTerm);
- novel.genres = loadedCheerio('.categories ul li')
- .map((a, ex) => loadedCheerio(ex).text().trim())
- .toArray()
- .join(',');
+ const body = await fetchApi(url, {
+ method: 'POST',
+ headers: { LNRequestVerifyToken: verifytoken! },
+ body: formData,
+ }).then(r => r.text());
- return novel;
- }
+ let loadedCheerio = parseHTML(body);
- async parsePage(novelPath: string, page: string): Promise {
- 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();
+ let novels: Plugin.NovelItem[] = [];
- const releaseDate = loadedCheerio(this)
- .find('.chapter-update')
- .attr('datetime');
+ let results = JSON.parse(loadedCheerio('body').text());
- const chapterUrl = loadedCheerio(this).find('a').attr('href')?.substring(1);
- if (!chapterUrl) return;
+ loadedCheerio = parseHTML(results.resultview);
- chapter.push({
- name: chapterName,
- path: chapterUrl,
- releaseTime: dayjs(releaseDate).toISOString(),
- })
- });
- const chapters = chapter;
- return { chapters };
- }
+ loadedCheerio('.novel-item').each((idx, ele) => {
+ const novelName = loadedCheerio(ele).find('h4.novel-title').text().trim();
+ const novelCover = loadedCheerio(ele).find('img').attr('src');
+ const novelUrl = loadedCheerio(ele).find('a').attr('href')?.substring(1);
+ if (!novelUrl) return;
+ novels.push({
+ name: novelName,
+ path: novelUrl,
+ cover: novelCover,
+ });
+ });
- async parseChapter(chapterPath: string): Promise {
- const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
+ return novels;
+ }
- const loadedCheerio = parseHTML(body);
+ async fetchImage(url: string): Promise {
+ return fetchFile(url, { headers: this.headers });
+ }
- let chapterText = loadedCheerio("#chapter-container").html() || '';
-
- return chapterText;
- }
-
- async searchNovels(searchTerm: string, pageNo: number): Promise {
- 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");
-
- let formData = new FormData();
- formData.append("inputContent", searchTerm);
-
- const body = await fetchApi(
- url,
- {
- method: "POST",
- headers: { LNRequestVerifyToken: verifytoken! },
- body: formData,
- },
- ).then((r) => r.text());
-
- let loadedCheerio = parseHTML(body);
-
- let novels: Plugin.NovelItem[] = [];
-
- let results = JSON.parse(loadedCheerio("body").text());
-
- loadedCheerio = parseHTML(results.resultview);
-
- loadedCheerio(".novel-item").each((idx, ele) => {
- const novelName = loadedCheerio(ele)
- .find("h4.novel-title")
- .text()
- .trim();
- const novelCover = loadedCheerio(ele).find("img").attr("src");
- const novelUrl = loadedCheerio(ele).find("a").attr("href")?.substring(1);
- if (!novelUrl) return;
- novels.push({
- name: novelName,
- path: novelUrl,
- cover: novelCover,
- });
- });
-
- return novels;
- }
-
- async fetchImage(url: string): Promise {
- 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;
+ 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();
diff --git a/plugins/english/lnmtl.ts b/plugins/english/lnmtl.ts
index b257446..41a6571 100644
--- a/plugins/english/lnmtl.ts
+++ b/plugins/english/lnmtl.ts
@@ -1,247 +1,245 @@
-import { load as parseHTML } from "cheerio";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { FilterTypes, Filters } from "@libs/filterInputs";
-import { Plugin } from "@typings/plugin";
+import { load as parseHTML } from 'cheerio';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { FilterTypes, Filters } from '@libs/filterInputs';
+import { Plugin } from '@typings/plugin';
class LnMTLPlugin implements Plugin.PagePlugin {
- id = "lnmtl";
- name = "LnMTL";
- icon = "src/en/lnmtl/icon.png";
- site = "https://lnmtl.com/";
- version = "1.0.1";
+ id = 'lnmtl';
+ name = 'LnMTL';
+ icon = 'src/en/lnmtl/icon.png';
+ site = 'https://lnmtl.com/';
+ version = '1.0.1';
- async sleep (ms: number) {
- return new Promise(resolve => setTimeout(resolve, ms))
+ async sleep(ms: number) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+
+ async popularNovels(
+ page: number,
+ { filters }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ let link = this.site + 'novel?';
+ link += `orderBy=${filters.order.value}`;
+ link += `&order=${filters.sort.value}`;
+ link += `&filter=${filters.storyStatus.value}`;
+ link += `&page=${page}`;
+
+ const body = await fetchApi(link).then(result => result.text());
+
+ const loadedCheerio = parseHTML(body);
+ const novels: Plugin.NovelItem[] = [];
+
+ 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,
+ path: novelUrl.replace(this.site, ''),
+ };
+
+ novels.push(novel);
+ });
+
+ return novels;
+ }
+
+ async parseNovel(
+ novelPath: string,
+ ): Promise {
+ 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 = ', '') || '',
+ );
+
+ 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: [],
+ };
+
+ loadedCheerio('.panel-body > dl').each(function () {
+ const detailName = loadedCheerio(this).find('dt').text().trim();
+ const 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()
+ .find('a')
+ .map((i, el) => loadedCheerio(el).text())
+ .toArray()
+ .join(',');
+
+ return novel;
+ }
+
+ async parsePage(novelPath: string, page: string): Promise {
+ 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[] = [];
+
+ 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);
+
+ 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();
+
+ 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 {
+ const result = await fetchApi(this.site + chapterPath);
+ const body = await result.text();
+
+ const loadedCheerio = parseHTML(body);
+
+ loadedCheerio('.original, script').remove();
+ loadedCheerio('sentence.translated').wrap('');
+
+ let chapterText = loadedCheerio('.chapter-body').html()?.replace(/„/g, '“');
+
+ if (!chapterText) {
+ chapterText = loadedCheerio('.alert.alert-warning').text();
}
- async popularNovels(
- page: number,
- { filters }: Plugin.PopularNovelsOptions
- ): Promise {
- let link = this.site + "novel?";
- link += `orderBy=${filters.order.value}`;
- link += `&order=${filters.sort.value}`;
- link += `&filter=${filters.storyStatus.value}`;
- link += `&page=${page}`;
+ return chapterText;
+ }
- const body = await fetchApi(link).then((result) =>
- result.text()
- );
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ const body = await fetchApi(this.site).then(r => r.text());
+ const loadedCheerio = parseHTML(body);
- const loadedCheerio = parseHTML(body);
- const novels: Plugin.NovelItem[] = [];
+ const list = loadedCheerio('footer')
+ .next()
+ .next()
+ .html()
+ ?.match(/prefetch: '\/(.*json)/)![1];
- 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");
+ const search = await fetch(`${this.site}${list}`);
+ const data = await search.json();
- if (!novelUrl) return;
+ const nov = data.filter((res: { name: string }) =>
+ res.name.toLowerCase().includes(searchTerm.toLowerCase()),
+ );
- const novel = {
- name: novelName,
- cover: novelCover,
- path: novelUrl.replace(this.site, ''),
- };
+ const novels: Plugin.NovelItem[] = [];
+ nov.map((res: { name: string; slug: string; image: string }) => {
+ const novelName = res.name;
+ const novelUrl = `novel/${res.slug}`;
+ const novelCover = res.image;
- novels.push(novel);
- });
+ const novel = {
+ path: novelUrl,
+ name: novelName,
+ cover: novelCover,
+ };
+ novels.push(novel);
+ });
+ return novels;
+ }
- return novels;
- };
+ async fetchImage(url: string): Promise {
+ return await fetchFile(url);
+ }
- async parseNovel(novelPath: string): Promise {
- 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 = ", "") || ""
- );
-
- 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: [],
- };
-
- loadedCheerio(".panel-body > dl").each(function () {
- const detailName = loadedCheerio(this).find("dt").text().trim();
- const 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()
- .find('a')
- .map((i,el) => loadedCheerio(el).text())
- .toArray()
- .join(",");
-
- return novel;
- };
-
- async parsePage(novelPath: string, page: string): Promise {
- 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[] = [];
-
- 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);
-
- 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();
-
- 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 {
- const result = await fetchApi(this.site + chapterPath);
- const body = await result.text();
-
- const loadedCheerio = parseHTML(body);
-
- loadedCheerio('.original, script').remove();
- loadedCheerio('sentence.translated').wrap('');
-
- let chapterText = loadedCheerio('.chapter-body').html()?.replace(/„/g, '“');
-
- if (!chapterText) {
- chapterText = loadedCheerio(".alert.alert-warning").text();
- }
-
- return chapterText;
- };
-
- async searchNovels(
- searchTerm: string,
- pageNo: number
- ): Promise {
- const body = await fetchApi(this.site).then((r) => r.text());
- const loadedCheerio = parseHTML(body);
-
- const list = loadedCheerio('footer')
- .next()
- .next()
- .html()
- ?.match(/prefetch: '\/(.*json)/)![1];
-
- const search = await fetch(`${this.site}${list}`);
- const data = await search.json();
-
- const nov = data.filter((res: { name: string; }) =>
- res.name.toLowerCase().includes(searchTerm.toLowerCase()),
- );
-
- const novels: Plugin.NovelItem[] = [];
- nov.map((res: { name: string; slug: string; image: string; }) => {
- const novelName = res.name;
- const novelUrl = `novel/${res.slug}`;
- const novelCover = res.image;
-
- const novel = {
- path: novelUrl,
- name: novelName,
- cover: novelCover,
- };
- novels.push(novel);
- });
- return novels;
- };
-
- async fetchImage(url: string): Promise {
- return await fetchFile(url);
- }
-
- filters = {
- order: {
- value: "favourites",
- label: "Order by",
- options: [
- { label: "Favourites", value: "favourites" },
- { label: "Name", value: "name" },
- { label: "Addition Date", value: "date" },
- ],
- 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: "Finished", value: "finished" },
- ],
- type: FilterTypes.Picker,
- },
- } satisfies Filters;
+ filters = {
+ order: {
+ value: 'favourites',
+ label: 'Order by',
+ options: [
+ { label: 'Favourites', value: 'favourites' },
+ { label: 'Name', value: 'name' },
+ { label: 'Addition Date', value: 'date' },
+ ],
+ 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: 'Finished', value: 'finished' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ } satisfies Filters;
}
export default new LnMTLPlugin();
interface ChapterEntry {
- number: number;
- title: string;
- slug: string;
- created_at: string;
-}
\ No newline at end of file
+ number: number;
+ title: string;
+ slug: string;
+ created_at: string;
+}
diff --git a/plugins/english/mtlnovel.ts b/plugins/english/mtlnovel.ts
index ef04f4e..2d52bd5 100644
--- a/plugins/english/mtlnovel.ts
+++ b/plugins/english/mtlnovel.ts
@@ -1,191 +1,187 @@
-import { load as parseHTML } from "cheerio";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { Plugin } from "@typings/plugin";
-import { Filters, FilterTypes } from "@libs/filterInputs";
+import { load as parseHTML } from 'cheerio';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { Plugin } from '@typings/plugin';
+import { Filters, FilterTypes } from '@libs/filterInputs';
class MTLNovel implements Plugin.PluginBase {
- id = "mtlnovel";
- name = "MTL Novel";
- version = "1.0.0";
- icon = "src/en/mtlnovel/icon.png";
- site = "https://www.mtlnovel.com/";
- async popularNovels(
- page: number,
- { filters }: Plugin.PopularNovelsOptions
- ): Promise {
- 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());
+ id = 'mtlnovel';
+ name = 'MTL Novel';
+ version = '1.0.0';
+ icon = 'src/en/mtlnovel/icon.png';
+ site = 'https://www.mtlnovel.com/';
+ async popularNovels(
+ page: number,
+ { filters }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ 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 loadedCheerio = parseHTML(body);
+ const headers = new Headers();
+ headers.append('Alt-Used', 'www.mtlnovel.com');
+ const body = await fetch(link, { headers }).then(result => result.text());
- const novels: Plugin.NovelItem[] = [];
+ const loadedCheerio = parseHTML(body);
- loadedCheerio("div.box.wide").each((i,el) => {
- const novelName = loadedCheerio(el)
- .find("a.list-title")
- .text()
- .slice(4);
- const novelCover = loadedCheerio(el).find("amp-img").attr("src");
- const novelUrl = loadedCheerio(el).find("a.list-title").attr("href");
+ const novels: Plugin.NovelItem[] = [];
- if (!novelUrl) return;
+ loadedCheerio('div.box.wide').each((i, el) => {
+ const novelName = loadedCheerio(el).find('a.list-title').text().slice(4);
+ const novelCover = loadedCheerio(el).find('amp-img').attr('src');
+ const novelUrl = loadedCheerio(el).find('a.list-title').attr('href');
- const novel = {
- name: novelName,
- cover: novelCover,
- path: novelUrl.replace(this.site, ''),
- };
+ if (!novelUrl) return;
- novels.push(novel);
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl.replace(this.site, ''),
+ };
+
+ novels.push(novel);
+ });
+
+ return novels;
+ }
+ async parseNovel(novelPath: string): Promise {
+ const headers = new Headers();
+ headers.append('Referer', `${this.site}novel-list/`);
+ headers.append('Alt-Used', 'www.mtlnovel.com');
+
+ const body = await fetch(this.site + novelPath, {
+ headers,
+ }).then(result => result.text());
+
+ let loadedCheerio = parseHTML(body);
+
+ const novel: Plugin.SourceNovel = {
+ 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: [],
+ };
+
+ const chapterListUrl = this.site + novelPath + 'chapter-list/';
+
+ const getChapters = async () => {
+ const listResult = await fetch(chapterListUrl, { headers });
+ const listBody = await listResult.text();
+
+ loadedCheerio = parseHTML(listBody);
+
+ const chapter: Plugin.ChapterItem[] = [];
+
+ loadedCheerio('div.ch-list')
+ .find('a.ch-link')
+ .each((i, el) => {
+ const chapterName = loadedCheerio(el).text().replace('~ ', '');
+ const releaseDate = null;
+ const chapterUrl = loadedCheerio(el).attr('href');
+ if (!chapterUrl) return;
+ chapter.push({
+ path: chapterUrl.replace(this.site, ''),
+ name: chapterName,
+ releaseTime: releaseDate,
+ });
});
+ return chapter.reverse();
+ };
- return novels;
+ novel.chapters = await getChapters();
+
+ return novel;
+ }
+
+ async parseChapter(chapterPath: string): Promise {
+ 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);
+
+ const chapterText = loadedCheerio('div.par').html() || '';
+
+ return chapterText;
+ }
+
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ const headers = new Headers();
+ headers.append('Alt-Used', 'www.mtlnovel.com');
+ const searchUrl =
+ 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, { headers });
+ const result = await res.json();
+
+ const novels: Plugin.NovelItem[] = [];
+ interface SearchEntry {
+ title: string;
+ thumbnail: string;
+ permalink: string;
}
- async parseNovel(novelPath: string): Promise {
- const headers = new Headers();
- headers.append('Referer', `${this.site}novel-list/`)
- headers.append('Alt-Used', 'www.mtlnovel.com');
+ result.items[0].results.map((item: SearchEntry) => {
+ const novelName = item.title.replace(/<\/?strong>/g, '');
+ const novelCover = item.thumbnail;
+ const novelUrl = item.permalink.replace(this.site, '');
- const body = await fetch(
- this.site + novelPath,
- {
- headers
- },
- ).then((result) => result.text());
+ const novel = { name: novelName, cover: novelCover, path: novelUrl };
- let loadedCheerio = parseHTML(body);
+ novels.push(novel);
+ });
- const novel: Plugin.SourceNovel = {
- 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: [],
- };
+ return novels;
+ }
+ async fetchImage(url: string): Promise {
+ const headers = new Headers();
+ headers.append('Alt-Used', 'www.mtlnovel.com');
+ return fetchFile(url, { headers });
+ }
- const chapterListUrl = this.site + novelPath + "chapter-list/";
-
- const getChapters = async () => {
- const listResult = await fetch(chapterListUrl, {headers});
- const listBody = await listResult.text();
-
- loadedCheerio = parseHTML(listBody);
-
- const chapter: Plugin.ChapterItem[] = [];
-
- loadedCheerio("div.ch-list")
- .find("a.ch-link")
- .each((i,el) => {
- const chapterName = loadedCheerio(el)
- .text()
- .replace("~ ", "");
- const releaseDate = null;
- const chapterUrl = loadedCheerio(el).attr("href");
- if (!chapterUrl) return;
- chapter.push({
- path: chapterUrl.replace(this.site, ''),
- name: chapterName,
- releaseTime: releaseDate,
- });
-
- });
- return chapter.reverse();
- }
-
- novel.chapters = await getChapters();
-
- return novel;
- }
-
- async parseChapter(chapterPath: string): Promise {
- 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);
-
- const chapterText = loadedCheerio("div.par").html() || '';
-
- return chapterText;
- }
-
- async searchNovels(searchTerm: string, pageNo: number): Promise {
- const headers = new Headers();
- headers.append('Alt-Used', 'www.mtlnovel.com');
- const searchUrl =
- 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, {headers});
- const result = await res.json();
-
- const novels: Plugin.NovelItem[] = [];
- interface SearchEntry {
- title: string;
- thumbnail: string;
- permalink: string;
- }
- result.items[0].results.map((item: SearchEntry) => {
- const novelName = item.title.replace(/<\/?strong>/g, "");
- const novelCover = item.thumbnail;
- const novelUrl = item.permalink.replace(this.site, '');
-
- const novel = { name: novelName, cover: novelCover, path: novelUrl };
-
- novels.push(novel);
- });
-
- return novels;
- }
- async fetchImage(url: string): Promise {
- 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;
+ 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();
diff --git a/plugins/english/mtlreader.ts b/plugins/english/mtlreader.ts
index 163ce85..6dc3932 100644
--- a/plugins/english/mtlreader.ts
+++ b/plugins/english/mtlreader.ts
@@ -1,106 +1,111 @@
-import { CheerioAPI, load as parseHTML } from "cheerio";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { Plugin } from "@typings/plugin";
-import { Filters } from "@libs/filterInputs";
+import { CheerioAPI, load as parseHTML } from 'cheerio';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { Plugin } from '@typings/plugin';
+import { Filters } from '@libs/filterInputs';
class MTLReader implements Plugin.PluginBase {
- id = "mtlreader";
- name = "MTL Reader";
- version = "1.0.0";
- icon = "src/en/mtlreader/icon.png";
- site = "https://mtlreader.com/";
- parseNovels(loadedCheerio: CheerioAPI){
- const novels: Plugin.NovelItem[] = [];
+ id = 'mtlreader';
+ name = 'MTL Reader';
+ version = '1.0.0';
+ icon = 'src/en/mtlreader/icon.png';
+ site = 'https://mtlreader.com/';
+ parseNovels(loadedCheerio: CheerioAPI) {
+ const novels: Plugin.NovelItem[] = [];
- 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");
+ 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;
+ if (!novelUrl) return;
- const novel = {
- name: novelName,
- cover: novelCover,
- path: novelUrl.replace(this.site, ''),
- };
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl.replace(this.site, ''),
+ };
- novels.push(novel);
- });
+ novels.push(novel);
+ });
- return novels;
- }
+ return novels;
+ }
- async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise {
- const url = `${this.site}novels?page=${pageNo}`;
+ async popularNovels(
+ pageNo: number,
+ options: Plugin.PopularNovelsOptions,
+ ): Promise {
+ const url = `${this.site}novels?page=${pageNo}`;
- const body = await fetchApi(url).then((r) => r.text());
+ const body = await fetchApi(url).then(r => r.text());
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
- async parseNovel(novelPath: string): Promise {
- const body = await fetchApi(this.site + novelPath).then((r) => r.text());
+ async parseNovel(novelPath: string): Promise {
+ const body = await fetchApi(this.site + novelPath).then(r => r.text());
- const loadedCheerio = parseHTML(body);
+ const loadedCheerio = parseHTML(body);
- const novel: Plugin.SourceNovel = {
- path: novelPath,
- name: loadedCheerio(".agent-title").text().trim() || "Untitled",
- cover: loadedCheerio(".agent-p-img > img").attr("src"),
- summary: loadedCheerio("#editdescription").text().trim(),
- chapters: [],
- };
+ const novel: Plugin.SourceNovel = {
+ path: novelPath,
+ name: loadedCheerio('.agent-title').text().trim() || 'Untitled',
+ cover: loadedCheerio('.agent-p-img > img').attr('src'),
+ summary: loadedCheerio('#editdescription').text().trim(),
+ chapters: [],
+ };
- novel.author = loadedCheerio("i.fa-user")
- .parent()
- .text()
- .replace("Author: ", "")
- .trim();
+ novel.author = loadedCheerio('i.fa-user')
+ .parent()
+ .text()
+ .replace('Author: ', '')
+ .trim();
- const chapter: Plugin.ChapterItem[] = [];
+ const chapter: Plugin.ChapterItem[] = [];
- loadedCheerio("tr.spaceUnder").each((i,el) => {
- const chapterName = loadedCheerio(el).find("a").text().trim();
- const chapterUrl = loadedCheerio(el).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;
+ if (!chapterUrl) return;
- chapter.push({
- name: chapterName,
- path: chapterUrl.replace(this.site, ''),
- });
- });
+ chapter.push({
+ name: chapterName,
+ path: chapterUrl.replace(this.site, ''),
+ });
+ });
- novel.chapters = chapter;
+ novel.chapters = chapter;
- return novel;
- }
- async parseChapter(chapterPath: string): Promise {
- const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
+ return novel;
+ }
+ async parseChapter(chapterPath: string): Promise {
+ const body = await fetchApi(this.site + chapterPath).then(r => r.text());
- const loadedCheerio = parseHTML(body);
+ const loadedCheerio = parseHTML(body);
- loadedCheerio(".container ins,script,p.mtlreader").remove();
- const chapterText = loadedCheerio(".container").html() || '';
+ loadedCheerio('.container ins,script,p.mtlreader').remove();
+ const chapterText = loadedCheerio('.container').html() || '';
- return chapterText;
- }
- async searchNovels(searchTerm: string, pageNo: number): Promise {
- const body = await fetchApi(this.site).then((r) => r.text());
- const tokenCheerio = parseHTML(body);
- const token = tokenCheerio('input[name="_token"]').attr('value');
+ return chapterText;
+ }
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ const body = await fetchApi(this.site).then(r => r.text());
+ const tokenCheerio = parseHTML(body);
+ const token = tokenCheerio('input[name="_token"]').attr('value');
- 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 {
- return fetchFile(url);
- }
+ 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 {
+ return fetchFile(url);
+ }
}
export default new MTLReader();
diff --git a/plugins/english/novelbin.ts b/plugins/english/novelbin.ts
index 4c98249..676025d 100644
--- a/plugins/english/novelbin.ts
+++ b/plugins/english/novelbin.ts
@@ -1,231 +1,233 @@
-import { CheerioAPI, load as parseHTML } from "cheerio";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { Plugin } from "@typings/plugin";
-import { Filters, FilterTypes } from "@libs/filterInputs";
+import { CheerioAPI, load as parseHTML } from 'cheerio';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { Plugin } from '@typings/plugin';
+import { Filters, FilterTypes } from '@libs/filterInputs';
class NovelBin implements Plugin.PluginBase {
- id = "novelbin";
- name = "Novel Bin";
- icon = "src/en/novelbin/icon.png";
- site = "https://binnovel.com/";
- version = "1.0.0";
- parseNovels(loadedCheerio:CheerioAPI){
- const novels: Plugin.NovelItem[] = [];
+ id = 'novelbin';
+ name = 'Novel Bin';
+ icon = 'src/en/novelbin/icon.png';
+ site = 'https://binnovel.com/';
+ version = '1.0.0';
+ parseNovels(loadedCheerio: CheerioAPI) {
+ const novels: Plugin.NovelItem[] = [];
- 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");
+ 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');
- if (!novelUrl) return;
+ if (!novelUrl) return;
- const novel = {
- name: novelName,
- cover: novelCover,
- path: novelUrl.replace(this.site, ''),
- };
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl.replace(this.site, ''),
+ };
- novels.push(novel);
+ novels.push(novel);
+ });
+
+ return novels;
+ }
+
+ async popularNovels(
+ page: number,
+ { filters }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ 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 {
+ const body = await fetchApi(this.site + novelPath).then(r => r.text());
+
+ let loadedCheerio = parseHTML(body);
+
+ const novel: Plugin.SourceNovel = {
+ 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: [],
+ };
+
+ loadedCheerio('ul.info > li > h3').each(function () {
+ let detailName = loadedCheerio(this).text();
+ let detail = loadedCheerio(this)
+ .siblings()
+ .map((i, el) => loadedCheerio(el).text())
+ .toArray()
+ .join(',');
+
+ switch (detailName) {
+ case 'Author:':
+ novel.author = detail;
+ break;
+ case 'Status:':
+ novel.status = detail;
+ break;
+ case 'Genre:':
+ novel.genres = detail;
+ break;
+ }
+ });
+
+ const novelId = loadedCheerio('#rating').attr('data-novel-id');
+
+ const getChapters = async (id: string) => {
+ const chapterListUrl = this.site + 'ajax/chapter-archive?novelId=' + id;
+
+ const data = await fetchApi(chapterListUrl);
+ const chapterdata = await data.text();
+
+ loadedCheerio = parseHTML(chapterdata);
+
+ const chapter: Plugin.ChapterItem[] = [];
+
+ 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,
+ path: chapterUrl.replace(this.site, ''),
});
+ });
+ return chapter;
+ };
- return novels;
+ if (novelId) {
+ novel.chapters = await getChapters(novelId);
}
- async popularNovels(page: number, {filters}: Plugin.PopularNovelsOptions): Promise {
- let link = this.site;
- if (filters.genre.value !== '')
- link += `gen/${filters.genre.value}`;
- else
- link += `sort/${filters.sort.value}`;
+ return novel;
+ }
- if (filters.complete.value)
- link += `/completed`;
+ async parseChapter(chapterPath: string): Promise {
+ const body = await fetchApi(this.site + chapterPath).then(r => r.text());
- link += `?page=${page}`;
+ const loadedCheerio = parseHTML(body);
- const body = await fetchApi(link).then((r) => r.text());
+ loadedCheerio('#chr-content > div,h6,p[style="display: none;"]').remove();
+ const chapterText = loadedCheerio('#chr-content').html() || '';
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
+ return chapterText;
+ }
- async parseNovel(novelPath: string): Promise {
- const body = await fetchApi(this.site + novelPath).then((r) => r.text());
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ const url = `${this.site}search/?keyword=${searchTerm}`;
+ const body = await fetchApi(url).then(r => r.text());
- let loadedCheerio = parseHTML(body);
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
- const novel: Plugin.SourceNovel = {
- 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: [],
- };
+ async fetchImage(url: string): Promise {
+ return fetchFile(url);
+ }
- loadedCheerio("ul.info > li > h3").each(function () {
- let detailName = loadedCheerio(this).text();
- let detail = loadedCheerio(this)
- .siblings()
- .map((i, el) => loadedCheerio(el).text())
- .toArray()
- .join(",");
-
- switch (detailName) {
- case "Author:":
- novel.author = detail;
- break;
- case "Status:":
- novel.status = detail;
- break;
- case "Genre:":
- novel.genres = detail;
- break;
- }
- });
-
- const novelId = loadedCheerio("#rating").attr("data-novel-id");
-
- const getChapters = async (id: string) => {
- const chapterListUrl =
- this.site + "ajax/chapter-archive?novelId=" + id;
-
- const data = await fetchApi(chapterListUrl);
- const chapterdata = await data.text();
-
- loadedCheerio = parseHTML(chapterdata);
-
- const chapter: Plugin.ChapterItem[] = [];
-
- 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,
- path: chapterUrl.replace(this.site, ''),
- });
- });
- return chapter;
- }
-
- if (novelId) {
- novel.chapters = await getChapters(novelId);
- }
-
- return novel;
- }
-
- async parseChapter(chapterPath: string): Promise {
- 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();
- const chapterText = loadedCheerio("#chr-content").html() || '';
-
- return chapterText;
- }
-
- async searchNovels(searchTerm: string, pageNo: number): Promise {
- const url = `${this.site}search/?keyword=${searchTerm}`;
- const body = await fetchApi(url).then((r) => r.text());
-
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
-
- async fetchImage(url: string): Promise {
- 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;
+ 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();
diff --git a/plugins/english/novelbuddy.ts b/plugins/english/novelbuddy.ts
index 7bce39a..f4352df 100644
--- a/plugins/english/novelbuddy.ts
+++ b/plugins/english/novelbuddy.ts
@@ -1,171 +1,178 @@
-import { CheerioAPI, load as parseHTML } from "cheerio";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { Plugin } from "@typings/plugin";
-import { Filters } from "@libs/filterInputs";
+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
+ 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[] = [];
+ 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);
+ 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;
+ if (!novelUrl) return;
- const novel = { name: novelName, cover: novelCover, path: novelUrl };
+ const novel = { name: novelName, cover: novelCover, path: novelUrl };
- novels.push(novel);
+ novels.push(novel);
+ });
+
+ return novels;
+ }
+
+ async popularNovels(
+ pageNo: number,
+ options: Plugin.PopularNovelsOptions,
+ ): Promise {
+ 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 {
+ 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();
+
+ 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;
+ };
- return novels;
- }
+ novel.chapters = (await getChapters(novelId)).reverse();
- async popularNovels(
- pageNo: number,
- options: Plugin.PopularNovelsOptions
- ): Promise {
- const url = `${this.site}popular?page=${pageNo}`;
+ return novel;
+ }
- const result = await fetchApi(url);
- const body = await result.text();
+ async parseChapter(chapterPath: string): Promise {
+ const result = await fetchApi(this.site + chapterPath);
+ const body = await result.text();
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
+ const loadedCheerio = parseHTML(body);
- async parseNovel(novelPath: string): Promise {
- const result = await fetchApi(this.site + novelPath);
- const body = await result.text();
+ loadedCheerio('#listen-chapter').remove();
+ loadedCheerio('#google_translate_element').remove();
- let loadedCheerio = parseHTML(body);
+ const chapterText = loadedCheerio('.chapter__content').html() || '';
- 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: [],
- };
+ return chapterText;
+ }
- 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[] = [];
+ async searchNovels(
+ searchTerm: string,
+ page: number,
+ ): Promise {
+ const url = `${this.site}search?q=${searchTerm}&page=${page}`;
- 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();
+ const result = await fetchApi(url);
+ const body = await result.text();
- loadedCheerio = parseHTML(chapterlist);
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
- 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 {
- 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 {
- 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 {
- return fetchFile(url);
- }
-
+ async fetchImage(url: string): Promise {
+ return fetchFile(url);
+ }
}
export default new NovelBuddy();
diff --git a/plugins/english/novelfull.ts b/plugins/english/novelfull.ts
index e0acfe6..e0b3c80 100644
--- a/plugins/english/novelfull.ts
+++ b/plugins/english/novelfull.ts
@@ -1,200 +1,201 @@
-import { CheerioAPI, load as parseHTML } from "cheerio";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { Plugin } from "@typings/plugin";
-import { Filters, FilterTypes } from "@libs/filterInputs";
-
+import { CheerioAPI, load as parseHTML } from 'cheerio';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { Plugin } from '@typings/plugin';
+import { Filters, FilterTypes } from '@libs/filterInputs';
class NovelFull implements Plugin.PluginBase {
- id = "novelfull";
- name = "NovelFull";
- version = "1.0.0";
- icon = "src/en/novelfull/icon.png";
- site = "https://novelfull.com/";
+ id = 'novelfull';
+ name = 'NovelFull';
+ version = '1.0.0';
+ icon = 'src/en/novelfull/icon.png';
+ site = 'https://novelfull.com/';
- parseNovels(loadedCheerio:CheerioAPI){
- const novels: Plugin.NovelItem[] = [];
+ parseNovels(loadedCheerio: CheerioAPI) {
+ const novels: Plugin.NovelItem[] = [];
- loadedCheerio(".col-truyen-main .list-truyen .row").each((idx, ele) => {
- const novelName = loadedCheerio(ele)
- .find("h3.truyen-title > a")
- .text();
+ loadedCheerio('.col-truyen-main .list-truyen .row').each((idx, ele) => {
+ const novelName = loadedCheerio(ele).find('h3.truyen-title > a').text();
- const novelCover =
- this.site + loadedCheerio(ele).find("img").attr("src")?.slice(1);
+ const novelCover =
+ this.site + loadedCheerio(ele).find('img').attr('src')?.slice(1);
- const novelUrl = 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;
+ if (!novelUrl) return;
- const novel = {
- name: novelName,
- cover: novelCover,
- path: novelUrl,
- };
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl,
+ };
- novels.push(novel);
+ novels.push(novel);
+ });
+
+ return novels;
+ }
+
+ async popularNovels(
+ page: number,
+ { filters }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ 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 {
+ const result = await fetchApi(this.site + novelPath);
+ const body = await result.text();
+
+ let loadedCheerio = parseHTML(body);
+
+ const novel: Plugin.SourceNovel = {
+ 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.author = loadedCheerio('h3:contains("Author")')
+ .parent()
+ .contents()
+ .text()
+ .replace('Author:', '');
+
+ novel.genres = loadedCheerio('h3:contains("Genre")')
+ .siblings()
+ .map((i, el) => loadedCheerio(el).text())
+ .toArray()
+ .join(',');
+
+ const novelId = loadedCheerio('#rating').attr('data-novel-id')!;
+ const chapter: Plugin.ChapterItem[] = [];
+
+ 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);
+
+ loadedCheerio('select > option').each(function () {
+ const chapterName = loadedCheerio(this).text();
+ const chapterUrl = loadedCheerio(this).attr('value')?.slice(1);
+ if (!chapterUrl) return;
+
+ chapter.push({
+ name: chapterName,
+ path: chapterUrl,
});
+ });
+ return chapter;
+ };
- return novels;
- }
+ novel.chapters = await getChapters(novelId);
- async popularNovels(page: number, {filters}: Plugin.PopularNovelsOptions): Promise {
- let link = this.site;
- if (filters.genre.value !== '')
- link += `genre/${filters.genre.value}`;
- else
- link += filters.sort.value
+ return novel;
+ }
- link += `?page=${page}`;
+ async parseChapter(chapterPath: string): Promise {
+ const result = await fetchApi(this.site + chapterPath);
+ const body = await result.text();
- const body = await fetchApi(link).then((r) => r.text());
+ const loadedCheerio = parseHTML(body);
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
+ loadedCheerio('#chapter-content div.ads').remove();
+ const chapterText = loadedCheerio('#chapter-content').html() || '';
- async parseNovel(novelPath: string): Promise {
- const result = await fetchApi(this.site + novelPath);
- const body = await result.text();
+ return chapterText;
+ }
- let loadedCheerio = parseHTML(body);
+ async searchNovels(
+ searchTerm: string,
+ page: number,
+ ): Promise {
+ const searchUrl = `${this.site}search?keyword=${searchTerm}&page=${page}`;
- const novel: Plugin.SourceNovel = {
- 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: [],
- };
+ const result = await fetchApi(searchUrl);
+ const body = await result.text();
- novel.author = loadedCheerio('h3:contains("Author")')
- .parent()
- .contents()
- .text()
- .replace("Author:", "");
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
- novel.genres = loadedCheerio('h3:contains("Genre")')
- .siblings()
- .map((i, el) => loadedCheerio(el).text())
- .toArray()
- .join(",");
+ async fetchImage(url: string): Promise {
+ return fetchFile(url);
+ }
- const novelId = loadedCheerio("#rating").attr("data-novel-id")!;
- const chapter: Plugin.ChapterItem[] = [];
-
- 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);
-
- loadedCheerio("select > option").each(function () {
- const chapterName = loadedCheerio(this).text();
- const chapterUrl = loadedCheerio(this).attr("value")?.slice(1);
- if (!chapterUrl) return;
-
- chapter.push({
- name: chapterName,
- path: chapterUrl,
- });
- });
- return chapter;
- }
-
- novel.chapters = await getChapters(novelId);
-
- return novel;
- }
-
- async parseChapter(chapterPath: string): Promise {
- const result = await fetchApi(this.site + chapterPath);
- const body = await result.text();
-
- const loadedCheerio = parseHTML(body);
-
- loadedCheerio("#chapter-content div.ads").remove();
- const chapterText = loadedCheerio("#chapter-content").html() || '';
-
- return chapterText;
- }
-
- async searchNovels(searchTerm: string, page: number): Promise {
- const searchUrl = `${this.site}search?keyword=${searchTerm}&page=${page}`;
-
- const result = await fetchApi(searchUrl);
- const body = await result.text();
-
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
-
- async fetchImage(url: string): Promise {
- 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;
+ 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();
diff --git a/plugins/english/novelhall.ts b/plugins/english/novelhall.ts
index 3d75180..9a0d3c4 100644
--- a/plugins/english/novelhall.ts
+++ b/plugins/english/novelhall.ts
@@ -1,128 +1,133 @@
-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";
+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; //TODO: Filters Requires hideOnSelect
- site = "https://novelhall.com/";
+ id = 'novelhall';
+ name = 'Novel Hall';
+ version = '1.0.0';
+ icon = 'src/en/novelhall/icon.png';
+ filters?: Filters | undefined; //TODO: Filters Requires hideOnSelect
+ site = 'https://novelhall.com/';
- async popularNovels(page: number, options: Plugin.PopularNovelsOptions): Promise {
- const url = `${this.site}all2022-${page}.html`;
+ async popularNovels(
+ page: number,
+ options: Plugin.PopularNovelsOptions,
+ ): Promise {
+ const url = `${this.site}all2022-${page}.html`;
- const body = await fetchApi(url).then((r) => r.text());
-
- const loadedCheerio = parseHTML(body);
+ const body = await fetchApi(url).then(r => r.text());
- const novels: Plugin.NovelItem[] = [];
+ const loadedCheerio = parseHTML(body);
- loadedCheerio("li.btm").each((idx, ele) => {
- const novelName = loadedCheerio(ele).text().trim()
- const novelUrl = loadedCheerio(ele).find("a").attr("href");
- if(!novelUrl) return;
+ const novels: Plugin.NovelItem[] = [];
- const novel = {
- name: novelName,
- cover: defaultCover,
- path: novelUrl,
- };
+ loadedCheerio('li.btm').each((idx, ele) => {
+ const novelName = loadedCheerio(ele).text().trim();
+ const novelUrl = loadedCheerio(ele).find('a').attr('href');
+ if (!novelUrl) return;
- novels.push(novel);
- });
+ const novel = {
+ name: novelName,
+ cover: defaultCover,
+ path: novelUrl,
+ };
- return novels;
- }
- async parseNovel(novelPath: string): Promise {
- const body = await fetchApi(this.site + novelPath).then((r) => r.text());
+ novels.push(novel);
+ });
- const loadedCheerio = parseHTML(body);
+ return novels;
+ }
+ async parseNovel(novelPath: string): Promise {
+ const body = await fetchApi(this.site + novelPath).then(r => r.text());
- const novel: Plugin.SourceNovel = {
- path: novelPath,
- name: loadedCheerio(".book-info > h1").text() || 'Untitled',
- cover: loadedCheerio('meta[property="og:image"]').attr('content'),
- summary: loadedCheerio(".intro").text().trim(),
- chapters: [],
- };
+ const loadedCheerio = parseHTML(body);
- loadedCheerio('.total').find('p').remove();
- novel.author = loadedCheerio('.total span:contains("Author")')
- .text()
- .replace("Author:", "")
- .trim();
+ const novel: Plugin.SourceNovel = {
+ path: novelPath,
+ name: loadedCheerio('.book-info > h1').text() || 'Untitled',
+ cover: loadedCheerio('meta[property="og:image"]').attr('content'),
+ summary: loadedCheerio('.intro').text().trim(),
+ chapters: [],
+ };
- novel.status = loadedCheerio('.total span:contains("Status")')
- .text()
- .replace("Status:", "")
- .replace("Active", "Ongoing")
- .trim();
+ loadedCheerio('.total').find('p').remove();
+ novel.author = loadedCheerio('.total span:contains("Author")')
+ .text()
+ .replace('Author:', '')
+ .trim();
- novel.genres = loadedCheerio('.total a')
- .map((a,ex) => loadedCheerio(ex).text())
- .toArray()
- .join(',');
+ novel.status = loadedCheerio('.total span:contains("Status")')
+ .text()
+ .replace('Status:', '')
+ .replace('Active', 'Ongoing')
+ .trim();
- const chapter: Plugin.ChapterItem[] = [];
+ novel.genres = loadedCheerio('.total a')
+ .map((a, ex) => loadedCheerio(ex).text())
+ .toArray()
+ .join(',');
- loadedCheerio("#morelist ul > li").each((idx, ele) => {
- const chapterName = loadedCheerio(ele).find("a").text().trim();
- const chapterUrl = loadedCheerio(ele).find("a").attr("href");
- if (!chapterUrl) return;
+ const chapter: Plugin.ChapterItem[] = [];
- chapter.push({
- name: chapterName,
- path: chapterUrl,
- });
- });
+ loadedCheerio('#morelist ul > li').each((idx, ele) => {
+ const chapterName = loadedCheerio(ele).find('a').text().trim();
+ const chapterUrl = loadedCheerio(ele).find('a').attr('href');
+ if (!chapterUrl) return;
- novel.chapters = chapter;
- return novel;
- }
+ chapter.push({
+ name: chapterName,
+ path: chapterUrl,
+ });
+ });
- async parseChapter(chapterPath: string): Promise {
- const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
- const loadedCheerio = parseHTML(body);
- const chapterText = loadedCheerio(".content").html() || '';
- return chapterText;
- }
+ novel.chapters = chapter;
+ return novel;
+ }
- async searchNovels(searchTerm: string, pageNo: number): Promise {
- 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);
+ async parseChapter(chapterPath: string): Promise {
+ const body = await fetchApi(this.site + chapterPath).then(r => r.text());
+ const loadedCheerio = parseHTML(body);
+ const chapterText = loadedCheerio('.content').html() || '';
+ return chapterText;
+ }
- const novels: Plugin.NovelItem[] = [];
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ 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);
- loadedCheerio("#article_list_content > li").each((idx, ele) => {
- const novelName = loadedCheerio(ele)
- .find("h3")
- .text()
- .replace(/\t+/g, "")
- .replace(/\n/g, " ");
- const novelCover = loadedCheerio(ele).find("img").attr("data-src");
- const novelUrl = loadedCheerio(ele).find("a").attr("href")?.slice(1);
- if (!novelUrl) return;
+ const novels: Plugin.NovelItem[] = [];
- const novel = {
- name: novelName,
- cover: novelCover,
- path: novelUrl,
- };
+ loadedCheerio('#article_list_content > li').each((idx, ele) => {
+ const novelName = loadedCheerio(ele)
+ .find('h3')
+ .text()
+ .replace(/\t+/g, '')
+ .replace(/\n/g, ' ');
+ const novelCover = loadedCheerio(ele).find('img').attr('data-src');
+ const novelUrl = loadedCheerio(ele).find('a').attr('href')?.slice(1);
+ if (!novelUrl) return;
- novels.push(novel);
- });
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl,
+ };
- return novels;
- }
- async fetchImage(url: string): Promise {
- return fetchFile(url);
- }
+ novels.push(novel);
+ });
+ return novels;
+ }
+ async fetchImage(url: string): Promise {
+ return fetchFile(url);
+ }
}
export default new NovelHall();
diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts
index 5be0dfc..8fd57ff 100644
--- a/plugins/english/novelupdates.ts
+++ b/plugins/english/novelupdates.ts
@@ -1,463 +1,470 @@
-import { CheerioAPI, load as parseHTML } from "cheerio";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { Filters, FilterTypes } from "@libs/filterInputs";
-import { Plugin } from "@typings/plugin";
+import { CheerioAPI, load as parseHTML } from 'cheerio';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { Filters, FilterTypes } from '@libs/filterInputs';
+import { Plugin } from '@typings/plugin';
class NovelUpdates implements Plugin.PluginBase {
- id = "novelupdates";
- name = "Novel Updates";
- version = "0.5.1";
- icon = "src/en/novelupdates/icon.png";
- site = "https://www.novelupdates.com/";
+ id = 'novelupdates';
+ name = 'Novel Updates';
+ version = '0.5.1';
+ icon = 'src/en/novelupdates/icon.png';
+ site = 'https://www.novelupdates.com/';
- parseNovels(loadedCheerio: CheerioAPI){
- const novels: Plugin.NovelItem[] = [];
+ parseNovels(loadedCheerio: CheerioAPI) {
+ const novels: Plugin.NovelItem[] = [];
- loadedCheerio("div.search_main_box_nu").each((idx, ele) => {
- const novelCover = loadedCheerio(ele).find("img").attr("src");
- const novelName = loadedCheerio(ele).find(".search_title > a").text();
- const novelUrl = loadedCheerio(ele)
- .find(".search_title > a")
- .attr("href");
+ loadedCheerio('div.search_main_box_nu').each((idx, ele) => {
+ const novelCover = loadedCheerio(ele).find('img').attr('src');
+ const novelName = loadedCheerio(ele).find('.search_title > a').text();
+ const novelUrl = loadedCheerio(ele)
+ .find('.search_title > a')
+ .attr('href');
- if (!novelUrl) return;
+ if (!novelUrl) return;
- const novel = {
- name: novelName,
- cover: novelCover,
- path: novelUrl.replace(this.site, ''),
- };
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl.replace(this.site, ''),
+ };
- novels.push(novel);
- });
+ novels.push(novel);
+ });
- return novels;
+ return novels;
+ }
+
+ async popularNovels(
+ page: number,
+ {
+ showLatestNovels,
+ filters,
+ }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ let link = `${this.site}`;
+ if (
+ filters.language.value.length ||
+ filters.novelType.value.length ||
+ filters.genres.value.include?.length ||
+ filters.genres.value.exclude?.length ||
+ filters.storyStatus.value !== ''
+ ) {
+ link += 'series-finder/?sf=1';
+ } else if (showLatestNovels) {
+ link += 'latest-series/?st=1';
+ } else {
+ link += 'series-ranking/?rank=week';
}
- async popularNovels(page: number, { showLatestNovels, filters }: Plugin.PopularNovelsOptions): Promise {
- let link = `${this.site}`;
- if (filters.language.value.length ||
- filters.novelType.value.length ||
- filters.genres.value.include?.length ||
- filters.genres.value.exclude?.length ||
- filters.storyStatus.value !== '' ){
- link += "series-finder/?sf=1"
- } else if (showLatestNovels){
- link += "latest-series/?st=1"
- } else {
- link += "series-ranking/?rank=week"
- };
-
- if (filters.language.value.length)
- link += '&org=' + filters.language.value.join(',');
-
- if (filters.novelType.value.length)
- link += '&nt=' + filters.novelType.value.join(',');
+ if (filters.language.value.length)
+ link += '&org=' + filters.language.value.join(',');
- if (filters.genres.value.include?.length)
- link += '&gi=' + filters.genres.value.include.join(',');
+ if (filters.novelType.value.length)
+ link += '&nt=' + filters.novelType.value.join(',');
- if (filters.genres.value.exclude?.length)
- link += '&ge=' + filters.genres.value.exclude.join(',');
+ if (filters.genres.value.include?.length)
+ link += '&gi=' + filters.genres.value.include.join(',');
- if (filters.genres.value.include?.length || filters.genres.value.exclude?.length)
- link += '&mgi=' + filters.genre_operator.value;
+ if (filters.genres.value.exclude?.length)
+ link += '&ge=' + filters.genres.value.exclude.join(',');
- if (filters.storyStatus.value.length)
- link += '&ss=' + filters.storyStatus.value;
+ if (
+ filters.genres.value.include?.length ||
+ filters.genres.value.exclude?.length
+ )
+ link += '&mgi=' + filters.genre_operator.value;
- link += '&sort=' + filters.sort.value;
+ if (filters.storyStatus.value.length)
+ link += '&ss=' + filters.storyStatus.value;
- link += '&order=' + filters.order.value;
+ link += '&sort=' + filters.sort.value;
- link += '&pg=' + page;
+ link += '&order=' + filters.order.value;
- const body = await fetchApi(link).then((result) =>
- result.text()
- );
+ link += '&pg=' + page;
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
+ const body = await fetchApi(link).then(result => result.text());
+
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
+
+ async parseNovel(novelPath: string): Promise {
+ const url = this.site + novelPath;
+ const result = await fetchApi(url);
+ const body = await result.text();
+
+ let loadedCheerio = parseHTML(body);
+
+ const novel: Plugin.SourceNovel = {
+ path: novelPath,
+ name: loadedCheerio('.seriestitlenu').text() || 'Untitled',
+ cover: loadedCheerio('.wpb_wrapper img').attr('src'),
+ chapters: [],
+ };
+
+ novel.author = loadedCheerio('#authtag')
+ .map((i, el) => loadedCheerio(el).text().trim())
+ .toArray()
+ .join(', ');
+
+ novel.genres = loadedCheerio('#seriesgenre')
+ .children('a')
+ .map((i, el) => loadedCheerio(el).text())
+ .toArray()
+ .join(',');
+
+ novel.status = loadedCheerio('#editstatus').text().includes('Ongoing')
+ ? 'Ongoing'
+ : 'Completed';
+
+ const type = loadedCheerio('#showtype').text().trim();
+
+ const summary = loadedCheerio('#editdescription').text().trim();
+
+ novel.summary = summary + `\n\nType: ${type}`;
+
+ const chapter: Plugin.ChapterItem[] = [];
+
+ const novelId = loadedCheerio('input#mypostid').attr('value')!;
+
+ const formData = new FormData();
+ formData.append('action', 'nd_getchapters');
+ formData.append('mygrr', '0');
+ formData.append('mypostid', novelId);
+
+ const link = `${this.site}wp-admin/admin-ajax.php`;
+
+ const text = await fetchApi(link, {
+ method: 'POST',
+ body: formData,
+ }).then(data => data.text());
+
+ loadedCheerio = parseHTML(text);
+
+ loadedCheerio('li.sp_li_chp').each((i, el) => {
+ const chapterName = loadedCheerio(el).text().trim();
+ const chapterUrl =
+ 'https:' + loadedCheerio(el).find('a').first().next().attr('href');
+
+ chapter.push({
+ name: chapterName,
+ path: chapterUrl.replace(this.site, ''),
+ });
+ });
+
+ novel.chapters = chapter.reverse();
+
+ return novel;
+ }
+
+ getLocation(href: string) {
+ var match = href.match(
+ /^(https?:)\/\/(([^:/?#]*)(?::([0-9]+))?)([/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/,
+ );
+ return match && `${match[1]}//${match[3]}`;
+ }
+
+ async parseChapter(chapterPath: string): Promise {
+ let chapterText = '';
+
+ const result = await fetchApi(this.site + chapterPath);
+ const body = await result.text();
+
+ // console.log(result.chapterUrl);
+
+ // console.log('Redirected URL: ', result.chapterUrl);
+
+ const loadedCheerio = parseHTML(body);
+
+ let isWuxiaWorld = result.url.toLowerCase().includes('wuxiaworld');
+
+ let isBlogspot = result.url.toLowerCase().includes('blogspot');
+
+ let isTumblr = result.url.toLowerCase().includes('tumblr');
+
+ let isWattpad = result.url.toLowerCase().includes('wattpad');
+
+ let isAnomalously = result.url.toLowerCase().includes('anotivereads');
+
+ let kofi = result.url.toLowerCase().includes('ko-fi');
+
+ let isBlossomTranslation = result.url
+ .toLowerCase()
+ .includes('blossomtranslation');
+
+ let isLightNovelsTls = result.url
+ .toLowerCase()
+ .includes('lightnovelstranslations');
+
+ let isiNovelTranslation = result.url
+ .toLowerCase()
+ .includes('inoveltranslation');
+
+ let isTravisTranslation = result.url
+ .toLowerCase()
+ .includes('travistranslations');
+
+ /**
+ * Checks if its a wordpress site
+ */
+ let isWordPressStr =
+ loadedCheerio('meta[name="generator"]').attr('content') ||
+ loadedCheerio('footer').text();
+
+ let isWordPress = false;
+
+ if (isWordPressStr) {
+ isWordPress =
+ isWordPressStr.toLowerCase().includes('wordpress') ||
+ isWordPressStr.includes('Site Kit by Google') ||
+ loadedCheerio('.powered-by').text().toLowerCase().includes('wordpress');
}
- async parseNovel(novelPath: string): Promise {
- const url = this.site + novelPath;
- const result = await fetchApi(url);
- const body = await result.text();
+ let isRainOfSnow = result.url.toLowerCase().includes('rainofsnow');
- let loadedCheerio = parseHTML(body);
+ let isWebNovel = result.url.toLowerCase().includes('webnovel');
- const novel: Plugin.SourceNovel = {
- path: novelPath,
- name: loadedCheerio(".seriestitlenu").text() || "Untitled",
- cover: loadedCheerio('.wpb_wrapper img').attr('src'),
- chapters: [],
- };
+ let isHostedNovel = result.url.toLowerCase().includes('hostednovel');
- novel.author = loadedCheerio('#authtag')
- .map((i, el) => loadedCheerio(el).text().trim())
- .toArray()
- .join(', ');
+ let isScribbleHub = result.url.toLowerCase().includes('scribblehub');
- novel.genres = loadedCheerio("#seriesgenre")
- .children("a")
- .map((i, el) => loadedCheerio(el).text())
- .toArray()
- .join(",");
+ if (isWuxiaWorld) {
+ chapterText = loadedCheerio('#chapter-content').html()!;
+ } else if (isRainOfSnow) {
+ chapterText = loadedCheerio('div.content').html()!;
+ } else if (isTumblr) {
+ chapterText = loadedCheerio('.post').html()!;
+ } else if (isBlogspot) {
+ loadedCheerio('.post-share-buttons').remove();
+ chapterText =
+ loadedCheerio('.entry-content').html() ||
+ loadedCheerio('#post-body').html()!;
+ } else if (isHostedNovel) {
+ chapterText = loadedCheerio('.chapter').html()!;
+ } else if (isScribbleHub) {
+ chapterText = loadedCheerio('div.chp_raw').html()!;
+ } else if (isWattpad) {
+ chapterText = loadedCheerio('.container pre').html()!;
+ } else if (isTravisTranslation) {
+ chapterText = loadedCheerio('.reader-content').html()!;
+ } else if (isLightNovelsTls) {
+ chapterText = loadedCheerio('.text_story').html()!;
+ } else if (isBlossomTranslation) {
+ chapterText = loadedCheerio('.manga-child-content').html()!;
+ } else if (isAnomalously) {
+ chapterText = loadedCheerio('#comic').html()!;
+ } else if (kofi) {
+ chapterText = loadedCheerio('script:contains("shadowDom.innerHTML")')
+ .html()
+ ?.match(/shadowDom\.innerHTML \+= '( r.json());
+ chapterText = json.content.replace(/\n/g, '
');
+ if (json.notes) {
+ chapterText +=
+ '
TL Notes:
' + json.notes.replace(/\n/g, '
');
+ }
+ } else if (isWordPress) {
+ /**
+ * Remove wordpress bloat tags
+ */
- novel.status = loadedCheerio("#editstatus").text().includes("Ongoing")
- ? "Ongoing"
- : "Completed";
+ const bloatClasses = [
+ '.c-ads',
+ '#madara-comments',
+ '#comments',
+ '.content-comments',
+ '.sharedaddy',
+ '.wp-dark-mode-switcher',
+ '.wp-next-post-navi',
+ '.wp-block-buttons',
+ '.wp-block-columns',
+ '.post-cats',
+ '.sidebar',
+ '.author-avatar',
+ '.ezoic-ad',
+ ];
- const type = loadedCheerio("#showtype").text().trim();
+ bloatClasses.map(tag => loadedCheerio(tag).remove());
- const summary = loadedCheerio("#editdescription").text().trim();
+ chapterText =
+ loadedCheerio('.entry-content').html() ||
+ loadedCheerio('.single_post').html() ||
+ loadedCheerio('.post-entry').html() ||
+ loadedCheerio('.main-content').html() ||
+ loadedCheerio('article.post').html() ||
+ loadedCheerio('.content').html() ||
+ loadedCheerio('#content').html() ||
+ loadedCheerio('.page-body').html() ||
+ loadedCheerio('.td-page-content').html()!;
+ } else if (isWebNovel) {
+ chapterText = loadedCheerio('.cha-words').html()!;
- novel.summary = summary + `\n\nType: ${type}`;
+ if (!chapterText) {
+ chapterText = loadedCheerio('._content').html()!;
+ }
+ } else {
+ /**
+ * Remove unnecessary tags
+ */
+ const tags = ['nav', 'header', 'footer', '.hidden'];
- const chapter: Plugin.ChapterItem[] = [];
+ tags.map(tag => loadedCheerio(tag).remove());
- const novelId = loadedCheerio("input#mypostid").attr("value")!;
-
- const formData = new FormData();
- formData.append("action", "nd_getchapters");
- formData.append("mygrr", "0");
- formData.append("mypostid", novelId);
-
- const link = `${this.site}wp-admin/admin-ajax.php`;
-
- const text = await fetchApi(
- link,
- {
- method: "POST",
- body: formData,
- },
- ).then((data) => data.text());
-
- loadedCheerio = parseHTML(text);
-
- loadedCheerio("li.sp_li_chp").each((i,el) => {
- const chapterName = loadedCheerio(el).text().trim();
- const chapterUrl =
- "https:" +
- loadedCheerio(el).find("a").first().next().attr("href");
-
- chapter.push({
- name: chapterName,
- path: chapterUrl.replace(this.site, ''),
- });
- });
-
- novel.chapters = chapter.reverse();
-
- return novel;
+ chapterText = loadedCheerio('body').html()!;
}
- getLocation(href: string) {
- var match = href.match(
- /^(https?:)\/\/(([^:/?#]*)(?::([0-9]+))?)([/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/
- );
- return match && `${match[1]}//${match[3]}`;
+ if (chapterText) {
+ /**
+ * Convert relative urls to absolute
+ */
+ chapterText = chapterText.replace(
+ /href="\//g,
+ `href="${this.getLocation(result.url)}/`,
+ );
}
- async parseChapter(chapterPath: string): Promise {
- let chapterText = "";
+ return chapterText;
+ }
- const result = await fetchApi(
- this.site + chapterPath,
- );
- const body = await result.text();
+ async searchNovels(
+ searchTerm: string,
+ page: number,
+ ): Promise {
+ const url =
+ this.site +
+ '?s=' +
+ searchTerm.replace(/\s+/g, '+') +
+ '&post_type=seriesplans';
+ const result = await fetchApi(url);
+ const body = await result.text();
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
- // console.log(result.chapterUrl);
+ fetchImage(url: string): Promise {
+ return fetchFile(url);
+ }
- // console.log('Redirected URL: ', result.chapterUrl);
-
- const loadedCheerio = parseHTML(body);
-
- let isWuxiaWorld = result.url.toLowerCase().includes("wuxiaworld");
-
- let isBlogspot = result.url.toLowerCase().includes("blogspot");
-
- let isTumblr = result.url.toLowerCase().includes("tumblr");
-
- let isWattpad = result.url.toLowerCase().includes("wattpad");
-
- let isAnomalously = result.url.toLowerCase().includes('anotivereads');
-
- let kofi = result.url.toLowerCase().includes('ko-fi');
-
- let isBlossomTranslation = result.url
- .toLowerCase()
- .includes('blossomtranslation');
-
- let isLightNovelsTls = result.url
- .toLowerCase()
- .includes("lightnovelstranslations");
-
- let isiNovelTranslation = result.url
- .toLowerCase()
- .includes("inoveltranslation");
-
- let isTravisTranslation = result.url
- .toLowerCase()
- .includes("travistranslations");
-
- /**
- * Checks if its a wordpress site
- */
- let isWordPressStr =
- loadedCheerio('meta[name="generator"]').attr("content") ||
- loadedCheerio("footer").text();
-
- let isWordPress = false;
-
- if (isWordPressStr) {
- isWordPress =
- isWordPressStr.toLowerCase().includes("wordpress") ||
- isWordPressStr.includes("Site Kit by Google") ||
- loadedCheerio(".powered-by")
- .text()
- .toLowerCase()
- .includes("wordpress");
- }
-
- let isRainOfSnow = result.url.toLowerCase().includes("rainofsnow");
-
- let isWebNovel = result.url.toLowerCase().includes("webnovel");
-
- let isHostedNovel = result.url.toLowerCase().includes("hostednovel");
-
- let isScribbleHub = result.url.toLowerCase().includes("scribblehub");
-
- if (isWuxiaWorld) {
- chapterText = loadedCheerio("#chapter-content").html()!;
- } else if (isRainOfSnow) {
- chapterText = loadedCheerio("div.content").html()!;
- } else if (isTumblr) {
- chapterText = loadedCheerio(".post").html()!;
- } else if (isBlogspot) {
- loadedCheerio('.post-share-buttons').remove();
- chapterText =
- loadedCheerio('.entry-content').html() ||
- loadedCheerio('#post-body').html()!;
- } else if (isHostedNovel) {
- chapterText = loadedCheerio(".chapter").html()!;
- } else if (isScribbleHub) {
- chapterText = loadedCheerio("div.chp_raw").html()!;
- } else if (isWattpad) {
- chapterText = loadedCheerio(".container pre").html()!;
- } else if (isTravisTranslation) {
- chapterText = loadedCheerio(".reader-content").html()!;
- } else if (isLightNovelsTls) {
- chapterText = loadedCheerio('.text_story').html()!;
- } else if (isBlossomTranslation) {
- chapterText = loadedCheerio('.manga-child-content').html()!;
- } else if (isAnomalously) {
- chapterText = loadedCheerio('#comic').html()!;
- } else if (kofi) {
- chapterText = loadedCheerio('script:contains("shadowDom.innerHTML")')
- .html()
- ?.match(/shadowDom\.innerHTML \+= '( r.json());
- chapterText = json.content.replace(/\n/g, '
');
- if (json.notes) {
- chapterText +=
- '
TL Notes:
' + json.notes.replace(/\n/g, '
');
- }
- } else if (isWordPress) {
- /**
- * Remove wordpress bloat tags
- */
-
- const bloatClasses = [
- ".c-ads",
- "#madara-comments",
- "#comments",
- ".content-comments",
- ".sharedaddy",
- ".wp-dark-mode-switcher",
- ".wp-next-post-navi",
- ".wp-block-buttons",
- ".wp-block-columns",
- ".post-cats",
- ".sidebar",
- ".author-avatar",
- ".ezoic-ad",
- ];
-
- bloatClasses.map((tag) => loadedCheerio(tag).remove());
-
- chapterText =
- loadedCheerio(".entry-content").html() ||
- loadedCheerio(".single_post").html() ||
- loadedCheerio(".post-entry").html() ||
- loadedCheerio(".main-content").html() ||
- loadedCheerio("article.post").html() ||
- loadedCheerio(".content").html() ||
- loadedCheerio("#content").html() ||
- loadedCheerio(".page-body").html() ||
- loadedCheerio(".td-page-content").html()!;
- } else if (isWebNovel) {
- chapterText = loadedCheerio(".cha-words").html()!;
-
- if (!chapterText) {
- chapterText = loadedCheerio("._content").html()!;
- }
- } else {
- /**
- * Remove unnecessary tags
- */
- const tags = ["nav", "header", "footer", ".hidden"];
-
- tags.map((tag) => loadedCheerio(tag).remove());
-
- chapterText = loadedCheerio("body").html()!;
- }
-
- if (chapterText) {
- /**
- * Convert relative urls to absolute
- */
- chapterText = chapterText.replace(
- /href="\//g,
- `href="${this.getLocation(result.url)}/`
- );
- }
-
- return chapterText;
- }
-
- async searchNovels(searchTerm: string, page: number): Promise {
- const url = this.site + '?s=' + searchTerm.replace(/\s+/g, '+') + '&post_type=seriesplans';
- const result = await fetchApi(url);
- const body = await result.text();
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
-
- fetchImage(url: string): Promise {
- return fetchFile(url);
- }
-
- filters = {
- sort: {
- label: "Sort Results By",
- value: "sdate",
- options: [
- { label: "Last Updated", value: "sdate" },
- { label: "Rating", value: "srate" },
- { label: "Rank", value: "srank" },
- { label: "Reviews", value: "sreview" },
- { label: "Chapters", value: "srel" },
- { label: "Title", value: "abc" },
- { label: "Readers", value: "sread" },
- { label: "Frequency", value: "sfrel" },
- ],
- type: FilterTypes.Picker,
- },
- order: {
- label: "Order",
- value: "desc",
- options: [
- { label: "Descending", value: "desc" },
- { label: "Ascending", value: "asc" },
- ],
- type: FilterTypes.Picker,
- },
- storyStatus: {
- label: "Story Status (Translation)",
- value: "",
- options: [
- { label: "All", value: "" },
- { label: "Completed", value: "2" },
- { label: "Ongoing", value: "3" },
- { label: "Hiatus", value: "4" },
- ],
- type: FilterTypes.Picker,
- },
- language: {
- label: "Language",
- value: [],
- options: [
- { label: "Chinese", value: "495" },
- { label: "Filipino", value: "9181" },
- { label: "Indonesian", value: "9179" },
- { label: "Japanese", value: "496" },
- { label: "Khmer", value: "18657" },
- { label: "Korean", value: "497" },
- { label: "Malaysian", value: "9183" },
- { label: "Thai", value: "9954" },
- { label: "Vietnamese", value: "9177" },
- ],
- type: FilterTypes.CheckboxGroup,
- },
- novelType: {
- label: "Novel Type",
- value: [],
- options: [
- { label: "Light Novel", value: "2443" },
- { label: "Published Novel", value: "26874" },
- { label: "Web Novel", value: "2444" },
- ],
- type: FilterTypes.CheckboxGroup,
- },
- genres: {
- label: "Genres",
- type: FilterTypes.ExcludableCheckboxGroup,
- value: {
- include: [],
- exclude: [],
- },
- options: [
- { label: "Action", value: "8" },
- { label: "Adult", value: "280" },
- { label: "Adventure", value: "13" },
- { label: "Comedy", value: "17" },
- { label: "Drama", value: "9" },
- { label: "Ecchi", value: "292" },
- { label: "Fantasy", value: "5" },
- { label: "Gender Bender", value: "168" },
- { label: "Harem", value: "3" },
- { label: "Historical", value: "330" },
- { label: "Horror", value: "343" },
- { label: "Josei", value: "324" },
- { label: "Martial Arts", value: "14" },
- { label: "Mature", value: "4" },
- { label: "Mecha", value: "10" },
- { label: "Mystery", value: "245" },
- { label: "Psychoical", value: "486" },
- { label: "Romance", value: "15" },
- { label: "School Life", value: "6" },
- { label: "Sci-fi", value: "11" },
- { label: "Seinen", value: "18" },
- { label: "Shoujo", value: "157" },
- { label: "Shoujo Ai", value: "851" },
- { label: "Shounen", value: "12" },
- { label: "Shounen Ai", value: "1692" },
- { label: "Slice of Life", value: "7" },
- { label: "Smut", value: "281" },
- { label: "Sports", value: "1357" },
- { label: "Supernatural", value: "16" },
- { label: "Tragedy", value: "132" },
- { label: "Wuxia", value: "479" },
- { label: "Xianxia", value: "480" },
- { label: "Xuanhuan", value: "3954" },
- { label: "Yaoi", value: "560" },
- { label: "Yuri", value: "922" },
- ],
- },
- genre_operator: {
- label: "Genre (AND/OR)",
- value: "and",
- options: [
- { label: "AND", value: "and" },
- { label: "OR", value: "or" },
- ],
- type: FilterTypes.Picker,
- },
- } satisfies Filters;
+ filters = {
+ sort: {
+ label: 'Sort Results By',
+ value: 'sdate',
+ options: [
+ { label: 'Last Updated', value: 'sdate' },
+ { label: 'Rating', value: 'srate' },
+ { label: 'Rank', value: 'srank' },
+ { label: 'Reviews', value: 'sreview' },
+ { label: 'Chapters', value: 'srel' },
+ { label: 'Title', value: 'abc' },
+ { label: 'Readers', value: 'sread' },
+ { label: 'Frequency', value: 'sfrel' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ order: {
+ label: 'Order',
+ value: 'desc',
+ options: [
+ { label: 'Descending', value: 'desc' },
+ { label: 'Ascending', value: 'asc' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ storyStatus: {
+ label: 'Story Status (Translation)',
+ value: '',
+ options: [
+ { label: 'All', value: '' },
+ { label: 'Completed', value: '2' },
+ { label: 'Ongoing', value: '3' },
+ { label: 'Hiatus', value: '4' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ language: {
+ label: 'Language',
+ value: [],
+ options: [
+ { label: 'Chinese', value: '495' },
+ { label: 'Filipino', value: '9181' },
+ { label: 'Indonesian', value: '9179' },
+ { label: 'Japanese', value: '496' },
+ { label: 'Khmer', value: '18657' },
+ { label: 'Korean', value: '497' },
+ { label: 'Malaysian', value: '9183' },
+ { label: 'Thai', value: '9954' },
+ { label: 'Vietnamese', value: '9177' },
+ ],
+ type: FilterTypes.CheckboxGroup,
+ },
+ novelType: {
+ label: 'Novel Type',
+ value: [],
+ options: [
+ { label: 'Light Novel', value: '2443' },
+ { label: 'Published Novel', value: '26874' },
+ { label: 'Web Novel', value: '2444' },
+ ],
+ type: FilterTypes.CheckboxGroup,
+ },
+ genres: {
+ label: 'Genres',
+ type: FilterTypes.ExcludableCheckboxGroup,
+ value: {
+ include: [],
+ exclude: [],
+ },
+ options: [
+ { label: 'Action', value: '8' },
+ { label: 'Adult', value: '280' },
+ { label: 'Adventure', value: '13' },
+ { label: 'Comedy', value: '17' },
+ { label: 'Drama', value: '9' },
+ { label: 'Ecchi', value: '292' },
+ { label: 'Fantasy', value: '5' },
+ { label: 'Gender Bender', value: '168' },
+ { label: 'Harem', value: '3' },
+ { label: 'Historical', value: '330' },
+ { label: 'Horror', value: '343' },
+ { label: 'Josei', value: '324' },
+ { label: 'Martial Arts', value: '14' },
+ { label: 'Mature', value: '4' },
+ { label: 'Mecha', value: '10' },
+ { label: 'Mystery', value: '245' },
+ { label: 'Psychoical', value: '486' },
+ { label: 'Romance', value: '15' },
+ { label: 'School Life', value: '6' },
+ { label: 'Sci-fi', value: '11' },
+ { label: 'Seinen', value: '18' },
+ { label: 'Shoujo', value: '157' },
+ { label: 'Shoujo Ai', value: '851' },
+ { label: 'Shounen', value: '12' },
+ { label: 'Shounen Ai', value: '1692' },
+ { label: 'Slice of Life', value: '7' },
+ { label: 'Smut', value: '281' },
+ { label: 'Sports', value: '1357' },
+ { label: 'Supernatural', value: '16' },
+ { label: 'Tragedy', value: '132' },
+ { label: 'Wuxia', value: '479' },
+ { label: 'Xianxia', value: '480' },
+ { label: 'Xuanhuan', value: '3954' },
+ { label: 'Yaoi', value: '560' },
+ { label: 'Yuri', value: '922' },
+ ],
+ },
+ genre_operator: {
+ label: 'Genre (AND/OR)',
+ value: 'and',
+ options: [
+ { label: 'AND', value: 'and' },
+ { label: 'OR', value: 'or' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ } satisfies Filters;
}
export default new NovelUpdates();
diff --git a/plugins/english/pawread.ts b/plugins/english/pawread.ts
index 6f6c75f..1aa0629 100644
--- a/plugins/english/pawread.ts
+++ b/plugins/english/pawread.ts
@@ -1,226 +1,230 @@
-import { CheerioAPI, load as parseHTML } from "cheerio";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { Plugin } from "@typings/plugin";
-import { Filters, FilterTypes } from "@libs/filterInputs";
+import { CheerioAPI, load as parseHTML } from 'cheerio';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { Plugin } from '@typings/plugin';
+import { Filters, FilterTypes } from '@libs/filterInputs';
class PawRead implements Plugin.PluginBase {
- id = "pawread";
- name = "PawRead";
- version = "2.0.0";
- icon = "src/en/pawread/icon.png";
- site = "https://m.pawread.com/";
+ id = 'pawread';
+ name = 'PawRead';
+ version = '2.0.0';
+ icon = 'src/en/pawread/icon.png';
+ site = 'https://m.pawread.com/';
- parseNovels(loadedCheerio:CheerioAPI){
- const novels: Plugin.NovelItem[] = [];
+ parseNovels(loadedCheerio: CheerioAPI) {
+ const novels: Plugin.NovelItem[] = [];
- loadedCheerio(".list-comic, .itemBox").each((idx, ele) => {
- loadedCheerio(ele).find('.serialise').remove();
- const novelName = loadedCheerio(ele).find('a').text().trim();
- const novelCover = loadedCheerio(ele).find('img').attr('src');
- const novelUrl = loadedCheerio(ele).find('a').attr('href')?.slice(1);
+ loadedCheerio('.list-comic, .itemBox').each((idx, ele) => {
+ loadedCheerio(ele).find('.serialise').remove();
+ const novelName = loadedCheerio(ele).find('a').text().trim();
+ const novelCover = loadedCheerio(ele).find('img').attr('src');
+ const novelUrl = loadedCheerio(ele).find('a').attr('href')?.slice(1);
- if (!novelUrl) return;
+ if (!novelUrl) return;
- const novel = {
- name: novelName,
- cover: novelCover,
- path: novelUrl,
- };
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl,
+ };
- novels.push(novel);
- });
+ novels.push(novel);
+ });
- return novels;
- }
+ return novels;
+ }
- async popularNovels(
- page: number,
- { filters }: Plugin.PopularNovelsOptions
- ): Promise {
- let link = `${this.site}list/`;
+ async popularNovels(
+ page: number,
+ { filters }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ let link = `${this.site}list/`;
- const filterValues = [
- filters.status.value,
- filters.lang.value,
- filters.genre.value
- ];
+ const filterValues = [
+ filters.status.value,
+ filters.lang.value,
+ filters.genre.value,
+ ];
- link += filterValues.filter(value => value !== '').join('-');
- if (filterValues.some(value => value !== ''))
- link += '/';
+ link += filterValues.filter(value => value !== '').join('-');
+ if (filterValues.some(value => value !== '')) link += '/';
- if (filters.order.value)
- link += '-';
-
- link += filters.sort.value;
- link += `/?page=${page}`;
+ if (filters.order.value) link += '-';
- const body = await fetchApi(link).then((r) => r.text());
+ link += filters.sort.value;
+ link += `/?page=${page}`;
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
+ const body = await fetchApi(link).then(r => r.text());
- async parseNovel(novelPath: string): Promise {
- const result = await fetchApi(this.site + novelPath);
- const body = await result.text();
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
- const loadedCheerio = parseHTML(body);
+ async parseNovel(novelPath: string): Promise {
+ const result = await fetchApi(this.site + novelPath);
+ const body = await result.text();
- const novel: Plugin.SourceNovel = {
- path: novelPath,
- name: loadedCheerio("#Cover img").attr('title') || "Untitled",
- cover: loadedCheerio("#Cover img").attr('src'),
- author: loadedCheerio('.icon01 <').text().trim(),
- status: loadedCheerio('.txtItme:first').text().trim(),
- chapters: [],
- };
+ const loadedCheerio = parseHTML(body);
- novel.summary = loadedCheerio("#full-des")
- .find('br')
- .replaceWith('\n')
- .end()
- .text()
- .trim();
+ const novel: Plugin.SourceNovel = {
+ path: novelPath,
+ name: loadedCheerio('#Cover img').attr('title') || 'Untitled',
+ cover: loadedCheerio('#Cover img').attr('src'),
+ author: loadedCheerio('.icon01 <').text().trim(),
+ status: loadedCheerio('.txtItme:first').text().trim(),
+ chapters: [],
+ };
- novel.genres = loadedCheerio('a.btn-default')
- .map((i, el) => loadedCheerio(el).text().trim())
- .toArray()
- .join(",");
+ novel.summary = loadedCheerio('#full-des')
+ .find('br')
+ .replaceWith('\n')
+ .end()
+ .text()
+ .trim();
- const chapter: Plugin.ChapterItem[] = [];
+ novel.genres = loadedCheerio('a.btn-default')
+ .map((i, el) => loadedCheerio(el).text().trim())
+ .toArray()
+ .join(',');
- loadedCheerio(".item-box").each((idx, ele) => {
- const smallText = loadedCheerio(ele).find('span:last').text().trim();
- if (smallText === "Advanced Chapter") return;
- const releaseDate = smallText.split('.').map(x => Number(x));
- const chapterName = loadedCheerio(ele).find("span:first").text().trim();
- const chapterUrl = loadedCheerio(ele).attr("onclick")?.match(/\d+/)![0];
- if (!chapterUrl) return;
+ const chapter: Plugin.ChapterItem[] = [];
- chapter.push({
- name: chapterName,
- path: `${novelPath}${chapterUrl}.html`,
- releaseTime: new Date(releaseDate[0], releaseDate[1], releaseDate[2]).toISOString(),
- });
- });
+ loadedCheerio('.item-box').each((idx, ele) => {
+ const smallText = loadedCheerio(ele).find('span:last').text().trim();
+ if (smallText === 'Advanced Chapter') return;
+ const releaseDate = smallText.split('.').map(x => Number(x));
+ const chapterName = loadedCheerio(ele).find('span:first').text().trim();
+ const chapterUrl = loadedCheerio(ele).attr('onclick')?.match(/\d+/)![0];
+ if (!chapterUrl) return;
- novel.chapters = chapter;
- return novel;
- }
+ chapter.push({
+ name: chapterName,
+ path: `${novelPath}${chapterUrl}.html`,
+ releaseTime: new Date(
+ releaseDate[0],
+ releaseDate[1],
+ releaseDate[2],
+ ).toISOString(),
+ });
+ });
+ novel.chapters = chapter;
+ return novel;
+ }
- async parseChapter(chapterPath: string): Promise {
- const result = await fetchApi(this.site + chapterPath);
- const body = await result.text();
+ async parseChapter(chapterPath: string): Promise {
+ const result = await fetchApi(this.site + chapterPath);
+ const body = await result.text();
- const loadedCheerio = parseHTML(body);
+ const loadedCheerio = parseHTML(body);
- const steal = ['bit.ly', 'tinyurl', 'pawread'];
- steal.map(tag => loadedCheerio(`p:icontains(${tag})`).remove());
- const chapterText = loadedCheerio('.main').html() || "";
+ const steal = ['bit.ly', 'tinyurl', 'pawread'];
+ steal.map(tag => loadedCheerio(`p:icontains(${tag})`).remove());
+ const chapterText = loadedCheerio('.main').html() || '';
- return chapterText;
- }
+ return chapterText;
+ }
- async searchNovels(searchTerm: string, page: number): Promise {
- const searchUrl = `${this.site}search/?keywords=${searchTerm}&page=${page}`;
+ async searchNovels(
+ searchTerm: string,
+ page: number,
+ ): Promise {
+ const searchUrl = `${this.site}search/?keywords=${searchTerm}&page=${page}`;
- const result = await fetchApi(searchUrl);
- const body = await result.text();
+ const result = await fetchApi(searchUrl);
+ const body = await result.text();
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
- async fetchImage(url: string): Promise {
- return fetchFile(url);
- }
+ async fetchImage(url: string): Promise {
+ return fetchFile(url);
+ }
- filters = {
- status: {
- value: "",
- label: "Status",
- options: [
- { label: "All", value: "" },
- { label: "Completed", value: "wanjie" },
- { label: "Ongoing", value: "lianzai" },
- { label: "Hiatus", value: "hiatus" },
- ],
- type: FilterTypes.Picker,
- },
- lang: {
- value: "",
- label: "Languages",
- options: [
- { label: "All", value: "" },
- { label: "Chinese", value: "chinese" },
- { label: "Korean", value: "korean" },
- { label: "Japanese", value: "japanese" },
- ],
- type: FilterTypes.Picker,
- },
- genre: {
- value: "",
- label: "Genres",
- options: [
- { label: "All", value: "" },
- { label: "Fantasy", value: "Fantasy" },
- { label: "Action", value: "Action" },
- { label: "Xuanhuan", value: "Xuanhuan" },
- { label: "Romance", value: "Romance" },
- { label: "Comedy", value: "Comedy" },
- { label: "Mystery", value: "Mystery" },
- { label: "Mature", value: "Mature" },
- { label: "Harem", value: "Harem" },
- { label: "Wuxia", value: "Wuxia" },
- { label: "Xianxia", value: "Xianxia" },
- { label: "Tragedy", value: "Tragedy" },
- { label: "Sci-fi", value: "Scifi" },
- { label: "Historical", value: "Historical" },
- { label: "Ecchi", value: "Ecchi" },
- { label: "Adventure", value: "Adventure" },
- { label: "Adult", value: "Adult" },
- { label: "Supernatural", value: "Supernatural" },
- { label: "Psychological", value: "Psychological" },
- { label: "Drama", value: "Drama" },
- { label: "Horror", value: "Horror" },
- { label: "Josei", value: "Josei" },
- { label: "Mecha", value: "Mecha" },
- { label: "Seinen", value: "Seinen" },
- { label: "Shoujo", value: "Shoujo" },
- { label: "Shounen", value: "Shounen" },
- { label: "Smut", value: "Smut" },
- { label: "Yaoi", value: "Yaoi" },
- { label: "Yuri", value: "Yuri" },
- { label: "Martial Arts", value: "MartialArts" },
- { label: "School Life", value: "SchoolLife" },
- { label: "Shoujo Ai", value: "ShoujoAi" },
- { label: "Shounen Ai", value: "ShounenAi" },
- { label: "Slice of Life", value: "SliceofLife" },
- { label: "Gender Bender", value: "GenderBender" },
- { label: "Sports", value: "Sports" },
- { label: "Urban", value: "Urban" },
- { label: "Adventurer", value: "Adventurer" }
- ],
- type: FilterTypes.Picker,
- },
- sort: {
- value: "click",
- label: "Sort By",
- options: [
- { label: "Time Updated", value: "update" },
- { label: "Time Posted", value: "post" },
- { label: "Clicks", value: "click" },
- ],
- type: FilterTypes.Picker,
- },
- order: {
- value: false,
- label: "Order ↑ ↓",
- type: FilterTypes.Switch,
- },
- } satisfies Filters;
+ filters = {
+ status: {
+ value: '',
+ label: 'Status',
+ options: [
+ { label: 'All', value: '' },
+ { label: 'Completed', value: 'wanjie' },
+ { label: 'Ongoing', value: 'lianzai' },
+ { label: 'Hiatus', value: 'hiatus' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ lang: {
+ value: '',
+ label: 'Languages',
+ options: [
+ { label: 'All', value: '' },
+ { label: 'Chinese', value: 'chinese' },
+ { label: 'Korean', value: 'korean' },
+ { label: 'Japanese', value: 'japanese' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ genre: {
+ value: '',
+ label: 'Genres',
+ options: [
+ { label: 'All', value: '' },
+ { label: 'Fantasy', value: 'Fantasy' },
+ { label: 'Action', value: 'Action' },
+ { label: 'Xuanhuan', value: 'Xuanhuan' },
+ { label: 'Romance', value: 'Romance' },
+ { label: 'Comedy', value: 'Comedy' },
+ { label: 'Mystery', value: 'Mystery' },
+ { label: 'Mature', value: 'Mature' },
+ { label: 'Harem', value: 'Harem' },
+ { label: 'Wuxia', value: 'Wuxia' },
+ { label: 'Xianxia', value: 'Xianxia' },
+ { label: 'Tragedy', value: 'Tragedy' },
+ { label: 'Sci-fi', value: 'Scifi' },
+ { label: 'Historical', value: 'Historical' },
+ { label: 'Ecchi', value: 'Ecchi' },
+ { label: 'Adventure', value: 'Adventure' },
+ { label: 'Adult', value: 'Adult' },
+ { label: 'Supernatural', value: 'Supernatural' },
+ { label: 'Psychological', value: 'Psychological' },
+ { label: 'Drama', value: 'Drama' },
+ { label: 'Horror', value: 'Horror' },
+ { label: 'Josei', value: 'Josei' },
+ { label: 'Mecha', value: 'Mecha' },
+ { label: 'Seinen', value: 'Seinen' },
+ { label: 'Shoujo', value: 'Shoujo' },
+ { label: 'Shounen', value: 'Shounen' },
+ { label: 'Smut', value: 'Smut' },
+ { label: 'Yaoi', value: 'Yaoi' },
+ { label: 'Yuri', value: 'Yuri' },
+ { label: 'Martial Arts', value: 'MartialArts' },
+ { label: 'School Life', value: 'SchoolLife' },
+ { label: 'Shoujo Ai', value: 'ShoujoAi' },
+ { label: 'Shounen Ai', value: 'ShounenAi' },
+ { label: 'Slice of Life', value: 'SliceofLife' },
+ { label: 'Gender Bender', value: 'GenderBender' },
+ { label: 'Sports', value: 'Sports' },
+ { label: 'Urban', value: 'Urban' },
+ { label: 'Adventurer', value: 'Adventurer' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ sort: {
+ value: 'click',
+ label: 'Sort By',
+ options: [
+ { label: 'Time Updated', value: 'update' },
+ { label: 'Time Posted', value: 'post' },
+ { label: 'Clicks', value: 'click' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ order: {
+ value: false,
+ label: 'Order ↑ ↓',
+ type: FilterTypes.Switch,
+ },
+ } satisfies Filters;
}
-export default new PawRead();
\ No newline at end of file
+export default new PawRead();
diff --git a/plugins/english/ranobes.ts b/plugins/english/ranobes.ts
index 13e82e9..32985ff 100644
--- a/plugins/english/ranobes.ts
+++ b/plugins/english/ranobes.ts
@@ -1,184 +1,188 @@
-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 { CheerioAPI, load as parseHTML } from 'cheerio';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { FilterTypes, Filters } from '@libs/filterInputs';
+import { Plugin } from '@typings/plugin';
interface ChapterEntry {
- id: number;
- title: string;
- date: string;
- link: string;
+ id: number;
+ title: string;
+ date: string;
+ link: string;
}
class RanobesPlugin implements Plugin.PagePlugin {
- id = 'ranobes';
- name = 'Ranobes';
- icon = 'src/en/ranobes/icon.png';
- site = 'https://ranobes.top';
- filters?: Filters | undefined;
- version = '1.0.0';
+ id = 'ranobes';
+ name = 'Ranobes';
+ icon = 'src/en/ranobes/icon.png';
+ site = 'https://ranobes.top';
+ filters?: Filters | undefined;
+ version = '1.0.0';
- async sleep (ms: number) {
- return new Promise(resolve => setTimeout(resolve, ms))
+ async sleep(ms: number) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+
+ parseNovels(loadedCheerio: CheerioAPI) {
+ const novels: Plugin.NovelItem[] = [];
+
+ loadedCheerio('.short-cont').each((i, el) => {
+ const novelName = loadedCheerio(el).find('h2.title').text();
+ const novelUrl = loadedCheerio(el).find('h2.title a').attr('href');
+ const novelCover = loadedCheerio(el)
+ .find('figure')
+ .attr('style')
+ ?.replace(/.*url\((.*?)\)./g, '$1');
+
+ if (!novelUrl) return;
+
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl?.replace(this.site, ''),
+ };
+ novels.push(novel);
+ });
+ return novels;
+ }
+
+ parseChapters(data: { chapters: ChapterEntry[] }) {
+ const chapter: Plugin.ChapterItem[] = [];
+ data.chapters.map((item: ChapterEntry) => {
+ chapter.push({
+ name: item.title,
+ releaseTime: new Date(item.date).toISOString(),
+ path: item.link.replace(this.site, ''),
+ });
+ });
+ return chapter.reverse();
+ }
+
+ async popularNovels(
+ page: number,
+ {
+ showLatestNovels,
+ filters,
+ }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ let link = `${this.site}/novels/page/${page}/`;
+ const body = await fetchApi(link).then(result => result.text());
+
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
+
+ async parseNovel(
+ novelPath: string,
+ ): Promise {
+ const url = this.site + novelPath;
+ const result = await fetchApi(url);
+ const body = await result.text();
+
+ let loadedCheerio = parseHTML(body);
+
+ const novel: Plugin.SourceNovel & { totalPages: number } = {
+ path: novelPath,
+ name: loadedCheerio('.poster img').attr('alt') || 'Untitled',
+ cover: this.site + loadedCheerio('.poster img').attr('src'),
+ author: loadedCheerio('[itemprop="creator"]').text(),
+ chapters: [],
+ totalPages: 1,
+ };
+
+ novel.summary = loadedCheerio('.moreless')
+ .find('br')
+ .replaceWith('\n')
+ .end()
+ .text()
+ .trim();
+
+ novel.status =
+ loadedCheerio('li:contains("Status") span').text() === 'Ongoing'
+ ? 'Ongoing'
+ : 'Completed';
+
+ novel.genres = loadedCheerio('div#mc-fs-genre').text().trim();
+
+ let chapterListUrl = loadedCheerio('.read-continue:last').attr('href');
+
+ if (!chapterListUrl?.startsWith('http')) {
+ chapterListUrl = this.site + chapterListUrl;
}
- parseNovels(loadedCheerio: CheerioAPI){
- const novels: Plugin.NovelItem[] = [];
+ const chaptersHtmlToString = await fetchApi(chapterListUrl).then(r =>
+ r.text(),
+ );
+ const pageCheerio = parseHTML(chaptersHtmlToString);
- loadedCheerio('.short-cont').each((i,el) => {
- const novelName = loadedCheerio(el).find('h2.title').text();
- const novelUrl = loadedCheerio(el).find('h2.title a').attr('href');
- const novelCover = loadedCheerio(el)
- .find('figure')
- .attr('style')
- ?.replace(/.*url\((.*?)\)./g, '$1');
-
- if (!novelUrl) return;
+ const json = pageCheerio('#dle-content main').next().html()!;
+ const data = JSON.parse(json.replace('window.__DATA__ =', ''));
+ novel.totalPages = Number(data.pages_count);
+ novel.chapters = this.parseChapters(data);
- const novel = {
- name: novelName,
- cover: novelCover,
- path: novelUrl?.replace(this.site, ''),
- }
- novels.push(novel);
- });
- return novels;
- }
+ return novel;
+ }
- parseChapters(data: {chapters: ChapterEntry[]}){
- const chapter: Plugin.ChapterItem[] = [];
- data.chapters.map((item: ChapterEntry) => {
- chapter.push({
- name: item.title,
- releaseTime: new Date(item.date).toISOString(),
- path: item.link.replace(this.site, ''),
- });
- });
- return chapter.reverse();
- }
+ async parsePage(novelPath: string, page: string): Promise {
+ const pagePath = novelPath.split('-')[0];
+ const firstUrl = this.site + '/chapters' + pagePath.replace('novels/', '');
+ const pageUrl = firstUrl + '/page/' + page;
- async popularNovels(
- page: number,
- { showLatestNovels, filters }: Plugin.PopularNovelsOptions
- ): Promise {
- let link = `${this.site}/novels/page/${page}/`;
- const body = await fetchApi(link).then((result) =>
- result.text()
- );
+ const body = await fetchApi(firstUrl).then(r => r.text());
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
+ let loadedCheerio = parseHTML(body);
+ let json = loadedCheerio('#dle-content main').next().html()!;
+ let data = JSON.parse(json.replace('window.__DATA__ =', ''));
- async parseNovel(novelPath: string): Promise {
- const url = this.site + novelPath;
- const result = await fetchApi(url);
- const body = await result.text();
+ const latestChapterUrl = data.chapters[0].link;
+ const latestChapterName = data.chapters[0].title;
+ const latestChapterDate = data.chapters[0].date;
- let loadedCheerio = parseHTML(body);
-
- const novel: Plugin.SourceNovel & { totalPages: number; } = {
- path: novelPath,
- name: loadedCheerio('.poster img').attr('alt') || "Untitled",
- cover: this.site + loadedCheerio('.poster img').attr('src'),
- author: loadedCheerio('[itemprop="creator"]').text(),
- chapters: [],
- totalPages: 1,
- };
-
- novel.summary = loadedCheerio('.moreless')
- .find('br')
- .replaceWith('\n')
- .end()
- .text()
- .trim();
-
- novel.status = loadedCheerio('li:contains("Status") span')
- .text() === 'Ongoing'
- ? 'Ongoing'
- : 'Completed';
-
- novel.genres = loadedCheerio('div#mc-fs-genre').text().trim();
-
- let chapterListUrl = loadedCheerio('.read-continue:last').attr('href');
-
- if (!chapterListUrl?.startsWith('http')) {
- chapterListUrl = this.site + chapterListUrl;
+ const latestChapter: Plugin.ChapterItem | undefined = latestChapterUrl
+ ? {
+ path: latestChapterUrl.replace(this.site, ''),
+ name: latestChapterName,
+ releaseTime: new Date(latestChapterDate).toISOString(),
}
+ : undefined;
- const chaptersHtmlToString = await fetchApi(chapterListUrl).then(r => r.text());
- const pageCheerio = parseHTML(chaptersHtmlToString);
+ await this.sleep(1000);
- const json = pageCheerio('#dle-content main').next().html()!;
- const data = JSON.parse(json.replace('window.__DATA__ =', ''));
- novel.totalPages = Number(data.pages_count);
- novel.chapters = this.parseChapters(data);
-
- return novel;
+ const pageBody = await fetchApi(pageUrl).then(r => r.text());
+ loadedCheerio = parseHTML(pageBody);
+ json = loadedCheerio('#dle-content main').next().html()!;
+ data = JSON.parse(json.replace('window.__DATA__ =', ''));
+ const chapters = this.parseChapters(data);
+ return {
+ chapters,
+ latestChapter,
};
+ }
- async parsePage(novelPath: string, page: string): Promise {
- const pagePath = novelPath.split('-')[0];
- const firstUrl = this.site +
- '/chapters' +
- pagePath.replace('novels/', '');
- const pageUrl = firstUrl + '/page/' + page;
+ async parseChapter(chapterPath: string): Promise {
+ const result = await fetchApi(this.site + chapterPath);
+ const body = await result.text();
- const body = await fetchApi(firstUrl).then(r => r.text());
-
- let loadedCheerio = parseHTML(body);
- let json = loadedCheerio('#dle-content main').next().html()!;
- let data = JSON.parse(json.replace('window.__DATA__ =', ''));
+ const loadedCheerio = parseHTML(body);
- const latestChapterUrl = data.chapters[0].link;
- const latestChapterName = data.chapters[0].title;
- const latestChapterDate = data.chapters[0].date;
+ const chapterText = loadedCheerio('#arrticle').html() || '';
- const latestChapter: Plugin.ChapterItem | undefined = latestChapterUrl ? {
- path: latestChapterUrl.replace(this.site, ''),
- name: latestChapterName,
- releaseTime: new Date(latestChapterDate).toISOString(),
- } : undefined
+ return chapterText;
+ }
- await this.sleep(1000);
+ async searchNovels(
+ searchTerm: string,
+ page: number,
+ ): Promise {
+ let link = `${this.site}/search/${searchTerm}/page/${page}`;
- const pageBody = await fetchApi(pageUrl).then(r => r.text());
- loadedCheerio = parseHTML(pageBody);
- json = loadedCheerio('#dle-content main').next().html()!;
- data = JSON.parse(json.replace('window.__DATA__ =', ''));
- const chapters = this.parseChapters(data);
- return {
- chapters,
- latestChapter
- }
- }
+ const body = await fetchApi(link).then(r => r.text());
- async parseChapter(chapterPath: string): Promise {
- const result = await fetchApi(this.site + chapterPath);
- const body = await result.text();
-
- const loadedCheerio = parseHTML(body);
-
- const chapterText = loadedCheerio('#arrticle').html() || "";
-
- return chapterText;
- };
-
- async searchNovels(
- searchTerm: string,
- page: number
- ): Promise {
- let link = `${this.site}/search/${searchTerm}/page/${page}`;
-
- const body = await fetchApi(link).then(r => r.text());
-
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio)
- };
-
- async fetchImage(url: string): Promise {
- return await fetchFile(url);
- }
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
+ async fetchImage(url: string): Promise {
+ return await fetchFile(url);
+ }
}
export default new RanobesPlugin();
diff --git a/plugins/english/readlitenovel.ts b/plugins/english/readlitenovel.ts
index 197b611..017658a 100644
--- a/plugins/english/readlitenovel.ts
+++ b/plugins/english/readlitenovel.ts
@@ -1,189 +1,196 @@
-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";
+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";
+ 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
- }
+ 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 (date.includes("years ago") || date.includes("year ago")) {
- dayJSDate.subtract(timeAgoInt, "years"); // go back N years
- }
+ if (!timeAgo) return null; // there is no number!
- return dayJSDate.toISOString();
- }
- return null; // there is no "ago" so give up
- };
+ if (date.includes('hours ago') || date.includes('hour ago')) {
+ dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours
+ }
- async popularNovels(
- page: number,
- { filters }: Plugin.PopularNovelsOptions
- ): Promise {
- const link = `${this.site}/ranking/${filters.order.value}/${page}`;
- const result = await fetchApi(link);
- const body = await result.text();
+ if (date.includes('days ago') || date.includes('day ago')) {
+ dayJSDate.subtract(timeAgoInt, 'days'); // go back N days
+ }
- const loadedCheerio = parseHTML(body);
+ if (date.includes('months ago') || date.includes('month ago')) {
+ dayJSDate.subtract(timeAgoInt, 'months'); // go back N months
+ }
- const novels: Plugin.NovelItem[] = [];
+ if (date.includes('years ago') || date.includes('year ago')) {
+ dayJSDate.subtract(timeAgoInt, 'years'); // go back N years
+ }
- 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 {
- 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 {
- 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 {
- 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 {
- return fetchFile(url);
+ return dayJSDate.toISOString();
}
+ return null; // there is no "ago" so give up
+ }
- 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;
+ async popularNovels(
+ page: number,
+ { filters }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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();
diff --git a/plugins/english/royalroad.ts b/plugins/english/royalroad.ts
index 82d8caf..00216d8 100644
--- a/plugins/english/royalroad.ts
+++ b/plugins/english/royalroad.ts
@@ -1,185 +1,191 @@
-import { CheerioAPI, load as parseHTML } from "cheerio";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { Plugin } from "@typings/plugin";
-import { Filters, FilterTypes } from "@libs/filterInputs";
+import { CheerioAPI, load as parseHTML } from 'cheerio';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { Plugin } from '@typings/plugin';
+import { Filters, FilterTypes } from '@libs/filterInputs';
class RoyalRoad implements Plugin.PluginBase {
- id = "royalroad";
- name = "Royal Road";
- version = "1.0.1";
- icon = "src/en/royalroad/icon.png";
- site = "https://www.royalroad.com/";
+ id = 'royalroad';
+ name = 'Royal Road';
+ version = '1.0.1';
+ icon = 'src/en/royalroad/icon.png';
+ site = 'https://www.royalroad.com/';
- parseNovels(loadedCheerio:CheerioAPI){
- const novels: Plugin.NovelItem[] = [];
+ parseNovels(loadedCheerio: CheerioAPI) {
+ const novels: Plugin.NovelItem[] = [];
- loadedCheerio(".fiction-list-item").each((idx, ele) => {
- const novelName = loadedCheerio(ele).find('a.bold').text();
- const novelCover = loadedCheerio(ele).find('img').attr('src');
- const novelUrl = loadedCheerio(ele).find('a.bold').attr('href')?.slice(1);
+ loadedCheerio('.fiction-list-item').each((idx, ele) => {
+ const novelName = loadedCheerio(ele).find('a.bold').text();
+ const novelCover = loadedCheerio(ele).find('img').attr('src');
+ const novelUrl = loadedCheerio(ele).find('a.bold').attr('href')?.slice(1);
- if (!novelUrl) return;
+ if (!novelUrl) return;
- const novel = {
- name: novelName,
- cover: novelCover,
- path: novelUrl,
- };
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl,
+ };
- novels.push(novel);
- });
+ novels.push(novel);
+ });
- return novels;
- }
+ return novels;
+ }
- async popularNovels(
- page: number,
- { filters }: Plugin.PopularNovelsOptions
- ): Promise {
- let link = `${this.site}fictions/`;
+ async popularNovels(
+ page: number,
+ { filters }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ let link = `${this.site}fictions/`;
- link += filters.order.value;
- link += `?page=${page}`;
+ link += filters.order.value;
+ link += `?page=${page}`;
- if (filters.genre.value !== '')
- link += `&genre=${filters.genre.value}`;
+ if (filters.genre.value !== '') link += `&genre=${filters.genre.value}`;
- const body = await fetchApi(link).then((r) => r.text());
+ const body = await fetchApi(link).then(r => r.text());
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
- async parseNovel(novelPath: string): Promise {
- const result = await fetchApi(this.site + novelPath);
- const body = await result.text();
+ async parseNovel(novelPath: string): Promise {
+ const result = await fetchApi(this.site + novelPath);
+ const body = await result.text();
- const loadedCheerio = parseHTML(body);
+ const loadedCheerio = parseHTML(body);
- const novel: Plugin.SourceNovel = {
- path: novelPath,
- name: loadedCheerio("h1").text() || "Untitled",
- cover: loadedCheerio('.cover-art-container img').attr('src'),
- author: loadedCheerio('h4 a').text(),
- summary: loadedCheerio(".description").text().trim(),
- status: loadedCheerio('span.label-sm:last').text().trim(),
- chapters: [],
- };
+ const novel: Plugin.SourceNovel = {
+ path: novelPath,
+ name: loadedCheerio('h1').text() || 'Untitled',
+ cover: loadedCheerio('.cover-art-container img').attr('src'),
+ author: loadedCheerio('h4 a').text(),
+ summary: loadedCheerio('.description').text().trim(),
+ status: loadedCheerio('span.label-sm:last').text().trim(),
+ chapters: [],
+ };
- novel.genres = loadedCheerio('span.tags a')
- .map((i, el) => loadedCheerio(el).text())
- .toArray()
- .join(",");
+ novel.genres = loadedCheerio('span.tags a')
+ .map((i, el) => loadedCheerio(el).text())
+ .toArray()
+ .join(',');
- const chapterJson = JSON.parse(loadedCheerio('script:contains("window.chapters")')
- .html()
- ?.match(/window.chapters = (.+])(?=;)/)![1] || "");
+ const chapterJson = JSON.parse(
+ loadedCheerio('script:contains("window.chapters")')
+ .html()
+ ?.match(/window.chapters = (.+])(?=;)/)![1] || '',
+ );
- const volumeJson = JSON.parse(loadedCheerio('script:contains("window.chapters")')
- .html()
- ?.match(/window.volumes = (.+])(?=;)/)![1] || "");
+ const volumeJson = JSON.parse(
+ loadedCheerio('script:contains("window.chapters")')
+ .html()
+ ?.match(/window.volumes = (.+])(?=;)/)![1] || '',
+ );
- const chapter : Plugin.ChapterItem[] = chapterJson.map(
- (chapter: ChapterEntry) => {
- const matchingVolume = volumeJson.find(
- (volume: VolumeEntry) => volume.id === chapter.volumeId);
- return {
- name: chapter.title,
- path: chapter.url.slice(1),
- releaseTime: chapter.date,
- chapterNumber: chapter?.order,
- page: matchingVolume ? matchingVolume.title : null,
- };
- }
+ const chapter: Plugin.ChapterItem[] = chapterJson.map(
+ (chapter: ChapterEntry) => {
+ const matchingVolume = volumeJson.find(
+ (volume: VolumeEntry) => volume.id === chapter.volumeId,
);
+ return {
+ name: chapter.title,
+ path: chapter.url.slice(1),
+ releaseTime: chapter.date,
+ chapterNumber: chapter?.order,
+ page: matchingVolume ? matchingVolume.title : null,
+ };
+ },
+ );
- novel.chapters = chapter;
- return novel;
- }
+ novel.chapters = chapter;
+ return novel;
+ }
+ async parseChapter(chapterPath: string): Promise {
+ const result = await fetchApi(this.site + chapterPath);
+ const body = await result.text();
- async parseChapter(chapterPath: string): Promise {
- const result = await fetchApi(this.site + chapterPath);
- const body = await result.text();
+ const loadedCheerio = parseHTML(body);
- const loadedCheerio = parseHTML(body);
+ const chapterText = loadedCheerio('.chapter-content').html() || '';
- const chapterText = loadedCheerio(".chapter-content").html() || '';
+ return chapterText;
+ }
- return chapterText;
- }
+ async searchNovels(
+ searchTerm: string,
+ page: number,
+ ): Promise {
+ const searchUrl = `${this.site}fictions/search?page=${page}&title=${searchTerm}`;
- async searchNovels(searchTerm: string, page: number): Promise {
- const searchUrl = `${this.site}fictions/search?page=${page}&title=${searchTerm}`;
+ const result = await fetchApi(searchUrl);
+ const body = await result.text();
- const result = await fetchApi(searchUrl);
- const body = await result.text();
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
+ async fetchImage(url: string): Promise {
+ return fetchFile(url);
+ }
- async fetchImage(url: string): Promise {
- return fetchFile(url);
- }
-
- filters = {
- order: {
- value: "weekly-popular",
- label: "Order By",
- options: [
- { label: "Best Rated", value: "best-rated" },
- { label: "Trending", value: "trending" },
- { label: "Ongoing Fictions", value: "active-popular" },
- { label: "Complete", value: "complete" },
- { label: "Popular this week", value: "weekly-popular" },
- { label: "Latest Updates", value: "latest-updates" },
- { label: "Newest Fictions", value: "new" },
- { label: "Rising Stars", value: "rising-stars" },
- { label: "Writathon", value: "writathon" },
- ],
- type: FilterTypes.Picker,
- },
- genre: {
- value: "",
- label: "Genres",
- options: [
- { label: "ALL", value: "" },
- { label: "Action", value: "action" },
- { label: "Adventure", value: "adventure" },
- { label: "Comedy", value: "comedy" },
- { label: "Contemporary", value: "contemporary" },
- { label: "Drama", value: "drama" },
- { label: "Fantasy", value: "fantasy" },
- { label: "Historical", value: "historical" },
- { label: "Horror", value: "horror" },
- { label: "Mystery", value: "mystery" },
- { label: "Psychological", value: "psychological" },
- { label: "Romance", value: "romance" },
- { label: "Satire", value: "satire" },
- { label: "Sci-fi", value: "sci_fi" },
- { label: "Short Story", value: "one_shot" },
- { label: "Tragedy", value: "tragedy" }
- ],
- type: FilterTypes.Picker,
- }
- } satisfies Filters;
+ filters = {
+ order: {
+ value: 'weekly-popular',
+ label: 'Order By',
+ options: [
+ { label: 'Best Rated', value: 'best-rated' },
+ { label: 'Trending', value: 'trending' },
+ { label: 'Ongoing Fictions', value: 'active-popular' },
+ { label: 'Complete', value: 'complete' },
+ { label: 'Popular this week', value: 'weekly-popular' },
+ { label: 'Latest Updates', value: 'latest-updates' },
+ { label: 'Newest Fictions', value: 'new' },
+ { label: 'Rising Stars', value: 'rising-stars' },
+ { label: 'Writathon', value: 'writathon' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ genre: {
+ value: '',
+ label: 'Genres',
+ options: [
+ { label: 'ALL', value: '' },
+ { label: 'Action', value: 'action' },
+ { label: 'Adventure', value: 'adventure' },
+ { label: 'Comedy', value: 'comedy' },
+ { label: 'Contemporary', value: 'contemporary' },
+ { label: 'Drama', value: 'drama' },
+ { label: 'Fantasy', value: 'fantasy' },
+ { label: 'Historical', value: 'historical' },
+ { label: 'Horror', value: 'horror' },
+ { label: 'Mystery', value: 'mystery' },
+ { label: 'Psychological', value: 'psychological' },
+ { label: 'Romance', value: 'romance' },
+ { label: 'Satire', value: 'satire' },
+ { label: 'Sci-fi', value: 'sci_fi' },
+ { label: 'Short Story', value: 'one_shot' },
+ { label: 'Tragedy', value: 'tragedy' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ } satisfies Filters;
}
export default new RoyalRoad();
interface ChapterEntry {
- id: number;
- volumeId: number;
- title: string;
- date: string;
- order: number;
- url: string;
+ id: number;
+ volumeId: number;
+ title: string;
+ date: string;
+ order: number;
+ url: string;
}
-
+
interface VolumeEntry {
- id: number;
- title: string;
- cover: string;
- order: number;
-}
\ No newline at end of file
+ id: number;
+ title: string;
+ cover: string;
+ order: number;
+}
diff --git a/plugins/english/scribblehub.ts b/plugins/english/scribblehub.ts
index 997318c..d7b4ea4 100644
--- a/plugins/english/scribblehub.ts
+++ b/plugins/english/scribblehub.ts
@@ -1,306 +1,307 @@
-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";
+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 ScribbleHubPlugin implements Plugin.PluginBase {
- id = "scribblehub";
- name = "ScribbleHub";
- icon = "src/en/scribblehub/icon.png";
- site = "https://www.scribblehub.com/";
- version = "1.0.0";
- parseNovels(loadedCheerio: CheerioAPI){
- const novels: Plugin.NovelItem[] = [];
+ id = 'scribblehub';
+ name = 'ScribbleHub';
+ icon = 'src/en/scribblehub/icon.png';
+ site = 'https://www.scribblehub.com/';
+ version = '1.0.0';
+ parseNovels(loadedCheerio: CheerioAPI) {
+ const novels: Plugin.NovelItem[] = [];
- 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;
+ 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');
- const novel = {
- name: novelName,
- cover: novelCover,
- path: novelUrl.replace(this.site, ''),
- }
- novels.push(novel);
- });
- return novels;
- }
+ if (!novelUrl) return;
- async popularNovels(
- page: number,
- { showLatestNovels, filters }: Plugin.PopularNovelsOptions
- ): Promise {
- let link = `${this.site}`;
- if (showLatestNovels)
- link += "latest-series/"
- else // if (filters.genres.value.include?.length ||
- // filters.genres.value.exclude?.length)
- link += "series-finder/?sf=1"
- // else
- // link += "series-ranking/";
- // TODO, series-ranking filters when hideOnSelect come out
+ const novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl.replace(this.site, ''),
+ };
+ novels.push(novel);
+ });
+ return novels;
+ }
- if (filters.genres.value.include?.length)
- link += '&gi=' + filters.genres.value.include.join(',');
+ async popularNovels(
+ page: number,
+ {
+ showLatestNovels,
+ filters,
+ }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ let link = `${this.site}`;
+ if (showLatestNovels) link += 'latest-series/';
+ // if (filters.genres.value.include?.length ||
+ // filters.genres.value.exclude?.length)
+ else link += 'series-finder/?sf=1';
+ // else
+ // link += "series-ranking/";
+ // TODO, series-ranking filters when hideOnSelect come out
- if (filters.genres.value.exclude?.length)
- link += '&ge=' + filters.genres.value.exclude.join(',');
+ if (filters.genres.value.include?.length)
+ link += '&gi=' + filters.genres.value.include.join(',');
- if (filters.genres.value.include?.length || filters.genres.value.exclude?.length)
- link += '&mgi=' + filters.genre_operator.value;
+ if (filters.genres.value.exclude?.length)
+ link += '&ge=' + filters.genres.value.exclude.join(',');
- if (filters.content_warning.value.include?.length)
- link += '&cti=' + filters.content_warning.value.include.join(',');
+ if (
+ filters.genres.value.include?.length ||
+ filters.genres.value.exclude?.length
+ )
+ link += '&mgi=' + filters.genre_operator.value;
- if (filters.content_warning.value.exclude?.length)
- link += '&cte=' + filters.content_warning.value.exclude.join(',');
+ if (filters.content_warning.value.include?.length)
+ link += '&cti=' + filters.content_warning.value.include.join(',');
- if (filters.content_warning.value.include?.length ||
- filters.content_warning.value.exclude?.length)
- link += '&mct' + filters.content_warning_operator.value;
+ if (filters.content_warning.value.exclude?.length)
+ link += '&cte=' + filters.content_warning.value.exclude.join(',');
- link += '&sort=' + filters.sort.value;
- link += '&order=' + filters.order.value;
+ if (
+ filters.content_warning.value.include?.length ||
+ filters.content_warning.value.exclude?.length
+ )
+ link += '&mct' + filters.content_warning_operator.value;
- const body = await fetchApi(link).then((result) =>
- result.text()
- );
+ link += '&sort=' + filters.sort.value;
+ link += '&order=' + filters.order.value;
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
+ const body = await fetchApi(link).then(result => result.text());
- async parseNovel(novelPath: string): Promise {
- const result = await fetchApi(this.site + novelPath);
- const body = await result.text();
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
- let loadedCheerio = parseHTML(body);
+ async parseNovel(novelPath: string): Promise {
+ const result = await fetchApi(this.site + novelPath);
+ const body = await result.text();
- const novel: Plugin.SourceNovel = {
- 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.genres = loadedCheerio('.fic_genre')
- .map((i, el) => loadedCheerio(el).text())
- .toArray()
- .join(",");
-
- novel.status = loadedCheerio('.rnd_stats')
- .next()
- .text()
- .includes('Ongoing')
- ? 'Ongoing'
- : 'Completed';
+ let loadedCheerio = parseHTML(body);
- const formData = new FormData();
- formData.append('action', 'wi_getreleases_pagination');
- formData.append('pagenum', '-1');
- formData.append('mypostid', novelPath.split('/')[1]);
+ const novel: Plugin.SourceNovel = {
+ 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: [],
+ };
- const data = await fetchApi(
- `${this.site}wp-admin/admin-ajax.php`,
- {
- method: 'POST',
- body: formData,
- },
- );
- const text = await data.text();
+ novel.genres = loadedCheerio('.fic_genre')
+ .map((i, el) => loadedCheerio(el).text())
+ .toArray()
+ .join(',');
- loadedCheerio = parseHTML(text);
+ novel.status = loadedCheerio('.rnd_stats').next().text().includes('Ongoing')
+ ? 'Ongoing'
+ : 'Completed';
- const chapter: Plugin.ChapterItem[] = [];
+ const formData = new FormData();
+ formData.append('action', 'wi_getreleases_pagination');
+ formData.append('pagenum', '-1');
+ formData.append('mypostid', novelPath.split('/')[1]);
- const parseISODate = (date: string) => {
- 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
- }
-
- return dayJSDate.toISOString();
- }
- return null;
- };
+ const data = await fetchApi(`${this.site}wp-admin/admin-ajax.php`, {
+ method: 'POST',
+ body: formData,
+ });
+ const text = await data.text();
- 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');
+ loadedCheerio = parseHTML(text);
- if (!chapterUrl) return;
- chapter.push({
- name: chapterName,
- releaseTime: parseISODate(releaseDate),
- path: chapterUrl.replace(this.site, ''),
- });
- });
+ const chapter: Plugin.ChapterItem[] = [];
- novel.chapters = chapter.reverse();
+ const parseISODate = (date: string) => {
+ if (date.includes('ago')) {
+ const dayJSDate = dayjs(new Date()); // today
+ const timeAgo = date.match(/\d+/)?.[0] || '';
+ const timeAgoInt = parseInt(timeAgo, 10);
- return novel;
- }
+ if (!timeAgo) return null; // there is no number!
- async parseChapter(chapterPath: string): Promise {
- const result = await fetchApi(this.site + chapterPath);
- const body = await result.text();
+ if (date.includes('hours ago') || date.includes('hour ago')) {
+ dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours
+ }
- const loadedCheerio = parseHTML(body);
+ if (date.includes('days ago') || date.includes('day ago')) {
+ dayJSDate.subtract(timeAgoInt, 'days'); // go back N days
+ }
- const chapterText = loadedCheerio('div.chp_raw').html() || '';
- return chapterText;
- }
+ if (date.includes('months ago') || date.includes('month ago')) {
+ dayJSDate.subtract(timeAgoInt, 'months'); // go back N months
+ }
- async searchNovels(
- searchTerm: string,
- pageNo: number
- ): Promise {
- const url =`${this.site}?s=${searchTerm}&post_type=fictionposts`;
- const result = await fetchApi(url);
- const body = await result.text();
+ return dayJSDate.toISOString();
+ }
+ return null;
+ };
- const loadedCheerio = parseHTML(body);
- return this.parseNovels(loadedCheerio);
- }
+ 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');
- async fetchImage(url: string): Promise {
- return await fetchFile(url);
- }
+ if (!chapterUrl) return;
+ chapter.push({
+ name: chapterName,
+ releaseTime: parseISODate(releaseDate),
+ path: chapterUrl.replace(this.site, ''),
+ });
+ });
- filters = {
- sort: {
- label: "Sort Results By",
- value: "ratings",
- options: [
- { label: "Chapters", value: "chapters" },
- { label: "Chapters per week", value: "frequency" },
- { label: "Date Added", value: "dateadded" },
- { label: "Favorites", value: "favorites" },
- { label: "Last Updated", value: "lastchdate" },
- { label: "Number of Ratings", value: "numofrate" },
- { label: "Pages", value: "pages" },
- { label: "Pageviews", value: "pageviews" },
- { label: "Ratings", value: "ratings" },
- { label: "Readers", value: "readers" },
- { label: "Reviews", value: "reviews" },
- { label: "Total Words", value: "totalwords" },
- ],
- type: FilterTypes.Picker,
- },
- order: {
- label: "Order by",
- value: "desc",
- options: [
- { label: "Descending", value: "desc" },
- { label: "Ascending", value: "asc" },
- ],
- type: FilterTypes.Picker,
- },
- storyStatus: {
- label: "Story Status (Translation)",
- value: "",
- options: [
- { label: "All", value: "all" },
- { label: "Completed", value: "completed" },
- { label: "Ongoing", value: "ongoing" },
- { label: "Hiatus", value: "hiatus" },
- ],
- type: FilterTypes.Picker,
- },
- genres: {
- label: 'Genres',
- value: {
- include: [],
- exclude: [],
- },
- options: [
- { label: 'Action', value: '9' },
- { label: 'Adult', value: '902' },
- { label: 'Adventure', value: '8' },
- { label: 'Boys Love', value: '891' },
- { label: 'Comedy', value: '7' },
- { label: 'Drama', value: '903' },
- { label: 'Ecchi', value: '904' },
- { label: 'Fanfiction', value: '38' },
- { label: 'Fantasy', value: '19' },
- { label: 'Gender Bender', value: '905' },
- { label: 'Girls Love', value: '892' },
- { label: 'Harem', value: '1015' },
- { label: 'Historical', value: '21' },
- { label: 'Horror', value: '22' },
- { label: 'Isekai', value: '37' },
- { label: 'Josei', value: '906' },
- { label: 'LitRPG', value: '1180' },
- { label: 'Martial Arts', value: '907' },
- { label: 'Mature', value: '20' },
- { label: 'Mecha', value: '908' },
- { label: 'Mystery', value: '909' },
- { label: 'Psychological', value: '910' },
- { label: 'Romance', value: '6' },
- { label: 'School Life', value: '911' },
- { label: 'Sci-fi', value: '912' },
- { label: 'Seinen', value: '913' },
- { label: 'Slice of Life', value: '914' },
- { label: 'Smut', value: '915' },
- { label: 'Sports', value: '916' },
- { label: 'Supernatural', value: '5' },
- { label: 'Tragedy', value: '901' },
- ],
- type: FilterTypes.ExcludableCheckboxGroup,
- },
- genre_operator : {
- value: 'and',
- label: 'Genre And/Or',
- options: [
- { label: 'OR', value: 'or' },
- { label: 'AND', value: 'and' },
- ],
- type: FilterTypes.Picker,
- },
- content_warning: {
- value: {
- include: [],
- exclude: [],
- },
- label: "Mature Content",
- options: [
- { label: 'Gore', value: '48' },
- { label: 'Sexual Content', value: '50' },
- { label: 'Strong Language', value: '49' },
- ],
- type: FilterTypes.ExcludableCheckboxGroup
- },
- content_warning_operator: {
- value: 'and',
- label: 'Mature Content And/Or',
- options: [
- { label: 'OR', value: 'or' },
- { label: 'AND', value: 'and' },
- ],
- type: FilterTypes.Picker,
- },
- } satisfies Filters;
+ novel.chapters = chapter.reverse();
+
+ return novel;
+ }
+
+ async parseChapter(chapterPath: string): Promise {
+ const result = await fetchApi(this.site + chapterPath);
+ const body = await result.text();
+
+ const loadedCheerio = parseHTML(body);
+
+ const chapterText = loadedCheerio('div.chp_raw').html() || '';
+ return chapterText;
+ }
+
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ const url = `${this.site}?s=${searchTerm}&post_type=fictionposts`;
+ const result = await fetchApi(url);
+ const body = await result.text();
+
+ const loadedCheerio = parseHTML(body);
+ return this.parseNovels(loadedCheerio);
+ }
+
+ async fetchImage(url: string): Promise {
+ return await fetchFile(url);
+ }
+
+ filters = {
+ sort: {
+ label: 'Sort Results By',
+ value: 'ratings',
+ options: [
+ { label: 'Chapters', value: 'chapters' },
+ { label: 'Chapters per week', value: 'frequency' },
+ { label: 'Date Added', value: 'dateadded' },
+ { label: 'Favorites', value: 'favorites' },
+ { label: 'Last Updated', value: 'lastchdate' },
+ { label: 'Number of Ratings', value: 'numofrate' },
+ { label: 'Pages', value: 'pages' },
+ { label: 'Pageviews', value: 'pageviews' },
+ { label: 'Ratings', value: 'ratings' },
+ { label: 'Readers', value: 'readers' },
+ { label: 'Reviews', value: 'reviews' },
+ { label: 'Total Words', value: 'totalwords' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ order: {
+ label: 'Order by',
+ value: 'desc',
+ options: [
+ { label: 'Descending', value: 'desc' },
+ { label: 'Ascending', value: 'asc' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ storyStatus: {
+ label: 'Story Status (Translation)',
+ value: '',
+ options: [
+ { label: 'All', value: 'all' },
+ { label: 'Completed', value: 'completed' },
+ { label: 'Ongoing', value: 'ongoing' },
+ { label: 'Hiatus', value: 'hiatus' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ genres: {
+ label: 'Genres',
+ value: {
+ include: [],
+ exclude: [],
+ },
+ options: [
+ { label: 'Action', value: '9' },
+ { label: 'Adult', value: '902' },
+ { label: 'Adventure', value: '8' },
+ { label: 'Boys Love', value: '891' },
+ { label: 'Comedy', value: '7' },
+ { label: 'Drama', value: '903' },
+ { label: 'Ecchi', value: '904' },
+ { label: 'Fanfiction', value: '38' },
+ { label: 'Fantasy', value: '19' },
+ { label: 'Gender Bender', value: '905' },
+ { label: 'Girls Love', value: '892' },
+ { label: 'Harem', value: '1015' },
+ { label: 'Historical', value: '21' },
+ { label: 'Horror', value: '22' },
+ { label: 'Isekai', value: '37' },
+ { label: 'Josei', value: '906' },
+ { label: 'LitRPG', value: '1180' },
+ { label: 'Martial Arts', value: '907' },
+ { label: 'Mature', value: '20' },
+ { label: 'Mecha', value: '908' },
+ { label: 'Mystery', value: '909' },
+ { label: 'Psychological', value: '910' },
+ { label: 'Romance', value: '6' },
+ { label: 'School Life', value: '911' },
+ { label: 'Sci-fi', value: '912' },
+ { label: 'Seinen', value: '913' },
+ { label: 'Slice of Life', value: '914' },
+ { label: 'Smut', value: '915' },
+ { label: 'Sports', value: '916' },
+ { label: 'Supernatural', value: '5' },
+ { label: 'Tragedy', value: '901' },
+ ],
+ type: FilterTypes.ExcludableCheckboxGroup,
+ },
+ genre_operator: {
+ value: 'and',
+ label: 'Genre And/Or',
+ options: [
+ { label: 'OR', value: 'or' },
+ { label: 'AND', value: 'and' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ content_warning: {
+ value: {
+ include: [],
+ exclude: [],
+ },
+ label: 'Mature Content',
+ options: [
+ { label: 'Gore', value: '48' },
+ { label: 'Sexual Content', value: '50' },
+ { label: 'Strong Language', value: '49' },
+ ],
+ type: FilterTypes.ExcludableCheckboxGroup,
+ },
+ content_warning_operator: {
+ value: 'and',
+ label: 'Mature Content And/Or',
+ options: [
+ { label: 'OR', value: 'or' },
+ { label: 'AND', value: 'and' },
+ ],
+ type: FilterTypes.Picker,
+ },
+ } satisfies Filters;
}
export default new ScribbleHubPlugin();
diff --git a/plugins/english/wtrlab.ts b/plugins/english/wtrlab.ts
index 4fe9bb3..07903d8 100644
--- a/plugins/english/wtrlab.ts
+++ b/plugins/english/wtrlab.ts
@@ -1,230 +1,247 @@
-import { Plugin } from "@typings/plugin";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { FilterTypes, Filters } from "@libs/filterInputs";
-import { load as parseHTML } from "cheerio";
+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/"
+ 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,
+ {
+ showLatestNovels,
+ filters,
+ }: Plugin.PopularNovelsOptions,
): Promise {
- 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){
+ 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'
+ 'Content-Type': 'application/json',
},
- body: JSON.stringify({ page: page })
+ 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 || "",
- }));
+ 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());
+ } else {
+ const body = await fetchApi(link).then(res => res.text());
const loadedCheerio = parseHTML(body);
//console.log(link);
- const novels: Plugin.NovelItem[] = loadedCheerio(".serie-item")
+ 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") || "",
+ 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;
+ .filter(novel => novel.name && novel.path);
+ return novels;
}
-
}
async parseNovel(novelPath: string): Promise {
- const body = await fetchApi(this.site + novelPath).then((res) => res.text());
+ 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(),
+ 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.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, "");
+ .replace(/[\t\n]/g, '');
novel.status = loadedCheerio('td:contains("Status")')
.next()
.text()
- .replace(/[\t\n]/g, "");
+ .replace(/[\t\n]/g, '');
-
- const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + "";
+ 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
- }));
+ 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 {
- const body = await fetchApi(this.site + chapterPath).then((res) => res.text());
+ const body = await fetchApi(this.site + chapterPath).then(res =>
+ res.text(),
+ );
const loadedCheerio = parseHTML(body);
- const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + "";
+ 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);
+ 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 += `${text}
`;
- }
-
-
-
+ }
return htmlString;
-}
+ }
async searchNovels(searchTerm: string): Promise {
- const response = await fetch(this.site + "api/search", {
+ const response = await fetch(this.site + 'api/search', {
headers: {
- "Content-Type": "application/json",
+ 'Content-Type': 'application/json',
Referer: this.site + this.sourceLang,
Origin: this.site,
},
- method: "POST",
- body: JSON.stringify({ text: searchTerm}),
+ 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 || "",
- }));
+ // 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,
+ 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,
+ 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,
+ 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;
+ props: Props;
+ page: string;
}
interface Props {
pageProps: PageProps;
- __N_SSP: boolean;
+ __N_SSP: boolean;
}
interface PageProps {
- serie: Serie;
+ serie: Serie;
server_time: Date;
}
interface Serie {
- serie_data: SerieData;
- chapters: Chapter[];
+ serie_data: SerieData;
+ chapters: Chapter[];
recommendation: SerieData[];
- chapter_data: ChapterData;
- id: number;
- raw_id: number;
- slug: string;
- data: Data;
+ chapter_data: ChapterData;
+ id: number;
+ raw_id: number;
+ slug: string;
+ data: Data;
is_default: boolean;
- raw_type: string;
+ raw_type: string;
}
interface Chapter {
- serie_id: number;
- id: number;
- slug: string;
- title: string;
- name: string;
+ serie_id: number;
+ id: number;
+ slug: string;
+ title: string;
+ name: string;
created_at: string;
}
interface ChapterData {
@@ -236,51 +253,47 @@ interface ChapterContent {
}
interface SerieData {
- serie_id?: number;
+ 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[];
+ 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;
+ title: string;
+ author: string;
description: string;
- image: string;
+ image: string;
}
interface JsonNovel {
success: boolean;
- data: Datum[];
+ data: Datum[];
}
interface Datum {
- serie: Serie;
- chapters: Chapter[];
+ serie: Serie;
+ chapters: Chapter[];
updated_at: Date;
- raw_id: number;
- slug: string;
- data: Data;
-
+ raw_id: number;
+ slug: string;
+ data: Data;
}
-
-
-
export default new WTRLAB();
diff --git a/plugins/english/wuxiaworld.ts b/plugins/english/wuxiaworld.ts
index 0a0aaad..f824c91 100644
--- a/plugins/english/wuxiaworld.ts
+++ b/plugins/english/wuxiaworld.ts
@@ -1,283 +1,295 @@
-import { load as parseHTML } from "cheerio";
-import { fetchApi, fetchFile, fetchProto } from "@libs/fetch";
-import { Plugin } from "@typings/plugin";
-import { Filters } from "@libs/filterInputs";
-import { NovelStatus } from "@libs/novelStatus";
+import { load as parseHTML } from 'cheerio';
+import { fetchApi, fetchFile, fetchProto } from '@libs/fetch';
+import { Plugin } from '@typings/plugin';
+import { Filters } from '@libs/filterInputs';
+import { NovelStatus } from '@libs/novelStatus';
interface StringValue {
- value: string;
+ value: string;
}
interface BoolValue {
- value: boolean;
+ value: boolean;
}
interface DecimalValue {
- units: bigint;
- nanos: number;
+ units: bigint;
+ nanos: number;
}
interface Timestamp {
- seconds: number;
- nanos: number;
+ seconds: number;
+ nanos: number;
}
interface NovelEntry {
- name: string;
- coverUrl: string;
- slug: string;
+ name: string;
+ coverUrl: string;
+ slug: string;
}
interface RelatedChapterUserInfo {
- isChapterUnlocked?: BoolValue;
+ isChapterUnlocked?: BoolValue;
}
interface ChapterItem {
- entityId: number;
- name: string;
- slug: string;
- number?: DecimalValue;
- content?: StringValue;
- relatedUserInfo?: RelatedChapterUserInfo;
- offset: number;
- publishedAt?: Timestamp;
+ entityId: number;
+ name: string;
+ slug: string;
+ number?: DecimalValue;
+ content?: StringValue;
+ relatedUserInfo?: RelatedChapterUserInfo;
+ offset: number;
+ publishedAt?: Timestamp;
}
interface GetChapterResponse {
- item?: ChapterItem;
+ item?: ChapterItem;
}
interface ChapterGroupCounts {
- total: number;
- advance: number;
- normal: number;
+ total: number;
+ advance: number;
+ normal: number;
}
interface ChapterGroupItem {
- id: number;
- title: string;
- order: number;
- fromChapterNumber?: DecimalValue;
- toChapterNumber?: DecimalValue;
- chapterList: ChapterItem[];
- counts?: ChapterGroupCounts;
+ id: number;
+ title: string;
+ order: number;
+ fromChapterNumber?: DecimalValue;
+ toChapterNumber?: DecimalValue;
+ chapterList: ChapterItem[];
+ counts?: ChapterGroupCounts;
}
interface GetChapterListResponse {
- items: ChapterGroupItem[];
+ items: ChapterGroupItem[];
}
enum NovelItem_Status {
- Finished = 0,
- Active = 1,
- Hiatus = 2,
- All = -1
+ Finished = 0,
+ Active = 1,
+ Hiatus = 2,
+ All = -1,
}
interface NovelKarmaInfo {
- isActive: boolean;
- isFree: boolean;
- maxFreeChapter?: DecimalValue;
- canUnlockWithVip: boolean;
+ isActive: boolean;
+ isFree: boolean;
+ maxFreeChapter?: DecimalValue;
+ canUnlockWithVip: boolean;
}
interface NovelItem {
- id: number;
- name: string;
- slug: string;
- status: NovelItem_Status;
- visible: boolean;
- description?: StringValue;
- synopsis?: StringValue;
- coverUrl?: StringValue;
- translatorName?: StringValue;
- authorName?: StringValue;
- karmaInfo?: NovelKarmaInfo;
- genres: string[];
+ id: number;
+ name: string;
+ slug: string;
+ status: NovelItem_Status;
+ visible: boolean;
+ description?: StringValue;
+ synopsis?: StringValue;
+ coverUrl?: StringValue;
+ translatorName?: StringValue;
+ authorName?: StringValue;
+ karmaInfo?: NovelKarmaInfo;
+ genres: string[];
}
interface GetNovelResponse {
- item?: NovelItem;
+ item?: NovelItem;
}
class WuxiaWorld implements Plugin.PluginBase {
- id = "wuxiaworld";
- name = "Wuxia World";
- icon = "src/en/wuxiaworld/icon.png";
- site = "https://www.wuxiaworld.com/";
- filters?: Filters | undefined;
- version = "0.5.0";
+ id = 'wuxiaworld';
+ name = 'Wuxia World';
+ icon = 'src/en/wuxiaworld/icon.png';
+ site = 'https://www.wuxiaworld.com/';
+ filters?: Filters | undefined;
+ version = '0.5.0';
- parseNovels(data: {items: NovelEntry[]}){
- const novels: Plugin.NovelItem[] = [];
+ parseNovels(data: { items: NovelEntry[] }) {
+ const novels: Plugin.NovelItem[] = [];
- data.items.map((novel: NovelEntry) => {
- const name = novel.name;
- const cover = novel.coverUrl;
- const path = `novel/${novel.slug}/`;
+ data.items.map((novel: NovelEntry) => {
+ const name = novel.name;
+ const cover = novel.coverUrl;
+ const path = `novel/${novel.slug}/`;
- novels.push({
- name,
- cover,
- path,
- });
- });
+ novels.push({
+ name,
+ cover,
+ path,
+ });
+ });
- return novels;
+ return novels;
+ }
+
+ async popularNovels(
+ pageNo: number,
+ options: Plugin.PopularNovelsOptions,
+ ): Promise {
+ const link = `${this.site}api/novels`;
+
+ const result = await fetchApi(link);
+ const data = await result.json();
+
+ return this.parseNovels(data);
+ }
+
+ async parseNovel(novelPath: string): Promise {
+ const data = await fetchProto(
+ {
+ proto: this.proto,
+ requestType: 'GetNovelRequest',
+ responseType: 'GetNovelResponse',
+ requestData: { slug: novelPath.split('/')[1] },
+ },
+ 'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Novels/GetNovel',
+ {
+ headers: {
+ 'Content-Type': 'application/grpc-web+proto',
+ },
+ },
+ );
+
+ const novel: Plugin.SourceNovel = {
+ path: novelPath,
+ name: data.item?.name || 'Untitled',
+ cover: data.item?.coverUrl?.value,
+ summary: parseHTML(
+ data.item?.description?.value + '\n\n' + data.item?.synopsis?.value,
+ ).text(),
+ author: data.item?.authorName?.value,
+ genres: data.item?.genres.join(','),
+ chapters: [],
+ };
+
+ const status = data.item?.status;
+ switch (status) {
+ case NovelItem_Status.Active:
+ novel.status = NovelStatus.Ongoing;
+ break;
+ case NovelItem_Status.Hiatus:
+ novel.status = NovelStatus.OnHiatus;
+ break;
+ case NovelItem_Status.All:
+ novel.status = NovelStatus.Unknown;
+ break;
+ default:
+ novel.status = NovelStatus.Completed;
}
- async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise {
- const link = `${this.site}api/novels`;
+ // novel.summary = loadedCheerio('.relative > .absolute:first')
+ // .children('span')
+ // .map((i,el) => loadedCheerio(el).text().trim())
+ // .toArray()
+ // .join('\n');
- const result = await fetchApi(link);
- const data = await result.json();
+ // novel.genres = loadedCheerio("a.MuiLink-underlineNone")
+ // .map((i,el) => loadedCheerio(el).text())
+ // .toArray()
+ // .join(',');
- return this.parseNovels(data)
- }
+ // novel.status = loadedCheerio("div.font-set-b10").text();
- async parseNovel(novelPath: string): Promise {
- const data = await fetchProto(
- {
- proto: this.proto,
- requestType: 'GetNovelRequest',
- responseType: 'GetNovelResponse',
- requestData: {slug: novelPath.split('/')[1]},
+ const list = await fetchProto(
+ {
+ proto: this.proto,
+ requestType: 'GetChapterListRequest',
+ responseType: 'GetChapterListResponse',
+ requestData: { novelId: data.item?.id },
+ },
+ 'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Chapters/GetChapterList',
+ {
+ headers: {
+ 'Content-Type': 'application/grpc-web+proto',
+ },
+ },
+ );
+
+ const freeChapter =
+ Number(data.item?.karmaInfo?.maxFreeChapter?.units) +
+ (data.item?.karmaInfo?.maxFreeChapter?.nanos || 0) / 1000000000 || 50;
+
+ const chapter: Plugin.ChapterItem[] = list.items.flatMap(
+ (ChapterGroupItem: ChapterGroupItem) =>
+ ChapterGroupItem.chapterList.map((chapterItem: ChapterItem) => ({
+ page: ChapterGroupItem.title,
+ name:
+ chapterItem.name +
+ (chapterItem.relatedUserInfo?.isChapterUnlocked?.value === false ||
+ (!chapterItem.relatedUserInfo &&
+ Number(chapterItem.number?.units) +
+ (chapterItem.number?.nanos || 0) / 1000000000 >
+ freeChapter)
+ ? ' 🔒'
+ : ''),
+ path: novelPath + chapterItem.slug,
+ chapterNumber: chapterItem.offset,
+ releaseTime: new Date(
+ (chapterItem.publishedAt?.seconds || 0) * 1000 +
+ (chapterItem.publishedAt?.nanos || 0) / 1000000,
+ ).toISOString(),
+ })),
+ );
+
+ novel.chapters = chapter;
+
+ return novel;
+ }
+
+ async parseChapter(chapterPath: string): Promise {
+ const paths = chapterPath.split('/');
+ const data = await fetchProto(
+ {
+ proto: this.proto,
+ requestType: 'GetChapterRequest',
+ responseType: 'GetChapterResponse',
+ requestData: {
+ chapterProperty: {
+ slugs: {
+ novelSlug: paths[1],
+ chapterSlug: paths[2],
},
- 'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Novels/GetNovel',
- {
- headers: {
- "Content-Type": "application/grpc-web+proto",
- }
- }
- )
+ },
+ },
+ },
+ 'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Chapters/GetChapter',
+ {
+ headers: {
+ 'Content-Type': 'application/grpc-web+proto',
+ },
+ },
+ );
+ // loadedCheerio(".chapter-nav").remove();
+ // loadedCheerio("#chapter-content > script").remove();
+ // const chapterText = loadedCheerio("#chapter-content").html() || '';
- const novel: Plugin.SourceNovel = {
- path: novelPath,
- name: data.item?.name || "Untitled",
- cover: data.item?.coverUrl?.value,
- summary: parseHTML(data.item?.description?.value + '\n\n' + data.item?.synopsis?.value).text(),
- author: data.item?.authorName?.value,
- genres: data.item?.genres.join(','),
- chapters: [],
- };
+ const chapterText = data.item?.content?.value || '';
+ return chapterText;
+ }
- const status = data.item?.status;
- switch (status){
- case NovelItem_Status.Active:
- novel.status = NovelStatus.Ongoing;
- break;
- case NovelItem_Status.Hiatus:
- novel.status = NovelStatus.OnHiatus;
- break;
- case NovelItem_Status.All:
- novel.status = NovelStatus.Unknown;
- break;
- default: novel.status = NovelStatus.Completed;
- }
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ const searchUrl = 'https://www.wuxiaworld.com/api/novels/search?query=';
- // novel.summary = loadedCheerio('.relative > .absolute:first')
- // .children('span')
- // .map((i,el) => loadedCheerio(el).text().trim())
- // .toArray()
- // .join('\n');
+ const url = searchUrl + searchTerm;
- // novel.genres = loadedCheerio("a.MuiLink-underlineNone")
- // .map((i,el) => loadedCheerio(el).text())
- // .toArray()
- // .join(',');
+ const result = await fetchApi(url);
+ const data = await result.json();
- // novel.status = loadedCheerio("div.font-set-b10").text();
+ return this.parseNovels(data);
+ }
- const list = await fetchProto(
- {
- proto: this.proto,
- requestType: 'GetChapterListRequest',
- responseType: 'GetChapterListResponse',
- requestData: {novelId: data.item?.id},
- },
- 'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Chapters/GetChapterList',
- {
- headers: {
- "Content-Type": "application/grpc-web+proto",
- }
- }
- )
+ fetchImage(url: string): Promise {
+ return fetchFile(url);
+ }
- const freeChapter = Number(data.item?.karmaInfo?.maxFreeChapter?.units) +
- (data.item?.karmaInfo?.maxFreeChapter?.nanos || 0) / 1000000000 || 50;
-
- const chapter: Plugin.ChapterItem[] = list.items.flatMap((ChapterGroupItem: ChapterGroupItem) =>
- ChapterGroupItem.chapterList.map((chapterItem: ChapterItem) => ({
- page: ChapterGroupItem.title,
- name: chapterItem.name + (
- (chapterItem.relatedUserInfo?.isChapterUnlocked?.value === false) ||
- (!chapterItem.relatedUserInfo &&
- (Number(chapterItem.number?.units) +
- (chapterItem.number?.nanos || 0) / 1000000000) > freeChapter)
- ? ' 🔒'
- : ''
- ),
- path: novelPath + chapterItem.slug,
- chapterNumber: chapterItem.offset,
- releaseTime: new Date(
- (chapterItem.publishedAt?.seconds || 0) * 1000 +
- (chapterItem.publishedAt?.nanos || 0) / 1000000
- ).toISOString(),
- })
- ));
-
- novel.chapters = chapter;
-
- return novel;
- }
-
- async parseChapter(chapterPath: string): Promise {
- const paths = chapterPath.split('/');
- const data = await fetchProto(
- {
- proto: this.proto,
- requestType: 'GetChapterRequest',
- responseType: 'GetChapterResponse',
- requestData: {
- chapterProperty: {
- slugs: {
- novelSlug: paths[1],
- chapterSlug: paths[2],
- }
- }
- },
- },
- 'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Chapters/GetChapter',
- {
- headers: {
- "Content-Type": "application/grpc-web+proto",
- }
- }
- )
- // loadedCheerio(".chapter-nav").remove();
- // loadedCheerio("#chapter-content > script").remove();
- // const chapterText = loadedCheerio("#chapter-content").html() || '';
-
- const chapterText = data.item?.content?.value || "";
- return chapterText
- }
-
- async searchNovels(searchTerm: string, pageNo: number): Promise {
- const searchUrl = "https://www.wuxiaworld.com/api/novels/search?query=";
-
- const url = searchUrl + searchTerm;
-
- const result = await fetchApi(url);
- const data = await result.json();
-
- return this.parseNovels(data);
- }
-
- fetchImage(url: string): Promise {
- return fetchFile(url);
- }
-
- proto = `
+ proto = `
syntax = "proto3";
option optimize_for = CODE_SIZE;
package wuxiaworld.api.v2;
@@ -460,7 +472,7 @@ class WuxiaWorld implements Plugin.PluginBase {
service Novels {
rpc GetNovel(GetNovelRequest) returns (GetNovelResponse);
}
- `
+ `;
}
-export default new WuxiaWorld();
\ No newline at end of file
+export default new WuxiaWorld();
diff --git a/plugins/french/chireads.ts b/plugins/french/chireads.ts
index 7d4b6ed..5118bd3 100644
--- a/plugins/french/chireads.ts
+++ b/plugins/french/chireads.ts
@@ -1,265 +1,341 @@
-import { CheerioAPI, load } from "cheerio";
-import { fetchApi, fetchFile } from "@libs/fetch";
-import { Plugin } from "@typings/plugin";
-import { Filters, FilterTypes } from "@libs/filterInputs";
-import dayjs from "dayjs";
-import { defaultCover } from "@libs/defaultCover";
-import { NovelStatus } from "@libs/novelStatus";
+import { CheerioAPI, load } from 'cheerio';
+import { fetchApi, fetchFile } from '@libs/fetch';
+import { Plugin } from '@typings/plugin';
+import { Filters, FilterTypes } from '@libs/filterInputs';
+import dayjs from 'dayjs';
+import { defaultCover } from '@libs/defaultCover';
+import { NovelStatus } from '@libs/novelStatus';
class ChireadsPlugin implements Plugin.PluginBase {
- id = "chireads";
- name = "Chireads";
- icon = "src/fr/chireads/icon.png";
- site = "https://chireads.com";
- version = "1.0.2";
+ id = 'chireads';
+ name = 'Chireads';
+ icon = 'src/fr/chireads/icon.png';
+ site = 'https://chireads.com';
+ version = '1.0.2';
- async getCheerio(url: string): Promise {
- const r = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }});
- const body = await r.text();
- const $ = load(body);
- return ($);
- }
+ async getCheerio(url: string): Promise {
+ const r = await fetchApi(url, {
+ headers: { 'Accept-Encoding': 'deflate' },
+ });
+ const body = await r.text();
+ const $ = load(body);
+ return $;
+ }
- async popularNovels(pageNo: number, { filters, showLatestNovels }: Plugin.PopularNovelsOptions): Promise {
- let url = this.site;
- let tag: string = "all";
- if (showLatestNovels)
- url += "/category/translatedtales/page/" + pageNo;
- else {
- if (filters && typeof filters.tag.value === "string" && filters.tag.value !== "all")
- tag = filters.tag.value;
- if (tag !== "all")
- url += "/tag/" + tag + "/page/" + pageNo;
- else if (pageNo > 1)
- return [];
- }
- let $ = await this.getCheerio(url);
+ async popularNovels(
+ pageNo: number,
+ { filters, showLatestNovels }: Plugin.PopularNovelsOptions,
+ ): Promise {
+ let url = this.site;
+ let tag: string = 'all';
+ if (showLatestNovels) url += '/category/translatedtales/page/' + pageNo;
+ else {
+ if (
+ filters &&
+ typeof filters.tag.value === 'string' &&
+ filters.tag.value !== 'all'
+ )
+ tag = filters.tag.value;
+ if (tag !== 'all') url += '/tag/' + tag + '/page/' + pageNo;
+ else if (pageNo > 1) return [];
+ }
+ let $ = await this.getCheerio(url);
- let novels: Plugin.NovelItem[] = [];
- let novel: Plugin.NovelItem;
+ let novels: Plugin.NovelItem[] = [];
+ let novel: Plugin.NovelItem;
- if (showLatestNovels || tag !== "all") {
- let loop = 1;
- if (showLatestNovels)
- loop = 2;
- for (let i = 0; i < loop; i++) {
- if (i === 1)
- $ = await this.getCheerio(this.site + "/category/original/page/" + pageNo);
- let romans = $(".romans-content li");
- if (!romans.length)
- romans = $("#content li");
- romans.each((i, elem) => {
- const novelName = $(elem).contents().find("div").first().text().trim();
- const novelCover = $(elem).find("div").first().find("img").attr("src");
- const novelUrl = $(elem).find("div").first().find("a").attr("href")
-
- if (novelUrl){
- novel = { name: novelName, cover: novelCover, path: novelUrl.replace(this.site, "") };
- novels.push(novel);
- }
- });
- }
- }
- else {
- const populaire = $(":contains(\"Populaire\")").last().parent().next().find("li > div")
- if (populaire.length === 12) // pc
- {
- let novelCover : string | undefined;
- let novelName : string | undefined;
- let novelUrl : string | undefined;
- populaire.each((i, elem) => {
- if (i % 2 === 0)
- novelCover = $(elem).find("img").attr("src");
- else {
- novelName = $(elem).text().trim();
- novelUrl = $(elem).find("a").attr("href");
-
- if (!novelUrl) return;
-
- novel = { name: novelName, cover: novelCover || defaultCover, path: novelUrl.replace(this.site, "") };
-
- novels.push(novel);
- }
- });
- }
- else // mobile
- {
- const imgs = populaire.find("div.popular-list-img img");
- const txts = populaire.find("div.popular-list-name");
+ if (showLatestNovels || tag !== 'all') {
+ let loop = 1;
+ if (showLatestNovels) loop = 2;
+ for (let i = 0; i < loop; i++) {
+ if (i === 1)
+ $ = await this.getCheerio(
+ this.site + '/category/original/page/' + pageNo,
+ );
+ let romans = $('.romans-content li');
+ if (!romans.length) romans = $('#content li');
+ romans.each((i, elem) => {
+ const novelName = $(elem)
+ .contents()
+ .find('div')
+ .first()
+ .text()
+ .trim();
+ const novelCover = $(elem)
+ .find('div')
+ .first()
+ .find('img')
+ .attr('src');
+ const novelUrl = $(elem).find('div').first().find('a').attr('href');
- txts.each((i, elem) => {
- const novelName = $(elem).text().trim();
- const novelCover = $(imgs[i]).attr("src");
- const novelUrl = $(elem).find("a").attr("href");
-
- if (novelUrl){
- novel = { name: novelName, cover: novelCover, path: novelUrl.replace(this.site, "") };
- novels.push(novel);
- }
- });
- }
- }
- return novels;
- };
-
- async parseNovel(novelPath: string): Promise {
- const novel: Plugin.SourceNovel = { path: novelPath, name: "Sans titre" };
-
- let $ = await this.getCheerio(this.site + novelPath);
-
- novel.name = $(".inform-product-txt").first().text().trim() || $(".inform-title").text().trim();
- novel.cover = $(".inform-product img").attr("src") || $(".inform-product-img img").attr("src") || defaultCover;
- novel.summary = $(".inform-inform-txt").text().trim() || $(".inform-intr-txt").text().trim();
-
- let infos = $("div.inform-product-txt > div.inform-intr-col").text().trim() || $("div.inform-inform-data > h6").text().trim();
- if (infos.includes("Auteur : "))
- novel.author = infos.substring(infos.indexOf("Auteur : ") + 9, infos.indexOf("Statut de Parution : ")).trim();
- else if (infos.includes("Fantrad : "))
- novel.author = infos.substring(infos.indexOf("Fantrad : ") + 10, infos.indexOf("Statut de Parution : ")).trim();
- else
- novel.author = "Inconnu";
- switch (infos.substring(infos.indexOf("Statut de Parution : ") + 21).toLowerCase()) {
- case "en pause":
- novel.status = NovelStatus.OnHiatus;
- break;
- case "complet":
- novel.status = NovelStatus.Completed;
- break;
- default:
- novel.status = NovelStatus.Ongoing;
- break;
- }
-
- let chapters: Plugin.ChapterItem[] = [];
-
- let chapterList = $(".chapitre-table a");
- if (!chapterList.length)
- {
- $("div.inform-annexe-list").first().remove();
- chapterList = $(".inform-annexe-list").find("a");
- }
- chapterList.each((i, elem) => {
- const chapterName = $(elem).text().trim();
- const chapterUrl = $(elem).attr("href");
- const releaseDate = dayjs(chapterUrl?.substring(chapterUrl.length - 11, chapterUrl.length - 1)).format("DD MMMM YYYY");
-
- if (chapterUrl){
- chapters.push({
- name: chapterName,
- releaseTime: releaseDate,
- path: chapterUrl.replace(this.site, ""),
- });
- }
+ if (novelUrl) {
+ novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl.replace(this.site, ''),
+ };
+ novels.push(novel);
+ }
});
+ }
+ } else {
+ const populaire = $(':contains("Populaire")')
+ .last()
+ .parent()
+ .next()
+ .find('li > div');
+ if (populaire.length === 12) {
+ // pc
+ let novelCover: string | undefined;
+ let novelName: string | undefined;
+ let novelUrl: string | undefined;
+ populaire.each((i, elem) => {
+ if (i % 2 === 0) novelCover = $(elem).find('img').attr('src');
+ else {
+ novelName = $(elem).text().trim();
+ novelUrl = $(elem).find('a').attr('href');
- novel.chapters = chapters;
+ if (!novelUrl) return;
- return novel;
- };
+ novel = {
+ name: novelName,
+ cover: novelCover || defaultCover,
+ path: novelUrl.replace(this.site, ''),
+ };
- async parseChapter(chapterUrl: string): Promise {
- const $ = await this.getCheerio(this.site + chapterUrl);
+ novels.push(novel);
+ }
+ });
+ } // mobile
+ else {
+ const imgs = populaire.find('div.popular-list-img img');
+ const txts = populaire.find('div.popular-list-name');
- const chapterText = $("#content").html() || "";
+ txts.each((i, elem) => {
+ const novelName = $(elem).text().trim();
+ const novelCover = $(imgs[i]).attr('src');
+ const novelUrl = $(elem).find('a').attr('href');
- return chapterText;
- };
+ if (novelUrl) {
+ novel = {
+ name: novelName,
+ cover: novelCover,
+ path: novelUrl.replace(this.site, ''),
+ };
+ novels.push(novel);
+ }
+ });
+ }
+ }
+ return novels;
+ }
- async searchNovels(searchTerm: string, pageNo: number): Promise {
- if (pageNo !== 1)
- return [];
- let novels: Plugin.NovelItem[] = [];
+ async parseNovel(novelPath: string): Promise {
+ const novel: Plugin.SourceNovel = { path: novelPath, name: 'Sans titre' };
- let i = 1;
- let finised = false;
- while ( !finised )
+ let $ = await this.getCheerio(this.site + novelPath);
+
+ novel.name =
+ $('.inform-product-txt').first().text().trim() ||
+ $('.inform-title').text().trim();
+ novel.cover =
+ $('.inform-product img').attr('src') ||
+ $('.inform-product-img img').attr('src') ||
+ defaultCover;
+ novel.summary =
+ $('.inform-inform-txt').text().trim() ||
+ $('.inform-intr-txt').text().trim();
+
+ let infos =
+ $('div.inform-product-txt > div.inform-intr-col').text().trim() ||
+ $('div.inform-inform-data > h6').text().trim();
+ if (infos.includes('Auteur : '))
+ novel.author = infos
+ .substring(
+ infos.indexOf('Auteur : ') + 9,
+ infos.indexOf('Statut de Parution : '),
+ )
+ .trim();
+ else if (infos.includes('Fantrad : '))
+ novel.author = infos
+ .substring(
+ infos.indexOf('Fantrad : ') + 10,
+ infos.indexOf('Statut de Parution : '),
+ )
+ .trim();
+ else novel.author = 'Inconnu';
+ switch (
+ infos.substring(infos.indexOf('Statut de Parution : ') + 21).toLowerCase()
+ ) {
+ case 'en pause':
+ novel.status = NovelStatus.OnHiatus;
+ break;
+ case 'complet':
+ novel.status = NovelStatus.Completed;
+ break;
+ default:
+ novel.status = NovelStatus.Ongoing;
+ break;
+ }
+
+ let chapters: Plugin.ChapterItem[] = [];
+
+ let chapterList = $('.chapitre-table a');
+ if (!chapterList.length) {
+ $('div.inform-annexe-list').first().remove();
+ chapterList = $('.inform-annexe-list').find('a');
+ }
+ chapterList.each((i, elem) => {
+ const chapterName = $(elem).text().trim();
+ const chapterUrl = $(elem).attr('href');
+ const releaseDate = dayjs(
+ chapterUrl?.substring(chapterUrl.length - 11, chapterUrl.length - 1),
+ ).format('DD MMMM YYYY');
+
+ if (chapterUrl) {
+ chapters.push({
+ name: chapterName,
+ releaseTime: releaseDate,
+ path: chapterUrl.replace(this.site, ''),
+ });
+ }
+ });
+
+ novel.chapters = chapters;
+
+ return novel;
+ }
+
+ async parseChapter(chapterUrl: string): Promise