* add prettier

* lint-staged

* prettier

* upgrade husky
This commit is contained in:
nyagami
2024-03-23 22:00:29 +07:00
committed by GitHub
parent c22979b10f
commit 5f9a067ab9
113 changed files with 79244 additions and 50841 deletions
+1
View File
@@ -0,0 +1 @@
_
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
+2
View File
@@ -0,0 +1,2 @@
node_modules
.js
+10
View File
@@ -0,0 +1,10 @@
module.exports = {
tabWidth: 2,
bracketSpacing: true,
useTabs: false,
bracketSameLine: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
quoteProps: 'preserve',
};
+20 -20
View File
@@ -1,18 +1,18 @@
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 { 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";
id = '';
name = '';
icon = '';
site = 'https://example.com';
version = '1.0.0';
filters: Filters | undefined = undefined;
async popularNovels(
@@ -20,14 +20,14 @@ class TemplatePlugin implements Plugin.PluginBase {
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = [];
/** Add your fetching code here */
novels.push({
name: "Novel1",
path: "/novels/1",
name: 'Novel1',
path: '/novels/1',
cover: defaultCover,
});
return novels;
@@ -35,7 +35,7 @@ class TemplatePlugin implements Plugin.PluginBase {
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const novel: Plugin.SourceNovel = {
path: novelPath,
name: 'Untitled'
name: 'Untitled',
};
// TODO: get here data from the site and
@@ -55,9 +55,9 @@ class TemplatePlugin implements Plugin.PluginBase {
// TODO: add each chapter to the list using
const chapter: Plugin.ChapterItem = {
name: "",
path: "",
releaseTime: "",
name: '',
path: '',
releaseTime: '',
chapterNumber: 0,
};
chapters.push(chapter);
@@ -67,12 +67,12 @@ class TemplatePlugin implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
// parse chapter text here
const chapterText = "";
const chapterText = '';
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo: number
pageNo: number,
): Promise<Plugin.NovelItem[]> {
let novels: Plugin.NovelItem[] = [];
+61 -67
View File
@@ -1,70 +1,64 @@
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);
}
const host = "localhost";
const host = 'localhost';
const dirname = path.join(__dirname, "..");
const dirname = path.join(__dirname, '..');
export const getHeaders = () => {
return pluginApi.fetchHeaders;
};
app.use((req, res, next) => {
const xHeaders = req.headers["x-custom-headers"];
if (xHeaders && typeof xHeaders === "string") {
const xHeaders = req.headers['x-custom-headers'];
if (xHeaders && typeof xHeaders === 'string') {
for (const [headerName, headerValue] of Object.entries(
pluginApi.fetchHeaders
pluginApi.fetchHeaders,
)) {
if (
typeof headerName === "string" &&
typeof headerValue === "string"
)
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"
)
if (typeof headerName === 'string' && typeof headerValue === 'string')
pluginApi.fetchHeaders[headerName] = headerValue;
}
}
if (pluginApi.fetchHeaders.userAgent)
console.log("Custom headers: ", pluginApi.fetchHeaders);
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) => {
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"]);
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;
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"]);
: req.body['filters'] || defaultFilters;
const page = parseInt(req.body['page']);
try {
const novels = await pluginApi.popularNovels(requirePath, page || 1, {
showLatestNovels,
@@ -75,80 +69,80 @@ app.post("/popularNovels/", async (req, res) => {
res.json({ error: String(err) });
}
});
app.post("/searchNovels/", async (req, res) => {
const page = parseInt(req.body["page"]) || 1;
app.post('/searchNovels/', async (req, res) => {
const page = parseInt(req.body['page']) || 1;
try {
const novels = await pluginApi.searchNovels(
req.body["pluginRequirePath"],
req.body['pluginRequirePath'],
page,
req.body["searchTerm"]
req.body['searchTerm'],
);
res.json(novels);
} catch (err: unknown) {
res.json({ error: String(err) });
}
});
app.post("/parseNovel/", async (req, res) => {
app.post('/parseNovel/', async (req, res) => {
try {
const sourceNovel = await pluginApi.parseNovel(
req.body["pluginRequirePath"],
req.body["novelPath"]
req.body['pluginRequirePath'],
req.body['novelPath'],
);
res.json(sourceNovel);
} catch (err: unknown) {
res.json({ error: String(err) });
}
});
app.post("/hasParsePage", async (req, res) => {
app.post('/hasParsePage', async (req, res) => {
try {
const parsePage = await pluginApi.hasParsePage(
req.body["pluginRequirePath"]
)
req.body['pluginRequirePath'],
);
res.json({ hasParsePage: parsePage !== undefined });
} catch (err: unknown) {
res.json({ hasParsePage: false });
}
});
app.post("/parsePage/", async (req, res) => {
app.post('/parsePage/', async (req, res) => {
try {
const sourcePage = await pluginApi.parsePage(
req.body["pluginRequirePath"],
req.body["novelPath"],
req.body["page"]
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) => {
app.post('/parseChapter/', async (req, res) => {
try {
const chapterText = await pluginApi.parseChapter(
req.body["pluginRequirePath"],
req.body["chapterPath"]
req.body['pluginRequirePath'],
req.body['chapterPath'],
);
res.send(chapterText);
} catch (err: unknown) {
res.json({ error: String(err) });
}
});
app.post("/fetchImage/", async (req, res) => {
app.post('/fetchImage/', async (req, res) => {
try {
const base64 = await pluginApi.fetchImage(
req.body["pluginRequirePath"],
req.body["url"]
req.body['pluginRequirePath'],
req.body['url'],
);
res.send(base64);
} catch (err: unknown) {
res.json({ error: String(err) });
}
});
app.post("/resolveUrl/", async (req, res) => {
app.post('/resolveUrl/', async (req, res) => {
try {
const fullURL = await pluginApi.resolveUrl(
req.body["pluginRequirePath"],
req.body["path"],
req.body["isNovel"],
req.body['pluginRequirePath'],
req.body['path'],
req.body['isNovel'],
);
res.send(fullURL);
} catch (err: unknown) {
@@ -156,8 +150,8 @@ app.post("/resolveUrl/", async (req, res) => {
}
});
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, () => {
@@ -165,17 +159,17 @@ app.listen(port, host, () => {
});
//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);
+1 -1
View File
@@ -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';
+29 -22
View File
@@ -22,10 +22,10 @@ const makeInit = async (init?: FetchInit) => {
'Cache-Control': 'max-age=0',
};
try {
const { getHeaders } = await import("../index.js");
const { getHeaders } = await import('../index.js');
defaultHeaders = {
...defaultHeaders,
...getHeaders()
...getHeaders(),
};
} catch {
// nothing to do
@@ -75,11 +75,11 @@ export const fetchFile = async function (url: string, init?: FetchInit) {
console.log(url, init);
try {
const res = await fetch(url, init as RequestInit);
if (!res.ok) return "";
if (!res.ok) return '';
const arrayBuffer = await res.arrayBuffer();
return Buffer.from(arrayBuffer).toString("base64");
return Buffer.from(arrayBuffer).toString('base64');
} catch (e) {
return "";
return '';
}
};
@@ -94,18 +94,18 @@ export const fetchFile = async function (url: string, init?: FetchInit) {
export const fetchText = async function (
url: string,
init?: FetchInit,
encoding?: string
encoding?: string,
): Promise<string> {
init = await makeInit(init);
console.log(url, init);
try {
const res = await fetch(url, init as RequestInit);
if (!res.ok) return "";
if (!res.ok) return '';
const arrayBuffer = await res.arrayBuffer();
const decoder = new TextDecoder(encoding);
return decoder.decode(arrayBuffer);
} catch (e) {
return "";
return '';
}
};
@@ -119,40 +119,47 @@ interface ProtoRequestInit {
const BYTE_MARK = BigInt((1 << 8) - 1);
export const fetchProto = async function<ReturnType> (
export const fetchProto = async function <ReturnType>(
protoInit: ProtoRequestInit,
url: string,
init?: FetchInit,
) {
const protoRoot = parseProto(protoInit.proto).root;
const RequestMessge = protoRoot.lookupType(protoInit.requestType);
if (RequestMessge.verify(protoInit.requestData)){
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;
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)
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())
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 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
}
}) as ReturnType;
};
+14 -14
View File
@@ -4,11 +4,11 @@ export interface FilterOption {
}
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 = {
@@ -44,7 +44,7 @@ type ExcludableCheckboxFilter = {
export type Filters = Record<
string,
| { label: string } & (
{ label: string } & (
| PickerFilter
| CheckboxFilter
| TextFilter
@@ -54,25 +54,25 @@ export type Filters = Record<
>;
export type FilterToValues<
T extends Record<string, { type: FilterTypes }> | undefined
T extends Record<string, { type: FilterTypes }> | undefined,
> = T extends undefined
? undefined
: {
[K in keyof T]: Omit<
{ type: NonNullable<T>[K]["type"] } & Filters[string],
"label" | "options"
{ type: NonNullable<T>[K]['type'] } & Filters[string],
'label' | 'options'
>;
};
export type ValueOfFilter<T extends FilterTypes> =
T extends FilterTypes.CheckboxGroup
? CheckboxFilter["value"]
? CheckboxFilter['value']
: T extends FilterTypes.Picker
? PickerFilter["value"]
? PickerFilter['value']
: T extends FilterTypes.Switch
? SwitchFilter["value"]
? SwitchFilter['value']
: T extends FilterTypes.TextInput
? TextFilter["value"]
? TextFilter['value']
: T extends FilterTypes.ExcludableCheckboxGroup
? ExcludableCheckboxFilter["value"]
? ExcludableCheckboxFilter['value']
: never;
+6 -6
View File
@@ -1,21 +1,21 @@
export const isUrlAbsolute = (url: string) => {
if (url) {
if (url.indexOf("//") === 0) {
if (url.indexOf('//') === 0) {
return true;
} // URL is protocol-relative (= absolute)
if (url.indexOf("://") === -1) {
if (url.indexOf('://') === -1) {
return false;
} // URL has no protocol (= relative)
if (url.indexOf(".") === -1) {
if (url.indexOf('.') === -1) {
return false;
} // URL does not contain a dot, i.e. no TLD (= relative, possibly REST)
if (url.indexOf("/") === -1) {
if (url.indexOf('/') === -1) {
return false;
} // URL does not contain a single slash (= relative)
if (url.indexOf(":") > url.indexOf("/")) {
if (url.indexOf(':') > url.indexOf('/')) {
return false;
} // The first colon comes after the first slash (= relative)
if (url.indexOf("://") < url.indexOf(".")) {
if (url.indexOf('://') < url.indexOf('.')) {
return true;
} // Protocol is defined before first dot (= absolute)
}
+15 -15
View File
@@ -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;
+7 -7
View File
@@ -1,9 +1,9 @@
export const NovelStatus = {
Unknown: "Unknown",
Ongoing: "Ongoing",
Completed: "Completed",
Licensed: "Licensed",
PublishingFinished: "Publishing Finished",
Cancelled: "Cancelled",
OnHiatus: "On Hiatus",
Unknown: 'Unknown',
Ongoing: 'Ongoing',
Completed: 'Completed',
Licensed: 'Licensed',
PublishingFinished: 'Publishing Finished',
Cancelled: 'Cancelled',
OnHiatus: 'On Hiatus',
} as const;
+1028 -176
View File
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -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"
}
}
+50 -46
View File
@@ -1,22 +1,22 @@
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<typeof this.filters>
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url: string;
if (showLatestNovels) {
@@ -64,27 +64,29 @@ class Shu69 implements Plugin.PluginBase {
const novel: Plugin.SourceNovel = {
path: novelPath,
chapters: [],
name: loadedCheerio('h1').text().trim()
name: loadedCheerio('h1').text().trim(),
};
novel.cover = loadedCheerio('div.cover > img').attr('src');
novel.summary = loadedCheerio('#bookIntro').text().trim();
const bookInfo = loadedCheerio('div.caption-bookinfo > p')
const bookInfo = loadedCheerio('div.caption-bookinfo > p');
novel.author = bookInfo.find("a").attr('title');
novel.author = bookInfo.find('a').attr('title');
novel.artist = undefined;
novel.status = bookInfo.text().includes('连载') ? NovelStatus.Ongoing : NovelStatus.Completed;
novel.status = bookInfo.text().includes('连载')
? NovelStatus.Ongoing
: NovelStatus.Completed;
novel.genres = '';
// Table of Content is on a different page than the summary page
let chapters: Plugin.ChapterItem[] = [];
const allUrl = loadedCheerio('dd.all > a').attr('href')
const allUrl = loadedCheerio('dd.all > a').attr('href');
if (allUrl) {
const chaptersUrl = this.site + allUrl;
const chaptersBody = await fetchText(chaptersUrl);
@@ -94,7 +96,7 @@ class Shu69 implements Plugin.PluginBase {
chaptersLoadedCheerio('dd').each((i, el) => {
const chapterUrl = chaptersLoadedCheerio(el).find('a').attr('href');
const chapterName = chaptersLoadedCheerio(el).find('a').text().trim();
if(chapterUrl){
if (chapterUrl) {
chapters.push({
name: chapterName,
path: chapterUrl,
@@ -102,10 +104,12 @@ class Shu69 implements Plugin.PluginBase {
}
});
} else {
loadedCheerio('div.panel.hidden-xs > dl.panel-chapterlist:nth-child(2) > dd').each((i, el) => {
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){
if (chapterUrl) {
chapters.push({
name: chapterName,
path: chapterUrl,
@@ -134,11 +138,11 @@ class Shu69 implements Plugin.PluginBase {
.join('\n');
return chapterText;
};
}
async searchNovels(
searchTerm: string,
pageNo: number
pageNo: number,
): Promise<Plugin.NovelItem[]> {
if (pageNo > 1) return [];
@@ -180,39 +184,39 @@ class Shu69 implements Plugin.PluginBase {
filters = {
rank: {
label: "排行榜",
value: "allvisit",
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" },
{ 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",
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" },
{ 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;
}
+51 -40
View File
@@ -1,20 +1,20 @@
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<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url: string;
if (pageNo === 1) {
@@ -30,12 +30,19 @@ class XinShu69 implements Plugin.PluginBase {
const novels: Plugin.NovelItem[] = [];
const novelsList = pageNo === 1 ? loadedCheerio('div.newbox > ul > li') : loadedCheerio('li');
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');
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 novelName = loadedCheerio(e)
.find('li > div > h3 > a:nth-child(2)')
.text();
const novelCover = loadedCheerio(e)
.find('li > a > img')
.attr('data-src');
@@ -64,16 +71,14 @@ class XinShu69 implements Plugin.PluginBase {
const novel: Plugin.SourceNovel = {
path: novelPath,
chapters: [],
name: loadedCheerio('h1 > a').text()
name: loadedCheerio('h1 > a').text(),
};
novel.cover = loadedCheerio('div.bookimg2 > img').attr(
'src',
);
novel.cover = loadedCheerio('div.bookimg2 > img').attr('src');
novel.summary = loadedCheerio('div.navtxt').text().trim();
const bookInfo = loadedCheerio('div.booknav2').text()
const bookInfo = loadedCheerio('div.booknav2').text();
const authorMatch = bookInfo.match(/作者:(.*)/);
if (authorMatch) {
@@ -82,7 +87,9 @@ class XinShu69 implements Plugin.PluginBase {
novel.artist = undefined;
novel.status = bookInfo.includes('连载') ? NovelStatus.Ongoing : NovelStatus.Completed;
novel.status = bookInfo.includes('连载')
? NovelStatus.Ongoing
: NovelStatus.Completed;
const genresMatch = bookInfo.match(/分类:(.*)/);
if (genresMatch) {
@@ -101,7 +108,7 @@ class XinShu69 implements Plugin.PluginBase {
chaptersLoadedCheerio('li').each((i, e) => {
const chapterUrl = chaptersLoadedCheerio(e).find('a').attr('href');
if (chapterUrl?.startsWith("https://")) {
if (chapterUrl?.startsWith('https://')) {
const chapterName = chaptersLoadedCheerio(e).find('a').text().trim();
chapters.push({
@@ -133,11 +140,11 @@ class XinShu69 implements Plugin.PluginBase {
.join('\n');
return chapterText;
};
}
async searchNovels(
searchTerm: string,
pageNo: number
pageNo: number,
): Promise<Plugin.NovelItem[]> {
if (pageNo > 1) return [];
@@ -146,7 +153,11 @@ class XinShu69 implements Plugin.PluginBase {
formData.append('searchkey', encode(searchTerm, 'gbk'));
formData.append('searchtype', 'all');
const body = await fetchText(searchUrl, { method: 'post', body: formData, }, 'gbk');
const body = await fetchText(
searchUrl,
{ method: 'post', body: formData },
'gbk',
);
if (body === '') throw Error('无法获取搜索结果,请检查网络');
let loadedCheerio = parseHTML(body);
@@ -174,22 +185,22 @@ class XinShu69 implements Plugin.PluginBase {
filters = {
class: {
label: "分类",
value: "0",
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" },
{ 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,
},
+189 -199
View File
@@ -1,22 +1,22 @@
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<typeof this.filters>
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
const rank = showLatestNovels ? 'lastupdate' : filters.rank.value;
const url = `${this.site}/top/${rank}/${pageNo}.html`;
@@ -28,13 +28,13 @@ class Linovelib implements Plugin.PluginBase {
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 novelName = loadedCheerio(el).find('.book-title').text();
const novelCover = loadedCheerio(el)
.find("div.book-cover > img")
.attr("data-src");
.find('div.book-cover > img')
.attr('data-src');
if (!url) return;
const novel = {
@@ -60,27 +60,27 @@ class Linovelib implements Plugin.PluginBase {
const novel: Plugin.SourceNovel = {
path: novelPath,
chapters: [],
name: loadedCheerio("#bookDetailWrapper .book-title").text()
name: loadedCheerio('#bookDetailWrapper .book-title').text(),
};
novel.cover = loadedCheerio("#bookDetailWrapper img.book-cover").attr(
"src"
novel.cover = loadedCheerio('#bookDetailWrapper img.book-cover').attr(
'src',
);
novel.summary = loadedCheerio("#bookSummary content").text();
novel.summary = loadedCheerio('#bookSummary content').text();
novel.author = loadedCheerio(
"#bookDetailWrapper .book-rand-a a"
).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;
const meta = loadedCheerio('#bookDetailWrapper .book-meta').text();
novel.status = meta.includes('完结')
? NovelStatus.Completed
: NovelStatus.Ongoing;
novel.genres = loadedCheerio(".tag-small.red")
.children("a")
novel.genres = loadedCheerio('.tag-small.red')
.children('a')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
.join(',');
// Table of Content is on a different page than the summary page
let chapter: Plugin.ChapterItem[] = [];
@@ -88,22 +88,22 @@ class Linovelib implements Plugin.PluginBase {
const idPattern = /\/(\d+)\.html/;
const novelId = url.match(idPattern)?.[1];
const chaptersUrl =
this.site + loadedCheerio("#btnReadBook").attr("href");
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")) {
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");
.find('.chapter-li-a')
.attr('href');
const chapterIdMatch = urlPart?.match(idPattern);
// Sometimes the href attribute does not contain the url, but javascript:cid(0).
@@ -118,8 +118,8 @@ class Linovelib implements Plugin.PluginBase {
const chapterUrl = `/novel/${novelId}/${chapterId}.html`;
const chapterName =
volumeName +
" — " +
chaptersLoadedCheerio(el).find(".chapter-index").text().trim();
' — ' +
chaptersLoadedCheerio(el).find('.chapter-index').text().trim();
const releaseDate = null;
if (!chapterId) return;
@@ -129,7 +129,8 @@ class Linovelib implements Plugin.PluginBase {
releaseTime: releaseDate,
path: chapterUrl,
});
});
},
);
novel.chapters = chapter;
@@ -138,10 +139,10 @@ class Linovelib implements Plugin.PluginBase {
async parseChapter(chapterPath: string): Promise<string> {
let chapterName,
chapterText = "",
chapterText = '',
hasNextPage,
pageHasNextPage,
pageText = "";
pageText = '';
let pageNumber = 1;
/*
@@ -256,167 +257,163 @@ class Linovelib implements Plugin.PluginBase {
// '': '裸',
// };
const skillgg: Record<string, string> = {
"\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",
'\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();
pageCheerio('#acontentz .adsbygoogle').remove();
pageCheerio('#acontentz script').remove();
// Load lazyloaded images
pageCheerio("#acontentz img.imagecontent").each((i, el) => {
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");
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");
.attr('src', imgSrc)
.removeAttr('data-src')
.removeClass('lazyload');
}
});
// Recover the original character
pageText = pageCheerio("#acontentz").html() || "";
pageText = pageText.replace(
/./g,
(char) => skillgg[char] || char
);
pageText = pageCheerio('#acontentz').html() || '';
pageText = pageText.replace(/./g, char => skillgg[char] || char);
return Promise.resolve();
};
await formatPage();
chapterName =
pageCheerio("#atitle + h3").text() +
" — " +
pageCheerio("#atitle").text();
if (chapterText === "") {
chapterText = "<h2>" + chapterName + "</h2>";
pageCheerio('#atitle + h3').text() +
' — ' +
pageCheerio('#atitle').text();
if (chapterText === '') {
chapterText = '<h2>' + chapterName + '</h2>';
}
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",
}
'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;
pageCheerio('#footlink a:last').text() === '下一页' ? true : false;
return { pageCheerio, pageHasNextPage };
};
@@ -426,10 +423,7 @@ class Linovelib implements Plugin.PluginBase {
hasNextPage = page.pageHasNextPage;
if (hasNextPage === true) {
pageNumber++;
url = url.replace(
/\.html/gi,
`_${pageNumber}` + ".html"
);
url = url.replace(/\.html/gi, `_${pageNumber}` + '.html');
}
} while (hasNextPage === true);
@@ -438,7 +432,7 @@ class Linovelib implements Plugin.PluginBase {
async searchNovels(
searchTerm: string,
pageNo: number
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/search/${encodeURI(searchTerm)}_${pageNo}.html`;
@@ -504,15 +498,13 @@ class Linovelib implements Plugin.PluginBase {
// 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");
pageCheerio('.book-ol .book-layout').each((i, el) => {
let nUrl = pageCheerio(el).attr('href');
const novelName = pageCheerio(el)
.find(".book-title")
.text();
const novelName = pageCheerio(el).find('.book-title').text();
const novelCover = pageCheerio(el)
.find("div.book-cover > img")
.attr("data-src");
.find('div.book-cover > img')
.attr('data-src');
if (!nUrl) return;
@@ -527,31 +519,29 @@ class Linovelib implements Plugin.PluginBase {
}
async fetchImage(url: string): Promise<string | undefined> {
const headers = new Headers(
{
"Referer": `${this.site}/`,
}
)
const headers = new Headers({
'Referer': `${this.site}/`,
});
return await fetchFile(url, { headers });
}
filters = {
rank: {
label: "排行榜",
value: "monthvisit",
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" },
{ 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,
},
+101 -96
View File
@@ -1,37 +1,37 @@
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<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site;
if (filters.genre.value !== '')
link += `/category/${filters.genre.value}/`;
else
link += `/top-novel/`;
if (filters.genre.value !== '') link += `/category/${filters.genre.value}/`;
else link += `/top-novel/`;
link += page
link += `?change_type=${filters.sort.value}`
link += page;
link += `?change_type=${filters.sort.value}`;
const body = await fetchApi(link).then((r) => r.text());
const body = await fetchApi(link).then(r => r.text());
const loadedCheerio = parseHTML(body);
const novel: Plugin.NovelItem[] = [];
loadedCheerio('.top-novel-block').each((i,el) => {
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 novelCover = loadedCheerio(el)
.find('.top-novel-cover img')
.attr('src');
const novelUrl = loadedCheerio(el).find('h2 a').attr('href');
if (!novelUrl) return;
@@ -39,51 +39,55 @@ class NovelsOnline implements Plugin.PluginBase {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
})
})
});
});
return novel;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath).then((res) => res.text());
const result = await fetchApi(this.site + novelPath).then(res =>
res.text(),
);
let $ = parseHTML(result);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: $("h1").text() || 'Untitled',
cover: $(".novel-cover").find("a > img").attr("src"),
name: $('h1').text() || 'Untitled',
cover: $('.novel-cover').find('a > img').attr('src'),
chapters: [],
};
$('.novel-detail-item').each((i,el) => {
$('.novel-detail-item').each((i, el) => {
let detailName = $(el).find('h6').text();
let detail = $(el).find('.novel-detail-body');
switch(detailName){
case "Description":
switch (detailName) {
case 'Description':
novel.summary = detail.text();
break;
case "Genre":
novel.genres = detail.find('li')
case 'Genre':
novel.genres = detail
.find('li')
.map((_, el) => $(el).text())
.get()
.join(', ');
break;
case "Author(s)":
novel.author = detail.find('li')
case 'Author(s)':
novel.author = detail
.find('li')
.map((_, el) => $(el).text())
.get()
.join(', ');
break;
case "Status":
case 'Status':
novel.status = detail.text().trim();
break;
}
})
});
novel.chapters = $("ul.chapter-chs > li > a")
novel.chapters = $('ul.chapter-chs > li > a')
.map((_, el) => {
const chapterUrl = $(el).attr("href");
const chapterUrl = $(el).attr('href');
const chapterName = $(el).text();
return {
@@ -97,39 +101,40 @@ class NovelsOnline implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath).then((res) => res.text());
const result = await fetchApi(this.site + chapterPath).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(result);
const chapterText = loadedCheerio("#contentall").html() || "";
const chapterText = loadedCheerio('#contentall').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const result = await fetchApi(
"https://novelsonline.net/sResults.php",
{
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
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",
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",
method: 'POST',
body: qs.stringify({ q: searchTerm }),
},
).then((res) => res.text());
}).then(res => res.text());
const $ = parseHTML(result);
const headers = $("li");
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");
const novelUrl = $(h).find('a').attr('href');
const novelCover = $(h).find('img').attr('src');
if (!novelUrl) return
if (!novelUrl) return;
return {
name: novelName,
@@ -138,7 +143,7 @@ class NovelsOnline implements Plugin.PluginBase {
};
})
.get()
.filter((sr) => sr !== null)
.filter(sr => sr !== null);
}
async fetchImage(url: string): Promise<string | undefined> {
@@ -147,54 +152,54 @@ class NovelsOnline implements Plugin.PluginBase {
filters = {
sort: {
value: "top_rated",
label: "Sort by",
value: 'top_rated',
label: 'Sort by',
options: [
{ label: "Top Rated", value: "top_rated" },
{ label: "Most Viewed", value: "view" },
{ label: 'Top Rated', value: 'top_rated' },
{ label: 'Most Viewed', value: 'view' },
],
type: FilterTypes.Picker,
},
genre: {
value: "",
label: "Category",
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" }
{ 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,
},
+72 -48
View File
@@ -1,40 +1,45 @@
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<Plugin.NovelItem[]> {
let novels: Plugin.NovelItem[] = [];
let url = "https://storyseedling.com/ajax";
let url = 'https://storyseedling.com/ajax';
const postUrl = `https://storyseedling.com/browse/`;
const body = await fetchApi(postUrl).then((r) => r.text());
const body = await fetchApi(postUrl).then(r => r.text());
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");
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, ""),
path: element.permalink.replace(this.site, ''),
};
novels.push(novel);
});
@@ -46,43 +51,54 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
var site = this.site;
const novel: Plugin.SourceNovel = {
path: novelPath,
name: ""
name: '',
};
const body = await fetchApi(this.site+novelPath).then((r) => r.text());
const body = await fetchApi(this.site + novelPath).then(r => r.text());
const loadedCheerio = parseHTML(body);
novel.name = loadedCheerio("h1").text().trim();
novel.name = loadedCheerio('h1').text().trim();
//novel.artist = "";
//novel.author = "";
novel.cover = loadedCheerio('img[x-ref="art"].w-full.rounded.shadow-md').attr("src");
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(){
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.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, "");
novel.summary = loadedCheerio('div.mb-4.order-2:not(.lg\\:grid-in-buttons)')
.text()
.trim()
.replace(/(\r\n|\n|\r)/gm, '');
let chapters: Plugin.ChapterItem[] = [];
loadedCheerio("div.grid.w-full.grid-cols-1.gap-4.md\\:grid-cols-2 > a").each(function() {
if(loadedCheerio(this).find("> div").length == 2){
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];
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, ""),
path: url.replace(site, ''),
releaseTime: releaseTime,
chapterNumber: parseInt(chapterNumber),
};
@@ -92,40 +108,48 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site+chapterPath).then((r) => r.text());
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 t = loadedCheerio('div.justify-center > div.mb-4');
let chapterText = t.html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
let novels: Plugin.NovelItem[] = [];
let url = "https://storyseedling.com/ajax";
let url = 'https://storyseedling.com/ajax';
const postUrl = `https://storyseedling.com/browse/`;
const body = await fetchApi(postUrl).then((r) => r.text());
const body = await fetchApi(postUrl).then(r => r.text());
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", searchTerm);
data.set("orderBy", "recent");
data.set("curpage", pageNo.toString());
data.set("post", postValue);
data.set("action", "fetch_browse");
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();
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, ""),
path: element.permalink.replace(this.site, ''),
};
novels.push(novel);
});
+85 -86
View File
@@ -1,29 +1,29 @@
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[] = [];
loadedCheerio(".col-truyen-main .list-truyen .row").each((i,el) => {
const novelUrl = loadedCheerio(el).find("h3.truyen-title > a").attr("href");
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)){
let novelCover = loadedCheerio(el).find('img.cover').attr('src');
if (novelCover && !isUrlAbsolute(novelCover)) {
novelCover = this.site + novelCover;
}
@@ -35,18 +35,16 @@ class AllNovelFullPlugin implements Plugin.PluginBase {
novels.push(novel);
});
return novels;
};
}
async popularNovels(
pageNo: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site;
if (filters.genres.value.length)
link += filters.genres.value;
else
link += filters.order.value;
if (filters.genres.value.length) link += filters.genres.value;
else link += filters.order.value;
link += `?page=${pageNo}`;
@@ -66,17 +64,17 @@ class AllNovelFullPlugin implements Plugin.PluginBase {
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(),
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();
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())
.map((a, ex) => loadedCheerio(ex).text())
.toArray()
.join(',');
@@ -90,12 +88,13 @@ class AllNovelFullPlugin implements Plugin.PluginBase {
case 'Genre:':
novel.genres = detail;
break;
}});
}
});
const novelId = loadedCheerio("#rating").attr("data-novel-id");
const novelId = loadedCheerio('#rating').attr('data-novel-id');
const getChapters = async (id: string) => {
const chaptersUrl = this.site + "/ajax/chapter-option?novelId=" + id;
const chaptersUrl = this.site + '/ajax/chapter-option?novelId=' + id;
const data = await fetchApi(chaptersUrl);
const chapters = await data.text();
@@ -104,11 +103,11 @@ class AllNovelFullPlugin implements Plugin.PluginBase {
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio("select > option").each((i,el) => {
loadedCheerio('select > option').each((i, el) => {
const chapterName = loadedCheerio(el).text();
const chapterUrl = loadedCheerio(el).attr("value");
const chapterUrl = loadedCheerio(el).attr('value');
if (!chapterUrl ) return;
if (!chapterUrl) return;
chapter.push({
name: chapterName,
@@ -117,14 +116,14 @@ class AllNovelFullPlugin implements Plugin.PluginBase {
});
return chapter;
}
};
if (novelId) {
novel.chapters = await getChapters(novelId);
}
return novel;
};
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
@@ -132,14 +131,14 @@ class AllNovelFullPlugin implements Plugin.PluginBase {
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("#chapter-content").html() || "";
const chapterText = loadedCheerio('#chapter-content').html() || '';
return chapterText;
};
}
async searchNovels(
searchTerm: string,
page: number
page: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/search?keyword=${searchTerm}&page=${page}`;
const result = await fetchApi(url);
@@ -147,7 +146,7 @@ class AllNovelFullPlugin implements Plugin.PluginBase {
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
};
}
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
@@ -155,57 +154,57 @@ class AllNovelFullPlugin implements Plugin.PluginBase {
filters = {
order: {
value: "/most-popular",
label: "Order by",
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" },
{ 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",
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" },
{ 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,
},
+134 -137
View File
@@ -1,30 +1,30 @@
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";
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";
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");
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!");
console.error('No novel url!');
return;
}
const novelName = loadedCheerio(el).find("h3 > a").text();
const novelCover = loadedCheerio(el).find("img").attr("src");
const novelName = loadedCheerio(el).find('h3 > a').text();
const novelCover = loadedCheerio(el).find('img').attr('src');
const novel = {
name: novelName,
@@ -40,9 +40,9 @@ class BLN implements Plugin.PluginBase {
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site + "novel_list?"
let link = this.site + 'novel_list?';
link += 'type=' + filters.type.value;
link += '&category=' + filters.category.value;
link += '&state=' + filters.status.value;
@@ -68,70 +68,68 @@ class BLN implements Plugin.PluginBase {
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() || "",
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() {
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())
.map((a, ex) => loadedCheerio(ex).text())
.toArray()
.join(', ');
switch(detailName) {
case "Author(s): ":
switch (detailName) {
case 'Author(s): ':
novel.author = detail;
break;
case "GENRES: ":
case 'GENRES: ':
novel.genres = detail;
break;
case "STATUS : ":
case 'STATUS : ':
novel.status =
detail === "Ongoing"
detail === 'Ongoing'
? NovelStatus.Ongoing
: detail === "Completed"
: 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();
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);
'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");
const chapterUrl = loadedCheerio(el).find('a').attr('href');
if (!chapterUrl) {
// TODO: Handle error
console.error("No chapter url!");
console.error('No chapter url!');
return;
}
@@ -153,14 +151,14 @@ class BLN implements Plugin.PluginBase {
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("#vung_doc").html() || "";
const chapterText = loadedCheerio('#vung_doc').html() || '';
return chapterText;
}
async searchNovels(
searchTerm: string,
page: number
page: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}search_novels/${searchTerm}?page=${page}`;
@@ -177,99 +175,98 @@ class BLN implements Plugin.PluginBase {
filters = {
status: {
label: "Status",
value: "all",
label: 'Status',
value: 'all',
options: [
{ label: "ALL", value: "all" },
{ label: "Completed", value: "completed" },
{ label: "Ongoing", value: "ongoing" },
{ label: 'ALL', value: 'all' },
{ label: 'Completed', value: 'completed' },
{ label: 'Ongoing', value: 'ongoing' },
],
type: FilterTypes.Picker,
},
type: {
value: "topview",
label: "Type",
value: 'topview',
label: 'Type',
options: [
{ label: "Recently updated", value: "latest" },
{ label: "Newest", value: "newest" },
{ label: "Top view", value: "topview" },
{ label: 'Recently updated', value: 'latest' },
{ label: 'Newest', value: 'newest' },
{ label: 'Top view', value: 'topview' },
],
type: FilterTypes.Picker,
},
category: {
label: "Category",
value: "all",
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" },
{ 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,
},
+55 -59
View File
@@ -1,25 +1,25 @@
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[] = [];
loadedCheerio(".listupd")
.find("div.bs")
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");
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;
@@ -41,16 +41,16 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
link += filters.category.value;
link += "page/" + pageNo + "/?post_type=novel";
link += 'page/' + pageNo + '/?post_type=novel';
const body = await fetchApi(link).then((result) => result.text());
const body = await fetchApi(link).then(result => result.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(novelUrl).then((result) => result.text());
const body = await fetchApi(novelUrl).then(result => result.text());
let loadedCheerio = parseHTML(body);
@@ -59,25 +59,25 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
chapters: [],
};
novel.name = loadedCheerio(".entry-title").text().trim();
novel.name = loadedCheerio('.entry-title').text().trim();
novel.cover = loadedCheerio("div.thumbook > div > img").attr("src");
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();
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: ":
case 'Publisher: ':
novel.author = detail.trim();
break;
case "Status: ":
case 'Status: ':
novel.status = detail.trim();
break;
case "Genres:":
novel.genres = detail.trim().replace(/ , /g, ",");
case 'Genres:':
novel.genres = detail.trim().replace(/ , /g, ',');
break;
case "Description: ":
case 'Description: ':
novel.summary = detail.trim();
break;
}
@@ -85,47 +85,43 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio("#chapterlist")
.find("li")
loadedCheerio('#chapterlist')
.find('li')
.each(function () {
const chapterUrl = loadedCheerio(this).find("a").attr("href");
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();
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";
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")
releaseTime.includes('hours ago') ||
releaseTime.includes('hour ago')
) {
dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours
dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours
}
if (
releaseTime.includes("days ago") ||
releaseTime.includes("day ago")
releaseTime.includes('days ago') ||
releaseTime.includes('day ago')
) {
dayJSDate.subtract(timeAgoInt, "days"); // go back N days
dayJSDate.subtract(timeAgoInt, 'days'); // go back N days
}
if (
releaseTime.includes("months ago") ||
releaseTime.includes("month ago")
releaseTime.includes('months ago') ||
releaseTime.includes('month ago')
) {
dayJSDate.subtract(timeAgoInt, "months"); // go back N months
dayJSDate.subtract(timeAgoInt, 'months'); // go back N months
}
releaseTime = dayJSDate.format("LL");
releaseTime = dayJSDate.format('LL');
}
}
chapter.push({
@@ -141,10 +137,10 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
}
async parseChapter(chapterUrl: string): Promise<string> {
const body = await fetchApi(chapterUrl).then((result) => result.text());
const body = await fetchApi(chapterUrl).then(result => result.text());
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("#chaptercontent").html()?.trim() || "";
const chapterText = loadedCheerio('#chaptercontent').html()?.trim() || '';
return chapterText;
}
@@ -155,7 +151,7 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}page/${pageNo}/?s=${searchTerm}&post_type=novel`;
const body = await fetchApi(url).then((result) => result.text());
const body = await fetchApi(url).then(result => result.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
@@ -167,14 +163,14 @@ class ComradeMaoPlugin implements Plugin.PluginBase {
filters = {
category: {
value: "",
label: "Type",
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/" },
{ 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,
},
+37 -38
View File
@@ -1,25 +1,25 @@
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";
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){
parseNovels(loadedCheerio: CheerioAPI, searchTerm?: string) {
let novels: Plugin.NovelItem[] = [];
loadedCheerio("#main")
.find("li")
.each((i,el) => {
const novelName = loadedCheerio(el).find("a").text();
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");
const novelUrl = loadedCheerio(el).find('a').attr('href');
if (!novelUrl) return;
@@ -33,20 +33,20 @@ class DDLPlugin implements Plugin.PluginBase {
});
if (searchTerm) {
novels = novels.filter((novel) =>
novel.name.toLowerCase().includes(searchTerm.toLowerCase())
novels = novels.filter(novel =>
novel.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
}
return novels;
};
}
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions
options: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
let link = this.site + "novels";
let link = this.site + 'novels';
const body = await fetchApi(link).then((res) => res.text());
const body = await fetchApi(link).then(res => res.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
@@ -60,29 +60,29 @@ class DDLPlugin implements Plugin.PluginBase {
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio("h1.entry-title").text().trim() || "Untitled",
name: loadedCheerio('h1.entry-title').text().trim() || 'Untitled',
cover:
loadedCheerio(".entry-content").find("img").attr("data-ezsrc") ||
loadedCheerio('.entry-content').find('img').attr('data-ezsrc') ||
defaultCover,
chapters: [],
};
novel.summary = loadedCheerio("#main > article > div > p:nth-child(6)")
novel.summary = loadedCheerio('#main > article > div > p:nth-child(6)')
.text()
.trim();
novel.author = loadedCheerio("#main > article > div > h3:nth-child(2)")
novel.author = loadedCheerio('#main > article > div > h3:nth-child(2)')
.text()
.replace(/Author:/g, "")
.replace(/Author:/g, '')
.trim();
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio("#main")
.find("li > span > a")
loadedCheerio('#main')
.find('li > span > a')
.each((i, el) => {
const chapterName = loadedCheerio(el).text().trim();
const chapterUrl = loadedCheerio(el).attr("href");
const chapterUrl = loadedCheerio(el).attr('href');
if (!chapterUrl) return;
@@ -95,7 +95,7 @@ class DDLPlugin implements Plugin.PluginBase {
novel.chapters = chapter;
return novel;
};
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
@@ -103,32 +103,31 @@ class DDLPlugin implements Plugin.PluginBase {
const loadedCheerio = parseHTML(body);
const chapterName = loadedCheerio(".entry-title").text().trim();
const chapterName = loadedCheerio('.entry-title').text().trim();
let chapterText = loadedCheerio(".entry-content").html();
let chapterText = loadedCheerio('.entry-content').html();
if (!chapterText) {
chapterText = loadedCheerio(".page-header").html();
chapterText = loadedCheerio('.page-header').html();
}
chapterText = `<p><h1>${chapterName}</h1></p>` + chapterText;
return chapterText;
};
}
async searchNovels(
searchTerm: string,
pageNo: number
pageNo: number,
): Promise<Plugin.NovelItem[]> {
let url = this.site + "novels";
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<string | undefined> {
return await fetchFile(url);
+95 -102
View File
@@ -1,25 +1,24 @@
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){
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");
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({
@@ -27,51 +26,45 @@ class EarlyNovelPlugin implements Plugin.PagePlugin {
name: novelName,
cover: novelCover,
});
}
);
});
return novels;
};
}
parseChapters(loadedCheerio:CheerioAPI){
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);
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
})});
path: chapterUrl,
});
});
return chapter;
}
async popularNovels(
pageNo: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site;
if (filters.genres.value.length)
link += filters.genres.value;
else
link += filters.order.value;
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 body = await fetchApi(link).then(res => res.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
@@ -79,33 +72,33 @@ class EarlyNovelPlugin implements Plugin.PagePlugin {
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 lastPage = Number(lastPageStr?.[1] || '0');
const novel: Plugin.SourceNovel & {totalPages: number}= {
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(),
name: loadedCheerio('.book > img').attr('alt') || 'Untitled',
cover: loadedCheerio('.book > img').attr('src'),
summary: loadedCheerio('.desc-text').text().trim(),
chapters: [],
totalPages: lastPage
totalPages: lastPage,
};
loadedCheerio(".info > div > h3").each(function () {
loadedCheerio('.info > div > h3').each(function () {
let detailName = loadedCheerio(this).text();
let detail = loadedCheerio(this)
.siblings()
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
.join(',');
switch (detailName) {
case "Author:":
case 'Author:':
novel.author = detail;
break;
case "Status:":
case 'Status:':
novel.status = detail;
break;
case "Genre:":
case 'Genre:':
novel.genres = detail;
break;
}
@@ -117,10 +110,10 @@ class EarlyNovelPlugin implements Plugin.PagePlugin {
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const url = this.site + novelPath + '?page=' + page;
const body = await fetchApi(url).then((res) => res.text());
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const chapters = this.parseChapters(loadedCheerio);
return {chapters}
return { chapters };
}
async parseChapter(chapterPath: string): Promise<string> {
@@ -129,14 +122,14 @@ class EarlyNovelPlugin implements Plugin.PagePlugin {
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("#chapter-c").html() || "";
const chapterText = loadedCheerio('#chapter-c').html() || '';
return chapterText;
};
}
async searchNovels(
searchTerm: string,
pageNo: number
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}search?keyword=${searchTerm}`;
const result = await fetchApi(url);
@@ -144,7 +137,7 @@ class EarlyNovelPlugin implements Plugin.PagePlugin {
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
};
}
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
@@ -152,58 +145,58 @@ class EarlyNovelPlugin implements Plugin.PagePlugin {
filters = {
order: {
value: "/most-popular",
label: "Order by",
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" },
{ 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",
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" },
{ 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,
},
+78 -52
View File
@@ -1,38 +1,42 @@
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) {
parseNovels(loadedCheerio: CheerioAPI, searchTerm?: string) {
let novels: Plugin.NovelItem[] = [];
loadedCheerio("figure.wp-block-image").each((index, element) => {
loadedCheerio('figure.wp-block-image').each((index, element) => {
const name = loadedCheerio(element).text();
let cover = loadedCheerio(element).find("img").attr("data-ezsrc");
let cover = loadedCheerio(element).find('img').attr('data-ezsrc');
// Remove the appended query string
if (cover) {
cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic!
} else {
cover = loadedCheerio(element).find("img").attr("src");
cover = loadedCheerio(element).find('img').attr('src');
}
const path = loadedCheerio(element).find("a").attr("href")?.replace('tp:', 'tps:')?.slice(this.site.length);
const path = loadedCheerio(element)
.find('a')
.attr('href')
?.replace('tp:', 'tps:')
?.slice(this.site.length);
if (!path) return;
novels.push({ name, cover, path });
})
});
if (searchTerm) {
novels = novels.filter((novel) =>
novel.name.toLowerCase().includes(searchTerm.toLowerCase())
novels = novels.filter(novel =>
novel.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
}
@@ -43,56 +47,72 @@ class FaqWikiUs implements Plugin.PluginBase {
page: number,
{ showLatestNovels }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site).then((res) => res.text());
const body = await fetchApi(this.site).then(res => res.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(path: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + path).then((res) => res.text());
const body = await fetchApi(this.site + path).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path,
name: ""
name: '',
};
novel.name = loadedCheerio(".entry-title").text().replace("Novel – All Chapters", "").trim();
novel.name = loadedCheerio('.entry-title')
.text()
.replace('Novel – All Chapters', '')
.trim();
novel.cover = loadedCheerio(".wp-block-image")
.find("img")
.attr("data-ezsrc")
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 status = loadedCheerio(
"#lcp_instance_0 +:icontains('complete')",
).text();
novel.status = status ? NovelStatus.Completed : NovelStatus.Ongoing;
const div = loadedCheerio(".book-review-block__meta-item-value");
const div = loadedCheerio('.book-review-block__meta-item-value');
div.html(div.html()?.replace(/(?<=>)([^<]+)(?=<br\s*\/?>)/g, '<p>$1</p>')!);
loadedCheerio(".book-review-block__meta-item-value strong").each((i,el) => {
const key = loadedCheerio(el).text().replace(':', '').trim().toLowerCase();
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":
switch (key) {
case 'description':
novel.summary = value;
break;
case "author(s)":
case 'author(s)':
novel.author = value;
break;
case "genre":
case 'genre':
novel.genres = value.split(' ').join(',');
}
})
},
);
const chapters: Plugin.ChapterItem[] = loadedCheerio(".lcp_catlist > li > a")
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),
name: loadedCheerio(element)
.text()
.replace(novel.name + '', '')
.replace('Novel' + '', '')
.trim(),
path: '' + loadedCheerio(element).attr('href')?.slice(this.site.length),
chapterNumber: chapterIndex + 1,
}))
.get();
@@ -102,35 +122,41 @@ class FaqWikiUs implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
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);
loadedCheerio("span").remove();
loadedCheerio('span').remove();
const chapterParagraphs = loadedCheerio(".entry-content p");
const chapterParagraphs = loadedCheerio('.entry-content p');
let chapterContent; // Variable to store the result
if (chapterParagraphs.length < 5) { //some chapter in this site store their whole text in 1-4 <p>,
chapterContent = chapterParagraphs.map((index, element) => {
const text = loadedCheerio(element).html()
if (chapterParagraphs.length < 5) {
//some chapter in this site store their whole text in 1-4 <p>,
chapterContent = chapterParagraphs
.map((index, element) => {
const text = loadedCheerio(element).html();
return text;
}).get().join('\n\n');
}
else {
})
.get()
.join('\n\n');
} else {
// Multiple paragraphs case
chapterContent = chapterParagraphs.map((index, element) => {
chapterContent = chapterParagraphs
.map((index, element) => {
const text = loadedCheerio(element).text().trim();
return `<p>${text}</p>`;
}).get().join('\n\n');
})
.get()
.join('\n\n');
}
return chapterContent;
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site).then((res) => res.text());
const body = await fetchApi(this.site).then(res => res.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio, searchTerm);
+51 -44
View File
@@ -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<Plugin.NovelItem[]> {
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<Plugin.SourceNovel> {
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<string> {
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<Plugin.NovelItem[]> {
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;
}
+111 -110
View File
@@ -1,23 +1,23 @@
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/";
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",
Accept: 'application/json',
'Content-Type': 'application/json',
};
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}browse/`;
link += `${filters.genres.value}/`;
@@ -25,24 +25,20 @@ class LightNovelPub implements Plugin.PagePlugin {
link += `${filters.status.value}/`;
link += page;
const body = await fetchApi(link).then((r) => r.text());
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.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")
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;
@@ -58,19 +54,24 @@ class LightNovelPub implements Plugin.PagePlugin {
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & { totalPages: number }> {
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
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 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(),
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: [],
};
@@ -85,7 +86,7 @@ class LightNovelPub implements Plugin.PagePlugin {
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const url = this.site + novelPath + '/chapters/page-' + page;
const body = await fetchApi(url).then((res) => res.text());
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio('.chapter-list li').each(function () {
@@ -99,63 +100,63 @@ class LightNovelPub implements Plugin.PagePlugin {
.find('.chapter-update')
.attr('datetime');
const chapterUrl = loadedCheerio(this).find('a').attr('href')?.substring(1);
const chapterUrl = loadedCheerio(this)
.find('a')
.attr('href')
?.substring(1);
if (!chapterUrl) return;
chapter.push({
name: chapterName,
path: chapterUrl,
releaseTime: dayjs(releaseDate).toISOString(),
})
});
});
const chapters = chapter;
return { chapters };
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
const body = await fetchApi(this.site + chapterPath).then(r => r.text());
const loadedCheerio = parseHTML(body);
let chapterText = loadedCheerio("#chapter-container").html() || '';
let chapterText = loadedCheerio('#chapter-container').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}lnsearchlive`;
const link = `${this.site}search`;
const response = await fetchApi(link).then((r) => r.text());
const response = await fetchApi(link).then(r => r.text());
const token = parseHTML(response);
let verifytoken = token("#novelSearchForm > input").attr("value");
let verifytoken = token('#novelSearchForm > input').attr('value');
let formData = new FormData();
formData.append("inputContent", searchTerm);
formData.append('inputContent', searchTerm);
const body = await fetchApi(
url,
{
method: "POST",
const body = await fetchApi(url, {
method: 'POST',
headers: { LNRequestVerifyToken: verifytoken! },
body: formData,
},
).then((r) => r.text());
}).then(r => r.text());
let loadedCheerio = parseHTML(body);
let novels: Plugin.NovelItem[] = [];
let results = JSON.parse(loadedCheerio("body").text());
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);
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,
@@ -173,71 +174,71 @@ class LightNovelPub implements Plugin.PagePlugin {
filters = {
order: {
value: "popular",
label: "Order by",
value: 'popular',
label: 'Order by',
options: [
{ label: "New", value: "new" },
{ label: "Popular", value: "popular" },
{ label: "Updates", value: "updated" },
{ label: 'New', value: 'new' },
{ label: 'Popular', value: 'popular' },
{ label: 'Updates', value: 'updated' },
],
type: FilterTypes.Picker,
},
status: {
value: "all",
label: "Status",
value: 'all',
label: 'Status',
options: [
{ label: "All", value: "all" },
{ label: "Completed", value: "completed" },
{ label: "Ongoing", value: "ongoing" }
{ label: 'All', value: 'all' },
{ label: 'Completed', value: 'completed' },
{ label: 'Ongoing', value: 'ongoing' },
],
type: FilterTypes.Picker,
},
genres: {
value: "all",
label: "Genre",
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" },
{ 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,
},
+111 -110
View File
@@ -1,23 +1,23 @@
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";
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",
Accept: 'application/json',
'Content-Type': 'application/json',
};
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}browse/`;
link += `${filters.genres.value}/`;
@@ -25,24 +25,20 @@ class LightNovelWorld implements Plugin.PagePlugin {
link += `${filters.status.value}/`;
link += page;
const body = await fetchApi(link).then((r) => r.text());
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.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")
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;
@@ -58,19 +54,24 @@ class LightNovelWorld implements Plugin.PagePlugin {
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & { totalPages: number }> {
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
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 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(),
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: [],
};
@@ -85,7 +86,7 @@ class LightNovelWorld implements Plugin.PagePlugin {
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const url = this.site + novelPath + '/chapters/page-' + page;
const body = await fetchApi(url).then((res) => res.text());
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio('.chapter-list li').each(function () {
@@ -99,63 +100,63 @@ class LightNovelWorld implements Plugin.PagePlugin {
.find('.chapter-update')
.attr('datetime');
const chapterUrl = loadedCheerio(this).find('a').attr('href')?.substring(1);
const chapterUrl = loadedCheerio(this)
.find('a')
.attr('href')
?.substring(1);
if (!chapterUrl) return;
chapter.push({
name: chapterName,
path: chapterUrl,
releaseTime: dayjs(releaseDate).toISOString(),
})
});
});
const chapters = chapter;
return { chapters };
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
const body = await fetchApi(this.site + chapterPath).then(r => r.text());
const loadedCheerio = parseHTML(body);
let chapterText = loadedCheerio("#chapter-container").html() || '';
let chapterText = loadedCheerio('#chapter-container').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}lnsearchlive`;
const link = `${this.site}search`;
const response = await fetchApi(link).then((r) => r.text());
const response = await fetchApi(link).then(r => r.text());
const token = parseHTML(response);
let verifytoken = token("#novelSearchForm > input").attr("value");
let verifytoken = token('#novelSearchForm > input').attr('value');
let formData = new FormData();
formData.append("inputContent", searchTerm);
formData.append('inputContent', searchTerm);
const body = await fetchApi(
url,
{
method: "POST",
const body = await fetchApi(url, {
method: 'POST',
headers: { LNRequestVerifyToken: verifytoken! },
body: formData,
},
).then((r) => r.text());
}).then(r => r.text());
let loadedCheerio = parseHTML(body);
let novels: Plugin.NovelItem[] = [];
let results = JSON.parse(loadedCheerio("body").text());
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);
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,
@@ -173,71 +174,71 @@ class LightNovelWorld implements Plugin.PagePlugin {
filters = {
order: {
value: "popular",
label: "Order by",
value: 'popular',
label: 'Order by',
options: [
{ label: "New", value: "new" },
{ label: "Popular", value: "popular" },
{ label: "Updates", value: "updated" },
{ label: 'New', value: 'new' },
{ label: 'Popular', value: 'popular' },
{ label: 'Updates', value: 'updated' },
],
type: FilterTypes.Picker,
},
status: {
value: "all",
label: "Status",
value: 'all',
label: 'Status',
options: [
{ label: "All", value: "all" },
{ label: "Completed", value: "completed" },
{ label: "Ongoing", value: "ongoing" }
{ label: 'All', value: 'all' },
{ label: 'Completed', value: 'completed' },
{ label: 'Ongoing', value: 'ongoing' },
],
type: FilterTypes.Picker,
},
genres: {
value: "all",
label: "Genre",
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" },
{ 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,
},
+68 -70
View File
@@ -1,40 +1,38 @@
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<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site + "novel?";
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 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");
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;
@@ -48,40 +46,42 @@ class LnMTLPlugin implements Plugin.PagePlugin {
});
return novels;
};
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const volumes = JSON.parse(
loadedCheerio("main")
loadedCheerio('main')
.next()
.html()
?.match(/lnmtl.volumes = \[(.*?)\]/)![0]
?.replace("lnmtl.volumes = ", "") || ""
?.replace('lnmtl.volumes = ', '') || '',
);
const novel: Plugin.SourceNovel & {totalPages: number} = {
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(),
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();
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":
case 'Authors':
novel.author = detail;
break;
case "Current status":
case 'Current status':
novel.status = detail;
break;
}
@@ -90,12 +90,12 @@ class LnMTLPlugin implements Plugin.PagePlugin {
novel.genres = loadedCheerio('.panel-heading:contains(" Genres ")')
.next()
.find('a')
.map((i,el) => loadedCheerio(el).text())
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
.join(',');
return novel;
};
}
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const result = await fetchApi(this.site + novelPath);
@@ -104,46 +104,44 @@ class LnMTLPlugin implements Plugin.PagePlugin {
const loadedCheerio = parseHTML(body);
const volumes = JSON.parse(
loadedCheerio("main")
loadedCheerio('main')
.next()
.html()
?.match(/lnmtl.volumes = \[(.*?)\]/)![0]
?.replace("lnmtl.volumes = ", "") || ""
)[+page-1];
?.replace('lnmtl.volumes = ', '') || '',
)[+page - 1];
const chapter: Plugin.ChapterItem[] = [];
await this.sleep(1000);
const volumeData = await fetchApi(
`${this.site}chapter?volumeId=${volumes.id}`
`${this.site}chapter?volumeId=${volumes.id}`,
);
const volumePage = await volumeData.json();
const firstPage = volumePage.data.map(
(chapter: ChapterEntry) => ({
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
}))
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}`
`${this.site}chapter?page=${i}&volumeId=${volumes.id}`,
);
const chapterInfo = await chapterData.json();
const chapterDetails = chapterInfo.data.map(
(chapter: ChapterEntry) => ({
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
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};
return { chapters };
}
async parseChapter(chapterPath: string): Promise<string> {
@@ -158,17 +156,17 @@ class LnMTLPlugin implements Plugin.PagePlugin {
let chapterText = loadedCheerio('.chapter-body').html()?.replace(/„/g, '“');
if (!chapterText) {
chapterText = loadedCheerio(".alert.alert-warning").text();
chapterText = loadedCheerio('.alert.alert-warning').text();
}
return chapterText;
};
}
async searchNovels(
searchTerm: string,
pageNo: number
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site).then((r) => r.text());
const body = await fetchApi(this.site).then(r => r.text());
const loadedCheerio = parseHTML(body);
const list = loadedCheerio('footer')
@@ -180,12 +178,12 @@ class LnMTLPlugin implements Plugin.PagePlugin {
const search = await fetch(`${this.site}${list}`);
const data = await search.json();
const nov = data.filter((res: { name: string; }) =>
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; }) => {
nov.map((res: { name: string; slug: string; image: string }) => {
const novelName = res.name;
const novelUrl = `novel/${res.slug}`;
const novelCover = res.image;
@@ -198,7 +196,7 @@ class LnMTLPlugin implements Plugin.PagePlugin {
novels.push(novel);
});
return novels;
};
}
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
@@ -206,31 +204,31 @@ class LnMTLPlugin implements Plugin.PagePlugin {
filters = {
order: {
value: "favourites",
label: "Order by",
value: 'favourites',
label: 'Order by',
options: [
{ label: "Favourites", value: "favourites" },
{ label: "Name", value: "name" },
{ label: "Addition Date", value: "date" },
{ label: 'Favourites', value: 'favourites' },
{ label: 'Name', value: 'name' },
{ label: 'Addition Date', value: 'date' },
],
type: FilterTypes.Picker,
},
sort: {
value: "desc",
label: "Sort by",
value: 'desc',
label: 'Sort by',
options: [
{ label: "Descending", value: "desc" },
{ label: "Ascending", value: "asc" },
{ label: 'Descending', value: 'desc' },
{ label: 'Ascending', value: 'asc' },
],
type: FilterTypes.Picker,
},
storyStatus: {
value: "all",
label: "Status",
value: 'all',
label: 'Status',
options: [
{ label: "All", value: "all" },
{ label: "Ongoing", value: "ongoing" },
{ label: "Finished", value: "finished" },
{ label: 'All', value: 'all' },
{ label: 'Ongoing', value: 'ongoing' },
{ label: 'Finished', value: 'finished' },
],
type: FilterTypes.Picker,
},
+62 -66
View File
@@ -1,39 +1,36 @@
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/";
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<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}novel-list/?`;
link += `orderby=${filters.order.value}`;
link += `&order=${filters.sort.value}`;
link += `&status=${filters.storyStatus.value}`;
link += `&pg=${page}`
link += `&pg=${page}`;
const headers = new Headers();
headers.append('Alt-Used', 'www.mtlnovel.com');
const body = await fetch(link, {headers}).then((result) => result.text());
const body = await fetch(link, { headers }).then(result => result.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
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");
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');
if (!novelUrl) return;
@@ -50,57 +47,51 @@ class MTLNovel implements Plugin.PluginBase {
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const headers = new Headers();
headers.append('Referer', `${this.site}novel-list/`)
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());
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, ""),
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 chapterListUrl = this.site + novelPath + 'chapter-list/';
const getChapters = async () => {
const listResult = await fetch(chapterListUrl, {headers});
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("~ ", "");
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");
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();
@@ -110,25 +101,30 @@ class MTLNovel implements Plugin.PluginBase {
async parseChapter(chapterPath: string): Promise<string> {
const headers = new Headers();
headers.append('Alt-Used', 'www.mtlnovel.com');
const body = await fetch(this.site + chapterPath, {headers}).then((r) => r.text());
const body = await fetch(this.site + chapterPath, { headers }).then(r =>
r.text(),
);
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("div.par").html() || '';
const chapterText = loadedCheerio('div.par').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const headers = new Headers();
headers.append('Alt-Used', 'www.mtlnovel.com');
const searchUrl =
this.site +
"wp-admin/admin-ajax.php?action=autosuggest&q=" +
'wp-admin/admin-ajax.php?action=autosuggest&q=' +
searchTerm +
"&__amp_source_origin=https%3A%2F%2Fwww.mtlnovel.com";
'&__amp_source_origin=https%3A%2F%2Fwww.mtlnovel.com';
const res = await fetch(searchUrl, {headers});
const res = await fetch(searchUrl, { headers });
const result = await res.json();
const novels: Plugin.NovelItem[] = [];
@@ -138,7 +134,7 @@ class MTLNovel implements Plugin.PluginBase {
permalink: string;
}
result.items[0].results.map((item: SearchEntry) => {
const novelName = item.title.replace(/<\/?strong>/g, "");
const novelName = item.title.replace(/<\/?strong>/g, '');
const novelCover = item.thumbnail;
const novelUrl = item.permalink.replace(this.site, '');
@@ -152,37 +148,37 @@ class MTLNovel implements Plugin.PluginBase {
async fetchImage(url: string): Promise<string | undefined> {
const headers = new Headers();
headers.append('Alt-Used', 'www.mtlnovel.com');
return fetchFile(url, {headers});
return fetchFile(url, { headers });
}
filters = {
order: {
value: "view",
label: "Order by",
value: 'view',
label: 'Order by',
options: [
{ label: "Date", value: "date" },
{ label: "Name", value: "name" },
{ label: "Rating", value: "rating" },
{ label: "View", value: "view" },
{ 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",
value: 'desc',
label: 'Sort by',
options: [
{ label: "Descending", value: "desc" },
{ label: "Ascending", value: "asc" },
{ label: 'Descending', value: 'desc' },
{ label: 'Ascending', value: 'asc' },
],
type: FilterTypes.Picker,
},
storyStatus: {
value: "all",
label: "Status",
value: 'all',
label: 'Status',
options: [
{ label: "All", value: "all" },
{ label: "Ongoing", value: "ongoing" },
{ label: "Complete", value: "completed" },
{ label: 'All', value: 'all' },
{ label: 'Ongoing', value: 'ongoing' },
{ label: 'Complete', value: 'completed' },
],
type: FilterTypes.Picker,
},
+37 -32
View File
@@ -1,21 +1,21 @@
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){
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;
@@ -31,39 +31,42 @@ class MTLReader implements Plugin.PluginBase {
return novels;
}
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}novels?page=${pageNo}`;
const body = await fetchApi(url).then((r) => r.text());
const body = await fetchApi(url).then(r => r.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
const body = await fetchApi(this.site + novelPath).then(r => r.text());
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(),
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")
novel.author = loadedCheerio('i.fa-user')
.parent()
.text()
.replace("Author: ", "")
.replace('Author: ', '')
.trim();
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;
@@ -78,22 +81,25 @@ class MTLReader implements Plugin.PluginBase {
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
const body = await fetchApi(this.site + chapterPath).then(r => r.text());
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<Plugin.NovelItem[]> {
const body = await fetchApi(this.site).then((r) => r.text());
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
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 seacrhBody = await fetchApi(searchUrl).then(r => r.text());
const loadedCheerio = parseHTML(seacrhBody);
return this.parseNovels(loadedCheerio);
}
@@ -101,6 +107,5 @@ class MTLReader implements Plugin.PluginBase {
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new MTLReader();
+113 -111
View File
@@ -1,23 +1,23 @@
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){
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");
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");
.find('h3.novel-title > a')
.attr('href');
if (!novelUrl) return;
@@ -33,63 +33,62 @@ class NovelBin implements Plugin.PluginBase {
return novels;
}
async popularNovels(page: number, {filters}: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site;
if (filters.genre.value !== '')
link += `gen/${filters.genre.value}`;
else
link += `sort/${filters.sort.value}`;
if (filters.genre.value !== '') link += `gen/${filters.genre.value}`;
else link += `sort/${filters.sort.value}`;
if (filters.complete.value)
link += `/completed`;
if (filters.complete.value) link += `/completed`;
link += `?page=${page}`;
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);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
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(),
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 () {
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(",");
.join(',');
switch (detailName) {
case "Author:":
case 'Author:':
novel.author = detail;
break;
case "Status:":
case 'Status:':
novel.status = detail;
break;
case "Genre:":
case 'Genre:':
novel.genres = detail;
break;
}
});
const novelId = loadedCheerio("#rating").attr("data-novel-id");
const novelId = loadedCheerio('#rating').attr('data-novel-id');
const getChapters = async (id: string) => {
const chapterListUrl =
this.site + "ajax/chapter-archive?novelId=" + id;
const chapterListUrl = this.site + 'ajax/chapter-archive?novelId=' + id;
const data = await fetchApi(chapterListUrl);
const chapterdata = await data.text();
@@ -98,9 +97,9 @@ class NovelBin implements Plugin.PluginBase {
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");
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;
@@ -110,7 +109,7 @@ class NovelBin implements Plugin.PluginBase {
});
});
return chapter;
}
};
if (novelId) {
novel.chapters = await getChapters(novelId);
@@ -120,19 +119,22 @@ class NovelBin implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
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() || '';
const chapterText = loadedCheerio('#chr-content').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}search/?keyword=${searchTerm}`;
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);
@@ -144,84 +146,84 @@ class NovelBin implements Plugin.PluginBase {
filters = {
sort: {
value: "popular-novels",
label: "Sort by",
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" },
{ 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",
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" }
{ 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",
label: 'Show Completed Novels Only',
value: false,
type: FilterTypes.Switch,
},
+57 -50
View File
@@ -1,24 +1,27 @@
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";
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){
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".book-item").each((idx, ele) => {
const novelName = loadedCheerio(ele).find(".title").text();
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);
'https:' + loadedCheerio(ele).find('img').attr('data-src');
const novelUrl = loadedCheerio(ele)
.find('.title a')
.attr('href')
?.substring(1);
if (!novelUrl) return;
@@ -32,7 +35,7 @@ class NovelBuddy implements Plugin.PluginBase {
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions
options: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}popular?page=${pageNo}`;
@@ -51,31 +54,32 @@ class NovelBuddy implements Plugin.PluginBase {
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".name h1").text().trim() || "Untitled",
cover: "https:" + loadedCheerio(".img-cover img").attr("data-src"),
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) => {
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())
switch (detailName) {
case 'Authors :':
novel.author = detail
.find('span')
.map((a, ex) => loadedCheerio(ex).text())
.toArray()
.join(', ');
break;
case "Status :":
case 'Status :':
novel.status = detail.text();
break;
case "Genres :":
case 'Genres :':
novel.genres = detail.text().trim();
break;
}
})
});
const novelId = loadedCheerio('script')
.text()
.match(/bookId = (\d+);/)![1];
@@ -88,39 +92,40 @@ class NovelBuddy implements Plugin.PluginBase {
loadedCheerio = parseHTML(chapterlist);
loadedCheerio("li").each((i,el) => {
loadedCheerio('li').each((i, el) => {
const chapterName = loadedCheerio(el)
.find(".chapter-title")
.find('.chapter-title')
.text()
.trim();
const releaseDate = loadedCheerio(el)
.find(".chapter-update")
.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);
'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);
const chapterUrl = loadedCheerio(el).find('a').attr('href')?.slice(1);
if (!chapterUrl) return;
@@ -131,7 +136,7 @@ class NovelBuddy implements Plugin.PluginBase {
});
});
return chapter;
}
};
novel.chapters = (await getChapters(novelId)).reverse();
@@ -144,15 +149,18 @@ class NovelBuddy implements Plugin.PluginBase {
const loadedCheerio = parseHTML(body);
loadedCheerio("#listen-chapter").remove();
loadedCheerio("#google_translate_element").remove();
loadedCheerio('#listen-chapter').remove();
loadedCheerio('#google_translate_element').remove();
const chapterText = loadedCheerio(".chapter__content").html() || '';
const chapterText = loadedCheerio('.chapter__content').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}search?q=${searchTerm}&page=${page}`;
const result = await fetchApi(url);
@@ -165,7 +173,6 @@ class NovelBuddy implements Plugin.PluginBase {
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new NovelBuddy();
+83 -82
View File
@@ -1,30 +1,27 @@
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){
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);
this.site + loadedCheerio(ele).find('img').attr('src')?.slice(1);
const novelUrl = loadedCheerio(ele)
.find("h3.truyen-title > a")
.attr("href")
.find('h3.truyen-title > a')
.attr('href')
?.slice(1);
if (!novelUrl) return;
@@ -41,16 +38,17 @@ class NovelFull implements Plugin.PluginBase {
return novels;
}
async popularNovels(page: number, {filters}: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site;
if (filters.genre.value !== '')
link += `genre/${filters.genre.value}`;
else
link += filters.sort.value
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 body = await fetchApi(link).then(r => r.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
@@ -64,9 +62,9 @@ class NovelFull implements Plugin.PluginBase {
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(),
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: [],
};
@@ -75,28 +73,28 @@ class NovelFull implements Plugin.PluginBase {
.parent()
.contents()
.text()
.replace("Author:", "");
.replace('Author:', '');
novel.genres = loadedCheerio('h3:contains("Genre")')
.siblings()
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
.join(',');
const novelId = loadedCheerio("#rating").attr("data-novel-id")!;
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 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 () {
loadedCheerio('select > option').each(function () {
const chapterName = loadedCheerio(this).text();
const chapterUrl = loadedCheerio(this).attr("value")?.slice(1);
const chapterUrl = loadedCheerio(this).attr('value')?.slice(1);
if (!chapterUrl) return;
chapter.push({
@@ -105,7 +103,7 @@ class NovelFull implements Plugin.PluginBase {
});
});
return chapter;
}
};
novel.chapters = await getChapters(novelId);
@@ -118,13 +116,16 @@ class NovelFull implements Plugin.PluginBase {
const loadedCheerio = parseHTML(body);
loadedCheerio("#chapter-content div.ads").remove();
const chapterText = loadedCheerio("#chapter-content").html() || '';
loadedCheerio('#chapter-content div.ads').remove();
const chapterText = loadedCheerio('#chapter-content').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const searchUrl = `${this.site}search?keyword=${searchTerm}&page=${page}`;
const result = await fetchApi(searchUrl);
@@ -140,57 +141,57 @@ class NovelFull implements Plugin.PluginBase {
filters = {
sort: {
value: "most-popular",
label: "Sort by",
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" },
{ 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",
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" }
{ 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,
},
+42 -37
View File
@@ -1,30 +1,33 @@
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";
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/";
site = 'https://novelhall.com/';
async popularNovels(page: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
async popularNovels(
page: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}all2022-${page}.html`;
const body = await fetchApi(url).then((r) => r.text());
const body = await fetchApi(url).then(r => r.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio("li.btm").each((idx, ele) => {
const novelName = loadedCheerio(ele).text().trim()
const novelUrl = loadedCheerio(ele).find("a").attr("href");
if(!novelUrl) return;
loadedCheerio('li.btm').each((idx, ele) => {
const novelName = loadedCheerio(ele).text().trim();
const novelUrl = loadedCheerio(ele).find('a').attr('href');
if (!novelUrl) return;
const novel = {
name: novelName,
@@ -38,40 +41,40 @@ class NovelHall implements Plugin.PluginBase {
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
const body = await fetchApi(this.site + novelPath).then(r => r.text());
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".book-info > h1").text() || 'Untitled',
name: loadedCheerio('.book-info > h1').text() || 'Untitled',
cover: loadedCheerio('meta[property="og:image"]').attr('content'),
summary: loadedCheerio(".intro").text().trim(),
summary: loadedCheerio('.intro').text().trim(),
chapters: [],
};
loadedCheerio('.total').find('p').remove();
novel.author = loadedCheerio('.total span:contains("Author")')
.text()
.replace("Author:", "")
.replace('Author:', '')
.trim();
novel.status = loadedCheerio('.total span:contains("Status")')
.text()
.replace("Status:", "")
.replace("Active", "Ongoing")
.replace('Status:', '')
.replace('Active', 'Ongoing')
.trim();
novel.genres = loadedCheerio('.total a')
.map((a,ex) => loadedCheerio(ex).text())
.map((a, ex) => loadedCheerio(ex).text())
.toArray()
.join(',');
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio("#morelist ul > li").each((idx, ele) => {
const chapterName = loadedCheerio(ele).find("a").text().trim();
const chapterUrl = loadedCheerio(ele).find("a").attr("href");
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;
chapter.push({
@@ -85,27 +88,30 @@ class NovelHall implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
const body = await fetchApi(this.site + chapterPath).then(r => r.text());
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio(".content").html() || '';
const chapterText = loadedCheerio('.content').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}index.php?s=so&module=book&keyword=${searchTerm}`;
const body = await fetchApi(url).then((r) => r.text());
const body = await fetchApi(url).then(r => r.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio("#article_list_content > li").each((idx, ele) => {
loadedCheerio('#article_list_content > li').each((idx, ele) => {
const novelName = loadedCheerio(ele)
.find("h3")
.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);
.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 novel = {
@@ -122,7 +128,6 @@ class NovelHall implements Plugin.PluginBase {
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new NovelHall();
+192 -185
View File
@@ -1,24 +1,24 @@
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){
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();
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");
.find('.search_title > a')
.attr('href');
if (!novelUrl) return;
@@ -34,19 +34,27 @@ class NovelUpdates implements Plugin.PluginBase {
return novels;
}
async popularNovels(page: number, { showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>): Promise<Plugin.NovelItem[]> {
async popularNovels(
page: number,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}`;
if (filters.language.value.length ||
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"
filters.storyStatus.value !== ''
) {
link += 'series-finder/?sf=1';
} else if (showLatestNovels) {
link += 'latest-series/?st=1';
} else {
link += "series-ranking/?rank=week"
};
link += 'series-ranking/?rank=week';
}
if (filters.language.value.length)
link += '&org=' + filters.language.value.join(',');
@@ -60,7 +68,10 @@ class NovelUpdates implements Plugin.PluginBase {
if (filters.genres.value.exclude?.length)
link += '&ge=' + filters.genres.value.exclude.join(',');
if (filters.genres.value.include?.length || filters.genres.value.exclude?.length)
if (
filters.genres.value.include?.length ||
filters.genres.value.exclude?.length
)
link += '&mgi=' + filters.genre_operator.value;
if (filters.storyStatus.value.length)
@@ -72,9 +83,7 @@ class NovelUpdates implements Plugin.PluginBase {
link += '&pg=' + page;
const body = await fetchApi(link).then((result) =>
result.text()
);
const body = await fetchApi(link).then(result => result.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
@@ -89,7 +98,7 @@ class NovelUpdates implements Plugin.PluginBase {
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".seriestitlenu").text() || "Untitled",
name: loadedCheerio('.seriestitlenu').text() || 'Untitled',
cover: loadedCheerio('.wpb_wrapper img').attr('src'),
chapters: [],
};
@@ -99,48 +108,44 @@ class NovelUpdates implements Plugin.PluginBase {
.toArray()
.join(', ');
novel.genres = loadedCheerio("#seriesgenre")
.children("a")
novel.genres = loadedCheerio('#seriesgenre')
.children('a')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
.join(',');
novel.status = loadedCheerio("#editstatus").text().includes("Ongoing")
? "Ongoing"
: "Completed";
novel.status = loadedCheerio('#editstatus').text().includes('Ongoing')
? 'Ongoing'
: 'Completed';
const type = loadedCheerio("#showtype").text().trim();
const type = loadedCheerio('#showtype').text().trim();
const summary = loadedCheerio("#editdescription").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 novelId = loadedCheerio('input#mypostid').attr('value')!;
const formData = new FormData();
formData.append("action", "nd_getchapters");
formData.append("mygrr", "0");
formData.append("mypostid", novelId);
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",
const text = await fetchApi(link, {
method: 'POST',
body: formData,
},
).then((data) => data.text());
}).then(data => data.text());
loadedCheerio = parseHTML(text);
loadedCheerio("li.sp_li_chp").each((i,el) => {
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");
'https:' + loadedCheerio(el).find('a').first().next().attr('href');
chapter.push({
name: chapterName,
@@ -155,17 +160,15 @@ class NovelUpdates implements Plugin.PluginBase {
getLocation(href: string) {
var match = href.match(
/^(https?:)\/\/(([^:/?#]*)(?::([0-9]+))?)([/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/
/^(https?:)\/\/(([^:/?#]*)(?::([0-9]+))?)([/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/,
);
return match && `${match[1]}//${match[3]}`;
}
async parseChapter(chapterPath: string): Promise<string> {
let chapterText = "";
let chapterText = '';
const result = await fetchApi(
this.site + chapterPath,
);
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
// console.log(result.chapterUrl);
@@ -174,13 +177,13 @@ class NovelUpdates implements Plugin.PluginBase {
const loadedCheerio = parseHTML(body);
let isWuxiaWorld = result.url.toLowerCase().includes("wuxiaworld");
let isWuxiaWorld = result.url.toLowerCase().includes('wuxiaworld');
let isBlogspot = result.url.toLowerCase().includes("blogspot");
let isBlogspot = result.url.toLowerCase().includes('blogspot');
let isTumblr = result.url.toLowerCase().includes("tumblr");
let isTumblr = result.url.toLowerCase().includes('tumblr');
let isWattpad = result.url.toLowerCase().includes("wattpad");
let isWattpad = result.url.toLowerCase().includes('wattpad');
let isAnomalously = result.url.toLowerCase().includes('anotivereads');
@@ -192,62 +195,59 @@ class NovelUpdates implements Plugin.PluginBase {
let isLightNovelsTls = result.url
.toLowerCase()
.includes("lightnovelstranslations");
.includes('lightnovelstranslations');
let isiNovelTranslation = result.url
.toLowerCase()
.includes("inoveltranslation");
.includes('inoveltranslation');
let isTravisTranslation = result.url
.toLowerCase()
.includes("travistranslations");
.includes('travistranslations');
/**
* Checks if its a wordpress site
*/
let isWordPressStr =
loadedCheerio('meta[name="generator"]').attr("content") ||
loadedCheerio("footer").text();
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");
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 isRainOfSnow = result.url.toLowerCase().includes('rainofsnow');
let isWebNovel = result.url.toLowerCase().includes("webnovel");
let isWebNovel = result.url.toLowerCase().includes('webnovel');
let isHostedNovel = result.url.toLowerCase().includes("hostednovel");
let isHostedNovel = result.url.toLowerCase().includes('hostednovel');
let isScribbleHub = result.url.toLowerCase().includes("scribblehub");
let isScribbleHub = result.url.toLowerCase().includes('scribblehub');
if (isWuxiaWorld) {
chapterText = loadedCheerio("#chapter-content").html()!;
chapterText = loadedCheerio('#chapter-content').html()!;
} else if (isRainOfSnow) {
chapterText = loadedCheerio("div.content").html()!;
chapterText = loadedCheerio('div.content').html()!;
} else if (isTumblr) {
chapterText = loadedCheerio(".post").html()!;
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()!;
chapterText = loadedCheerio('.chapter').html()!;
} else if (isScribbleHub) {
chapterText = loadedCheerio("div.chp_raw").html()!;
chapterText = loadedCheerio('div.chp_raw').html()!;
} else if (isWattpad) {
chapterText = loadedCheerio(".container pre").html()!;
chapterText = loadedCheerio('.container pre').html()!;
} else if (isTravisTranslation) {
chapterText = loadedCheerio(".reader-content").html()!;
chapterText = loadedCheerio('.reader-content').html()!;
} else if (isLightNovelsTls) {
chapterText = loadedCheerio('.text_story').html()!;
} else if (isBlossomTranslation) {
@@ -272,48 +272,48 @@ class NovelUpdates implements Plugin.PluginBase {
*/
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",
'.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());
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()!;
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()!;
chapterText = loadedCheerio('.cha-words').html()!;
if (!chapterText) {
chapterText = loadedCheerio("._content").html()!;
chapterText = loadedCheerio('._content').html()!;
}
} else {
/**
* Remove unnecessary tags
*/
const tags = ["nav", "header", "footer", ".hidden"];
const tags = ['nav', 'header', 'footer', '.hidden'];
tags.map((tag) => loadedCheerio(tag).remove());
tags.map(tag => loadedCheerio(tag).remove());
chapterText = loadedCheerio("body").html()!;
chapterText = loadedCheerio('body').html()!;
}
if (chapterText) {
@@ -322,15 +322,22 @@ class NovelUpdates implements Plugin.PluginBase {
*/
chapterText = chapterText.replace(
/href="\//g,
`href="${this.getLocation(result.url)}/`
`href="${this.getLocation(result.url)}/`,
);
}
return chapterText;
}
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
const url = this.site + '?s=' + searchTerm.replace(/\s+/g, '+') + '&post_type=seriesplans';
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
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);
@@ -343,117 +350,117 @@ class NovelUpdates implements Plugin.PluginBase {
filters = {
sort: {
label: "Sort Results By",
value: "sdate",
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" },
{ 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",
label: 'Order',
value: 'desc',
options: [
{ label: "Descending", value: "desc" },
{ label: "Ascending", value: "asc" },
{ label: 'Descending', value: 'desc' },
{ label: 'Ascending', value: 'asc' },
],
type: FilterTypes.Picker,
},
storyStatus: {
label: "Story Status (Translation)",
value: "",
label: 'Story Status (Translation)',
value: '',
options: [
{ label: "All", value: "" },
{ label: "Completed", value: "2" },
{ label: "Ongoing", value: "3" },
{ label: "Hiatus", value: "4" },
{ label: 'All', value: '' },
{ label: 'Completed', value: '2' },
{ label: 'Ongoing', value: '3' },
{ label: 'Hiatus', value: '4' },
],
type: FilterTypes.Picker,
},
language: {
label: "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" },
{ 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",
label: 'Novel Type',
value: [],
options: [
{ label: "Light Novel", value: "2443" },
{ label: "Published Novel", value: "26874" },
{ label: "Web Novel", value: "2444" },
{ label: 'Light Novel', value: '2443' },
{ label: 'Published Novel', value: '26874' },
{ label: 'Web Novel', value: '2444' },
],
type: FilterTypes.CheckboxGroup,
},
genres: {
label: "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" },
{ 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",
label: 'Genre (AND/OR)',
value: 'and',
options: [
{ label: "AND", value: "and" },
{ label: "OR", value: "or" },
{ label: 'AND', value: 'and' },
{ label: 'OR', value: 'or' },
],
type: FilterTypes.Picker,
},
+92 -88
View File
@@ -1,19 +1,19 @@
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){
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".list-comic, .itemBox").each((idx, ele) => {
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');
@@ -35,27 +35,25 @@ class PawRead implements Plugin.PluginBase {
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}list/`;
const filterValues = [
filters.status.value,
filters.lang.value,
filters.genre.value
filters.genre.value,
];
link += filterValues.filter(value => value !== '').join('-');
if (filterValues.some(value => value !== ''))
link += '/';
if (filterValues.some(value => value !== '')) link += '/';
if (filters.order.value)
link += '-';
if (filters.order.value) link += '-';
link += filters.sort.value;
link += `/?page=${page}`;
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);
@@ -69,14 +67,14 @@ class PawRead implements Plugin.PluginBase {
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio("#Cover img").attr('title') || "Untitled",
cover: loadedCheerio("#Cover img").attr('src'),
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.summary = loadedCheerio("#full-des")
novel.summary = loadedCheerio('#full-des')
.find('br')
.replaceWith('\n')
.end()
@@ -86,22 +84,26 @@ class PawRead implements Plugin.PluginBase {
novel.genres = loadedCheerio('a.btn-default')
.map((i, el) => loadedCheerio(el).text().trim())
.toArray()
.join(",");
.join(',');
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio(".item-box").each((idx, ele) => {
loadedCheerio('.item-box').each((idx, ele) => {
const smallText = loadedCheerio(ele).find('span:last').text().trim();
if (smallText === "Advanced Chapter") return;
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];
const chapterName = loadedCheerio(ele).find('span:first').text().trim();
const chapterUrl = loadedCheerio(ele).attr('onclick')?.match(/\d+/)![0];
if (!chapterUrl) return;
chapter.push({
name: chapterName,
path: `${novelPath}${chapterUrl}.html`,
releaseTime: new Date(releaseDate[0], releaseDate[1], releaseDate[2]).toISOString(),
releaseTime: new Date(
releaseDate[0],
releaseDate[1],
releaseDate[2],
).toISOString(),
});
});
@@ -109,7 +111,6 @@ class PawRead implements Plugin.PluginBase {
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
@@ -118,12 +119,15 @@ class PawRead implements Plugin.PluginBase {
const steal = ['bit.ly', 'tinyurl', 'pawread'];
steal.map(tag => loadedCheerio(`p:icontains(${tag})`).remove());
const chapterText = loadedCheerio('.main').html() || "";
const chapterText = loadedCheerio('.main').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const searchUrl = `${this.site}search/?keywords=${searchTerm}&page=${page}`;
const result = await fetchApi(searchUrl);
@@ -139,85 +143,85 @@ class PawRead implements Plugin.PluginBase {
filters = {
status: {
value: "",
label: "Status",
value: '',
label: 'Status',
options: [
{ label: "All", value: "" },
{ label: "Completed", value: "wanjie" },
{ label: "Ongoing", value: "lianzai" },
{ label: "Hiatus", value: "hiatus" },
{ label: 'All', value: '' },
{ label: 'Completed', value: 'wanjie' },
{ label: 'Ongoing', value: 'lianzai' },
{ label: 'Hiatus', value: 'hiatus' },
],
type: FilterTypes.Picker,
},
lang: {
value: "",
label: "Languages",
value: '',
label: 'Languages',
options: [
{ label: "All", value: "" },
{ label: "Chinese", value: "chinese" },
{ label: "Korean", value: "korean" },
{ label: "Japanese", value: "japanese" },
{ label: 'All', value: '' },
{ label: 'Chinese', value: 'chinese' },
{ label: 'Korean', value: 'korean' },
{ label: 'Japanese', value: 'japanese' },
],
type: FilterTypes.Picker,
},
genre: {
value: "",
label: "Genres",
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" }
{ 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",
value: 'click',
label: 'Sort By',
options: [
{ label: "Time Updated", value: "update" },
{ label: "Time Posted", value: "post" },
{ label: "Clicks", value: "click" },
{ label: 'Time Updated', value: 'update' },
{ label: 'Time Posted', value: 'post' },
{ label: 'Clicks', value: 'click' },
],
type: FilterTypes.Picker,
},
order: {
value: false,
label: "Order ↑ ↓",
label: 'Order ↑ ↓',
type: FilterTypes.Switch,
},
} satisfies Filters;
+38 -34
View File
@@ -1,7 +1,7 @@
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;
@@ -18,14 +18,14 @@ class RanobesPlugin implements Plugin.PagePlugin {
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){
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio('.short-cont').each((i,el) => {
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)
@@ -39,13 +39,13 @@ class RanobesPlugin implements Plugin.PagePlugin {
name: novelName,
cover: novelCover,
path: novelUrl?.replace(this.site, ''),
}
};
novels.push(novel);
});
return novels;
}
parseChapters(data: {chapters: ChapterEntry[]}){
parseChapters(data: { chapters: ChapterEntry[] }) {
const chapter: Plugin.ChapterItem[] = [];
data.chapters.map((item: ChapterEntry) => {
chapter.push({
@@ -59,27 +59,30 @@ class RanobesPlugin implements Plugin.PagePlugin {
async popularNovels(
page: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}/novels/page/${page}/`;
const body = await fetchApi(link).then((result) =>
result.text()
);
const body = await fetchApi(link).then(result => result.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & { totalPages: number; }> {
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
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; } = {
const novel: Plugin.SourceNovel & { totalPages: number } = {
path: novelPath,
name: loadedCheerio('.poster img').attr('alt') || "Untitled",
name: loadedCheerio('.poster img').attr('alt') || 'Untitled',
cover: this.site + loadedCheerio('.poster img').attr('src'),
author: loadedCheerio('[itemprop="creator"]').text(),
chapters: [],
@@ -93,8 +96,8 @@ class RanobesPlugin implements Plugin.PagePlugin {
.text()
.trim();
novel.status = loadedCheerio('li:contains("Status") span')
.text() === 'Ongoing'
novel.status =
loadedCheerio('li:contains("Status") span').text() === 'Ongoing'
? 'Ongoing'
: 'Completed';
@@ -106,7 +109,9 @@ class RanobesPlugin implements Plugin.PagePlugin {
chapterListUrl = this.site + chapterListUrl;
}
const chaptersHtmlToString = await fetchApi(chapterListUrl).then(r => r.text());
const chaptersHtmlToString = await fetchApi(chapterListUrl).then(r =>
r.text(),
);
const pageCheerio = parseHTML(chaptersHtmlToString);
const json = pageCheerio('#dle-content main').next().html()!;
@@ -115,13 +120,11 @@ class RanobesPlugin implements Plugin.PagePlugin {
novel.chapters = this.parseChapters(data);
return novel;
};
}
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const pagePath = novelPath.split('-')[0];
const firstUrl = this.site +
'/chapters' +
pagePath.replace('novels/', '');
const firstUrl = this.site + '/chapters' + pagePath.replace('novels/', '');
const pageUrl = firstUrl + '/page/' + page;
const body = await fetchApi(firstUrl).then(r => r.text());
@@ -134,11 +137,13 @@ class RanobesPlugin implements Plugin.PagePlugin {
const latestChapterName = data.chapters[0].title;
const latestChapterDate = data.chapters[0].date;
const latestChapter: Plugin.ChapterItem | undefined = latestChapterUrl ? {
const latestChapter: Plugin.ChapterItem | undefined = latestChapterUrl
? {
path: latestChapterUrl.replace(this.site, ''),
name: latestChapterName,
releaseTime: new Date(latestChapterDate).toISOString(),
} : undefined
}
: undefined;
await this.sleep(1000);
@@ -149,8 +154,8 @@ class RanobesPlugin implements Plugin.PagePlugin {
const chapters = this.parseChapters(data);
return {
chapters,
latestChapter
}
latestChapter,
};
}
async parseChapter(chapterPath: string): Promise<string> {
@@ -159,26 +164,25 @@ class RanobesPlugin implements Plugin.PagePlugin {
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio('#arrticle').html() || "";
const chapterText = loadedCheerio('#arrticle').html() || '';
return chapterText;
};
}
async searchNovels(
searchTerm: string,
page: number
page: number,
): Promise<Plugin.NovelItem[]> {
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)
};
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
return await fetchFile(url);
}
}
export default new RanobesPlugin();
+73 -66
View File
@@ -1,49 +1,50 @@
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")) {
parseAgoDate(date: string | undefined) {
//parseMadaraDate
if (date?.includes('ago')) {
const dayJSDate = dayjs(new Date()); // today
const timeAgo = date.match(/\d+/)?.[0] || "";
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('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('days ago') || date.includes('day ago')) {
dayJSDate.subtract(timeAgoInt, 'days'); // go back N days
}
if (date.includes("months ago") || date.includes("month ago")) {
dayJSDate.subtract(timeAgoInt, "months"); // go back N months
if (date.includes('months ago') || date.includes('month ago')) {
dayJSDate.subtract(timeAgoInt, 'months'); // go back N months
}
if (date.includes("years ago") || date.includes("year ago")) {
dayJSDate.subtract(timeAgoInt, "years"); // go back N years
if (date.includes('years ago') || date.includes('year ago')) {
dayJSDate.subtract(timeAgoInt, 'years'); // go back N years
}
return dayJSDate.toISOString();
}
return null; // there is no "ago" so give up
};
}
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
const link = `${this.site}/ranking/${filters.order.value}/${page}`;
const result = await fetchApi(link);
@@ -53,19 +54,15 @@ class ReadLiteNovel implements Plugin.PluginBase {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".category-items li").each((i,el) => {
const novelUrl = loadedCheerio(el)
.find(".category-name a")
.attr("href");
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")
.find('.category-name a')
.text()
.trim();
let novelCover = loadedCheerio(el)
.find(".category-img img")
.attr("src");
let novelCover = loadedCheerio(el).find('.category-img img').attr('src');
if (novelCover && !isUrlAbsolute(novelCover)) {
novelCover = this.site + novelCover;
@@ -77,9 +74,9 @@ class ReadLiteNovel implements Plugin.PluginBase {
cover: novelCover,
};
novels.push(novel);
})
});
return novels;
};
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
@@ -89,55 +86,60 @@ class ReadLiteNovel implements Plugin.PluginBase {
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".novel-title").text() || "Untitled",
cover: loadedCheerio(".novels-detail img").attr("src"),
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) => {
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:":
switch (detailName) {
case 'Status:':
novel.status = detail.text();
break;
case "Genres:":
novel.genres = detail.find('a')
.map((i,el) => loadedCheerio(el).text())
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())
case 'Author(s):':
novel.author =
detail
.find('a')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(', ') || detail.text();
break;
case "Translator:":
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");
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 chapterName = loadedCheerio(el).find('a').text().trim();
const releaseDate = this.parseAgoDate(
loadedCheerio(el).find('svg').attr('data-bs-original-title')!
loadedCheerio(el).find('svg').attr('data-bs-original-title')!,
);
chapter.push({
name: chapterName,
path: chapterUrl?.replace(this.site, ''),
releaseTime: releaseDate,
})})
});
});
novel.chapters = chapter;
return novel;
};
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
@@ -145,27 +147,32 @@ class ReadLiteNovel implements Plugin.PluginBase {
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("#chapterText").html() || "";
const chapterText = loadedCheerio('#chapterText').html() || '';
return chapterText;
};
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const url = this.site + `/search/autocomplete?dataType=json&query=${searchTerm}`;
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url =
this.site + `/search/autocomplete?dataType=json&query=${searchTerm}`;
const result = await fetchApi(url);
const body = await result.json();
const novels: Plugin.NovelItem[] = [];
body.results.forEach((item: {link: string; original_title:string; image:string;}) =>
body.results.forEach(
(item: { link: string; original_title: string; image: string }) =>
novels.push({
path: item.link,
name: item.original_title,
cover: item.image,
})
}),
);
return novels;
};
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
@@ -173,13 +180,13 @@ class ReadLiteNovel implements Plugin.PluginBase {
filters = {
order: {
value: "top-rated",
label: "Order by",
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" },
{ label: 'MOST VIEWED', value: 'most-viewed' },
{ label: 'TOP RATED', value: 'top-rated' },
{ label: 'BOOKMARKS', value: 'subscribers' },
{ label: 'NEW', value: 'new' },
],
type: FilterTypes.Picker,
},
+64 -58
View File
@@ -1,19 +1,19 @@
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){
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".fiction-list-item").each((idx, ele) => {
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);
@@ -34,17 +34,16 @@ class RoyalRoad implements Plugin.PluginBase {
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}fictions/`;
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);
@@ -58,10 +57,10 @@ class RoyalRoad implements Plugin.PluginBase {
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio("h1").text() || "Untitled",
name: loadedCheerio('h1').text() || 'Untitled',
cover: loadedCheerio('.cover-art-container img').attr('src'),
author: loadedCheerio('h4 a').text(),
summary: loadedCheerio(".description").text().trim(),
summary: loadedCheerio('.description').text().trim(),
status: loadedCheerio('span.label-sm:last').text().trim(),
chapters: [],
};
@@ -69,20 +68,25 @@ class RoyalRoad implements Plugin.PluginBase {
novel.genres = loadedCheerio('span.tags a')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
.join(',');
const chapterJson = JSON.parse(loadedCheerio('script:contains("window.chapters")')
const chapterJson = JSON.parse(
loadedCheerio('script:contains("window.chapters")')
.html()
?.match(/window.chapters = (.+])(?=;)/)![1] || "");
?.match(/window.chapters = (.+])(?=;)/)![1] || '',
);
const volumeJson = JSON.parse(loadedCheerio('script:contains("window.chapters")')
const volumeJson = JSON.parse(
loadedCheerio('script:contains("window.chapters")')
.html()
?.match(/window.volumes = (.+])(?=;)/)![1] || "");
?.match(/window.volumes = (.+])(?=;)/)![1] || '',
);
const chapter : Plugin.ChapterItem[] = chapterJson.map(
const chapter: Plugin.ChapterItem[] = chapterJson.map(
(chapter: ChapterEntry) => {
const matchingVolume = volumeJson.find(
(volume: VolumeEntry) => volume.id === chapter.volumeId);
(volume: VolumeEntry) => volume.id === chapter.volumeId,
);
return {
name: chapter.title,
path: chapter.url.slice(1),
@@ -90,26 +94,28 @@ class RoyalRoad implements Plugin.PluginBase {
chapterNumber: chapter?.order,
page: matchingVolume ? matchingVolume.title : null,
};
}
},
);
novel.chapters = chapter;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio(".chapter-content").html() || '';
const chapterText = loadedCheerio('.chapter-content').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const searchUrl = `${this.site}fictions/search?page=${page}&title=${searchTerm}`;
const result = await fetchApi(searchUrl);
@@ -125,44 +131,44 @@ class RoyalRoad implements Plugin.PluginBase {
filters = {
order: {
value: "weekly-popular",
label: "Order By",
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" },
{ 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",
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" }
{ 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;
}
+74 -73
View File
@@ -1,21 +1,23 @@
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){
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) => {
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 novelCover = loadedCheerio(el)
.find('.search_img > img')
.attr('src');
const novelUrl = loadedCheerio(el).find('.search_title > a').attr('href');
if (!novelUrl) return;
@@ -24,7 +26,7 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
}
};
novels.push(novel);
});
return novels;
@@ -32,14 +34,16 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
async popularNovels(
page: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}`;
if (showLatestNovels)
link += "latest-series/"
else // if (filters.genres.value.include?.length ||
if (showLatestNovels) link += 'latest-series/';
// if (filters.genres.value.include?.length ||
// filters.genres.value.exclude?.length)
link += "series-finder/?sf=1"
else link += 'series-finder/?sf=1';
// else
// link += "series-ranking/";
// TODO, series-ranking filters when hideOnSelect come out
@@ -50,7 +54,10 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
if (filters.genres.value.exclude?.length)
link += '&ge=' + filters.genres.value.exclude.join(',');
if (filters.genres.value.include?.length || filters.genres.value.exclude?.length)
if (
filters.genres.value.include?.length ||
filters.genres.value.exclude?.length
)
link += '&mgi=' + filters.genre_operator.value;
if (filters.content_warning.value.include?.length)
@@ -59,16 +66,16 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
if (filters.content_warning.value.exclude?.length)
link += '&cte=' + filters.content_warning.value.exclude.join(',');
if (filters.content_warning.value.include?.length ||
filters.content_warning.value.exclude?.length)
if (
filters.content_warning.value.include?.length ||
filters.content_warning.value.exclude?.length
)
link += '&mct' + filters.content_warning_operator.value;
link += '&sort=' + filters.sort.value;
link += '&order=' + filters.order.value;
const body = await fetchApi(link).then((result) =>
result.text()
);
const body = await fetchApi(link).then(result => result.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
@@ -92,12 +99,9 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
novel.genres = loadedCheerio('.fic_genre')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
.join(',');
novel.status = loadedCheerio('.rnd_stats')
.next()
.text()
.includes('Ongoing')
novel.status = loadedCheerio('.rnd_stats').next().text().includes('Ongoing')
? 'Ongoing'
: 'Completed';
@@ -106,13 +110,10 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
formData.append('pagenum', '-1');
formData.append('mypostid', novelPath.split('/')[1]);
const data = await fetchApi(
`${this.site}wp-admin/admin-ajax.php`,
{
const data = await fetchApi(`${this.site}wp-admin/admin-ajax.php`, {
method: 'POST',
body: formData,
},
);
});
const text = await data.text();
loadedCheerio = parseHTML(text);
@@ -120,23 +121,23 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
const chapter: Plugin.ChapterItem[] = [];
const parseISODate = (date: string) => {
if (date.includes("ago")) {
if (date.includes('ago')) {
const dayJSDate = dayjs(new Date()); // today
const timeAgo = date.match(/\d+/)?.[0] || "";
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('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('days ago') || date.includes('day ago')) {
dayJSDate.subtract(timeAgoInt, 'days'); // go back N days
}
if (date.includes("months ago") || date.includes("month ago")) {
dayJSDate.subtract(timeAgoInt, "months"); // go back N months
if (date.includes('months ago') || date.includes('month ago')) {
dayJSDate.subtract(timeAgoInt, 'months'); // go back N months
}
return dayJSDate.toISOString();
@@ -144,7 +145,7 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
return null;
};
loadedCheerio('.toc_w').each((i,el) => {
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');
@@ -174,9 +175,9 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
async searchNovels(
searchTerm: string,
pageNo: number
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url =`${this.site}?s=${searchTerm}&post_type=fictionposts`;
const url = `${this.site}?s=${searchTerm}&post_type=fictionposts`;
const result = await fetchApi(url);
const body = await result.text();
@@ -190,41 +191,41 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
filters = {
sort: {
label: "Sort Results By",
value: "ratings",
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" },
{ 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",
label: 'Order by',
value: 'desc',
options: [
{ label: "Descending", value: "desc" },
{ label: "Ascending", value: "asc" },
{ label: 'Descending', value: 'desc' },
{ label: 'Ascending', value: 'asc' },
],
type: FilterTypes.Picker,
},
storyStatus: {
label: "Story Status (Translation)",
value: "",
label: 'Story Status (Translation)',
value: '',
options: [
{ label: "All", value: "all" },
{ label: "Completed", value: "completed" },
{ label: "Ongoing", value: "ongoing" },
{ label: "Hiatus", value: "hiatus" },
{ label: 'All', value: 'all' },
{ label: 'Completed', value: 'completed' },
{ label: 'Ongoing', value: 'ongoing' },
{ label: 'Hiatus', value: 'hiatus' },
],
type: FilterTypes.Picker,
},
@@ -269,7 +270,7 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
],
type: FilterTypes.ExcludableCheckboxGroup,
},
genre_operator : {
genre_operator: {
value: 'and',
label: 'Genre And/Or',
options: [
@@ -283,13 +284,13 @@ class ScribbleHubPlugin implements Plugin.PluginBase {
include: [],
exclude: [],
},
label: "Mature Content",
label: 'Mature Content',
options: [
{ label: 'Gore', value: '48' },
{ label: 'Sexual Content', value: '50' },
{ label: 'Strong Language', value: '49' },
],
type: FilterTypes.ExcludableCheckboxGroup
type: FilterTypes.ExcludableCheckboxGroup,
},
content_warning_operator: {
value: 'and',
+95 -82
View File
@@ -1,119 +1,143 @@
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<typeof this.filters>,
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site + this.sourceLang + "novel-list?";
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){
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 || "",
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 || "",
}));
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);
.filter(novel => novel.name && novel.path);
return novels;
}
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then((res) => res.text());
const 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())
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
.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
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
}));
chapterNumber: chapterIndex + 1,
}),
);
novel.chapters = chapters;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((res) => res.text());
const 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 = '';
@@ -122,75 +146,68 @@ class WTRLAB implements Plugin.PluginBase {
htmlString += `<p>${text}</p>`;
}
return htmlString;
}
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
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 || "",
name: datum.data.title || '',
cover: datum.data.image,
path: this.sourceLang + "serie-" + datum.raw_id + "/" + datum.slug || "",
path: this.sourceLang + 'serie-' + datum.raw_id + '/' + datum.slug || '',
}));
return novels;
}
fetchImage = fetchFile;
filters = {
order: {
value: "chapter",
label: "Order by",
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" },
{ 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",
value: 'desc',
label: 'Sort by',
options: [
{ label: "Descending", value: "desc" },
{ label: "Ascending", value: "asc" },
{ label: 'Descending', value: 'desc' },
{ label: 'Ascending', value: 'asc' },
],
type: FilterTypes.Picker,
},
storyStatus: {
value: "all",
label: "Status",
value: 'all',
label: 'Status',
options: [
{ label: "All", value: "all" },
{ label: "Ongoing", value: "ongoing" },
{ label: "Completed", value: "completed" },
{ label: 'All', value: 'all' },
{ label: 'Ongoing', value: 'ongoing' },
{ label: 'Completed', value: 'completed' },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
interface NovelJson {
props: Props;
page: string;
@@ -277,10 +294,6 @@ interface Datum {
raw_id: number;
slug: string;
data: Data;
}
export default new WTRLAB();
+62 -50
View File
@@ -1,8 +1,8 @@
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;
@@ -71,7 +71,7 @@ enum NovelItem_Status {
Finished = 0,
Active = 1,
Hiatus = 2,
All = -1
All = -1,
}
interface NovelKarmaInfo {
@@ -101,14 +101,14 @@ interface GetNovelResponse {
}
class WuxiaWorld implements Plugin.PluginBase {
id = "wuxiaworld";
name = "Wuxia World";
icon = "src/en/wuxiaworld/icon.png";
site = "https://www.wuxiaworld.com/";
id = 'wuxiaworld';
name = 'Wuxia World';
icon = 'src/en/wuxiaworld/icon.png';
site = 'https://www.wuxiaworld.com/';
filters?: Filters | undefined;
version = "0.5.0";
version = '0.5.0';
parseNovels(data: {items: NovelEntry[]}){
parseNovels(data: { items: NovelEntry[] }) {
const novels: Plugin.NovelItem[] = [];
data.items.map((novel: NovelEntry) => {
@@ -126,13 +126,16 @@ class WuxiaWorld implements Plugin.PluginBase {
return novels;
}
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
const link = `${this.site}api/novels`;
const result = await fetchApi(link);
const data = await result.json();
return this.parseNovels(data)
return this.parseNovels(data);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
@@ -141,28 +144,30 @@ class WuxiaWorld implements Plugin.PluginBase {
proto: this.proto,
requestType: 'GetNovelRequest',
responseType: 'GetNovelResponse',
requestData: {slug: novelPath.split('/')[1]},
requestData: { slug: novelPath.split('/')[1] },
},
'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Novels/GetNovel',
{
headers: {
"Content-Type": "application/grpc-web+proto",
}
}
)
'Content-Type': 'application/grpc-web+proto',
},
},
);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: data.item?.name || "Untitled",
name: data.item?.name || 'Untitled',
cover: data.item?.coverUrl?.value,
summary: parseHTML(data.item?.description?.value + '\n\n' + data.item?.synopsis?.value).text(),
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){
switch (status) {
case NovelItem_Status.Active:
novel.status = NovelStatus.Ongoing;
break;
@@ -172,7 +177,8 @@ class WuxiaWorld implements Plugin.PluginBase {
case NovelItem_Status.All:
novel.status = NovelStatus.Unknown;
break;
default: novel.status = NovelStatus.Completed;
default:
novel.status = NovelStatus.Completed;
}
// novel.summary = loadedCheerio('.relative > .absolute:first')
@@ -193,38 +199,41 @@ class WuxiaWorld implements Plugin.PluginBase {
proto: this.proto,
requestType: 'GetChapterListRequest',
responseType: 'GetChapterListResponse',
requestData: {novelId: data.item?.id},
requestData: { novelId: data.item?.id },
},
'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Chapters/GetChapterList',
{
headers: {
"Content-Type": "application/grpc-web+proto",
}
}
)
'Content-Type': 'application/grpc-web+proto',
},
},
);
const freeChapter = Number(data.item?.karmaInfo?.maxFreeChapter?.units) +
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) =>
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) ||
name:
chapterItem.name +
(chapterItem.relatedUserInfo?.isChapterUnlocked?.value === false ||
(!chapterItem.relatedUserInfo &&
(Number(chapterItem.number?.units) +
(chapterItem.number?.nanos || 0) / 1000000000) > freeChapter)
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
(chapterItem.publishedAt?.nanos || 0) / 1000000,
).toISOString(),
})
));
})),
);
novel.chapters = chapter;
@@ -243,27 +252,30 @@ class WuxiaWorld implements Plugin.PluginBase {
slugs: {
novelSlug: paths[1],
chapterSlug: paths[2],
}
}
},
},
},
},
'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Chapters/GetChapter',
{
headers: {
"Content-Type": "application/grpc-web+proto",
}
}
)
'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
const chapterText = data.item?.content?.value || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const searchUrl = "https://www.wuxiaworld.com/api/novels/search?query=";
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const searchUrl = 'https://www.wuxiaworld.com/api/novels/search?query=';
const url = searchUrl + searchTerm;
@@ -460,7 +472,7 @@ class WuxiaWorld implements Plugin.PluginBase {
service Novels {
rpc GetNovel(GetNovelRequest) returns (GetNovelResponse);
}
`
`;
}
export default new WuxiaWorld();
+226 -150
View File
@@ -1,128 +1,176 @@
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<CheerioAPI> {
const r = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }});
const r = await fetchApi(url, {
headers: { 'Accept-Encoding': 'deflate' },
});
const body = await r.text();
const $ = load(body);
return ($);
return $;
}
async popularNovels(pageNo: number, { filters, showLatestNovels }: Plugin.PopularNovelsOptions): Promise<Plugin.NovelItem[]> {
async popularNovels(
pageNo: number,
{ filters, showLatestNovels }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
let url = this.site;
let tag: string = "all";
if (showLatestNovels)
url += "/category/translatedtales/page/" + pageNo;
let tag: string = 'all';
if (showLatestNovels) url += '/category/translatedtales/page/' + pageNo;
else {
if (filters && typeof filters.tag.value === "string" && filters.tag.value !== "all")
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 [];
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;
if (showLatestNovels || tag !== "all") {
if (showLatestNovels || tag !== 'all') {
let loop = 1;
if (showLatestNovels)
loop = 2;
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");
$ = 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")
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, "") };
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;
} 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");
if (i % 2 === 0) novelCover = $(elem).find('img').attr('src');
else {
novelName = $(elem).text().trim();
novelUrl = $(elem).find("a").attr("href");
novelUrl = $(elem).find('a').attr('href');
if (!novelUrl) return;
novel = { name: novelName, cover: novelCover || defaultCover, path: novelUrl.replace(this.site, "") };
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");
} // mobile
else {
const imgs = populaire.find('div.popular-list-img img');
const txts = populaire.find('div.popular-list-name');
txts.each((i, elem) => {
const novelName = $(elem).text().trim();
const novelCover = $(imgs[i]).attr("src");
const novelUrl = $(elem).find("a").attr("href");
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, "") };
if (novelUrl) {
novel = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
};
novels.push(novel);
}
});
}
}
return novels;
};
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const novel: Plugin.SourceNovel = { path: novelPath, name: "Sans titre" };
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();
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":
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":
case 'complet':
novel.status = NovelStatus.Completed;
break;
default:
@@ -132,22 +180,23 @@ class ChireadsPlugin implements Plugin.PluginBase {
let chapters: Plugin.ChapterItem[] = [];
let chapterList = $(".chapitre-table a");
if (!chapterList.length)
{
$("div.inform-annexe-list").first().remove();
chapterList = $(".inform-annexe-list").find("a");
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");
const chapterUrl = $(elem).attr('href');
const releaseDate = dayjs(
chapterUrl?.substring(chapterUrl.length - 11, chapterUrl.length - 1),
).format('DD MMMM YYYY');
if (chapterUrl){
if (chapterUrl) {
chapters.push({
name: chapterName,
releaseTime: releaseDate,
path: chapterUrl.replace(this.site, ""),
path: chapterUrl.replace(this.site, ''),
});
}
});
@@ -155,34 +204,37 @@ class ChireadsPlugin implements Plugin.PluginBase {
novel.chapters = chapters;
return novel;
};
}
async parseChapter(chapterUrl: string): Promise<string> {
const $ = await this.getCheerio(this.site + chapterUrl);
const chapterText = $("#content").html() || "";
const chapterText = $('#content').html() || '';
return chapterText;
};
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
if (pageNo !== 1)
return [];
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
if (pageNo !== 1) return [];
let novels: Plugin.NovelItem[] = [];
let i = 1;
let finised = false;
while ( !finised )
{
await this.popularNovels(i, { showLatestNovels: true, filters: undefined } ).then((res) => {
if (res.length === 0)
finised = true;
while (!finised) {
await this.popularNovels(i, {
showLatestNovels: true,
filters: undefined,
}).then(res => {
if (res.length === 0) finised = true;
novels.push(...res);
});
i++;
}
novels = novels.filter((novel) =>
novels = novels.filter(novel =>
novel.name
.toLowerCase()
.normalize('NFD')
@@ -191,75 +243,99 @@ class ChireadsPlugin implements Plugin.PluginBase {
searchTerm
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
)
.replace(/[\u0300-\u036f]/g, ''),
),
);
return novels;
};
}
fetchImage = fetchFile;
filters = {
tag: {
type: FilterTypes.Picker,
label: "Tag",
value: "all",
label: 'Tag',
value: 'all',
options: [
{ label: "Tous", value: "all" },
{ label: "Arts martiaux", value: "arts-martiaux" },
{ label: "De faible à fort", value: "de-faible-a-fort" },
{ label: "Adapté en manhua", value: "adapte-en-manhua" },
{ label: "Cultivation", value: "cultivation" },
{ label: "Action", value: "action" },
{ label: "Aventure", value: "aventure" },
{ label: "Monstres", value: "monstres" },
{ label: "Xuanhuan", value: "xuanhuan" },
{ label: "Fantastique", value: "fantastique" },
{ label: "Adapté en Animé", value: "adapte-en-anime" },
{ label: "Alchimie", value: "alchimie" },
{ label: "Éléments de jeux", value: "elements-de-jeux" },
{ label: "Calme Protagoniste", value: "calme-protagoniste" },
{ label: "Protagoniste intelligent", value: "protagoniste-intelligent" },
{ label: "Polygamie", value: "polygamie" },
{ label: "Belle femelle Lea", value: "belle-femelle-lea" },
{ label: "Personnages arrogants", value: "personnages-arrogants" },
{ label: "Système de niveau", value: "systeme-de-niveau" },
{ label: "Cheat", value: "cheat" },
{ label: "Protagoniste génie", value: "protagoniste-genie" },
{ label: "Comédie", value: "comedie" },
{ label: "Gamer", value: "gamer" },
{ label: "Mariage", value: "mariage" },
{ label: "seeking Protag", value: "seeking-protag" },
{ label: "Romance précoce", value: "romance-precoce" },
{ label: "Croissance accélérée", value: "croissance-acceleree" },
{ label: "Artefacts", value: "artefacts" },
{ label: "Intelligence artificielle", value: "intelligence-artificielle" },
{ label: "Mariage arrangé", value: "mariage-arrange" },
{ label: "Mature", value: "mature" },
{ label: "Adulte", value: "adulte" },
{ label: "Administrateur de système", value: "administrateur-de-systeme" },
{ label: "Beau protagoniste", value: "beau-protagoniste" },
{ label: "Protagoniste charismatique", value: "protagoniste-charismatique" },
{ label: "Protagoniste masculin", value: "protagoniste-masculin" },
{ label: "Démons", value: "demons" },
{ label: "Reincarnation", value: "reincarnation" },
{ label: "Académie", value: "academie" },
{ label: "Cacher les vraies capacités", value: "cacher-les-vraies-capacites" },
{ label: "Protagoniste surpuissant", value: "protagoniste-surpuissant" },
{ label: "Joueur", value: "joueur" },
{ label: "Protagoniste fort dès le départ", value: "protagoniste-fort-des-le-depart" },
{ label: "Immortels", value: "immortels" },
{ label: "Cultivation rapide", value: "cultivation-rapide" },
{ label: "Harem", value: "harem" },
{ label: "Assasins", value: "assasins" },
{ label: "De pauvre à riche", value: "de-pauvre-a-riche" },
{ label: "Système de classement de jeux", value: "systeme-de-classement-de-jeux" },
{ label: "Capacités spéciales", value: "capacites-speciales" },
{ label: "Vengeance", value: "vengeance" },
{ label: 'Tous', value: 'all' },
{ label: 'Arts martiaux', value: 'arts-martiaux' },
{ label: 'De faible à fort', value: 'de-faible-a-fort' },
{ label: 'Adapté en manhua', value: 'adapte-en-manhua' },
{ label: 'Cultivation', value: 'cultivation' },
{ label: 'Action', value: 'action' },
{ label: 'Aventure', value: 'aventure' },
{ label: 'Monstres', value: 'monstres' },
{ label: 'Xuanhuan', value: 'xuanhuan' },
{ label: 'Fantastique', value: 'fantastique' },
{ label: 'Adapté en Animé', value: 'adapte-en-anime' },
{ label: 'Alchimie', value: 'alchimie' },
{ label: 'Éléments de jeux', value: 'elements-de-jeux' },
{ label: 'Calme Protagoniste', value: 'calme-protagoniste' },
{
label: 'Protagoniste intelligent',
value: 'protagoniste-intelligent',
},
{ label: 'Polygamie', value: 'polygamie' },
{ label: 'Belle femelle Lea', value: 'belle-femelle-lea' },
{ label: 'Personnages arrogants', value: 'personnages-arrogants' },
{ label: 'Système de niveau', value: 'systeme-de-niveau' },
{ label: 'Cheat', value: 'cheat' },
{ label: 'Protagoniste génie', value: 'protagoniste-genie' },
{ label: 'Comédie', value: 'comedie' },
{ label: 'Gamer', value: 'gamer' },
{ label: 'Mariage', value: 'mariage' },
{ label: 'seeking Protag', value: 'seeking-protag' },
{ label: 'Romance précoce', value: 'romance-precoce' },
{ label: 'Croissance accélérée', value: 'croissance-acceleree' },
{ label: 'Artefacts', value: 'artefacts' },
{
label: 'Intelligence artificielle',
value: 'intelligence-artificielle',
},
{ label: 'Mariage arrangé', value: 'mariage-arrange' },
{ label: 'Mature', value: 'mature' },
{ label: 'Adulte', value: 'adulte' },
{
label: 'Administrateur de système',
value: 'administrateur-de-systeme',
},
{ label: 'Beau protagoniste', value: 'beau-protagoniste' },
{
label: 'Protagoniste charismatique',
value: 'protagoniste-charismatique',
},
{ label: 'Protagoniste masculin', value: 'protagoniste-masculin' },
{ label: 'Démons', value: 'demons' },
{ label: 'Reincarnation', value: 'reincarnation' },
{ label: 'Académie', value: 'academie' },
{
label: 'Cacher les vraies capacités',
value: 'cacher-les-vraies-capacites',
},
{
label: 'Protagoniste surpuissant',
value: 'protagoniste-surpuissant',
},
{ label: 'Joueur', value: 'joueur' },
{
label: 'Protagoniste fort dès le départ',
value: 'protagoniste-fort-des-le-depart',
},
{ label: 'Immortels', value: 'immortels' },
{ label: 'Cultivation rapide', value: 'cultivation-rapide' },
{ label: 'Harem', value: 'harem' },
{ label: 'Assasins', value: 'assasins' },
{ label: 'De pauvre à riche', value: 'de-pauvre-a-riche' },
{
label: 'Système de classement de jeux',
value: 'systeme-de-classement-de-jeux',
},
{ label: 'Capacités spéciales', value: 'capacites-speciales' },
{ label: 'Vengeance', value: 'vengeance' },
],
}
},
} satisfies Filters;
};
}
export default new ChireadsPlugin();
+196 -182
View File
@@ -1,49 +1,69 @@
import { load, CheerioAPI } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { NovelStatus } from "@libs/novelStatus";
import { Filters, FilterTypes } from "@libs/filterInputs";
import { defaultCover } from "@libs/defaultCover";
import dayjs from "dayjs";
import { load, CheerioAPI } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { NovelStatus } from '@libs/novelStatus';
import { Filters, FilterTypes } from '@libs/filterInputs';
import { defaultCover } from '@libs/defaultCover';
import dayjs from 'dayjs';
class NovelDeGlacePlugin implements Plugin.PluginBase{
id = "noveldeglace";
name = "NovelDeGlace";
icon = "src/fr/noveldeglace/icon.png";
site = "https://noveldeglace.com/";
version = "1.0.2";
class NovelDeGlacePlugin implements Plugin.PluginBase {
id = 'noveldeglace';
name = 'NovelDeGlace';
icon = 'src/fr/noveldeglace/icon.png';
site = 'https://noveldeglace.com/';
version = '1.0.2';
async getCheerio(url: string): Promise<CheerioAPI | undefined> {
const r = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }});
if (!r.ok) return (undefined);
const r = await fetchApi(url, {
headers: { 'Accept-Encoding': 'deflate' },
});
if (!r.ok) return undefined;
const body = await r.text();
const loadedCheerio = load(body);
return (loadedCheerio);
return loadedCheerio;
}
parseDate(date: string): string {
const monthMapping: { [key: string]: string } = { janvier: "January", fevrier: "February", mars: "March", avril: "April", mai: "May", juin: "June", juillet: "July", aout: "August", septembre: "September", octobre: "October", novembre: "November", decembre: "December"};
const [day, month, year] = date.split(" ");
return dayjs(`${day} ${monthMapping[month.normalize('NFD').replace(/[\u0300-\u036f]/g, '')]} ${year}`, "D MMMM YYYY").format("YYYY-MM-DD");
const monthMapping: { [key: string]: string } = {
janvier: 'January',
fevrier: 'February',
mars: 'March',
avril: 'April',
mai: 'May',
juin: 'June',
juillet: 'July',
aout: 'August',
septembre: 'September',
octobre: 'October',
novembre: 'November',
decembre: 'December',
};
const [day, month, year] = date.split(' ');
return dayjs(
`${day} ${monthMapping[month.normalize('NFD').replace(/[\u0300-\u036f]/g, '')]} ${year}`,
'D MMMM YYYY',
).format('YYYY-MM-DD');
}
parseNovels($: CheerioAPI, showLatestNovels: boolean | undefined): Plugin.NovelItem[] {
parseNovels(
$: CheerioAPI,
showLatestNovels: boolean | undefined,
): Plugin.NovelItem[] {
let novels: Plugin.NovelItem[] = [];
$("article").each((i, el) => {
$('article').each((i, el) => {
const cheerio = $(el);
const novelName = cheerio.find("h2").text().trim();
const novelCover = cheerio.find("img").attr("src");
const novelName = cheerio.find('h2').text().trim();
const novelCover = cheerio.find('img').attr('src');
let novelUrl: string | undefined;
if (showLatestNovels)
novelUrl = cheerio.find("span.Roman > a").attr("href");
else
novelUrl = cheerio.find("h2 > a").attr("href");
novelUrl = cheerio.find('span.Roman > a').attr('href');
else novelUrl = cheerio.find('h2 > a').attr('href');
if (novelUrl){
const novel:Plugin.NovelItem = {
if (novelUrl) {
const novel: Plugin.NovelItem = {
name: novelName,
path: novelUrl.replace(this.site, ""),
path: novelUrl.replace(this.site, ''),
cover: novelCover,
};
novels.push(novel);
@@ -53,98 +73,95 @@ class NovelDeGlacePlugin implements Plugin.PluginBase{
return novels;
}
async popularNovels(pageNo: number, { filters, showLatestNovels }: Plugin.PopularNovelsOptions<typeof this.filters>): Promise<Plugin.NovelItem[]> {
async popularNovels(
pageNo: number,
{
filters,
showLatestNovels,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.site;
if (showLatestNovels)
url += "chapitre"
else
{
let cat_gen: string = "all";
if (filters && typeof filters.categorie_genre.value == "string")
if (showLatestNovels) url += 'chapitre';
else {
let cat_gen: string = 'all';
if (filters && typeof filters.categorie_genre.value == 'string')
cat_gen = filters.categorie_genre.value;
if (cat_gen != "all" && cat_gen != "categorie_roman" && cat_gen != "genre")
{
if (cat_gen[0] == "c")
url += "categorie_roman/" + cat_gen.substring(2);
else if (cat_gen[0] == "g")
url += "genre/" + cat_gen.substring(2);
if (
cat_gen != 'all' &&
cat_gen != 'categorie_roman' &&
cat_gen != 'genre'
) {
if (cat_gen[0] == 'c') url += 'categorie_roman/' + cat_gen.substring(2);
else if (cat_gen[0] == 'g') url += 'genre/' + cat_gen.substring(2);
} else if (pageNo > 1)
return []; // when asking for all novels, there is only 1 page
else url += 'roman';
}
else if (pageNo > 1)
return ([]); // when asking for all novels, there is only 1 page
else
url += "roman";
}
url += "/page/" + pageNo;
url += '/page/' + pageNo;
let $ = await this.getCheerio(url);
if (!$)
return ([]);
if (!$) return [];
return this.parseNovels($, showLatestNovels);
};
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const $ = await this.getCheerio(this.site + novelPath);
if (!$)
throw new Error("Failed to load page (open in web view)");
if (!$) throw new Error('Failed to load page (open in web view)');
let novel: Plugin.SourceNovel = { path: novelPath, name: "Untitled" };
let novel: Plugin.SourceNovel = { path: novelPath, name: 'Untitled' };
novel.name = ($("div.entry-content > div > strong")[0]
.nextSibling as Text | null)
?.nodeValue
?.trim() || "Untitled";
novel.name =
(
$('div.entry-content > div > strong')[0].nextSibling as Text | null
)?.nodeValue?.trim() || 'Untitled';
novel.cover = $(".su-row > div > div > img")
.attr("src") || defaultCover;
novel.cover = $('.su-row > div > div > img').attr('src') || defaultCover;
let novelInfos = $("div[data-title=Tomes] >")
.toArray();
let novelInfos = $('div[data-title=Tomes] >').toArray();
novelInfos.pop();
novelInfos.shift();
novel.summary = $("div[data-title=Synopsis]")
.text().trim() +
"\n\n" + novelInfos.map((el) => $(el).text()).join("\n").trim();
novel.summary =
$('div[data-title=Synopsis]').text().trim() +
'\n\n' +
novelInfos
.map(el => $(el).text())
.join('\n')
.trim();
novel.author = $("strong:contains('Auteur :')")
.parent()
.text()
.replace("Auteur : ", "")
.replace('Auteur : ', '')
.trim();
novel.artist = $("strong:contains('Illustrateur :')")
.parent()
.text()
.replace("Illustrateur :", "")
.replace('Illustrateur :', '')
.trim();
const categorie = $(".categorie")
const categorie = $('.categorie').text().replace('Catégorie :', '').trim();
const genres = $('.genre')
.text()
.replace("Catégorie :", "")
.replace('Genre :', '')
.replace(/, /g, ',')
.trim();
const genres = $(".genre")
.text()
.replace("Genre :", "")
.replace(/, /g, ",")
.trim();
if (categorie && categorie != "Autre")
novel.genres = categorie;
if (categorie && categorie != 'Autre') novel.genres = categorie;
if (genres)
novel.genres = novel.genres ? novel.genres + "," + genres : genres;
novel.genres = novel.genres ? novel.genres + ',' + genres : genres;
let status = $("strong:contains('Statut :')")
.parent()
.attr("class");
let status = $("strong:contains('Statut :')").parent().attr('class');
switch (status) {
case "type etat0":
case "type etat1":
case 'type etat0':
case 'type etat1':
novel.status = NovelStatus.Ongoing;
break;
case "type etat4":
case 'type etat4':
novel.status = NovelStatus.OnHiatus;
break;
case "type etat5":
case 'type etat5':
novel.status = NovelStatus.Completed;
break;
case "type etat6":
case 'type etat6':
novel.status = NovelStatus.Cancelled;
break;
default:
@@ -154,66 +171,64 @@ class NovelDeGlacePlugin implements Plugin.PluginBase{
let novelChapters: Plugin.ChapterItem[] = [];
const volumes = $("div[data-title=Tomes] > div")
.last()
.contents();
const volumes = $('div[data-title=Tomes] > div').last().contents();
const hasMultipleVolumes = volumes.length > 1;
let chapterName = ""
let chapterName = '';
let site = this.site;
volumes.each((volumeIndex: number, el) => {
if (hasMultipleVolumes)
chapterName = "T." + (volumeIndex + 1) + " ";
if (hasMultipleVolumes) chapterName = 'T.' + (volumeIndex + 1) + ' ';
$(el)
.find(".chpt")
.find('.chpt')
.each((chapterIndex: number, el) => {
const cheerio = $(el);
const newChapterName = chapterName + cheerio
.find("a")
.first()
.text().trim() || "";
if (!cheerio.find("i.fa").length) // no parts
{
const dateHtml = cheerio.html()?.substring(cheerio.html()?.indexOf("</a>") || 0) || "";
let releaseDate = dateHtml?.substring(dateHtml.indexOf("(") + 1, dateHtml.indexOf(")")) || undefined;
const newChapterName =
chapterName + cheerio.find('a').first().text().trim() || '';
if (!cheerio.find('i.fa').length) {
// no parts
const dateHtml =
cheerio.html()?.substring(cheerio.html()?.indexOf('</a>') || 0) ||
'';
let releaseDate =
dateHtml?.substring(
dateHtml.indexOf('(') + 1,
dateHtml.indexOf(')'),
) || undefined;
if (releaseDate)
try {
releaseDate = this.parseDate(releaseDate);
} catch (e) {}
const chapterUrl = cheerio.find("a").attr("href");
if (chapterUrl)
{
const chapterUrl = cheerio.find('a').attr('href');
if (chapterUrl) {
const chapter: Plugin.ChapterItem = {
name: newChapterName,
releaseTime: releaseDate,
path: chapterUrl.replace(site, ""),
path: chapterUrl.replace(site, ''),
chapterNumber: chapterIndex,
};
novelChapters.push(chapter);
}
}
else // has parts that needs to be added individually
{
const items = cheerio
.find("i")
.parent()
.next()
.html()
?.split("</a>") || [];
} // has parts that needs to be added individually
else {
const items =
cheerio.find('i').parent().next().html()?.split('</a>') || [];
items?.shift();
const dates: string[] = [];
items?.forEach((item) => { // there is a date on every publish parts
dates.push(item.substring(item.indexOf("(") + 1, item.indexOf(")")));
items?.forEach(item => {
// there is a date on every publish parts
dates.push(
item.substring(item.indexOf('(') + 1, item.indexOf(')')),
);
});
const hrefs: string[] = [];
cheerio
.find("i")
.find('i')
.parent()
.next()
.find("a")
.find('a')
.each(function () {
hrefs.push(this["attribs"]["href"]);
hrefs.push(this['attribs']['href']);
});
if (dates.length == hrefs.length)
dates.forEach((date, index) => {
@@ -222,14 +237,13 @@ class NovelDeGlacePlugin implements Plugin.PluginBase{
releaseDate = this.parseDate(date);
} catch (e) {}
const chapter: Plugin.ChapterItem = {
name: newChapterName + " (" + (index + 1) + ")",
name: newChapterName + ' (' + (index + 1) + ')',
releaseTime: releaseDate,
path: hrefs[index].replace(site, ""),
path: hrefs[index].replace(site, ''),
chapterNumber: chapterIndex + (index + 1) / 1000,
};
novelChapters.push(chapter);
}
);
});
}
});
});
@@ -237,30 +251,30 @@ class NovelDeGlacePlugin implements Plugin.PluginBase{
novel.chapters = novelChapters;
return novel;
};
}
async parseChapter(chapterPath: string): Promise<string> {
const $ = await this.getCheerio(this.site + chapterPath);
if (!$)
throw new Error("Failed to load page (open in web view)");
if (!$) throw new Error('Failed to load page (open in web view)');
$(".mistape_caption").remove();
let chapterText = $(".chapter-content").html()
|| $(".entry-content").html()
|| "";
$('.mistape_caption').remove();
let chapterText =
$('.chapter-content').html() || $('.entry-content').html() || '';
return chapterText;
};
}
async searchNovels(searchTerm: string, num: number): Promise<Plugin.NovelItem[]> {
if (num !== 1) return ([]); // only 1 page of results
let url = this.site + "roman";
async searchNovels(
searchTerm: string,
num: number,
): Promise<Plugin.NovelItem[]> {
if (num !== 1) return []; // only 1 page of results
let url = this.site + 'roman';
const $ = await this.getCheerio(url);
if (!$)
throw new Error("Failed to load page (open in web view)");
if (!$) throw new Error('Failed to load page (open in web view)');
let novels = this.parseNovels($, false);
novels = novels.filter((novel) =>
novels = novels.filter(novel =>
novel.name
.toLowerCase()
.normalize('NFD')
@@ -271,62 +285,62 @@ class NovelDeGlacePlugin implements Plugin.PluginBase{
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.trim()
)
.trim(),
),
);
return novels;
};
}
fetchImage = fetchFile;
filters = {
categorie_genre: {
type: FilterTypes.Picker,
label: "Catégorie/Genre",
value: "all",
label: 'Catégorie/Genre',
value: 'all',
options: [
{ label: "Tous", value: "all" },
{ label: "═══CATÉGORIES═══", value: "categorie_roman" },
{ label: "Seinen", value: "c_seinen" },
{ label: "Shonen", value: "c_shonen" },
{ label: "Original", value: "c_original" },
{ label: "Yuri", value: "c_yuri" },
{ label: "Autre", value: "c_autre" },
{ label: "Fille", value: "c_fille" },
{ label: "Roman pour Adulte", value: "c_roman-pour-adulte" },
{ label: "Xuanhuan", value: "c_xuanhuan" },
{ label: "Yaoi", value: "c_yaoi" },
{ label: "═══GENRES═══", value: "genre" },
{ label: "Action", value: "g_action" },
{ label: "Aventure", value: "g_aventure" },
{ label: "Comédie", value: "g_comedie" },
{ label: "Drame", value: "g_drame" },
{ label: "Fantastique", value: "g_fantastique" },
{ label: "Harem", value: "g_harem" },
{ label: "Psychologique", value: "g_psychologique" },
{ label: "Romance", value: "g_romance" },
{ label: "Ecchi", value: "g_ecchi" },
{ label: "Mature", value: "g_mature" },
{ label: "Surnaturel", value: "g_surnaturel" },
{ label: "Vie scolaire", value: "g_vie-scolaire" },
{ label: "Adulte", value: "g_adulte" },
{ label: "Tragédie", value: "g_tragedie" },
{ label: "Arts Martiaux", value: "g_arts-martiaux" },
{ label: "Pas de harem", value: "g_pas-de-harem" },
{ label: "Tranche de vie", value: "g_tranche-de-vie" },
{ label: "Mecha", value: "g_mecha" },
{ label: "Sci-fi", value: "g_sci-fi" },
{ label: "Science-Fiction", value: "g_science-fiction" },
{ label: "Anti-Héros", value: "g_anti-heros" },
{ label: "Horreur", value: "g_horreur" },
{ label: "Insectes", value: "g_insectes" },
{ label: "Mystère", value: "g_mystere" },
{ label: "Lolicon", value: "g_lolicon" },
{ label: "Shoujo Ai", value: "g_shoujo-ai" },
{ label: "Smut", value: "g_smut" },
{ label: "Xuanhuan", value: "g_xuanhuan" },
{ label: "Shotacon", value: "g_shotacon" },
{ label: 'Tous', value: 'all' },
{ label: '═══CATÉGORIES═══', value: 'categorie_roman' },
{ label: 'Seinen', value: 'c_seinen' },
{ label: 'Shonen', value: 'c_shonen' },
{ label: 'Original', value: 'c_original' },
{ label: 'Yuri', value: 'c_yuri' },
{ label: 'Autre', value: 'c_autre' },
{ label: 'Fille', value: 'c_fille' },
{ label: 'Roman pour Adulte', value: 'c_roman-pour-adulte' },
{ label: 'Xuanhuan', value: 'c_xuanhuan' },
{ label: 'Yaoi', value: 'c_yaoi' },
{ label: '═══GENRES═══', value: 'genre' },
{ label: 'Action', value: 'g_action' },
{ label: 'Aventure', value: 'g_aventure' },
{ label: 'Comédie', value: 'g_comedie' },
{ label: 'Drame', value: 'g_drame' },
{ label: 'Fantastique', value: 'g_fantastique' },
{ label: 'Harem', value: 'g_harem' },
{ label: 'Psychologique', value: 'g_psychologique' },
{ label: 'Romance', value: 'g_romance' },
{ label: 'Ecchi', value: 'g_ecchi' },
{ label: 'Mature', value: 'g_mature' },
{ label: 'Surnaturel', value: 'g_surnaturel' },
{ label: 'Vie scolaire', value: 'g_vie-scolaire' },
{ label: 'Adulte', value: 'g_adulte' },
{ label: 'Tragédie', value: 'g_tragedie' },
{ label: 'Arts Martiaux', value: 'g_arts-martiaux' },
{ label: 'Pas de harem', value: 'g_pas-de-harem' },
{ label: 'Tranche de vie', value: 'g_tranche-de-vie' },
{ label: 'Mecha', value: 'g_mecha' },
{ label: 'Sci-fi', value: 'g_sci-fi' },
{ label: 'Science-Fiction', value: 'g_science-fiction' },
{ label: 'Anti-Héros', value: 'g_anti-heros' },
{ label: 'Horreur', value: 'g_horreur' },
{ label: 'Insectes', value: 'g_insectes' },
{ label: 'Mystère', value: 'g_mystere' },
{ label: 'Lolicon', value: 'g_lolicon' },
{ label: 'Shoujo Ai', value: 'g_shoujo-ai' },
{ label: 'Smut', value: 'g_smut' },
{ label: 'Xuanhuan', value: 'g_xuanhuan' },
{ label: 'Shotacon', value: 'g_shotacon' },
],
},
} satisfies Filters;
+96 -91
View File
@@ -1,27 +1,27 @@
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 IndoWebNovel implements Plugin.PluginBase {
id = "IDWN.id";
name = "IndoWebNovel";
icon = "src/id/indowebnovel/icon.png";
site = "https://indowebnovel.id/";
version = "1.2.0";
id = 'IDWN.id';
name = 'IndoWebNovel';
icon = 'src/id/indowebnovel/icon.png';
site = 'https://indowebnovel.id/';
version = '1.2.0';
parseNovels(loadedCheerio:CheerioAPI){
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".flexbox2-item").each((i,el) => {
loadedCheerio('.flexbox2-item').each((i, el) => {
const novelName = loadedCheerio(el)
.find(".flexbox2-title span")
.find('.flexbox2-title span')
.first()
.text();
const novelCover = loadedCheerio(el).find("img").attr("src");
const novelCover = loadedCheerio(el).find('img').attr('src');
const novelUrl = loadedCheerio(el)
.find(".flexbox2-content > a")
.attr("href");
.find('.flexbox2-content > a')
.attr('href');
if (!novelUrl) return;
@@ -37,23 +37,23 @@ class IndoWebNovel implements Plugin.PluginBase {
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}advanced-search/page/${page}/?title=&author=&yearx=`;
link += `&status=${filters.status.value}`
link += `&status=${filters.status.value}`;
link += `&type=${filters.type.value}`;
link += `&order=${filters.sort.value}`;
if (filters.lang.value.length)
link += filters.lang.value.map((i) => `&country[]=${i}`).join("");
link += filters.lang.value.map(i => `&country[]=${i}`).join('');
if (filters.genre.value.length)
link += filters.genre.value.map((i) => `&genre[]=${i}`).join("");
link += filters.genre.value.map(i => `&genre[]=${i}`).join('');
const result = await fetchApi(link);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio)
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
@@ -61,28 +61,30 @@ class IndoWebNovel implements Plugin.PluginBase {
const body = await result.text();
const loadedCheerio = parseHTML(body);
loadedCheerio(".series-synops div").remove();
loadedCheerio('.series-synops div').remove();
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".series-title").text().trim() || "Untitled",
cover: loadedCheerio(".series-thumb img").attr("src"),
author: loadedCheerio("ul.series-infolist b:contains('Author') +").text().trim(),
status: loadedCheerio(".status").text().trim(),
summary: loadedCheerio(".series-synops").text().trim(),
name: loadedCheerio('.series-title').text().trim() || 'Untitled',
cover: loadedCheerio('.series-thumb img').attr('src'),
author: loadedCheerio("ul.series-infolist b:contains('Author') +")
.text()
.trim(),
status: loadedCheerio('.status').text().trim(),
summary: loadedCheerio('.series-synops').text().trim(),
chapters: [],
};
novel.genres = loadedCheerio(".series-genres a")
.map((i,el) => loadedCheerio(el).text().trim())
novel.genres = loadedCheerio('.series-genres a')
.map((i, el) => loadedCheerio(el).text().trim())
.toArray()
.join(',');
const chapters: Plugin.ChapterItem[] = [];
loadedCheerio(".series-chapterlist li").each((i,el) => {
const chapterName = loadedCheerio(el).find("a").text().trim();
const chapterUrl = loadedCheerio(el).find("a").attr("href");
loadedCheerio('.series-chapterlist li').each((i, el) => {
const chapterName = loadedCheerio(el).find('a').text().trim();
const chapterUrl = loadedCheerio(el).find('a').attr('href');
if (!chapterUrl) return;
@@ -103,23 +105,26 @@ class IndoWebNovel implements Plugin.PluginBase {
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio(".readerss").html() || '';
const chapterText = loadedCheerio('.readerss').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}advanced-search/page/${page}/?title=${searchTerm}&author=&yearx=`;
link += `&status=${this.filters.status.value}`
link += `&status=${this.filters.status.value}`;
link += `&type=${this.filters.type.value}`;
link += `&order=${this.filters.sort.value}`;
link += this.filters.lang.value.map((i) => `&country[]=${i}`).join("");
link += this.filters.lang.value.map(i => `&country[]=${i}`).join('');
const result = await fetchApi(link);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio)
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
@@ -128,83 +133,83 @@ class IndoWebNovel implements Plugin.PluginBase {
filters = {
status: {
value: "",
label: "Status",
value: '',
label: 'Status',
options: [
{ label: "All", value: "" },
{ label: "Ongoing", value: "ongoing" },
{ label: "Completed", value: "completed" },
{ label: 'All', value: '' },
{ label: 'Ongoing', value: 'ongoing' },
{ label: 'Completed', value: 'completed' },
],
type: FilterTypes.Picker,
},
type: {
value: "",
label: "Type",
value: '',
label: 'Type',
options: [
{ label: "All", value: "" },
{ label: "Web Novel", value: "Web+Novel" },
{ label: "Light Novel", value: "Light+Novel" },
{ label: 'All', value: '' },
{ label: 'Web Novel', value: 'Web+Novel' },
{ label: 'Light Novel', value: 'Light+Novel' },
],
type: FilterTypes.Picker,
},
sort: {
value: "rating",
label: "Order By",
value: 'rating',
label: 'Order By',
options: [
{ label: "A-Z", value: "title" },
{ label: "Z-A", value: "titlereverse" },
{ label: "Latest Update", value: "update" },
{ label: "Latest Added", value: "latest" },
{ label: "Popular", value: "popular" },
{ label: "Rating", value: "rating" },
{ label: 'A-Z', value: 'title' },
{ label: 'Z-A', value: 'titlereverse' },
{ label: 'Latest Update', value: 'update' },
{ label: 'Latest Added', value: 'latest' },
{ label: 'Popular', value: 'popular' },
{ label: 'Rating', value: 'rating' },
],
type: FilterTypes.Picker,
},
lang: {
value: ['china','jepang','korea','unknown'],
label: "Country",
value: ['china', 'jepang', 'korea', 'unknown'],
label: 'Country',
options: [
{ label: "China", value: "china" },
{ label: "Jepang", value: "jepang" },
{ label: "Korea", value: "korea" },
{ label: "Unknown", value: "unknown" },
{ label: 'China', value: 'china' },
{ label: 'Jepang', value: 'jepang' },
{ label: 'Korea', value: 'korea' },
{ label: 'Unknown', value: 'unknown' },
],
type: FilterTypes.CheckboxGroup,
},
genre: {
value: [],
label: "Genres",
label: 'Genres',
options: [
{ label: "Action", value: "action" },
{ label: "Adult", value: "adult" },
{ label: "Adventure", value: "adventure" },
{ 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: "Horror", value: "horror" },
{ label: "Josei", value: "josei" },
{ 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: "Shoujo", value: "shoujo" },
{ label: "Shounen", value: "shounen" },
{ label: "Slice of Life", value: "slice-of-life" },
{ label: "Smut", value: "smut" },
{ label: "Supernatural", value: "supernatural" },
{ label: "Tragedy", value: "tragedy" },
{ label: "Wuxia", value: "wuxia" },
{ label: "Xianxia", value: "xianxia" },
{ label: "Xuanhuan", value: "xuanhuan" }
{ label: 'Action', value: 'action' },
{ label: 'Adult', value: 'adult' },
{ label: 'Adventure', value: 'adventure' },
{ 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: 'Horror', value: 'horror' },
{ label: 'Josei', value: 'josei' },
{ 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: 'Shoujo', value: 'shoujo' },
{ label: 'Shounen', value: 'shounen' },
{ label: 'Slice of Life', value: 'slice-of-life' },
{ label: 'Smut', value: 'smut' },
{ label: 'Supernatural', value: 'supernatural' },
{ label: 'Tragedy', value: 'tragedy' },
{ label: 'Wuxia', value: 'wuxia' },
{ label: 'Xianxia', value: 'xianxia' },
{ label: 'Xuanhuan', value: 'xuanhuan' },
],
type: FilterTypes.CheckboxGroup,
},
+126 -127
View File
@@ -1,27 +1,24 @@
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 NovelRingan implements Plugin.PluginBase {
id = "novelringan.com";
name = "NovelRingan";
icon = "src/id/novelringan/icon.png";
site = "https://novelringan.com/";
version = "1.0.0";
coverUriPrefix = "https://i0.wp.com/novelringan.com/wp-content/uploads/";
id = 'novelringan.com';
name = 'NovelRingan';
icon = 'src/id/novelringan/icon.png';
site = 'https://novelringan.com/';
version = '1.0.0';
coverUriPrefix = 'https://i0.wp.com/novelringan.com/wp-content/uploads/';
parseNovels(loadedCheerio:CheerioAPI) {
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio("article.post").each((idx, ele) => {
const novelName = loadedCheerio(ele)
.find(".entry-title")
.text()
?.trim();
loadedCheerio('article.post').each((idx, ele) => {
const novelName = loadedCheerio(ele).find('.entry-title').text()?.trim();
const novelCover =
this.coverUriPrefix + loadedCheerio(ele).find("img").attr("data-sxrx");
const novelUrl = loadedCheerio(ele).find("h2 > a").attr("href");
this.coverUriPrefix + loadedCheerio(ele).find('img').attr('data-sxrx');
const novelUrl = loadedCheerio(ele).find('h2 > a').attr('href');
if (!novelUrl) return;
@@ -39,17 +36,16 @@ class NovelRingan implements Plugin.PluginBase {
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}advanced-search/page/${page}/?title`;
link += `&status=${filters.status.value}`
link += `&status=${filters.status.value}`;
link += `&order=${filters.sort.value}`;
if (filters.type.value.length)
link += filters.type.value.map((i) => `&type[]=${i}`).join("");
link += filters.type.value.map(i => `&type[]=${i}`).join('');
if (filters.genre.value.length)
link += filters.genre.value.map((i) => `&genre[]=${i}`).join("");
link += filters.genre.value.map(i => `&genre[]=${i}`).join('');
const result = await fetchApi(link);
const body = await result.text();
@@ -58,7 +54,6 @@ class NovelRingan implements Plugin.PluginBase {
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
@@ -66,17 +61,15 @@ class NovelRingan implements Plugin.PluginBase {
const loadedCheerio = parseHTML(body);
const styletag = Array.from(
(
loadedCheerio('meta[name=msapplication-TileImage] + style')
.html()
?.matchAll(/"(.*?)"/g)
) || [],
m => m[1]
?.matchAll(/"(.*?)"/g) || [],
m => m[1],
);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: styletag[0] || loadedCheerio('.entry-title').text() || "Untitled",
name: styletag[0] || loadedCheerio('.entry-title').text() || 'Untitled',
author: styletag[1],
summary: loadedCheerio('.maininfo span p').text(),
chapters: [],
@@ -84,27 +77,27 @@ class NovelRingan implements Plugin.PluginBase {
novel.cover =
this.coverUriPrefix +
loadedCheerio("img.ts-post-image").attr("data-sxrx");
loadedCheerio('img.ts-post-image').attr('data-sxrx');
loadedCheerio('.maininfo li').each(function() {
loadedCheerio('.maininfo li').each(function () {
let detailName = loadedCheerio(this).find('b').text().trim();
let detail = loadedCheerio(this).find('b').remove().end().text().trim()
let detail = loadedCheerio(this).find('b').remove().end().text().trim();
switch(detailName) {
case "Status:":
switch (detailName) {
case 'Status:':
novel.status = detail;
break;
case "Genre:":
case 'Genre:':
novel.genres = detail;
break;
}
})
});
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio(".bxcl > ul > li").each((i,el) => {
const chapterName = loadedCheerio(el).find("a").text();
const chapterUrl = loadedCheerio(el).find("a").attr("href");
loadedCheerio('.bxcl > ul > li').each((i, el) => {
const chapterName = loadedCheerio(el).find('a').text();
const chapterUrl = loadedCheerio(el).find('a').attr('href');
if (!chapterUrl) return;
@@ -129,19 +122,22 @@ class NovelRingan implements Plugin.PluginBase {
loadedCheerio('.entry-content div[style="display:none"]').remove();
const chapterText = loadedCheerio(".entry-content").html() || '';
const chapterText = loadedCheerio('.entry-content').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
const url = this.site + "page/" + page + "/?s=" + searchTerm;
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const url = this.site + 'page/' + page + '/?s=' + searchTerm;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio)
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
@@ -150,108 +146,111 @@ class NovelRingan implements Plugin.PluginBase {
filters = {
status: {
value: "",
label: "Status",
value: '',
label: 'Status',
options: [
{ label: "All", value: "" },
{ label: "Ongoing", value: "Ongoing" },
{ label: "Completed", value: "Completed" },
{ label: 'All', value: '' },
{ label: 'Ongoing', value: 'Ongoing' },
{ label: 'Completed', value: 'Completed' },
],
type: FilterTypes.Picker,
},
sort: {
value: "popular",
label: "Urutkan",
value: 'popular',
label: 'Urutkan',
options: [
{ label: "A-Z", value: "title" },
{ label: "Z-A", value: "titlereverse" },
{ label: "Terbarui", value: "update" },
{ label: "Ditambahkan", value: "latest" },
{ label: "Terpopuler", value: "popular" },
{ label: 'A-Z', value: 'title' },
{ label: 'Z-A', value: 'titlereverse' },
{ label: 'Terbarui', value: 'update' },
{ label: 'Ditambahkan', value: 'latest' },
{ label: 'Terpopuler', value: 'popular' },
],
type: FilterTypes.Picker,
},
type: {
value: [],
label: "Tipe",
label: 'Tipe',
options: [
{ label: "Chinese Novel", value: "chinese-novel" },
{ label: "Chinese Web Novel", value: "chinese-web-novel" },
{ label: "Filipino Novel", value: "filipino-novel" },
{ label: "Indonesia Novel", value: "indonesia-novel" },
{ label: "Korean Novel", value: "korean-novel" },
{ label: "Light Novel", value: "light-novel" },
{ label: "Light Novel (CN)", value: "light-novel-cn" },
{ label: "Light Novel (JP)", value: "light-novel-jp" },
{ label: "Light Novel (KR)", value: "light-novel-kr" },
{ label: "Malaysian Novel", value: "malaysian-novel" },
{ label: "Published Novel (CN)", value: "published-novel-cn" },
{ label: "Published Novel (JP)", value: "published-novel-jp" },
{ label: "Published Novel (KR)", value: "published-novel-kr" },
{ label: "Published Novel (TH)", value: "published-novel-th" },
{ label: "Thai Novel", value: "thai-novel" },
{ label: "Vietnamese Novel", value: "vietnamese-novel" },
{ label: "Web Novel", value: "web-novel" },
{ label: "Webnovel", value: "webnovel" },
{ label: 'Chinese Novel', value: 'chinese-novel' },
{ label: 'Chinese Web Novel', value: 'chinese-web-novel' },
{ label: 'Filipino Novel', value: 'filipino-novel' },
{ label: 'Indonesia Novel', value: 'indonesia-novel' },
{ label: 'Korean Novel', value: 'korean-novel' },
{ label: 'Light Novel', value: 'light-novel' },
{ label: 'Light Novel (CN)', value: 'light-novel-cn' },
{ label: 'Light Novel (JP)', value: 'light-novel-jp' },
{ label: 'Light Novel (KR)', value: 'light-novel-kr' },
{ label: 'Malaysian Novel', value: 'malaysian-novel' },
{ label: 'Published Novel (CN)', value: 'published-novel-cn' },
{ label: 'Published Novel (JP)', value: 'published-novel-jp' },
{ label: 'Published Novel (KR)', value: 'published-novel-kr' },
{ label: 'Published Novel (TH)', value: 'published-novel-th' },
{ label: 'Thai Novel', value: 'thai-novel' },
{ label: 'Vietnamese Novel', value: 'vietnamese-novel' },
{ label: 'Web Novel', value: 'web-novel' },
{ label: 'Webnovel', value: 'webnovel' },
],
type: FilterTypes.CheckboxGroup,
},
genre: {
value: [],
label: "Genres",
label: 'Genres',
options: [
{ label: "Action", value: "action" },
{ label: "Adult", value: "adult" },
{ label: "Adventure", value: "adventure" },
{ label: "Celebrity", value: "celebrity" },
{ label: "Comedy", value: "comedy" },
{ label: "ction", value: "ction" },
{ label: "Drama", value: "drama" },
{ label: "Eastern", value: "eastern" },
{ label: "Ecchi", value: "ecchi" },
{ label: "Fantasy", value: "fantasy" },
{ label: "Game", value: "game" },
{ label: "Games", value: "games" },
{ label: "Gender Bender", value: "gender-bender" },
{ label: "Harem", value: "harem" },
{ label: "Historical", value: "historical" },
{ label: "Horror", value: "horror" },
{ label: "Isekai", value: "isekai" },
{ label: "Josei", value: "josei" },
{ label: "Life", value: "life" },
{ label: "LitRPG", value: "litrpg" },
{ label: "Magical Realism", value: "magical-realism" },
{ label: "Martial Arts", value: "martial-arts" },
{ label: "Mature", value: "mature" },
{ label: "Mecha", value: "mecha" },
{ label: "Mystery", value: "mystery" },
{ label: "Psychologic", value: "psychologic" },
{ label: "Psychological", value: "psychological" },
{ label: "Recarnation", value: "recarnation" },
{ label: "Reincarnation", value: "reincarnation" },
{ label: "Romance", value: "romance" },
{ label: "School", value: "school" },
{ 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: "Smut", value: "smut" },
{ label: "Sports", value: "sports" },
{ label: "Supernatural", value: "supernatural" },
{ label: "Tragedy", value: "tragedy" },
{ label: "Urban", value: "urban" },
{ label: "Urban Life", value: "urban-life" },
{ label: "ve names:N/A Genre:Romance", value: "ve-namesn-a-genreromance" },
{ label: "Wuxia", value: "wuxia" },
{ label: "Xianxia", value: "xianxia" },
{ label: "Xuanhuan", value: "xuanhuan" },
{ label: "Yaoi", value: "yaoi" },
{ label: "Yuri", value: "yuri" },
{ label: 'Action', value: 'action' },
{ label: 'Adult', value: 'adult' },
{ label: 'Adventure', value: 'adventure' },
{ label: 'Celebrity', value: 'celebrity' },
{ label: 'Comedy', value: 'comedy' },
{ label: 'ction', value: 'ction' },
{ label: 'Drama', value: 'drama' },
{ label: 'Eastern', value: 'eastern' },
{ label: 'Ecchi', value: 'ecchi' },
{ label: 'Fantasy', value: 'fantasy' },
{ label: 'Game', value: 'game' },
{ label: 'Games', value: 'games' },
{ label: 'Gender Bender', value: 'gender-bender' },
{ label: 'Harem', value: 'harem' },
{ label: 'Historical', value: 'historical' },
{ label: 'Horror', value: 'horror' },
{ label: 'Isekai', value: 'isekai' },
{ label: 'Josei', value: 'josei' },
{ label: 'Life', value: 'life' },
{ label: 'LitRPG', value: 'litrpg' },
{ label: 'Magical Realism', value: 'magical-realism' },
{ label: 'Martial Arts', value: 'martial-arts' },
{ label: 'Mature', value: 'mature' },
{ label: 'Mecha', value: 'mecha' },
{ label: 'Mystery', value: 'mystery' },
{ label: 'Psychologic', value: 'psychologic' },
{ label: 'Psychological', value: 'psychological' },
{ label: 'Recarnation', value: 'recarnation' },
{ label: 'Reincarnation', value: 'reincarnation' },
{ label: 'Romance', value: 'romance' },
{ label: 'School', value: 'school' },
{ 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: 'Smut', value: 'smut' },
{ label: 'Sports', value: 'sports' },
{ label: 'Supernatural', value: 'supernatural' },
{ label: 'Tragedy', value: 'tragedy' },
{ label: 'Urban', value: 'urban' },
{ label: 'Urban Life', value: 'urban-life' },
{
label: 've names:N/A Genre:Romance',
value: 've-namesn-a-genreromance',
},
{ label: 'Wuxia', value: 'wuxia' },
{ label: 'Xianxia', value: 'xianxia' },
{ label: 'Xuanhuan', value: 'xuanhuan' },
{ label: 'Yaoi', value: 'yaoi' },
{ label: 'Yuri', value: 'yuri' },
],
type: FilterTypes.CheckboxGroup,
},
+115 -97
View File
@@ -1,27 +1,27 @@
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 SakuraNovel implements Plugin.PluginBase {
id = "sakura.id";
name = "SakuraNovel";
icon = "src/id/sakuranovel/icon.png";
site = "https://sakuranovel.id/";
version = "1.0.1";
id = 'sakura.id';
name = 'SakuraNovel';
icon = 'src/id/sakuranovel/icon.png';
site = 'https://sakuranovel.id/';
version = '1.0.1';
parseNovels(loadedCheerio:CheerioAPI){
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".flexbox2-item").each((i,el) => {
loadedCheerio('.flexbox2-item').each((i, el) => {
const novelName = loadedCheerio(el)
.find(".flexbox2-title span")
.find('.flexbox2-title span')
.first()
.text();
const novelCover = loadedCheerio(el).find("img").attr("src");
const novelCover = loadedCheerio(el).find('img').attr('src');
const novelUrl = loadedCheerio(el)
.find(".flexbox2-content > a")
.attr("href");
.find('.flexbox2-content > a')
.attr('href');
if (!novelUrl) return;
@@ -37,17 +37,17 @@ class SakuraNovel implements Plugin.PluginBase {
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}advanced-search/page/${page}/?title&author&yearx`;
link += `&status=${filters.status.value}`
link += `&status=${filters.status.value}`;
link += `&type=${filters.type.value}`;
link += `&order=${filters.sort.value}`;
if (filters.lang.value.length)
link += filters.lang.value.map((i) => `&country[]=${i}`).join("");
link += filters.lang.value.map(i => `&country[]=${i}`).join('');
if (filters.genre.value.length)
link += filters.genre.value.map((i) => `&genre[]=${i}`).join("");
link += filters.genre.value.map(i => `&genre[]=${i}`).join('');
const result = await fetchApi(link);
const body = await result.text();
@@ -61,53 +61,67 @@ class SakuraNovel implements Plugin.PluginBase {
const body = await result.text();
const loadedCheerio = parseHTML(body);
loadedCheerio(".series-synops div").remove();
loadedCheerio('.series-synops div').remove();
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".series-title h2").text().trim() || "Untitled",
cover: loadedCheerio(".series-thumb img").attr("src"),
author: loadedCheerio(".series-infolist > li b:contains('Author') +").text().trim(),
status: loadedCheerio(".status").text().trim(),
summary: loadedCheerio(".series-synops").text().trim(),
chapters: []
name: loadedCheerio('.series-title h2').text().trim() || 'Untitled',
cover: loadedCheerio('.series-thumb img').attr('src'),
author: loadedCheerio(".series-infolist > li b:contains('Author') +")
.text()
.trim(),
status: loadedCheerio('.status').text().trim(),
summary: loadedCheerio('.series-synops').text().trim(),
chapters: [],
};
novel.genres = loadedCheerio(".series-genres")
.children("a")
novel.genres = loadedCheerio('.series-genres')
.children('a')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
.join(',');
const imageTitle = novel.cover?.split('/').pop()?.split('-').join(' ').split('.')[0];
const chapterTitle = novel.name.replace(/\(LN\)|\(WN\)/, "").split(',')[0].trim();
const imageTitle = novel.cover
?.split('/')
.pop()
?.split('-')
.join(' ')
.split('.')[0];
const chapterTitle = novel.name
.replace(/\(LN\)|\(WN\)/, '')
.split(',')[0]
.trim();
const chapter: Plugin.ChapterItem[] = [];
loadedCheerio(".series-flexright li").each((i,el) => {
loadedCheerio('.series-flexright li').each((i, el) => {
const chapterName = loadedCheerio(el)
.find("a span")
.find('a span')
.first()
.text()
.replace(chapterTitle, '')
.replace(imageTitle!, '')
.replace(/Bahasa Indonesia/, '')
.replace(/\s+/g, " ")
.replace(/\s+/g, ' ')
.trim();
const releaseDate = loadedCheerio(el)
.find(".date")
.find('.date')
.text()
.trim()
.split('/')
.map(x => Number(x));
const chapterUrl = loadedCheerio(el).find("a").attr("href");
const chapterUrl = loadedCheerio(el).find('a').attr('href');
if (!chapterUrl) return;
chapter.push({
name: chapterName,
releaseTime: new Date(releaseDate[2], releaseDate[1], releaseDate[0]).toISOString(),
releaseTime: new Date(
releaseDate[2],
releaseDate[1],
releaseDate[0],
).toISOString(),
path: chapterUrl.replace(this.site, ''),
});
});
@@ -124,15 +138,19 @@ class SakuraNovel implements Plugin.PluginBase {
const loadedCheerio = parseHTML(body);
const divi = loadedCheerio("div:contains('Daftar Isi') +")
.find("div:first")
.attr("class");
loadedCheerio(`.${divi}`).remove()
const chapterText = loadedCheerio("div:contains('Daftar Isi') +").html() || '';
.find('div:first')
.attr('class');
loadedCheerio(`.${divi}`).remove();
const chapterText =
loadedCheerio("div:contains('Daftar Isi') +").html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}page/${page}/?s=${searchTerm}`;
const result = await fetchApi(url);
const body = await result.text();
@@ -147,83 +165,83 @@ class SakuraNovel implements Plugin.PluginBase {
filters = {
status: {
value: "",
label: "Status",
value: '',
label: 'Status',
options: [
{ label: "All", value: "" },
{ label: "Ongoing", value: "ongoing" },
{ label: "Completed", value: "completed" },
{ label: 'All', value: '' },
{ label: 'Ongoing', value: 'ongoing' },
{ label: 'Completed', value: 'completed' },
],
type: FilterTypes.Picker,
},
type: {
value: "",
label: "Type",
value: '',
label: 'Type',
options: [
{ label: "All", value: "" },
{ label: "Web Novel", value: "Web+Novel" },
{ label: "Light Novel", value: "Light+Novel" },
{ label: 'All', value: '' },
{ label: 'Web Novel', value: 'Web+Novel' },
{ label: 'Light Novel', value: 'Light+Novel' },
],
type: FilterTypes.Picker,
},
sort: {
value: "rating",
label: "Order By",
value: 'rating',
label: 'Order By',
options: [
{ label: "A-Z", value: "title" },
{ label: "Z-A", value: "titlereverse" },
{ label: "Latest Update", value: "update" },
{ label: "Latest Added", value: "latest" },
{ label: "Popular", value: "popular" },
{ label: "Rating", value: "rating" },
{ label: 'A-Z', value: 'title' },
{ label: 'Z-A', value: 'titlereverse' },
{ label: 'Latest Update', value: 'update' },
{ label: 'Latest Added', value: 'latest' },
{ label: 'Popular', value: 'popular' },
{ label: 'Rating', value: 'rating' },
],
type: FilterTypes.Picker,
},
lang: {
value: ['china','jepang','korea','unknown'],
label: "Country",
value: ['china', 'jepang', 'korea', 'unknown'],
label: 'Country',
options: [
{ label: "China", value: "china" },
{ label: "Jepang", value: "jepang" },
{ label: "Korea", value: "korea" },
{ label: "Unknown", value: "unknown" },
{ label: 'China', value: 'china' },
{ label: 'Jepang', value: 'jepang' },
{ label: 'Korea', value: 'korea' },
{ label: 'Unknown', value: 'unknown' },
],
type: FilterTypes.CheckboxGroup,
},
genre: {
value: [],
label: "Genres",
label: 'Genres',
options: [
{ label: "Action", value: "action" },
{ label: "Adult", value: "adult" },
{ label: "Adventure", value: "adventure" },
{ 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: "Horror", value: "horror" },
{ label: "Josei", value: "josei" },
{ 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: "Shoujo", value: "shoujo" },
{ label: "Shounen", value: "shounen" },
{ label: "Slice of Life", value: "slice-of-life" },
{ label: "Smut", value: "smut" },
{ label: "Supernatural", value: "supernatural" },
{ label: "Tragedy", value: "tragedy" },
{ label: "Wuxia", value: "wuxia" },
{ label: "Xianxia", value: "xianxia" },
{ label: "Xuanhuan", value: "xuanhuan" }
{ label: 'Action', value: 'action' },
{ label: 'Adult', value: 'adult' },
{ label: 'Adventure', value: 'adventure' },
{ 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: 'Horror', value: 'horror' },
{ label: 'Josei', value: 'josei' },
{ 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: 'Shoujo', value: 'shoujo' },
{ label: 'Shounen', value: 'shounen' },
{ label: 'Slice of Life', value: 'slice-of-life' },
{ label: 'Smut', value: 'smut' },
{ label: 'Supernatural', value: 'supernatural' },
{ label: 'Tragedy', value: 'tragedy' },
{ label: 'Wuxia', value: 'wuxia' },
{ label: 'Xianxia', value: 'xianxia' },
{ label: 'Xuanhuan', value: 'xuanhuan' },
],
type: FilterTypes.CheckboxGroup,
},
+94 -102
View File
@@ -1,34 +1,34 @@
import { load, load as loadCheerio } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { defaultCover } from "@libs/defaultCover";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { NovelItem } from "../../test_web/static/js";
import { load, load as loadCheerio } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { defaultCover } from '@libs/defaultCover';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { NovelItem } from '../../test_web/static/js';
// const novelStatus = require('@libs/novelStatus');
// const isUrlAbsolute = require('@libs/isAbsoluteUrl');
// const parseDate = require('@libs/parseDate');
class Syosetu implements Plugin.PluginBase {
id = "yomou.syosetu";
name = "Syosetu";
icon = "src/jp/syosetu/icon.png";
site = "https://yomou.syosetu.com/";
novelPrefix = "https://ncode.syosetu.com";
version = "1.1.0";
id = 'yomou.syosetu';
name = 'Syosetu';
icon = 'src/jp/syosetu/icon.png';
site = 'https://yomou.syosetu.com/';
novelPrefix = 'https://ncode.syosetu.com';
version = '1.1.0';
headers = {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
};
searchUrl = (pagenum?: number, order?: string) => {
return `https://yomou.syosetu.com/search.php?order=${order || "hyoka"}${
return `https://yomou.syosetu.com/search.php?order=${order || 'hyoka'}${
pagenum !== undefined
? `&p=${pagenum <= 1 || pagenum > 100 ? "1" : pagenum}` // check if pagenum is between 1 and 100
: "" // if isn't don't set ?p
? `&p=${pagenum <= 1 || pagenum > 100 ? '1' : pagenum}` // check if pagenum is between 1 and 100
: '' // if isn't don't set ?p
}`;
};
async popularNovels(
pageNo: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
const getNovelsFromPage = async (pagenumber: number) => {
// load page
@@ -38,13 +38,9 @@ class Syosetu implements Plugin.PluginBase {
url += `rank/list/type/${filters.ranking.value}_${filters.modifier.value}/?p=${pagenumber}`;
} else {
url += `rank/${
filters.genre.value.length === 1
? "isekailist"
: "genrelist"
filters.genre.value.length === 1 ? 'isekailist' : 'genrelist'
}/type/${filters.ranking.value}_${filters.genre.value}${
filters.modifier.value === "total"
? ""
: `_${filters.modifier.value}`
filters.modifier.value === 'total' ? '' : `_${filters.modifier.value}`
}/?p=${pagenumber}`;
}
const html = await (await fetchApi(url)).text();
@@ -53,18 +49,13 @@ class Syosetu implements Plugin.PluginBase {
decodeEntities: false,
});
if (
parseInt(loadedCheerio(".is-current").html() || "1") !==
pagenumber
)
if (parseInt(loadedCheerio('.is-current').html() || '1') !== pagenumber)
return [];
const novels: NovelItem[] = [];
loadedCheerio(".c-card").each((_, e) => {
const anchor = loadedCheerio(e).find(
".p-ranklist-item__title a"
);
const url = anchor.attr("href");
loadedCheerio('.c-card').each((_, e) => {
const anchor = loadedCheerio(e).find('.p-ranklist-item__title a');
const url = anchor.attr('href');
if (!url) return;
const name = anchor.text();
const novel: NovelItem = {
@@ -81,40 +72,40 @@ class Syosetu implements Plugin.PluginBase {
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
let chapters: Plugin.ChapterItem[] = [];
const result = await fetch(this.novelPrefix + novelPath, { headers: this.headers });
const result = await fetch(this.novelPrefix + novelPath, {
headers: this.headers,
});
const body = await result.text();
const loadedCheerio = loadCheerio(body, { decodeEntities: false });
// create novel object
let novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".novel_title").text(),
author: loadedCheerio(".novel_writername")
.text()
.replace("作者:", ""),
name: loadedCheerio('.novel_title').text(),
author: loadedCheerio('.novel_writername').text().replace('作者:', ''),
cover: defaultCover,
};
// Get all the chapters
const cqGetChapters = loadedCheerio(".novel_sublist2");
const cqGetChapters = loadedCheerio('.novel_sublist2');
if (cqGetChapters.length !== 0) {
// has more than 1 chapter
novel.summary = loadedCheerio("#novel_ex")
novel.summary = loadedCheerio('#novel_ex')
.text()
.replace(/<\s*br.*?>/g, "\n");
.replace(/<\s*br.*?>/g, '\n');
cqGetChapters.each(function (i, e) {
const chapterA = loadedCheerio(this).find("a");
const chapterA = loadedCheerio(this).find('a');
const [chapterName, releaseDate, chapterUrl] = [
// set the variables
chapterA.text(),
loadedCheerio(this)
.find("dt") // get title
.find('dt') // get title
.text() // get text
.replace(/(.)/g, "") // remove "(edited)" mark
.replace(/(.)/g, '') // remove "(edited)" mark
.trim(), // trim spaces
chapterA.attr("href"),
chapterA.attr('href'),
];
if(chapterUrl) {
if (chapterUrl) {
chapters.push({
name: chapterName,
releaseTime: releaseDate,
@@ -130,27 +121,26 @@ class Syosetu implements Plugin.PluginBase {
*/
// get summary for oneshot chapter
const nameResult = await fetch(
this.searchUrl() + `&word=${novel.name}`,
{ headers: this.headers }
);
const nameResult = await fetch(this.searchUrl() + `&word=${novel.name}`, {
headers: this.headers,
});
const nameBody = await nameResult.text();
const summaryQuery = loadCheerio(nameBody, {
decodeEntities: false,
});
const foundText = summaryQuery(".searchkekka_box")
const foundText = summaryQuery('.searchkekka_box')
.first()
.find(".ex")
.find('.ex')
.text()
.replace(/\s{2,}/g, "\n");
.replace(/\s{2,}/g, '\n');
novel.summary = foundText;
// add single chapter
chapters.push({
name: "Oneshot",
releaseTime: loadedCheerio("head")
name: 'Oneshot',
releaseTime: loadedCheerio('head')
.find("meta[name='WWWC']")
.attr("content"), // get date from metadata
.attr('content'), // get date from metadata
path: novelPath, // set chapterUrl to oneshot so that chapterScraper knows it's a one-shot,
});
}
@@ -158,7 +148,9 @@ class Syosetu implements Plugin.PluginBase {
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetch(this.novelPrefix + chapterPath, { headers: this.headers });
const result = await fetch(this.novelPrefix + chapterPath, {
headers: this.headers,
});
const body = await result.text();
// create cheerioQuery
@@ -167,13 +159,13 @@ class Syosetu implements Plugin.PluginBase {
});
let chapterText =
cheerioQuery("#novel_honbun") // get chapter text
.html() || "";
cheerioQuery('#novel_honbun') // get chapter text
.html() || '';
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo: number
pageNo: number,
): Promise<Plugin.NovelItem[]> {
let novels = [];
@@ -188,14 +180,14 @@ class Syosetu implements Plugin.PluginBase {
let pageNovels: Plugin.NovelItem[] = [];
// find class=searchkekka_box
cheerioQuery(".searchkekka_box").each((i, e) => {
cheerioQuery('.searchkekka_box').each((i, e) => {
// get div with link and name
const novelDIV = cheerioQuery(e).find(".novel_h");
const novelDIV = cheerioQuery(e).find('.novel_h');
// get link element
const novelA = novelDIV.children()[0];
// add new novel to array
const novelPath = novelA.attribs.href.replace(this.novelPrefix, '');
if(novelPath){
if (novelPath) {
pageNovels.push({
name: novelDIV.text(), // get the name
path: novelPath, // get last part of the link
@@ -234,57 +226,57 @@ class Syosetu implements Plugin.PluginBase {
filters = {
ranking: {
type: FilterTypes.Picker,
label: "Ranked by",
label: 'Ranked by',
options: [
{ label: "日間", value: "daily" },
{ label: "週間", value: "weekly" },
{ label: "月間", value: "monthly" },
{ label: "四半期", value: "quarter" },
{ label: "年間", value: "yearly" },
{ label: "累計", value: "total" },
{ label: '日間', value: 'daily' },
{ label: '週間', value: 'weekly' },
{ label: '月間', value: 'monthly' },
{ label: '四半期', value: 'quarter' },
{ label: '年間', value: 'yearly' },
{ label: '累計', value: 'total' },
],
value: "total",
value: 'total',
},
genre: {
type: FilterTypes.Picker,
label: "Ranking Genre",
label: 'Ranking Genre',
options: [
{ label: "総ジャンル", value: "" },
{ label: "異世界転生/転移〔恋愛〕〕", value: "1" },
{ label: "異世界転生/転移〔ファンタジー〕", value: "2" },
{ label: "異世界転生/転移〔文芸・SF・その他〕", value: "o" },
{ label: "異世界〔恋愛〕", value: "101" },
{ label: "現実世界〔恋愛〕", value: "102" },
{ label: "ハイファンタジー〔ファンタジー〕", value: "201" },
{ label: "ローファンタジー〔ファンタジー〕", value: "202" },
{ label: "純文学〔文芸〕", value: "301" },
{ label: "ヒューマンドラマ〔文芸〕", value: "302" },
{ label: "歴史〔文芸〕", value: "303" },
{ label: "推理〔文芸〕", value: "304" },
{ label: "ホラー〔文芸〕", value: "305" },
{ label: "アクション〔文芸〕", value: "306" },
{ label: "コメディー〔文芸〕", value: "307" },
{ label: "VRゲーム〔SF〕", value: "401" },
{ label: "宇宙〔SF〕", value: "402" },
{ label: "空想科学〔SF〕", value: "403" },
{ label: "パニック〔SF〕", value: "404" },
{ label: "童話〔その他〕", value: "9901" },
{ label: "詩〔その他〕", value: "9902" },
{ label: "エッセイ〔その他〕", value: "9903" },
{ label: "その他〔その他〕", value: "9999" },
{ label: '総ジャンル', value: '' },
{ label: '異世界転生/転移〔恋愛〕〕', value: '1' },
{ label: '異世界転生/転移〔ファンタジー〕', value: '2' },
{ label: '異世界転生/転移〔文芸・SF・その他〕', value: 'o' },
{ label: '異世界〔恋愛〕', value: '101' },
{ label: '現実世界〔恋愛〕', value: '102' },
{ label: 'ハイファンタジー〔ファンタジー〕', value: '201' },
{ label: 'ローファンタジー〔ファンタジー〕', value: '202' },
{ label: '純文学〔文芸〕', value: '301' },
{ label: 'ヒューマンドラマ〔文芸〕', value: '302' },
{ label: '歴史〔文芸〕', value: '303' },
{ label: '推理〔文芸〕', value: '304' },
{ label: 'ホラー〔文芸〕', value: '305' },
{ label: 'アクション〔文芸〕', value: '306' },
{ label: 'コメディー〔文芸〕', value: '307' },
{ label: 'VRゲーム〔SF〕', value: '401' },
{ label: '宇宙〔SF〕', value: '402' },
{ label: '空想科学〔SF〕', value: '403' },
{ label: 'パニック〔SF〕', value: '404' },
{ label: '童話〔その他〕', value: '9901' },
{ label: '詩〔その他〕', value: '9902' },
{ label: 'エッセイ〔その他〕', value: '9903' },
{ label: 'その他〔その他〕', value: '9999' },
],
value: "",
value: '',
},
modifier: {
type: FilterTypes.Picker,
label: "Modifier",
label: 'Modifier',
options: [
{ label: "すべて", value: "total" },
{ label: "連載中", value: "r" },
{ label: "完結済", value: "er" },
{ label: "短編", value: "t" },
{ label: 'すべて', value: 'total' },
{ label: '連載中', value: 'r' },
{ label: '完結済', value: 'er' },
{ label: '短編', value: 't' },
],
value: "total",
value: 'total',
},
} satisfies Filters;
}
+55 -52
View File
@@ -1,35 +1,35 @@
import { fetchFile, fetchApi } from "@libs/fetch";
import { Filters } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import { load as parseHTML } from "cheerio";
import qs from "qs";
import { fetchFile, fetchApi } from '@libs/fetch';
import { Filters } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
import { load as parseHTML } from 'cheerio';
import qs from 'qs';
class Agitoon implements Plugin.PluginBase {
id = "agit.xyz";
name = "Agitoon";
icon = "src/kr/agitoon/agit.png";
site = "https://agit501.xyz";
version = "1.0.0";
id = 'agit.xyz';
name = 'Agitoon';
icon = 'src/kr/agitoon/agit.png';
site = 'https://agit501.xyz';
version = '1.0.0';
async popularNovels(
pageNo: number,
{ filters, showLatestNovels }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
const res = await fetchApi(this.site + "/novel/index.update.php", {
const res = await fetchApi(this.site + '/novel/index.update.php', {
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
method: "POST",
method: 'POST',
body: qs.stringify({
mode: "get_data_novel_list_p",
novel_menu: showLatestNovels ? "1" : "3",
mode: 'get_data_novel_list_p',
novel_menu: showLatestNovels ? '1' : '3',
np_day: new Date().getDay(),
np_rank: "1",
np_distributor: "0",
np_genre: "00",
np_order: "1",
np_genre_ex_1: "00",
np_genre_ex_2: "00",
np_rank: '1',
np_distributor: '0',
np_genre: '00',
np_order: '1',
np_genre_ex_1: '00',
np_genre_ex_2: '00',
list_limit: 20 * (pageNo - 1),
is_query_first: pageNo == 1,
}),
@@ -37,10 +37,10 @@ class Agitoon implements Plugin.PluginBase {
const resJson = (await res.json()) as response;
const novels: Plugin.NovelItem[] = [];
resJson?.list?.forEach((novel) =>
resJson?.list?.forEach(novel =>
novels.push({
name: novel.wr_subject,
cover: this.site + novel.np_dir + "/thumbnail/" + novel.np_thumbnail,
cover: this.site + novel.np_dir + '/thumbnail/' + novel.np_thumbnail,
path: novel.wr_id,
}),
);
@@ -49,50 +49,51 @@ class Agitoon implements Plugin.PluginBase {
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.resolveUrl(novelPath, true))
.then((res) => res.text());
const result = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(result, { decodeEntities: false });
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio("h5.pt-2").text(),
cover: this.site + loadedCheerio("div.col-5.pr-0.pl-0 img").attr("src"),
summary: loadedCheerio(".pt-1.mt-1.pb-1.mb-1").text(),
name: loadedCheerio('h5.pt-2').text(),
cover: this.site + loadedCheerio('div.col-5.pr-0.pl-0 img').attr('src'),
summary: loadedCheerio('.pt-1.mt-1.pb-1.mb-1').text(),
};
novel.author = loadedCheerio(".post-item-list-cate-v")
novel.author = loadedCheerio('.post-item-list-cate-v')
.first()
.text()
.split(" : ")[1];
.split(' : ')[1];
const genres = loadedCheerio(".col-7 > .post-item-list-cate > span")
const genres = loadedCheerio('.col-7 > .post-item-list-cate > span')
.map((index, element) => loadedCheerio(element).text().trim())
.get();
if (genres.length) {
novel.genres = genres.join(", ");
novel.genres = genres.join(', ');
}
const chapters: Plugin.ChapterItem[] = [];
const res = await fetchApi(this.site + "/novel/list.update.php", {
const res = await fetchApi(this.site + '/novel/list.update.php', {
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
method: "POST",
method: 'POST',
body: qs.stringify({
mode: "get_data_novel_list_c",
mode: 'get_data_novel_list_c',
wr_id_p: novelPath,
page_no: "1",
cnt_list: "10000",
order_type: "Asc",
page_no: '1',
cnt_list: '10000',
order_type: 'Asc',
}),
});
const resJson = (await res.json()) as responseBook;
resJson?.list?.forEach((chapter) =>
resJson?.list?.forEach(chapter =>
chapters.push({
name: chapter.wr_subject,
path: chapter.wr_id + "/2",
path: chapter.wr_id + '/2',
releaseTime: chapter.wr_datetime,
}),
);
@@ -102,28 +103,29 @@ class Agitoon implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.resolveUrl(chapterPath))
.then((res) => res.text());
const result = await fetchApi(this.resolveUrl(chapterPath)).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(result);
let content = loadedCheerio("#id_wr_content").html() || "";
let content = loadedCheerio('#id_wr_content').html() || '';
// gets rid of the popup thingy
content = content
.replace("팝업메뉴는 빈공간을 더치하거나 스크룰시 사라집니다", "")
.replace('팝업메뉴는 빈공간을 더치하거나 스크룰시 사라집니다', '')
.trim();
return content;
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const rawResults = await fetchApi("https://agit501.xyz/novel/search.php", {
const rawResults = await fetchApi('https://agit501.xyz/novel/search.php', {
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
method: "POST",
method: 'POST',
body: qs.stringify({
mode: "get_data_novel_list_p_sch",
mode: 'get_data_novel_list_p_sch',
search_novel: searchTerm,
list_limit: 0,
}),
@@ -131,10 +133,11 @@ class Agitoon implements Plugin.PluginBase {
const resJson = (await rawResults.json()) as response;
const novels: Plugin.NovelItem[] = [];
resJson?.list?.forEach((novel) =>
resJson?.list?.forEach(novel =>
novels.push({
name: novel.wr_subject,
cover: this.site + "/" + novel.np_dir + "/thumbnail/" + novel.np_thumbnail,
cover:
this.site + '/' + novel.np_dir + '/thumbnail/' + novel.np_thumbnail,
path: novel.wr_id,
}),
);
@@ -144,7 +147,7 @@ class Agitoon implements Plugin.PluginBase {
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + (isNovel ? "/novel/list/" : "/novel/view/") + path;
this.site + (isNovel ? '/novel/list/' : '/novel/view/') + path;
}
export default new Agitoon();
+146 -122
View File
@@ -1,75 +1,78 @@
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { load as parseHTML } from "cheerio";
import { NovelStatus } from "@libs/novelStatus";
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { load as parseHTML } from 'cheerio';
import { NovelStatus } from '@libs/novelStatus';
class NovelkiPL implements Plugin.PluginBase {
id = "novelki.pl";
name = "Novelki";
icon = "src/pl/novelki/icon.png";
site = "https://novelki.pl";
version = "1.0.3";
id = 'novelki.pl';
name = 'Novelki';
icon = 'src/pl/novelki/icon.png';
site = 'https://novelki.pl';
version = '1.0.3';
async popularNovels(
page: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
//So far is working, If the author of the site changes it, it will have to be changed.
let link = this.site + "/projekty?filter=t"
let link = this.site + '/projekty?filter=t';
link += '&genre=' + filters.genres.value;
link += '&status=' + filters.status.value;
link += '&type=' + filters.type.value;
link += '&page=' + page;
const body = await fetchApi(link).then((res) => {
if(res.url == `${this.site}/guest`) throw new Error("Failed to load novels (open in web view and login)")
return res.text()
const body = await fetchApi(link).then(res => {
if (res.url == `${this.site}/guest`)
throw new Error('Failed to load novels (open in web view and login)');
return res.text();
});
const loadedCheerio = parseHTML(body);
const load = loadedCheerio("#projects > div")
const load = loadedCheerio('#projects > div');
//if(load.length == 0) throw new Error("Failed to load page (open in web view and login)");
const novels: Plugin.NovelItem[] = load
.map((index, element) => ({
name: loadedCheerio(element).find(".card-title").attr('title') as string,
cover: this.site+loadedCheerio(element).find(".card-img-top").attr("src"),
path: loadedCheerio(element).find("a").attr("href") || "",
name: loadedCheerio(element)
.find('.card-title')
.attr('title') as string,
cover:
this.site + loadedCheerio(element).find('.card-img-top').attr('src'),
path: loadedCheerio(element).find('a').attr('href') || '',
}))
.get()
.filter((novel) => novel.name && novel.path);
.filter(novel => novel.name && novel.path);
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then((r) => r.text());
const body = await fetchApi(this.site + novelPath).then(r => r.text());
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: ''
name: '',
};
loadedCheerio("p.h5").each((i, e) => {
const text = loadedCheerio(e).text().trim()
if(text.includes('Autor:')) novel.author = text.split(':')[1].trim()
if(text.includes("Status projektu:")) {
loadedCheerio('p.h5').each((i, e) => {
const text = loadedCheerio(e).text().trim();
if (text.includes('Autor:')) novel.author = text.split(':')[1].trim();
if (text.includes('Status projektu:')) {
switch (`${text.split(':')[1].trim()}`.toLowerCase()) {
case "wstrzymany":
case 'wstrzymany':
novel.status = NovelStatus.OnHiatus;
break;
case "zakończony":
case 'zakończony':
novel.status = NovelStatus.Completed;
break;
case "aktywny":
case 'aktywny':
novel.status = NovelStatus.Ongoing;
break;
case "porzucony":
case 'porzucony':
novel.status = NovelStatus.Cancelled;
break;
case "zlicencjonowany":
case 'zlicencjonowany':
novel.status = NovelStatus.Licensed;
break;
default:
@@ -77,65 +80,88 @@ class NovelkiPL implements Plugin.PluginBase {
break;
}
}
});
novel.name = loadedCheerio('div.col-sm-12.col-md-6.col-lg-8.mb-5')
.find('h3')
.text()
.trim();
novel.cover = this.site + loadedCheerio('.img-fluid').attr('src'); // TODO: return not only undefined
})
novel.name = loadedCheerio("div.col-sm-12.col-md-6.col-lg-8.mb-5").find("h3").text().trim();
novel.cover = this.site+loadedCheerio(".img-fluid").attr("src"); // TODO: return not only undefined
novel.genres = loadedCheerio('span.badge')
.map((i, e) => loadedCheerio(e).text())
.get()
.join(', ');
novel.genres = loadedCheerio("span.badge").map((i, e) => loadedCheerio(e).text()).get().join(", ")
novel.summary = loadedCheerio('.h5:contains("Opis:")').next("p").next("p").text().trim()
novel.summary = loadedCheerio('.h5:contains("Opis:")')
.next('p')
.next('p')
.text()
.trim();
//TODO: Dodać tłumacza danej serii :D
let chapters: Plugin.ChapterItem[] = [];
let chaptersList = loadedCheerio(".chapters > .col-md-3 > div").get()
let chaptersList = loadedCheerio('.chapters > .col-md-3 > div').get();
chaptersList.forEach((e, i) => {
let urlChapters = loadedCheerio(e).find("a").attr("href") || "";
let urlChapters = loadedCheerio(e).find('a').attr('href') || '';
const chapter: Plugin.ChapterItem = {
name: loadedCheerio(e).find("a")?.text().trim(),
name: loadedCheerio(e).find('a')?.text().trim(),
path: urlChapters,
releaseTime: loadedCheerio(e).find(".card-footer > span").text().trim().split('-').reverse().join('-'),
chapterNumber: chaptersList.length-i,
releaseTime: loadedCheerio(e)
.find('.card-footer > span')
.text()
.trim()
.split('-')
.reverse()
.join('-'),
chapterNumber: chaptersList.length - i,
};
chapters.push(chapter);
})
});
novel.chapters = chapters.reverse();
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
var pattern = /\/projekty\/([^\/]+)\/([^\/]+)/;
let codeChapter = pattern.exec(chapterPath) || "";
const body = await fetchApi(`${this.site}/api/reader/chapters/${codeChapter[2]}`).then((res) => {
if(res.url == `${this.site}/guest`) throw new Error("Failed to load chapter (open in web view and login)")
return res.json()
let codeChapter = pattern.exec(chapterPath) || '';
const body = await fetchApi(
`${this.site}/api/reader/chapters/${codeChapter[2]}`,
).then(res => {
if (res.url == `${this.site}/guest`)
throw new Error('Failed to load chapter (open in web view and login)');
return res.json();
});
const chapterText = body.data.content
const chapterText = body.data.content;
return chapterText;
}
async searchNovels(
searchTerm: string,
page: number
page: number,
): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(`${this.site}/projekty?filter=t&title=${searchTerm}+&page=${page}`).then((res) => {
if(res.url == `${this.site}/guest`) throw new Error("Failed to search novels (open in web view and login)")
return res.text()
const body = await fetchApi(
`${this.site}/projekty?filter=t&title=${searchTerm}+&page=${page}`,
).then(res => {
if (res.url == `${this.site}/guest`)
throw new Error('Failed to search novels (open in web view and login)');
return res.text();
});
const loadedCheerio = parseHTML(body);
let novels: Plugin.NovelItem[] = loadedCheerio("#projects > div")
let novels: Plugin.NovelItem[] = loadedCheerio('#projects > div')
.map((index, element) => ({
name: loadedCheerio(element).find(".card-title").attr('title') as string,
cover: this.site+loadedCheerio(element).find(".card-img-top").attr("src"),
path: loadedCheerio(element).find("a").attr("href") || "",
name: loadedCheerio(element)
.find('.card-title')
.attr('title') as string,
cover:
this.site + loadedCheerio(element).find('.card-img-top').attr('src'),
path: loadedCheerio(element).find('a').attr('href') || '',
}))
.get()
.filter((novel) => novel.name && novel.path)
.filter(novel => novel.name && novel.path);
return novels;
}
@@ -146,80 +172,78 @@ class NovelkiPL implements Plugin.PluginBase {
}
filters = {
genres: {
value: "",
label: "Genre",
value: '',
label: 'Genre',
options: [
{ label: "Wybierz gatunek", value: "" },
{ label: "Adaptacja do anime", value: "35" },
{ label: "Adaptacja do dramy", value: "40" },
{ label: "Adaptacja do mangi", value: "36" },
{ label: "Akcja", value: "1" },
{ label: "Boys Love", value: "22" },
{ label: "Bromans", value: "33" },
{ label: "Dojrzały", value: "11" },
{ label: "Dramat", value: "10" },
{ label: "Ecchi", value: "29" },
{ label: "Fantasy", value: "2" },
{ label: "Gender Bender", value: "28" },
{ label: "Girls Love", value: "23" },
{ label: "Harem", value: "8" },
{ label: "Hentai", value: "43" },
{ label: "Historyczny", value: "30" },
{ label: "Horror", value: "15" },
{ label: "Isekai", value: "44" },
{ label: "Josei", value: "27" },
{ label: "Komedia", value: "3" },
{ label: "Mecha", value: "16" },
{ label: "Nadprzyrodzone", value: "9" },
{ label: "Okruchy życia", value: "18" },
{ label: "Oneshot", value: "41" },
{ label: "Parodia", value: "19" },
{ label: "Przygodowy", value: "4" },
{ label: "Psychologiczny", value: "12" },
{ label: "Reinkarnacja", value: "42" },
{ label: "Romans", value: "7" },
{ label: "RPG", value: "17" },
{ label: "Sci-fi", value: "21" },
{ label: "Seinen", value: "26" },
{ label: "Shoujo", value: "25" },
{ label: "Shounen", value: "24" },
{ label: "Smut", value: "31" },
{ label: "Sport", value: "32" },
{ label: "Świat gry", value: "5" },
{ label: "Szkolne życie", value: "20" },
{ label: "Sztuki walki", value: "6" },
{ label: "Tajemnica", value: "14" },
{ label: "Tragedia", value: "13" },
{ label: 'Wybierz gatunek', value: '' },
{ label: 'Adaptacja do anime', value: '35' },
{ label: 'Adaptacja do dramy', value: '40' },
{ label: 'Adaptacja do mangi', value: '36' },
{ label: 'Akcja', value: '1' },
{ label: 'Boys Love', value: '22' },
{ label: 'Bromans', value: '33' },
{ label: 'Dojrzały', value: '11' },
{ label: 'Dramat', value: '10' },
{ label: 'Ecchi', value: '29' },
{ label: 'Fantasy', value: '2' },
{ label: 'Gender Bender', value: '28' },
{ label: 'Girls Love', value: '23' },
{ label: 'Harem', value: '8' },
{ label: 'Hentai', value: '43' },
{ label: 'Historyczny', value: '30' },
{ label: 'Horror', value: '15' },
{ label: 'Isekai', value: '44' },
{ label: 'Josei', value: '27' },
{ label: 'Komedia', value: '3' },
{ label: 'Mecha', value: '16' },
{ label: 'Nadprzyrodzone', value: '9' },
{ label: 'Okruchy życia', value: '18' },
{ label: 'Oneshot', value: '41' },
{ label: 'Parodia', value: '19' },
{ label: 'Przygodowy', value: '4' },
{ label: 'Psychologiczny', value: '12' },
{ label: 'Reinkarnacja', value: '42' },
{ label: 'Romans', value: '7' },
{ label: 'RPG', value: '17' },
{ label: 'Sci-fi', value: '21' },
{ label: 'Seinen', value: '26' },
{ label: 'Shoujo', value: '25' },
{ label: 'Shounen', value: '24' },
{ label: 'Smut', value: '31' },
{ label: 'Sport', value: '32' },
{ label: 'Świat gry', value: '5' },
{ label: 'Szkolne życie', value: '20' },
{ label: 'Sztuki walki', value: '6' },
{ label: 'Tajemnica', value: '14' },
{ label: 'Tragedia', value: '13' },
],
type: FilterTypes.Picker,
},
status: {
label: "Status",
value: "",
label: 'Status',
value: '',
options: [
{ label: "Wybierz status", value: "" },
{ label: "Zakończony", value: "0" },
{ label: "Aktywny", value: "1" },
{ label: "Porzucony", value: "2" },
{ label: "Wstrzymany", value: "3" },
{ label: "Zlicencjonowany", value: "4" },
{ label: 'Wybierz status', value: '' },
{ label: 'Zakończony', value: '0' },
{ label: 'Aktywny', value: '1' },
{ label: 'Porzucony', value: '2' },
{ label: 'Wstrzymany', value: '3' },
{ label: 'Zlicencjonowany', value: '4' },
],
type: FilterTypes.Picker,
},
type: {
label: "Type",
value: "",
label: 'Type',
value: '',
options: [
{ label: "Wybierz typ", value: "" },
{ label: "Nie zdefiniowano", value: "0" },
{ label: "Light Novel", value: "1" },
{ label: "Wuxia", value: "2" },
{ label: "Xianxia", value: "3" },
{ label: "Web Novel", value: "4" },
{ label: "Autorska", value: "5" },
{ label: "Inne", value: "6" },
{ label: 'Wybierz typ', value: '' },
{ label: 'Nie zdefiniowano', value: '0' },
{ label: 'Light Novel', value: '1' },
{ label: 'Wuxia', value: '2' },
{ label: 'Xianxia', value: '3' },
{ label: 'Web Novel', value: '4' },
{ label: 'Autorska', value: '5' },
{ label: 'Inne', value: '6' },
],
type: FilterTypes.Picker,
},
+152 -163
View File
@@ -1,46 +1,46 @@
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 LightNovelBrasil implements Plugin.PluginBase {
id = "lightnovelbrasil";
name = "Light Novel Brasil";
icon = "multisrc/wpmangastream/icons/lightnovelbrasil.png";
site = "https://lightnovelbrasil.com/";
version = "1.0.0";
id = 'lightnovelbrasil';
name = 'Light Novel Brasil';
icon = 'multisrc/wpmangastream/icons/lightnovelbrasil.png';
site = 'https://lightnovelbrasil.com/';
version = '1.0.0';
async popularNovels(
pageNo: number,
pageNo: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}series/?page=${pageNo}`;
if (filters.genres.value.length)
if (filters.genres.value.length)
link += filters.genres.value.map(i => `&genre[]=${i}`).join('');
if (filters.type.value.length)
if (filters.type.value.length)
link += filters.type.value.map(i => `&lang[]=${i}`).join('');
link += '&status=' + (filters?.status ? filters.status : '');
link += '&order=' + (filters?.order ? filters.order : 'popular');
const headers = new Headers();
const headers = new Headers();
const body = await fetchApi(link, { headers }).then((result) =>
const body = await fetchApi(link, { headers }).then(result =>
result.text(),
);
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio("article.bs").each(function () {
const novelName = loadedCheerio(this).find(".ntitle").text().trim();
let image = loadedCheerio(this).find("img");
loadedCheerio('article.bs').each(function () {
const novelName = loadedCheerio(this).find('.ntitle').text().trim();
let image = loadedCheerio(this).find('img');
const novelCover = image.attr('data-src') || image.attr('src');
const novelUrl = loadedCheerio(this).find('a').attr('href');
if (!novelUrl) return;
@@ -69,60 +69,49 @@ class LightNovelBrasil implements Plugin.PluginBase {
chapters: [],
};
novel.name = loadedCheerio('.entry-title').text();
novel.cover =
novel.cover =
loadedCheerio("img.wp-post-image").attr("data-src") ||
loadedCheerio('img.wp-post-image').attr('data-src') ||
loadedCheerio('img.wp-post-image').attr('src');
loadedCheerio("div.spe > span").each(function () {
const detailName = loadedCheerio(this).find("b").text().trim();
const detail = loadedCheerio(this)
.find("b")
.remove()
.end()
.text()
loadedCheerio('div.spe > span').each(function () {
const detailName = loadedCheerio(this).find('b').text().trim();
const detail = loadedCheerio(this).find('b').remove().end().text().trim();
switch (detailName) {
switch (detailName) {
case "Autor:":
case 'Autor:':
case 'Author:':
novel.author = detail;
break;
break;
case 'Status:':
novel.status = detail;
break;
}
});
novel.genres = loadedCheerio(".genxed")
.text()
.trim()
novel.genres = loadedCheerio('.genxed').text().trim().replace(/\s/g, ',');
loadedCheerio('div[itemprop="description"] h3,p.a,strong').remove();
novel.summary = loadedCheerio('div[itemprop="description"]')
novel.summary = loadedCheerio('div[itemprop="description"]')
.find("br")
.find('br')
.replaceWith('\n')
.end()
.text();
let chapter: Plugin.ChapterItem[] = [];
loadedCheerio(".eplister")
loadedCheerio('.eplister')
.find('li')
.each(function () {
const chapterName =
const chapterName =
loadedCheerio(this).find(".epl-num").text() +
" - " +
loadedCheerio(this).find('.epl-num').text() +
' - ' +
loadedCheerio(this).find('.epl-title').text();
const releaseDate = loadedCheerio(this)
.find(".epl-date")
.text()
const releaseDate = loadedCheerio(this).find('.epl-date').text().trim();
const chapterUrl = loadedCheerio(this).find('a').attr('href');
if (!chapterUrl) return;
@@ -144,14 +133,14 @@ class LightNovelBrasil implements Plugin.PluginBase {
const loadedCheerio = parseHTML(body);
let chapterText = loadedCheerio('.epcontent').html() || '';
return chapterText;
}
async searchNovels(
searchTerm: string,
searchTerm: string,
pageNo?: number | undefined,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}?s=${searchTerm}`;
const headers = new Headers();
@@ -162,10 +151,10 @@ class LightNovelBrasil implements Plugin.PluginBase {
const novels: Plugin.NovelItem[] = [];
loadedCheerio("article.bs").each(function () {
const novelName = loadedCheerio(this).find(".ntitle").text().trim();
const novelCover = loadedCheerio(this).find("img").attr("src");
loadedCheerio('article.bs').each(function () {
const novelName = loadedCheerio(this).find('.ntitle').text().trim();
const novelCover = loadedCheerio(this).find('img').attr('src');
const novelUrl = loadedCheerio(this).find('a').attr('href');
if (!novelUrl) return;
novels.push({
@@ -183,227 +172,227 @@ class LightNovelBrasil implements Plugin.PluginBase {
filters = {
order: {
order: {
label: "Sort By",
label: 'Sort By',
value: '',
options: [
options: [
{ label: 'Default', value: '' },
{ label: 'A-Z', value: 'title' },
{ label: 'Z-A', value: 'titlereverse' },
{ label: 'Latest Update', value: 'update' },
{ label: 'Latest Added', value: 'latest' },
{ label: 'Popular', value: 'popular' },
],
type: FilterTypes.Picker,
},
status: {
status: {
label: "Status",
label: 'Status',
value: '',
options: [
options: [
{ label: 'All', value: '' },
{ label: 'Ongoing', value: 'ongoing' },
{ label: 'Hiatus', value: 'hiatus' },
{ label: 'Completed', value: 'completed' },
],
type: FilterTypes.Picker,
},
type: {
type: {
label: 'Type',
value: [],
options: [
options: [
{ label: 'Chinese novel', value: 'chinese-novel' },
{ label: 'habyeol', value: 'habyeol' },
{ label: 'korean novel', value: 'korean-novel' },
{ label: 'Web Novel', value: 'web-novel' },
{ label: '삼심', value: '%ec%82%bc%ec%8b%ac' },
{ label: '호곡', value: '%ed%98%b8%ea%b3%a1' },
],
type: FilterTypes.CheckboxGroup,
},
genres: {
value: [],
value: [],
label: 'Genres',
options: [
options: [
{ label: 'A.I', value: 'a.i' },
{ label: 'Academy', value: 'academy' },
{ label: 'Action', value: 'action' },
{ label: 'Adult', value: 'adult' },
{ label: 'Adventure', value: 'adventure' },
{ label: 'Alternative History', value: 'alternative-history' },
{ label: 'Another World', value: 'another-world' },
{ label: 'Apocalypse', value: 'apocalypse' },
{ label: 'Bromance', value: 'bromance' },
{ label: 'Comedy', value: 'comedy' },
{ label: 'Dark fantasy', value: 'dark-fantasy' },
{ label: 'Demons', value: 'demons' },
{ label: 'Drama', value: 'drama' },
{ label: 'Dystopia', value: 'dystopia' },
{ label: 'Ecchi', value: 'ecchi' },
{ label: 'Entertainment', value: 'entertainment' },
{ label: 'Exhaustion', value: 'exhaustion' },
{ label: 'Fanfiction', value: 'fanfiction' },
{ label: 'fantasy', value: 'fantasy' },
{ label: 'finance', value: 'finance' },
{ label: 'Full color', value: 'full-color' },
{ label: 'Game', value: 'game' },
{ label: 'Gender Bender', value: 'gender-bender' },
{ label: 'Genius', value: 'genius' },
{ label: 'Harem', value: 'harem' },
{ label: 'Hero', value: 'hero' },
{ label: 'Historical', value: 'historical' },
{ label: 'Hunter', value: 'hunter' },
{ label: 'korean novel', value: 'korean-novel' },
{ label: 'Light Novel', value: 'light-novel' },
{
{
label: "List Adventure Manga Genres",
label: 'List Adventure Manga Genres',
value: 'list-adventure-manga-genres',
},
{ label: 'Long Strip', value: 'long-strip' },
{ label: 'Love comedy', value: 'love-comedy' },
{ label: 'magic', value: 'magic' },
{ label: 'Manhua', value: 'manhua' },
{ label: 'Martial Arts', value: 'martial-arts' },
{ label: 'Mature', value: 'mature' },
{ label: 'Medieval', value: 'medieval' },
{ label: 'Misunderstanding', value: 'misunderstanding' },
{ label: 'Modern', value: 'modern' },
{ label: 'modern fantasy', value: 'modern-fantasy' },
{ label: 'music', value: 'music' },
{ label: 'Mystery', value: 'mystery' },
{ label: 'Necromancy', value: 'necromancy' },
{ label: 'No Romance', value: 'no-romance' },
{ label: 'NTL', value: 'ntl' },
{ label: 'o', value: 'o' },
{ label: 'Obsession', value: 'obsession' },
{ label: 'Politics', value: 'politics' },
{ label: 'Possession', value: 'possession' },
{ label: 'Programming', value: 'programming' },
{ label: 'Psychological', value: 'psychological' },
{ label: 'Pure Love', value: 'pure-love' },
{ label: 'Redemption', value: 'redemption' },
{ label: 'Regression', value: 'regression' },
{ label: 'Regret', value: 'regret' },
{ label: 'Reincarnation', value: 'reincarnation' },
{ label: 'Revenge', value: 'revenge' },
{ label: 'Romance', value: 'romance' },
{ label: 'Romance Fanrasy', value: 'romance-fanrasy' },
{ label: 'Salvation', value: 'salvation' },
{ label: 'School Life', value: 'school-life' },
{ label: 'Sci-fi', value: 'sci-fi' },
{ label: 'Science fiction', value: 'science-fiction' },
{ label: 'Seinen', value: 'seinen' },
{ label: 'Shounen', value: 'shounen' },
{ label: 'Slice of Life', value: 'slice-of-life' },
{ label: 'Soft yandere', value: 'soft-yandere' },
{ label: 'Sports', value: 'sports' },
{ label: 'Supernatural', value: 'supernatural' },
{ label: 'Survival', value: 'survival' },
{ label: 'system', value: 'system' },
{ label: 'Time limit', value: 'time-limit' },
{ label: 'Tragedy', value: 'tragedy' },
{ label: 'Transmigration', value: 'transmigration' },
{ label: 'TS', value: 'ts' },
{ label: 'Tsundere', value: 'tsundere' },
{ label: 'Unique', value: 'unique' },
{ label: 'Wholesome', value: 'wholesome' },
{ label: 'Wuxia', value: 'wuxia' },
{ label: 'Xuanhuan', value: 'xuanhuan' },
{ label: 'Yandere', value: 'yandere' },
{ label: 'Yuri', value: 'yuri' },
],
type: FilterTypes.CheckboxGroup,
},
+157 -150
View File
@@ -1,16 +1,16 @@
import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import dayjs from "dayjs";
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
class freedlit implements Plugin.PluginBase {
id = "freedlit.space";
name = "LitSpace";
site = "https://freedlit.space";
version = "1.0.0";
icon = "src/ru/freedlit/icon.png";
id = 'freedlit.space';
name = 'LitSpace';
site = 'https://freedlit.space';
version = '1.0.0';
icon = 'src/ru/freedlit/icon.png';
async popularNovels(
page: number,
@@ -19,73 +19,75 @@ class freedlit implements Plugin.PluginBase {
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.site + "/get-books/all/list/" + page + "?sort=";
url += showLatestNovels ? "recent" : filters?.sort?.value || "popular";
url += "&status=" + (filters?.status?.value || "all");
url += "&access=" + (filters?.access?.value || "all");
url += "&adult=" + (filters?.adult?.value || "hide");
let url = this.site + '/get-books/all/list/' + page + '?sort=';
url += showLatestNovels ? 'recent' : filters?.sort?.value || 'popular';
url += '&status=' + (filters?.status?.value || 'all');
url += '&access=' + (filters?.access?.value || 'all');
url += '&adult=' + (filters?.adult?.value || 'hide');
if (filters?.genre?.value?.include?.length) {
url += filters.genre.value.include
.map((id) => "&genres_included[]=" + id)
.join("");
.map(id => '&genres_included[]=' + id)
.join('');
}
if (filters?.genre?.value?.exclude?.length) {
url += filters.genre.value.exclude
.map((id) => "&genres_excluded[]=" + id)
.join("");
.map(id => '&genres_excluded[]=' + id)
.join('');
}
const body = await fetchApi(url).then((res) => res.text());
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio("#bookListBlock > div > div").each((index, element) => {
const name = loadedCheerio(element).find("div > h4 > a").text()?.trim();
loadedCheerio('#bookListBlock > div > div').each((index, element) => {
const name = loadedCheerio(element).find('div > h4 > a').text()?.trim();
const cover = loadedCheerio(element)
.find("div > a > img")
.attr("src")
.find('div > a > img')
.attr('src')
?.trim();
const url = loadedCheerio(element)
.find("div > h4 > a")
.attr("href")
.find('div > h4 > a')
.attr('href')
?.trim();
if (!name || !url) return;
novels.push({ name, cover, path: url.replace(this.site, "") });
novels.push({ name, cover, path: url.replace(this.site, '') });
});
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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(".book-info h4").text(),
cover: loadedCheerio(".book-cover > div > img").attr("src")?.trim(),
summary: loadedCheerio("#nav-home").text()?.trim(),
author: loadedCheerio(".book-info h5 > a").text(),
genres: loadedCheerio(".genre-list > a")
name: loadedCheerio('.book-info h4').text(),
cover: loadedCheerio('.book-cover > div > img').attr('src')?.trim(),
summary: loadedCheerio('#nav-home').text()?.trim(),
author: loadedCheerio('.book-info h5 > a').text(),
genres: loadedCheerio('.genre-list > a')
.map((index, element) => loadedCheerio(element).text())
.get()
.join(","),
.join(','),
};
const chapters: Plugin.ChapterItem[] = [];
loadedCheerio("a.chapter-line").each((chapterIndex, element) => {
const name = loadedCheerio(element).find("h6").text();
const url = loadedCheerio(element).attr("href");
loadedCheerio('a.chapter-line').each((chapterIndex, element) => {
const name = loadedCheerio(element).find('h6').text();
const url = loadedCheerio(element).attr('href');
if (!name || !url) return;
const releaseDate = loadedCheerio(element).find('span[class="date"]').text();
const releaseDate = loadedCheerio(element)
.find('span[class="date"]')
.text();
chapters.push({
name,
path: url.replace(this.site, ""),
path: url.replace(this.site, ''),
releaseTime: this.parseDate(releaseDate),
chapterNumber: chapterIndex + 1,
});
@@ -96,35 +98,37 @@ class freedlit implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
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);
loadedCheerio("div.mobile-block").remove();
loadedCheerio("div.standart-block").remove();
loadedCheerio('div.mobile-block').remove();
loadedCheerio('div.standart-block').remove();
const chapterText = loadedCheerio('div[class="chapter"]').html() || "";
const chapterText = loadedCheerio('div[class="chapter"]').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const url = this.site + "/search?query=" + searchTerm + "&type=all";
const body = await fetchApi(url).then((res) => res.text());
const url = this.site + '/search?query=' + searchTerm + '&type=all';
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio("#bookGridBlock > div").each((index, element) => {
const name = loadedCheerio(element).find("h4 > a").text()?.trim();
const cover = loadedCheerio(element).find("a > img").attr("src")?.trim();
const url = loadedCheerio(element).find("h4 > a").attr("href")?.trim();
loadedCheerio('#bookGridBlock > div').each((index, element) => {
const name = loadedCheerio(element).find('h4 > a').text()?.trim();
const cover = loadedCheerio(element).find('a > img').attr('src')?.trim();
const url = loadedCheerio(element).find('h4 > a').attr('href')?.trim();
if (!name || !url) return;
novels.push({ name, cover, path: url.replace(this.site, "") });
novels.push({ name, cover, path: url.replace(this.site, '') });
});
return novels;
}
parseDate = (dateString: string | undefined = "") => {
parseDate = (dateString: string | undefined = '') => {
const months: { [key: string]: number } = {
января: 1,
февраля: 2,
@@ -140,9 +144,9 @@ class freedlit implements Plugin.PluginBase {
декабря: 12,
};
const [day, month, year] = dateString.split(" ");
const [day, month, year] = dateString.split(' ');
if (day && months[month] && year) {
return dayjs(year + "-" + months[month] + "-" + day).format("LL");
return dayjs(year + '-' + months[month] + '-' + day).format('LL');
}
return dateString || null;
};
@@ -151,124 +155,127 @@ class freedlit implements Plugin.PluginBase {
filters = {
sort: {
label: "Сортировка:",
value: "popular",
label: 'Сортировка:',
value: 'popular',
options: [
{ label: "По популярности", value: "popular" },
{ label: "По количеству комментариев", value: "comments" },
{ label: "По количеству лайков", value: "likes" },
{ label: "По новизне", value: "recent" },
{ label: "По просмотрам", value: "views" },
{ label: 'По популярности', value: 'popular' },
{ label: 'По количеству комментариев', value: 'comments' },
{ label: 'По количеству лайков', value: 'likes' },
{ label: 'По новизне', value: 'recent' },
{ label: 'По просмотрам', value: 'views' },
],
type: FilterTypes.Picker,
},
genre: {
label: "Жанры:",
label: 'Жанры:',
value: { include: [], exclude: [] },
options: [
{ label: "Любой жанр", value: "all" },
{ label: "Альтернативная история", value: "alternative-history" },
{ label: "Антиутопия", value: "dystopia" },
{ label: "Бизнес-литература", value: "business-literature" },
{ label: "Боевая фантастика", value: "combat-fiction" },
{ label: "Боевик", value: "action" },
{ label: "Боевое фэнтези", value: "combat-fantasy" },
{ label: "Бояръ-Аниме", value: "boyar-anime" },
{ label: "Героическая фантастика", value: "heroic-fiction" },
{ label: "Героическое фэнтези", value: "heroic-fantasy" },
{ label: "Городское фэнтези", value: "urban-fantasy" },
{ label: "Гримдарк", value: "grimdark" },
{ label: "Детектив", value: "mystery" },
{ label: "Детская литература", value: "kids-literature" },
{ label: "Документальная проза", value: "biography" },
{ label: "Историческая проза", value: "historical-fiction" },
{ label: "Исторический детектив", value: "historical-mystery" },
{ label: "Исторический любовный роман", value: "historical-romantic-novel" },
{ label: "Историческое фэнтези", value: "historical-fantasy" },
{ label: "Киберпанк", value: "cyberpunk" },
{ label: "Космическая фантастика", value: "cosmic-fiction" },
{ label: "ЛитРПГ", value: "litrpg" },
{ label: "Лоу / Низкое фэнтези", value: "low-fantasy" },
{ label: "Любовное фэнтези", value: "romantic-fantasy" },
{ label: "Любовный роман", value: "romantic-novel" },
{ label: "Мистика", value: "mystic" },
{ label: "Мистический детектив", value: "mystic-detective" },
{ label: "Научная фантастика", value: "science-fiction" },
{ label: "Подростковая проза", value: "young-adult" },
{ label: "Политический роман", value: "political-romance" },
{ label: "Попаданцы", value: "accidental-travel" },
{ label: "Попаданцы в магические миры", value: "magic-worlds-travel" },
{ label: "Попаданцы во времени", value: "time-travel" },
{ label: "Порнотика", value: "pornotica" },
{ label: "Постапокалипсис", value: "post-apocalypse" },
{ label: "Поэзия", value: "poetry" },
{ label: "Приключения", value: "adventure" },
{ label: "Публицистика", value: "journalism" },
{ label: "Пьеса", value: "play" },
{ label: "Развитие личности", value: "how-to-book" },
{ label: "Разное", value: "other" },
{ label: "Реализм", value: "Realism" },
{ label: "РеалРПГ", value: "realrpg" },
{ label: "Репликация", value: "replication" },
{ label: "Романтическая эротика", value: "romantic-erotic-fiction" },
{ label: "Сказка", value: "fairy-tale" },
{ label: "Слэш", value: "slash" },
{ label: "Современная проза", value: "modern-prose" },
{ label: "Современный любовный роман", value: "modern-romantic-novel" },
{ label: "Социальная фантастика", value: "social-fiction" },
{ label: "Стимпанк", value: "steampunk" },
{ label: "Сценарий", value: "scenario" },
{ label: "Сюаньхуань", value: "xuanhuan" },
{ label: "Сянься", value: "xianxia" },
{ label: "Тёмное фэнтези", value: "dark-fantasy" },
{ label: "Триллер", value: "thriller" },
{ label: "Уся", value: "wuxia" },
{ label: "Фантастика", value: "fiction" },
{ label: "Фантастический детектив", value: "mystery-fiction" },
{ label: "Фанфик", value: "fan-fiction" },
{ label: "Фемслэш", value: "femslash" },
{ label: "Фэнтези", value: "fantasy" },
{ label: "Хоррор", value: "horror" },
{ label: "Шпионский детектив", value: "spy-crime" },
{ label: "Эпическое фэнтези", value: "epic-fantasy" },
{ label: "Эротика", value: "erotic-fiction" },
{ label: "Эротическая фантастика", value: "erotic-fiction" },
{ label: "Эротический фанфик", value: "erotic-fan-fiction" },
{ label: "Эротическое фэнтези", value: "erotic-fantasy" },
{ label: "Этническое фэнтези", value: "ethnic-fantasy" },
{ label: "Юмор", value: "humor" },
{ label: "Юмористическая фантастика", value: "humor-fiction" },
{ label: "Юмористическое фэнтези", value: "humor-fantasy" },
{ label: "RPS", value: "rps" },
{ label: 'Любой жанр', value: 'all' },
{ label: 'Альтернативная история', value: 'alternative-history' },
{ label: 'Антиутопия', value: 'dystopia' },
{ label: 'Бизнес-литература', value: 'business-literature' },
{ label: 'Боевая фантастика', value: 'combat-fiction' },
{ label: 'Боевик', value: 'action' },
{ label: 'Боевое фэнтези', value: 'combat-fantasy' },
{ label: 'Бояръ-Аниме', value: 'boyar-anime' },
{ label: 'Героическая фантастика', value: 'heroic-fiction' },
{ label: 'Героическое фэнтези', value: 'heroic-fantasy' },
{ label: 'Городское фэнтези', value: 'urban-fantasy' },
{ label: 'Гримдарк', value: 'grimdark' },
{ label: 'Детектив', value: 'mystery' },
{ label: 'Детская литература', value: 'kids-literature' },
{ label: 'Документальная проза', value: 'biography' },
{ label: 'Историческая проза', value: 'historical-fiction' },
{ label: 'Исторический детектив', value: 'historical-mystery' },
{
label: 'Исторический любовный роман',
value: 'historical-romantic-novel',
},
{ label: 'Историческое фэнтези', value: 'historical-fantasy' },
{ label: 'Киберпанк', value: 'cyberpunk' },
{ label: 'Космическая фантастика', value: 'cosmic-fiction' },
{ label: 'ЛитРПГ', value: 'litrpg' },
{ label: 'Лоу / Низкое фэнтези', value: 'low-fantasy' },
{ label: 'Любовное фэнтези', value: 'romantic-fantasy' },
{ label: 'Любовный роман', value: 'romantic-novel' },
{ label: 'Мистика', value: 'mystic' },
{ label: 'Мистический детектив', value: 'mystic-detective' },
{ label: 'Научная фантастика', value: 'science-fiction' },
{ label: 'Подростковая проза', value: 'young-adult' },
{ label: 'Политический роман', value: 'political-romance' },
{ label: 'Попаданцы', value: 'accidental-travel' },
{ label: 'Попаданцы в магические миры', value: 'magic-worlds-travel' },
{ label: 'Попаданцы во времени', value: 'time-travel' },
{ label: 'Порнотика', value: 'pornotica' },
{ label: 'Постапокалипсис', value: 'post-apocalypse' },
{ label: 'Поэзия', value: 'poetry' },
{ label: 'Приключения', value: 'adventure' },
{ label: 'Публицистика', value: 'journalism' },
{ label: 'Пьеса', value: 'play' },
{ label: 'Развитие личности', value: 'how-to-book' },
{ label: 'Разное', value: 'other' },
{ label: 'Реализм', value: 'Realism' },
{ label: 'РеалРПГ', value: 'realrpg' },
{ label: 'Репликация', value: 'replication' },
{ label: 'Романтическая эротика', value: 'romantic-erotic-fiction' },
{ label: 'Сказка', value: 'fairy-tale' },
{ label: 'Слэш', value: 'slash' },
{ label: 'Современная проза', value: 'modern-prose' },
{ label: 'Современный любовный роман', value: 'modern-romantic-novel' },
{ label: 'Социальная фантастика', value: 'social-fiction' },
{ label: 'Стимпанк', value: 'steampunk' },
{ label: 'Сценарий', value: 'scenario' },
{ label: 'Сюаньхуань', value: 'xuanhuan' },
{ label: 'Сянься', value: 'xianxia' },
{ label: 'Тёмное фэнтези', value: 'dark-fantasy' },
{ label: 'Триллер', value: 'thriller' },
{ label: 'Уся', value: 'wuxia' },
{ label: 'Фантастика', value: 'fiction' },
{ label: 'Фантастический детектив', value: 'mystery-fiction' },
{ label: 'Фанфик', value: 'fan-fiction' },
{ label: 'Фемслэш', value: 'femslash' },
{ label: 'Фэнтези', value: 'fantasy' },
{ label: 'Хоррор', value: 'horror' },
{ label: 'Шпионский детектив', value: 'spy-crime' },
{ label: 'Эпическое фэнтези', value: 'epic-fantasy' },
{ label: 'Эротика', value: 'erotic-fiction' },
{ label: 'Эротическая фантастика', value: 'erotic-fiction' },
{ label: 'Эротический фанфик', value: 'erotic-fan-fiction' },
{ label: 'Эротическое фэнтези', value: 'erotic-fantasy' },
{ label: 'Этническое фэнтези', value: 'ethnic-fantasy' },
{ label: 'Юмор', value: 'humor' },
{ label: 'Юмористическая фантастика', value: 'humor-fiction' },
{ label: 'Юмористическое фэнтези', value: 'humor-fantasy' },
{ label: 'RPS', value: 'rps' },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
status: {
label: "Статус:",
value: "all",
label: 'Статус:',
value: 'all',
options: [
{ label: "Любой статус", value: "all" },
{ label: "В процессе", value: "in-process" },
{ label: "Завершено", value: "finished" },
{ label: 'Любой статус', value: 'all' },
{ label: 'В процессе', value: 'in-process' },
{ label: 'Завершено', value: 'finished' },
],
type: FilterTypes.Picker,
},
access: {
label: "Доступ:",
value: "all",
label: 'Доступ:',
value: 'all',
options: [
{ label: "Любой доступ", value: "all" },
{ label: "Бесплатные", value: "free" },
{ label: "Платные", value: "paid" },
{ label: 'Любой доступ', value: 'all' },
{ label: 'Бесплатные', value: 'free' },
{ label: 'Платные', value: 'paid' },
],
type: FilterTypes.Picker,
},
adult: {
label: "Возрастные ограничения:",
value: "hide",
label: 'Возрастные ограничения:',
value: 'hide',
options: [
{ label: "Скрыть 18+", value: "hide" },
{ label: "Показать +18", value: "show" },
{ label: 'Скрыть 18+', value: 'hide' },
{ label: 'Показать +18', value: 'show' },
],
type: FilterTypes.Picker,
},
+159 -153
View File
@@ -1,39 +1,39 @@
import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { defaultCover } from "@libs/defaultCover";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import dayjs from "dayjs";
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { defaultCover } from '@libs/defaultCover';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
const apiUrl = "https://api.author.today/";
const token = "Bearer guest";
const apiUrl = 'https://api.author.today/';
const token = 'Bearer guest';
class AuthorToday implements Plugin.PluginBase {
id = "AT";
name = "Автор Тудей";
icon = "src/ru/authortoday/icon.png";
site = "https://author.today";
version = "1.0.0";
id = 'AT';
name = 'Автор Тудей';
icon = 'src/ru/authortoday/icon.png';
site = 'https://author.today';
version = '1.0.0';
async popularNovels(
pageNo: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
let url = apiUrl + "v1/catalog/search?page=" + pageNo;
let url = apiUrl + 'v1/catalog/search?page=' + pageNo;
if (filters?.genre?.value) {
url += "&genre=" + filters.genre.value;
url += '&genre=' + filters.genre.value;
}
url +=
"&sorting=" +
(showLatestNovels ? "recent" : filters?.sort?.value || "popular");
'&sorting=' +
(showLatestNovels ? 'recent' : filters?.sort?.value || 'popular');
url += "&form=" + (filters?.form?.value || "any");
url += "&state=" + (filters?.state?.value || "any");
url += "&series=" + (filters?.series?.value || "any");
url += "&access=" + (filters?.access?.value || "any");
url += "&promo=" + (filters?.promo?.value || "hide");
url += '&form=' + (filters?.form?.value || 'any');
url += '&state=' + (filters?.state?.value || 'any');
url += '&series=' + (filters?.series?.value || 'any');
url += '&access=' + (filters?.access?.value || 'any');
url += '&promo=' + (filters?.promo?.value || 'hide');
const result = await fetchApi(url, {
headers: {
@@ -43,15 +43,15 @@ class AuthorToday implements Plugin.PluginBase {
const json = (await result.json()) as response;
const novels: Plugin.NovelItem[] = [];
if (json.code === "NotFound") {
if (json.code === 'NotFound') {
return novels;
}
json?.searchResults?.forEach((novel) =>
json?.searchResults?.forEach(novel =>
novels.push({
name: novel.title,
cover: novel.coverUrl
? "https://cm.author.today/content/" + novel.coverUrl
? 'https://cm.author.today/content/' + novel.coverUrl
: defaultCover,
path: novel.id.toString(),
}),
@@ -71,24 +71,24 @@ class AuthorToday implements Plugin.PluginBase {
const novel: Plugin.SourceNovel = {
path: workID,
name: book.title,
cover: book.coverUrl ? book.coverUrl.split("?")[0] : defaultCover,
genres: book.tags?.join(", "),
summary: "",
cover: book.coverUrl ? book.coverUrl.split('?')[0] : defaultCover,
genres: book.tags?.join(', '),
summary: '',
author:
book.originalAuthor ||
book.authorFIO ||
book.coAuthorFIO ||
book.secondCoAuthorFIO ||
book.translator ||
"",
'',
status: book.isFinished ? NovelStatus.Completed : NovelStatus.Ongoing,
};
if (book.annotation) {
novel.summary += book.annotation + "\n";
novel.summary += book.annotation + '\n';
}
if (book.authorNotes) {
novel.summary += "Примечания автора:\n" + book.authorNotes;
novel.summary += 'Примечания автора:\n' + book.authorNotes;
}
const chaptersRaw = await fetchApi(`${apiUrl}v1/work/${workID}/content`, {
@@ -103,11 +103,11 @@ class AuthorToday implements Plugin.PluginBase {
chaptersJSON.forEach((chapter, chapterIndex) => {
if (chapter.isAvailable && !chapter.isDraft) {
chapters.push({
name: chapter.title || "Глава " + (chapterIndex + 1),
path: workID + "/" + chapter.id,
name: chapter.title || 'Глава ' + (chapterIndex + 1),
path: workID + '/' + chapter.id,
releaseTime: dayjs(
chapter.publishTime || chapter.lastModificationTime,
).format("LLL"),
).format('LLL'),
chapterNumber: (chapter.sortOrder || chapterIndex) + 1,
});
}
@@ -118,7 +118,7 @@ class AuthorToday implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const [workID, chapterID] = chapterPath.split("/");
const [workID, chapterID] = chapterPath.split('/');
const result = await fetchApi(
apiUrl + `v1/work/${workID}/chapter/${chapterID}/text`,
{
@@ -130,11 +130,11 @@ class AuthorToday implements Plugin.PluginBase {
const json = (await result.json()) as encryptedСhapter;
if (json.code) {
return json.code + "\n" + json?.message;
return json.code + '\n' + json?.message;
}
const key = json.key.split("").reverse().join("") + "@_@";
let text = "";
const key = json.key.split('').reverse().join('') + '@_@';
let text = '';
for (let i = 0; i < json.text.length; i++) {
text += String.fromCharCode(
@@ -143,10 +143,10 @@ class AuthorToday implements Plugin.PluginBase {
}
const loadedCheerio = parseHTML(text);
loadedCheerio("img").each((index, element) => {
if (!loadedCheerio(element).attr("src")?.startsWith("http")) {
const src = loadedCheerio(element).attr("src");
loadedCheerio(element).attr("src", this.site + src);
loadedCheerio('img').each((index, element) => {
if (!loadedCheerio(element).attr('src')?.startsWith('http')) {
const src = loadedCheerio(element).attr('src');
loadedCheerio(element).attr('src', this.site + src);
}
});
const chapterText = loadedCheerio.html();
@@ -158,23 +158,23 @@ class AuthorToday implements Plugin.PluginBase {
pageNo: number | undefined = 1,
): Promise<Plugin.NovelItem[]> {
const url =
this.site + "/search?category=works&q=" + searchTerm + "&page=" + pageNo;
const result = await fetchApi(url).then((res) => res.text());
this.site + '/search?category=works&q=' + searchTerm + '&page=' + pageNo;
const result = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(result);
const novels: Plugin.NovelItem[] = [];
loadedCheerio("a.work-row").each((index, element) => {
loadedCheerio('a.work-row').each((index, element) => {
const name = loadedCheerio(element)
.find('h4[class="work-title"]')
.text()
.trim();
let cover = loadedCheerio(element).find("img").attr("data-src");
const path = loadedCheerio(element).attr("href");
let cover = loadedCheerio(element).find('img').attr('data-src');
const path = loadedCheerio(element).attr('href');
cover = cover ? cover.split("?")[0] : defaultCover;
cover = cover ? cover.split('?')[0] : defaultCover;
if (!path) return;
novels.push({ name, cover, path: path.replace("/work/", "") });
novels.push({ name, cover, path: path.replace('/work/', '') });
});
return novels;
@@ -182,143 +182,149 @@ class AuthorToday implements Plugin.PluginBase {
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) =>
isNovel ? this.site + "/work/" + path : this.site + "/reader/" + path;
isNovel ? this.site + '/work/' + path : this.site + '/reader/' + path;
filters = {
sort: {
label: "Сортировка",
value: "popular",
label: 'Сортировка',
value: 'popular',
options: [
{ label: "По популярности", value: "popular" },
{ label: "По количеству лайков", value: "likes" },
{ label: "По комментариям", value: "comments" },
{ label: "По новизне", value: "recent" },
{ label: "По просмотрам", value: "views" },
{ label: "Набирающие популярность", value: "trending" },
{ label: 'По популярности', value: 'popular' },
{ label: 'По количеству лайков', value: 'likes' },
{ label: 'По комментариям', value: 'comments' },
{ label: 'По новизне', value: 'recent' },
{ label: 'По просмотрам', value: 'views' },
{ label: 'Набирающие популярность', value: 'trending' },
],
type: FilterTypes.Picker,
},
genre: {
label: "Жанры",
value: "",
label: 'Жанры',
value: '',
options: [
{ label: "Все", value: "" },
{ label: "Альтернативная история", value: "sf-history" },
{ label: "Антиутопия", value: "dystopia" },
{ label: "Бизнес-литература", value: "biznes-literatura" },
{ label: "Боевая фантастика", value: "sf-action" },
{ label: "Боевик", value: "action" },
{ label: "Боевое фэнтези", value: "fantasy-action" },
{ label: "Бояръ-Аниме", value: "boyar-anime" },
{ label: "Героическая фантастика", value: "sf-heroic" },
{ label: "Героическое фэнтези", value: "heroic-fantasy" },
{ label: "Городское фэнтези", value: "urban-fantasy" },
{ label: "Детектив", value: "detective" },
{ label: "Детская литература", value: "detskaya-literatura" },
{ label: "Документальная проза", value: "non-fiction" },
{ label: "Историческая проза", value: "historical-fiction" },
{ label: "Исторические приключения", value: "historical-adventure" },
{ label: "Исторический детектив", value: "historical-mystery" },
{ label: "Исторический любовный роман", value: "historical-romance" },
{ label: "Историческое фэнтези", value: "historical-fantasy" },
{ label: "Киберпанк", value: "cyberpunk" },
{ label: "Короткий любовный роман", value: "short-romance" },
{ label: "Космическая фантастика", value: "sf-space" },
{ label: "ЛитРПГ", value: "litrpg" },
{ label: "Любовное фэнтези", value: "love-fantasy" },
{ label: "Любовные романы", value: "romance" },
{ label: "Мистика", value: "paranormal" },
{ label: "Назад в СССР", value: "back-to-ussr" },
{ label: "Научная фантастика", value: "science-fiction" },
{ label: "Подростковая проза", value: "teen-prose" },
{ label: "Политический роман", value: "political-fiction" },
{ label: "Попаданцы", value: "popadantsy" },
{ label: "Попаданцы в космос", value: "popadantsy-v-kosmos" },
{ label: "Попаданцы в магические миры", value: "popadantsy-v-magicheskie-miry" },
{ label: "Попаданцы во времени", value: "popadantsy-vo-vremeni" },
{ label: "Постапокалипсис", value: "postapocalyptic" },
{ label: "Поэзия", value: "poetry" },
{ label: "Приключения", value: "adventure" },
{ label: "Публицистика", value: "publicism" },
{ label: "Развитие личности", value: "razvitie-lichnosti" },
{ label: "Разное", value: "other" },
{ label: "РеалРПГ", value: "realrpg" },
{ label: "Романтическая эротика", value: "romantic-erotika" },
{ label: "Сказка", value: "fairy-tale" },
{ label: "Современная проза", value: "modern-prose" },
{ label: "Современный любовный роман", value: "contemporary-romance" },
{ label: "Социальная фантастика", value: "sf-social" },
{ label: "Стимпанк", value: "steampunk" },
{ label: "Темное фэнтези", value: "dark-fantasy" },
{ label: "Триллер", value: "thriller" },
{ label: "Ужасы", value: "horror" },
{ label: "Фантастика", value: "sci-fi" },
{ label: "Фантастический детектив", value: "detective-science-fiction" },
{ label: "Фанфик", value: "fanfiction" },
{ label: "Фэнтези", value: "fantasy" },
{ label: "Шпионский детектив", value: "spy-mystery" },
{ label: "Эпическое фэнтези", value: "epic-fantasy" },
{ label: "Эротика", value: "erotica" },
{ label: "Эротическая фантастика", value: "sf-erotika" },
{ label: "Эротический фанфик", value: "fanfiction-erotika" },
{ label: "Эротическое фэнтези", value: "fantasy-erotika" },
{ label: "Юмор", value: "humor" },
{ label: "Юмористическая фантастика", value: "sf-humor" },
{ label: "Юмористическое фэнтези", value: "ironical-fantasy" },
{ label: 'Все', value: '' },
{ label: 'Альтернативная история', value: 'sf-history' },
{ label: 'Антиутопия', value: 'dystopia' },
{ label: 'Бизнес-литература', value: 'biznes-literatura' },
{ label: 'Боевая фантастика', value: 'sf-action' },
{ label: 'Боевик', value: 'action' },
{ label: 'Боевое фэнтези', value: 'fantasy-action' },
{ label: 'Бояръ-Аниме', value: 'boyar-anime' },
{ label: 'Героическая фантастика', value: 'sf-heroic' },
{ label: 'Героическое фэнтези', value: 'heroic-fantasy' },
{ label: 'Городское фэнтези', value: 'urban-fantasy' },
{ label: 'Детектив', value: 'detective' },
{ label: 'Детская литература', value: 'detskaya-literatura' },
{ label: 'Документальная проза', value: 'non-fiction' },
{ label: 'Историческая проза', value: 'historical-fiction' },
{ label: 'Исторические приключения', value: 'historical-adventure' },
{ label: 'Исторический детектив', value: 'historical-mystery' },
{ label: 'Исторический любовный роман', value: 'historical-romance' },
{ label: 'Историческое фэнтези', value: 'historical-fantasy' },
{ label: 'Киберпанк', value: 'cyberpunk' },
{ label: 'Короткий любовный роман', value: 'short-romance' },
{ label: 'Космическая фантастика', value: 'sf-space' },
{ label: 'ЛитРПГ', value: 'litrpg' },
{ label: 'Любовное фэнтези', value: 'love-fantasy' },
{ label: 'Любовные романы', value: 'romance' },
{ label: 'Мистика', value: 'paranormal' },
{ label: 'Назад в СССР', value: 'back-to-ussr' },
{ label: 'Научная фантастика', value: 'science-fiction' },
{ label: 'Подростковая проза', value: 'teen-prose' },
{ label: 'Политический роман', value: 'political-fiction' },
{ label: 'Попаданцы', value: 'popadantsy' },
{ label: 'Попаданцы в космос', value: 'popadantsy-v-kosmos' },
{
label: 'Попаданцы в магические миры',
value: 'popadantsy-v-magicheskie-miry',
},
{ label: 'Попаданцы во времени', value: 'popadantsy-vo-vremeni' },
{ label: 'Постапокалипсис', value: 'postapocalyptic' },
{ label: 'Поэзия', value: 'poetry' },
{ label: 'Приключения', value: 'adventure' },
{ label: 'Публицистика', value: 'publicism' },
{ label: 'Развитие личности', value: 'razvitie-lichnosti' },
{ label: 'Разное', value: 'other' },
{ label: 'РеалРПГ', value: 'realrpg' },
{ label: 'Романтическая эротика', value: 'romantic-erotika' },
{ label: 'Сказка', value: 'fairy-tale' },
{ label: 'Современная проза', value: 'modern-prose' },
{ label: 'Современный любовный роман', value: 'contemporary-romance' },
{ label: 'Социальная фантастика', value: 'sf-social' },
{ label: 'Стимпанк', value: 'steampunk' },
{ label: 'Темное фэнтези', value: 'dark-fantasy' },
{ label: 'Триллер', value: 'thriller' },
{ label: 'Ужасы', value: 'horror' },
{ label: 'Фантастика', value: 'sci-fi' },
{
label: 'Фантастический детектив',
value: 'detective-science-fiction',
},
{ label: 'Фанфик', value: 'fanfiction' },
{ label: 'Фэнтези', value: 'fantasy' },
{ label: 'Шпионский детектив', value: 'spy-mystery' },
{ label: 'Эпическое фэнтези', value: 'epic-fantasy' },
{ label: 'Эротика', value: 'erotica' },
{ label: 'Эротическая фантастика', value: 'sf-erotika' },
{ label: 'Эротический фанфик', value: 'fanfiction-erotika' },
{ label: 'Эротическое фэнтези', value: 'fantasy-erotika' },
{ label: 'Юмор', value: 'humor' },
{ label: 'Юмористическая фантастика', value: 'sf-humor' },
{ label: 'Юмористическое фэнтези', value: 'ironical-fantasy' },
],
type: FilterTypes.Picker,
},
form: {
label: "Форма произведения",
value: "any",
label: 'Форма произведения',
value: 'any',
options: [
{ label: "Любой", value: "any" },
{ label: "Перевод", value: "translation" },
{ label: "Повесть", value: "tale" },
{ label: "Рассказ", value: "story" },
{ label: "Роман", value: "novel" },
{ label: "Сборник поэзии", value: "poetry" },
{ label: "Сборник рассказов", value: "story-book" },
{ label: 'Любой', value: 'any' },
{ label: 'Перевод', value: 'translation' },
{ label: 'Повесть', value: 'tale' },
{ label: 'Рассказ', value: 'story' },
{ label: 'Роман', value: 'novel' },
{ label: 'Сборник поэзии', value: 'poetry' },
{ label: 'Сборник рассказов', value: 'story-book' },
],
type: FilterTypes.Picker,
},
state: {
label: "Статус произведения",
value: "any",
label: 'Статус произведения',
value: 'any',
options: [
{ label: "Любой статус", value: "any" },
{ label: "В процессе", value: "in-progress" },
{ label: "Завершено", value: "finished" },
{ label: 'Любой статус', value: 'any' },
{ label: 'В процессе', value: 'in-progress' },
{ label: 'Завершено', value: 'finished' },
],
type: FilterTypes.Picker,
},
series: {
label: "Статус цикла",
value: "any",
label: 'Статус цикла',
value: 'any',
options: [
{ label: "Не важно", value: "any" },
{ label: "Вне цикла", value: "out" },
{ label: "Цикл завершен", value: "finished" },
{ label: "Цикл не завершен", value: "unfinished" },
{ label: 'Не важно', value: 'any' },
{ label: 'Вне цикла', value: 'out' },
{ label: 'Цикл завершен', value: 'finished' },
{ label: 'Цикл не завершен', value: 'unfinished' },
],
type: FilterTypes.Picker,
},
access: {
label: "Тип доступа",
value: "any",
label: 'Тип доступа',
value: 'any',
options: [
{ label: "Любой", value: "any" },
{ label: "Платный", value: "paid" },
{ label: "Бесплатный", value: "free" },
{ label: 'Любой', value: 'any' },
{ label: 'Платный', value: 'paid' },
{ label: 'Бесплатный', value: 'free' },
],
type: FilterTypes.Picker,
},
promo: {
label: "Промо-фрагмент",
value: "hide",
label: 'Промо-фрагмент',
value: 'hide',
options: [
{ label: "Скрывать", value: "hide" },
{ label: "Показывать", value: "show" },
{ label: 'Скрывать', value: 'hide' },
{ label: 'Показывать', value: 'show' },
],
type: FilterTypes.Picker,
},
+106 -87
View File
@@ -1,16 +1,16 @@
import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import dayjs from "dayjs";
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
class Bookriver implements Plugin.PluginBase {
id = "bookriver";
name = "Bookriver";
site = "https://bookriver.ru";
version = "1.0.0";
icon = "src/ru/bookriver/icon.png";
id = 'bookriver';
name = 'Bookriver';
site = 'https://bookriver.ru';
version = '1.0.0';
icon = 'src/ru/bookriver/icon.png';
async popularNovels(
pageNo: number,
@@ -19,23 +19,23 @@ class Bookriver implements Plugin.PluginBase {
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.site + "/genre?page=" + pageNo + "&perPage=24&sortingType=";
let url = this.site + '/genre?page=' + pageNo + '&perPage=24&sortingType=';
url += showLatestNovels
? "last-update"
: filters?.sort?.value || "bestseller";
? 'last-update'
: filters?.sort?.value || 'bestseller';
if (filters?.genres?.value?.length) {
url += "&g=" + filters.genres.value.join(",");
url += '&g=' + filters.genres.value.join(',');
}
const result = await fetchApi(url).then((res) => res.text());
const result = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(result);
const novels: Plugin.NovelItem[] = [];
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
const jsonRaw = loadedCheerio('#__NEXT_DATA__').html();
if (jsonRaw) {
const json: response = JSON.parse(jsonRaw);
json.props.pageProps.state.pagesFilter?.genre?.books?.forEach((novel) =>
json.props.pageProps.state.pagesFilter?.genre?.books?.forEach(novel =>
novels.push({
name: novel.name,
cover: novel.coverImages[0].url,
@@ -47,23 +47,24 @@ class Bookriver implements Plugin.PluginBase {
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.resolveUrl(novelPath, true))
.then((res) => res.text());
const result = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(result);
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
const json: response = JSON.parse(jsonRaw || "{}");
const jsonRaw = loadedCheerio('#__NEXT_DATA__').html();
const json: response = JSON.parse(jsonRaw || '{}');
const book = json.props.pageProps.state.book?.bookPage;
const novel: Plugin.SourceNovel = {
path: novelPath,
name: book?.name || "",
name: book?.name || '',
cover: book?.coverImages[0].url,
summary: book?.annotation,
author: book?.author?.name,
genres: book?.tags?.map((tag) => tag.name).join(", "),
genres: book?.tags?.map(tag => tag.name).join(', '),
status:
book?.statusComplete === "writing"
book?.statusComplete === 'writing'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
@@ -73,10 +74,10 @@ class Bookriver implements Plugin.PluginBase {
if (chapter.available) {
chapters.push({
name: chapter.name,
path: book?.slug + "/" + chapter.chapterId,
path: book?.slug + '/' + chapter.chapterId,
releaseTime: dayjs(
chapter.firstPublishedAt || chapter.createdAt || undefined,
).format("LLL"),
).format('LLL'),
chapterNumber: chapterIndex + 1,
});
}
@@ -86,13 +87,13 @@ class Bookriver implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const url = "https://api.bookriver.ru/api/v1/books/chapter/text/";
const result = await fetchApi(url + chapterPath.split("/").pop());
const url = 'https://api.bookriver.ru/api/v1/books/chapter/text/';
const result = await fetchApi(url + chapterPath.split('/').pop());
const json = (await result.json()) as responseChapter;
let chapterText = json.data.content || "Конец произведения";
let chapterText = json.data.content || 'Конец произведения';
if (json.data?.audio?.available) {
chapterText += "\n" + json.data.audio.url;
chapterText += '\n' + json.data.audio.url;
}
return chapterText;
@@ -103,13 +104,16 @@ class Bookriver implements Plugin.PluginBase {
pageNo: number | undefined = 1,
): Promise<Plugin.NovelItem[]> {
const url =
"https://api.bookriver.ru/api/v1/search/autocomplete?keyword=" + searchTerm +
"&page=" + pageNo + "&perPage=10";
'https://api.bookriver.ru/api/v1/search/autocomplete?keyword=' +
searchTerm +
'&page=' +
pageNo +
'&perPage=10';
const result = await fetchApi(url);
const json = (await result.json()) as responseSearch;
const novels: Plugin.NovelItem[] = [];
json.data?.books?.forEach((novel) =>
json.data?.books?.forEach(novel =>
novels.push({
name: novel.name,
cover: novel.coverImages[0].url,
@@ -122,69 +126,84 @@ class Bookriver implements Plugin.PluginBase {
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + (isNovel ? "/book/" : "/reader/") + path;
this.site + (isNovel ? '/book/' : '/reader/') + path;
filters = {
sort: {
label: "Сортировка",
value: "bestseller",
label: 'Сортировка',
value: 'bestseller',
options: [
{ label: "Бестселлеры", value: "bestseller" },
{ label: "Дате добавления", value: "newest" },
{ label: "Дате обновления", value: "last-update" },
{ label: 'Бестселлеры', value: 'bestseller' },
{ label: 'Дате добавления', value: 'newest' },
{ label: 'Дате обновления', value: 'last-update' },
],
type: FilterTypes.Picker,
},
genres: {
label: "жанры",
label: 'жанры',
value: [],
options: [
{ label: "Альтернативная история", value: "alternativnaya-istoriya" },
{ label: "Боевая фантастика", value: "boevaya-fantastika" },
{ label: "Боевое фэнтези", value: "boevoe-fentezi" },
{ label: "Бытовое фэнтези", value: "bytovoe-fentezi" },
{ label: "Героическая фантастика", value: "geroicheskaya-fantastika" },
{ label: "Героическое фэнтези", value: "geroicheskoe-fentezi" },
{ label: "Городское фэнтези", value: "gorodskoe-fentezi" },
{ label: "Детектив", value: "detektiv" },
{ label: "Детективная фантастика", value: "detektivnaya-fantastika" },
{ label: "Жёсткая эротика", value: "zhyostkaya-erotika" },
{ label: "Исторический детектив", value: "istoricheskii-detektiv" },
{ label: "Исторический любовный роман", value: "istoricheskii-lyubovnyi-roman" },
{ label: "Историческое фэнтези", value: "istoricheskoe-fentezi" },
{ label: "Киберпанк", value: "kiberpank" },
{ label: "Классический детектив", value: "klassicheskii-detektiv" },
{ label: "Короткий любовный роман", value: "korotkii-lyubovnyi-roman" },
{ label: "Космическая фантастика", value: "kosmicheskaya-fantastika" },
{ label: "Криминальный детектив", value: "kriminalnyi-detektiv" },
{ label: "ЛитРПГ", value: "litrpg" },
{ label: "Любовная фантастика", value: "lyubovnaya-fantastika" },
{ label: "Любовное фэнтези", value: "lyubovnoe-fentezi" },
{ label: "Любовный роман", value: "lyubovnyi-roman" },
{ label: "Мистика", value: "mistika" },
{ label: "Молодежная проза", value: "molodezhnaya-proza" },
{ label: "Научная фантастика", value: "nauchnaya-fantastika" },
{ label: "Остросюжетный любовный роман", value: "ostrosyuzhetnyi-lyubovnyi-roman" },
{ label: "Политический детектив", value: "politicheskii-detektiv" },
{ label: "Попаданцы", value: "popadantsy" },
{ label: "Постапокалипсис", value: "postapokalipsis" },
{ label: "Приключенческое фэнтези", value: "priklyuchencheskoe-fentezi" },
{ label: "Романтическая эротика", value: "romanticheskaya-erotika" },
{ label: "С элементами эротики", value: "s-elementami-erotiki" },
{ label: "Славянское фэнтези", value: "slavyanskoe-fentezi" },
{ label: "Современный любовный роман", value: "sovremennyi-lyubovnyi-roman" },
{ label: "Социальная фантастика", value: "sotsialnaya-fantastika" },
{ label: "Тёмное фэнтези", value: "temnoe-fentezi" },
{ label: "Фантастика", value: "fantastika" },
{ label: "Фэнтези", value: "fentezi" },
{ label: "Шпионский детектив", value: "shpionskii-detektiv" },
{ label: "Эпическое фэнтези", value: "epicheskoe-fentezi" },
{ label: "Эротика", value: "erotika" },
{ label: "Эротическая фантастика", value: "eroticheskaya-fantastika" },
{ label: "Эротический фанфик", value: "eroticheskii-fanfik" },
{ label: "Эротическое фэнтези", value: "eroticheskoe-fentezi" },
{ label: "Юмористический детектив", value: "yumoristicheskii-detektiv" },
{ label: "Юмористическое фэнтези", value: "yumoristicheskoe-fentezi" },
{ label: 'Альтернативная история', value: 'alternativnaya-istoriya' },
{ label: 'Боевая фантастика', value: 'boevaya-fantastika' },
{ label: 'Боевое фэнтези', value: 'boevoe-fentezi' },
{ label: 'Бытовое фэнтези', value: 'bytovoe-fentezi' },
{ label: 'Героическая фантастика', value: 'geroicheskaya-fantastika' },
{ label: 'Героическое фэнтези', value: 'geroicheskoe-fentezi' },
{ label: 'Городское фэнтези', value: 'gorodskoe-fentezi' },
{ label: 'Детектив', value: 'detektiv' },
{ label: 'Детективная фантастика', value: 'detektivnaya-fantastika' },
{ label: 'Жёсткая эротика', value: 'zhyostkaya-erotika' },
{ label: 'Исторический детектив', value: 'istoricheskii-detektiv' },
{
label: 'Исторический любовный роман',
value: 'istoricheskii-lyubovnyi-roman',
},
{ label: 'Историческое фэнтези', value: 'istoricheskoe-fentezi' },
{ label: 'Киберпанк', value: 'kiberpank' },
{ label: 'Классический детектив', value: 'klassicheskii-detektiv' },
{ label: 'Короткий любовный роман', value: 'korotkii-lyubovnyi-roman' },
{ label: 'Космическая фантастика', value: 'kosmicheskaya-fantastika' },
{ label: 'Криминальный детектив', value: 'kriminalnyi-detektiv' },
{ label: 'ЛитРПГ', value: 'litrpg' },
{ label: 'Любовная фантастика', value: 'lyubovnaya-fantastika' },
{ label: 'Любовное фэнтези', value: 'lyubovnoe-fentezi' },
{ label: 'Любовный роман', value: 'lyubovnyi-roman' },
{ label: 'Мистика', value: 'mistika' },
{ label: 'Молодежная проза', value: 'molodezhnaya-proza' },
{ label: 'Научная фантастика', value: 'nauchnaya-fantastika' },
{
label: 'Остросюжетный любовный роман',
value: 'ostrosyuzhetnyi-lyubovnyi-roman',
},
{ label: 'Политический детектив', value: 'politicheskii-detektiv' },
{ label: 'Попаданцы', value: 'popadantsy' },
{ label: 'Постапокалипсис', value: 'postapokalipsis' },
{
label: 'Приключенческое фэнтези',
value: 'priklyuchencheskoe-fentezi',
},
{ label: 'Романтическая эротика', value: 'romanticheskaya-erotika' },
{ label: 'С элементами эротики', value: 's-elementami-erotiki' },
{ label: 'Славянское фэнтези', value: 'slavyanskoe-fentezi' },
{
label: 'Современный любовный роман',
value: 'sovremennyi-lyubovnyi-roman',
},
{ label: 'Социальная фантастика', value: 'sotsialnaya-fantastika' },
{ label: 'Тёмное фэнтези', value: 'temnoe-fentezi' },
{ label: 'Фантастика', value: 'fantastika' },
{ label: 'Фэнтези', value: 'fentezi' },
{ label: 'Шпионский детектив', value: 'shpionskii-detektiv' },
{ label: 'Эпическое фэнтези', value: 'epicheskoe-fentezi' },
{ label: 'Эротика', value: 'erotika' },
{ label: 'Эротическая фантастика', value: 'eroticheskaya-fantastika' },
{ label: 'Эротический фанфик', value: 'eroticheskii-fanfik' },
{ label: 'Эротическое фэнтези', value: 'eroticheskoe-fentezi' },
{
label: 'Юмористический детектив',
value: 'yumoristicheskii-detektiv',
},
{ label: 'Юмористическое фэнтези', value: 'yumoristicheskoe-fentezi' },
],
type: FilterTypes.CheckboxGroup,
},
+2496 -2482
View File
File diff suppressed because it is too large Load Diff
+155 -142
View File
@@ -1,16 +1,16 @@
import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import dayjs from "dayjs";
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
class Jaomix implements Plugin.PluginBase {
id = "jaomix.ru";
name = "Jaomix";
site = "https://jaomix.ru";
version = "1.0.0";
icon = "src/ru/jaomix/icon.png";
id = 'jaomix.ru';
name = 'Jaomix';
site = 'https://jaomix.ru';
version = '1.0.0';
icon = 'src/ru/jaomix/icon.png';
async popularNovels(
pageNo: number,
@@ -19,109 +19,116 @@ class Jaomix implements Plugin.PluginBase {
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.site + "/?searchrn";
let url = this.site + '/?searchrn';
if (filters?.lang?.value?.length) {
url += filters.lang.value
.map((lang, idx) => `&lang[${idx}]=${lang}`)
.join("");
.join('');
}
if (filters?.genre?.value?.include?.length) {
url += filters.genre.value.include
.map((genre, idx) => `&genre[${idx}]=${genre}`)
.join("");
.join('');
}
if (filters?.genre?.value?.exclude?.length) {
url += filters.genre.value.exclude
.map((genre, idx) => `&delgenre[${idx}]=del ${genre}`)
.join("");
.join('');
}
url += "&sortcountchapt=" + (filters?.sortcountchapt?.value || "1");
url += "&sortdaycreate=" + (filters?.sortdaycreate?.value || "1");
url += "&sortby=" + (showLatestNovels ? "upd" : filters?.sortby?.value || "topweek");
url += "&gpage=" + pageNo;
url += '&sortcountchapt=' + (filters?.sortcountchapt?.value || '1');
url += '&sortdaycreate=' + (filters?.sortdaycreate?.value || '1');
url +=
'&sortby=' +
(showLatestNovels ? 'upd' : filters?.sortby?.value || 'topweek');
url += '&gpage=' + pageNo;
const body = await fetchApi(url).then((res) => res.text());
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('div[class="block-home"] > div[class="one"]')
.each((index, element) => {
loadedCheerio('div[class="block-home"] > div[class="one"]').each(
(index, element) => {
const name = loadedCheerio(element)
.find('div[class="img-home"] > a')
.attr("title");
.attr('title');
const cover = loadedCheerio(element)
.find('div[class="img-home"] > a > img')
.attr("src")
?.replace("-150x150", "");
.attr('src')
?.replace('-150x150', '');
const url = loadedCheerio(element)
.find('div[class="img-home"] > a')
.attr("href");
.attr('href');
if (!name || !url) return;
novels.push({ name, cover, path: url.replace(this.site, "") });
});
novels.push({ name, cover, path: url.replace(this.site, '') });
},
);
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
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('div[class="desc-book"] > h1').text().trim(),
cover: loadedCheerio('div[class="img-book"] > img').attr("src"),
cover: loadedCheerio('div[class="img-book"] > img').attr('src'),
summary: loadedCheerio('div[id="desc-tab"]').text().trim(),
};
loadedCheerio("#info-book > p").each(function () {
let text = loadedCheerio(this).text().replace(/,/g, "").split(" ");
if (text[0] === "Автор:") {
novel.author = text.splice(1).join(" ");
} else if (text[0] === "Жанры:") {
novel.genres = text.splice(1).join(",");
} else if (text[0] === "Статус:") {
novel.status = text.includes("продолжается")
loadedCheerio('#info-book > p').each(function () {
let text = loadedCheerio(this).text().replace(/,/g, '').split(' ');
if (text[0] === 'Автор:') {
novel.author = text.splice(1).join(' ');
} else if (text[0] === 'Жанры:') {
novel.genres = text.splice(1).join(',');
} else if (text[0] === 'Статус:') {
novel.status = text.includes('продолжается')
? NovelStatus.Ongoing
: NovelStatus.Completed;
}
});
const chapters: Plugin.ChapterItem[] = [];
const totalChapters = loadedCheerio(".download-chapter div.title").length;
const totalChapters = loadedCheerio('.download-chapter div.title').length;
loadedCheerio(".download-chapter div.title").each((chapterIndex, element) => {
const name = loadedCheerio(element).find("a").attr("title");
const url = loadedCheerio(element).find("a").attr("href");
loadedCheerio('.download-chapter div.title').each(
(chapterIndex, element) => {
const name = loadedCheerio(element).find('a').attr('title');
const url = loadedCheerio(element).find('a').attr('href');
if (!name || !url) return;
const releaseDate = loadedCheerio(element).find("time").text();
const releaseDate = loadedCheerio(element).find('time').text();
chapters.push({
name,
path: url.replace(this.site, ""),
path: url.replace(this.site, ''),
releaseTime: this.parseDate(releaseDate),
chapterNumber: totalChapters - chapterIndex,
});
});
},
);
novel.chapters = chapters.reverse();
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
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);
loadedCheerio('div[class="adblock-service"]').remove();
const chapterText = loadedCheerio('div[class="entry-content"]').html();
return chapterText || "";
return chapterText || '';
}
async searchNovels(
@@ -129,34 +136,38 @@ class Jaomix implements Plugin.PluginBase {
pageNo: number | undefined = 1,
): Promise<Plugin.NovelItem[]> {
const url =
this.site + "/?searchrn=" + searchTerm +
"&but=Поиск по названию&sortby=upd&gpage=" + pageNo;
const body = await fetchApi(url).then((res) => res.text());
this.site +
'/?searchrn=' +
searchTerm +
'&but=Поиск по названию&sortby=upd&gpage=' +
pageNo;
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('div[class="block-home"] > div[class="one"]')
.each((index, element) => {
loadedCheerio('div[class="block-home"] > div[class="one"]').each(
(index, element) => {
const name = loadedCheerio(element)
.find('div[class="img-home"] > a')
.attr("title");
.attr('title');
const cover = loadedCheerio(element)
.find('div[class="img-home"] > a > img')
.attr("src")
?.replace("-150x150", "");
.attr('src')
?.replace('-150x150', '');
const url = loadedCheerio(element)
.find('div[class="img-home"] > a')
.attr("href");
.attr('href');
if (!name || !url) return;
novels.push({ name, cover, path: url.replace(this.site, "") });
});
novels.push({ name, cover, path: url.replace(this.site, '') });
},
);
return novels;
}
parseDate = (dateString: string | undefined = "") => {
parseDate = (dateString: string | undefined = '') => {
const months: { [key: string]: number } = {
Янв: 1,
Фев: 2,
@@ -172,9 +183,11 @@ class Jaomix implements Plugin.PluginBase {
Дек: 12,
};
const [time, day, month, year] = dateString.split(" ");
const [time, day, month, year] = dateString.split(' ');
if (time && day && months[month] && year) {
return dayjs(year + "-" + months[month] + "-" + day + " " + time).format("LLL");
return dayjs(year + '-' + months[month] + '-' + day + ' ' + time).format(
'LLL',
);
}
return dateString || null;
@@ -184,110 +197,110 @@ class Jaomix implements Plugin.PluginBase {
filters = {
sortby: {
label: "Сортировка:",
value: "topweek",
label: 'Сортировка:',
value: 'topweek',
options: [
{ label: "Топ недели", value: "topweek" },
{ label: "По алфавиту", value: "alphabet" },
{ label: "По дате обновления", value: "upd" },
{ label: "По дате создания", value: "new" },
{ label: "По просмотрам", value: "count" },
{ label: "Топ года", value: "topyear" },
{ label: "Топ дня", value: "topday" },
{ label: "Топ за все время", value: "alltime" },
{ label: "Топ месяца", value: "topmonth" },
{ label: 'Топ недели', value: 'topweek' },
{ label: 'По алфавиту', value: 'alphabet' },
{ label: 'По дате обновления', value: 'upd' },
{ label: 'По дате создания', value: 'new' },
{ label: 'По просмотрам', value: 'count' },
{ label: 'Топ года', value: 'topyear' },
{ label: 'Топ дня', value: 'topday' },
{ label: 'Топ за все время', value: 'alltime' },
{ label: 'Топ месяца', value: 'topmonth' },
],
type: FilterTypes.Picker,
},
sortdaycreate: {
label: "Дата добавления:",
value: "1",
label: 'Дата добавления:',
value: '1',
options: [
{ label: "Любое", value: "1" },
{ label: "От 120 до 180 дней", value: "1218" },
{ label: "От 180 до 365 дней", value: "1836" },
{ label: "От 30 до 60 дней", value: "3060" },
{ label: "От 365 дней", value: "365" },
{ label: "От 60 до 90 дней", value: "6090" },
{ label: "От 90 до 120 дней", value: "9012" },
{ label: "Послед. 30 дней", value: "30" },
{ label: 'Любое', value: '1' },
{ label: 'От 120 до 180 дней', value: '1218' },
{ label: 'От 180 до 365 дней', value: '1836' },
{ label: 'От 30 до 60 дней', value: '3060' },
{ label: 'От 365 дней', value: '365' },
{ label: 'От 60 до 90 дней', value: '6090' },
{ label: 'От 90 до 120 дней', value: '9012' },
{ label: 'Послед. 30 дней', value: '30' },
],
type: FilterTypes.Picker,
},
sortcountchapt: {
label: "Количество глав:",
value: "1",
label: 'Количество глав:',
value: '1',
options: [
{ label: "Любое кол-во глав", value: "1" },
{ label: "До 500", value: "500" },
{ label: "От 1000 до 2000", value: "1020" },
{ label: "От 2000 до 3000", value: "2030" },
{ label: "От 3000 до 4000", value: "3040" },
{ label: "От 4000", value: "400" },
{ label: "От 500 до 1000", value: "510" },
{ label: 'Любое кол-во глав', value: '1' },
{ label: 'До 500', value: '500' },
{ label: 'От 1000 до 2000', value: '1020' },
{ label: 'От 2000 до 3000', value: '2030' },
{ label: 'От 3000 до 4000', value: '3040' },
{ label: 'От 4000', value: '400' },
{ label: 'От 500 до 1000', value: '510' },
],
type: FilterTypes.Picker,
},
genre: {
label: "Жанры:",
label: 'Жанры:',
value: { include: [], exclude: [] },
options: [
{ label: "Боевые Искусства", value: "Боевые Искусства" },
{ label: "Виртуальный Мир", value: "Виртуальный Мир" },
{ label: "Гарем", value: "Гарем" },
{ label: "Детектив", value: "Детектив" },
{ label: "Драма", value: "Драма" },
{ label: "Игра", value: "Игра" },
{ label: "Истории из жизни", value: "Истории из жизни" },
{ label: "Исторический", value: "Исторический" },
{ label: "История", value: "История" },
{ label: "Исэкай", value: "Исэкай" },
{ label: "Комедия", value: "Комедия" },
{ label: "Меха", value: "Меха" },
{ label: "Мистика", value: "Мистика" },
{ label: "Научная Фантастика", value: "Научная Фантастика" },
{ label: "Повседневность", value: "Повседневность" },
{ label: "Постапокалипсис", value: "Постапокалипсис" },
{ label: "Приключения", value: "Приключения" },
{ label: "Психология", value: "Психология" },
{ label: "Романтика", value: "Романтика" },
{ label: "Сверхъестественное", value: "Сверхъестественное" },
{ label: "Сёнэн", value: "Сёнэн" },
{ label: "Сёнэн-ай", value: "Сёнэн-ай" },
{ label: "Спорт", value: "Спорт" },
{ label: "Сэйнэн", value: "Сэйнэн" },
{ label: "Сюаньхуа", value: "Сюаньхуа" },
{ label: "Трагедия", value: "Трагедия" },
{ label: "Триллер", value: "Триллер" },
{ label: "Фантастика", value: "Фантастика" },
{ label: "Фэнтези", value: "Фэнтези" },
{ label: "Хоррор", value: "Хоррор" },
{ label: "Школьная жизнь", value: "Школьная жизнь" },
{ label: "Шоунен", value: "Шоунен" },
{ label: "Экшн", value: "Экшн" },
{ label: "Этти", value: "Этти" },
{ label: "Юри", value: "Юри" },
{ label: "Adult", value: "Adult" },
{ label: "Ecchi", value: "Ecchi" },
{ label: "Josei", value: "Josei" },
{ label: "Lolicon", value: "Lolicon" },
{ label: "Mature", value: "Mature" },
{ label: "Shoujo", value: "Shoujo" },
{ label: "Wuxia", value: "Wuxia" },
{ label: "Xianxia", value: "Xianxia" },
{ label: "Xuanhuan", value: "Xuanhuan" },
{ label: "Yaoi", value: "Yaoi" },
{ label: 'Боевые Искусства', value: 'Боевые Искусства' },
{ label: 'Виртуальный Мир', value: 'Виртуальный Мир' },
{ label: 'Гарем', value: 'Гарем' },
{ label: 'Детектив', value: 'Детектив' },
{ label: 'Драма', value: 'Драма' },
{ label: 'Игра', value: 'Игра' },
{ label: 'Истории из жизни', value: 'Истории из жизни' },
{ label: 'Исторический', value: 'Исторический' },
{ label: 'История', value: 'История' },
{ label: 'Исэкай', value: 'Исэкай' },
{ label: 'Комедия', value: 'Комедия' },
{ label: 'Меха', value: 'Меха' },
{ label: 'Мистика', value: 'Мистика' },
{ label: 'Научная Фантастика', value: 'Научная Фантастика' },
{ label: 'Повседневность', value: 'Повседневность' },
{ label: 'Постапокалипсис', value: 'Постапокалипсис' },
{ label: 'Приключения', value: 'Приключения' },
{ label: 'Психология', value: 'Психология' },
{ label: 'Романтика', value: 'Романтика' },
{ label: 'Сверхъестественное', value: 'Сверхъестественное' },
{ label: 'Сёнэн', value: 'Сёнэн' },
{ label: 'Сёнэн-ай', value: 'Сёнэн-ай' },
{ label: 'Спорт', value: 'Спорт' },
{ label: 'Сэйнэн', value: 'Сэйнэн' },
{ label: 'Сюаньхуа', value: 'Сюаньхуа' },
{ label: 'Трагедия', value: 'Трагедия' },
{ label: 'Триллер', value: 'Триллер' },
{ label: 'Фантастика', value: 'Фантастика' },
{ label: 'Фэнтези', value: 'Фэнтези' },
{ label: 'Хоррор', value: 'Хоррор' },
{ label: 'Школьная жизнь', value: 'Школьная жизнь' },
{ label: 'Шоунен', value: 'Шоунен' },
{ label: 'Экшн', value: 'Экшн' },
{ label: 'Этти', value: 'Этти' },
{ label: 'Юри', value: 'Юри' },
{ label: 'Adult', value: 'Adult' },
{ label: 'Ecchi', value: 'Ecchi' },
{ label: 'Josei', value: 'Josei' },
{ label: 'Lolicon', value: 'Lolicon' },
{ label: 'Mature', value: 'Mature' },
{ label: 'Shoujo', value: 'Shoujo' },
{ label: 'Wuxia', value: 'Wuxia' },
{ label: 'Xianxia', value: 'Xianxia' },
{ label: 'Xuanhuan', value: 'Xuanhuan' },
{ label: 'Yaoi', value: 'Yaoi' },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
lang: {
label: "Выбрать языки:",
label: 'Выбрать языки:',
value: [],
options: [
{ label: "Английский", value: "Английский" },
{ label: "Китайский", value: "Китайский" },
{ label: "Корейский", value: "Корейский" },
{ label: "Японский", value: "Японский" },
{ label: 'Английский', value: 'Английский' },
{ label: 'Китайский', value: 'Китайский' },
{ label: 'Корейский', value: 'Корейский' },
{ label: 'Японский', value: 'Японский' },
],
type: FilterTypes.CheckboxGroup,
},
+100 -97
View File
@@ -1,61 +1,64 @@
import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { defaultCover } from "@libs/defaultCover";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { defaultCover } from '@libs/defaultCover';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
const statusKey: { [key: string]: string } = {
"0": NovelStatus.Unknown,
"1": NovelStatus.Ongoing,
"2": NovelStatus.Completed,
"3": NovelStatus.OnHiatus,
"4": NovelStatus.Cancelled,
'0': NovelStatus.Unknown,
'1': NovelStatus.Ongoing,
'2': NovelStatus.Completed,
'3': NovelStatus.OnHiatus,
'4': NovelStatus.Cancelled,
};
class Neobook implements Plugin.PluginBase {
id = "neobook";
name = "Neobook";
site = "https://neobook.org";
version = "1.0.0";
icon = "src/ru/neobook/icon.png";
id = 'neobook';
name = 'Neobook';
site = 'https://neobook.org';
version = '1.0.0';
icon = 'src/ru/neobook/icon.png';
async fetchNovels(
page: number,
{ filters, showLatestNovels }: Plugin.PopularNovelsOptions<typeof this.filters>,
{
filters,
showLatestNovels,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
searchTerm?: string,
): Promise<Plugin.NovelItem[]> {
const formData = new FormData();
formData.append("version", "4.0");
formData.append("uid", "0");
formData.append("utoken", "");
formData.append("resource", "general");
formData.append("action", "get_bundle");
formData.append("bundle", "bundle_books");
formData.append("target", "feed");
formData.append("page", page.toString());
formData.append("filter_category_id", filters?.category?.value || "0");
formData.append("filter_completed", "-1");
formData.append("filter_search", searchTerm || "");
formData.append("filter_tags", filters?.tags?.value || "");
formData.append('version', '4.0');
formData.append('uid', '0');
formData.append('utoken', '');
formData.append('resource', 'general');
formData.append('action', 'get_bundle');
formData.append('bundle', 'bundle_books');
formData.append('target', 'feed');
formData.append('page', page.toString());
formData.append('filter_category_id', filters?.category?.value || '0');
formData.append('filter_completed', '-1');
formData.append('filter_search', searchTerm || '');
formData.append('filter_tags', filters?.tags?.value || '');
formData.append(
"filter_sort",
showLatestNovels ? "new" : filters?.sort?.value || "popular",
'filter_sort',
showLatestNovels ? 'new' : filters?.sort?.value || 'popular',
);
formData.append("filter_timeread", filters?.timeread?.value || "0-999999");
formData.append('filter_timeread', filters?.timeread?.value || '0-999999');
const result = await fetchApi("https://api.neobook.org/", {
method: "post",
const result = await fetchApi('https://api.neobook.org/', {
method: 'post',
body: formData,
});
const json = (await result.json()) as { bundle_books: BundleBooks };
const novels: Plugin.NovelItem[] = [];
if (json.bundle_books.feed?.length) {
json.bundle_books.feed?.forEach((novel) =>
json.bundle_books.feed?.forEach(novel =>
novels.push({
name: novel.title,
cover: novel?.attachment?.image?.m || defaultCover,
path: novel.token + "/",
path: novel.token + '/',
}),
);
}
@@ -77,12 +80,13 @@ class Neobook implements Plugin.PluginBase {
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.resolveUrl(novelPath, true))
.then((res) => res.text());
const body = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
res.text(),
);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: "",
name: '',
cover: defaultCover,
};
@@ -91,22 +95,22 @@ class Neobook implements Plugin.PluginBase {
const book = JSON.parse(bookRaw[1]) as Novels;
novel.name = book.title;
novel.summary = book.text?.replace?.(/<br>/g, "\n") || book.text_fix;
novel.summary = book.text?.replace?.(/<br>/g, '\n') || book.text_fix;
novel.author =
book.user && book.user.firstname && book.user.lastname
? book.user.firstname + " " + book.user.lastname
: book.user?.initials || "";
novel.status = statusKey[book.status || "0"] || NovelStatus.Unknown;
? book.user.firstname + ' ' + book.user.lastname
: book.user?.initials || '';
novel.status = statusKey[book.status || '0'] || NovelStatus.Unknown;
if (book.attachment?.image?.m) novel.cover = book.attachment.image.m;
if (book.tags?.length) novel.genres = book.tags.join(",");
if (book.tags?.length) novel.genres = book.tags.join(',');
const chapters: Plugin.ChapterItem[] = [];
book.chapters?.forEach((chapter, chapterIndex) => {
if (chapter.access == "1" && chapter.status == "1") {
if (chapter.access == '1' && chapter.status == '1') {
chapters.push({
name: chapter.title || "Глава " + (chapterIndex + 1),
name: chapter.title || 'Глава ' + (chapterIndex + 1),
path: `?book=${book.token}&chapter=${chapter.token}`,
releaseTime: null,
chapterNumber: Number(chapter.sort) || chapterIndex + 1,
@@ -120,19 +124,18 @@ class Neobook implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.resolveUrl(chapterPath))
.then((res) => res.text());
const body = await fetchApi(this.resolveUrl(chapterPath)).then(res =>
res.text(),
);
const bookRaw = body.match(/var data = ({.*?});/);
let chapterText = "";
let chapterText = '';
if (bookRaw instanceof Array && bookRaw.length >= 2) {
const book = JSON.parse(bookRaw[1]) as Novels;
const token = chapterPath.split("=")[2];
const chapter = book.chapters?.find?.(
(chapter) => chapter.token == token,
);
chapterText = (chapter?.data?.html || "").replace(/<br>/g, "");
const token = chapterPath.split('=')[2];
const chapter = book.chapters?.find?.(chapter => chapter.token == token);
chapterText = (chapter?.data?.html || '').replace(/<br>/g, '');
}
return chapterText;
@@ -140,67 +143,67 @@ class Neobook implements Plugin.PluginBase {
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + (isNovel ? "/book/" : "/reader/") + path;
this.site + (isNovel ? '/book/' : '/reader/') + path;
filters = {
sort: {
label: "Сортировка:",
value: "popular",
label: 'Сортировка:',
value: 'popular',
options: [
{ label: "Сначала популярные", value: "popular" },
{ label: "Сначала новые", value: "new" },
{ label: "В случайном порядке", value: "rand" },
{ label: 'Сначала популярные', value: 'popular' },
{ label: 'Сначала новые', value: 'new' },
{ label: 'В случайном порядке', value: 'rand' },
],
type: FilterTypes.Picker,
},
timeread: {
label: "Время прочтения:",
value: "0-999999",
label: 'Время прочтения:',
value: '0-999999',
options: [
{ label: "Все", value: "0-999999" },
{ label: "Менее 15 минут", value: "0-900" },
{ label: "15-30 минут", value: "900-1800" },
{ label: "30-60 минут", value: "1800-3600" },
{ label: "1-2 часа", value: "3600-7200" },
{ label: "Более 2 часов", value: "7200-999999" },
{ label: 'Все', value: '0-999999' },
{ label: 'Менее 15 минут', value: '0-900' },
{ label: '15-30 минут', value: '900-1800' },
{ label: '30-60 минут', value: '1800-3600' },
{ label: '1-2 часа', value: '3600-7200' },
{ label: 'Более 2 часов', value: '7200-999999' },
],
type: FilterTypes.Picker,
},
category: {
label: "Жанр:",
value: "",
label: 'Жанр:',
value: '',
options: [
{ label: "Все", value: "" },
{ label: "Антиутопия", value: "10" },
{ label: "Детектив", value: "13" },
{ label: "Детские книги", value: "14" },
{ label: "Драма", value: "15" },
{ label: "Другое", value: "16" },
{ label: "История", value: "18" },
{ label: "Мелодрама", value: "21" },
{ label: "Мистика", value: "22" },
{ label: "Научная фантастика", value: "23" },
{ label: "Нон-фикшн", value: "35" },
{ label: "Подростки и молодежь", value: "26" },
{ label: "Постапокалипсис", value: "27" },
{ label: "Поэзия", value: "28" },
{ label: "Приключения", value: "29" },
{ label: "Рассказ", value: "34" },
{ label: "Роман", value: "36" },
{ label: "Творчество", value: "40" },
{ label: "Триллер", value: "91" },
{ label: "Ужасы", value: "90" },
{ label: "Фантастика", value: "41" },
{ label: "Фанфик", value: "42" },
{ label: "Фэнтези", value: "44" },
{ label: "Эротика", value: "46" },
{ label: "Юмор", value: "47" },
{ label: 'Все', value: '' },
{ label: 'Антиутопия', value: '10' },
{ label: 'Детектив', value: '13' },
{ label: 'Детские книги', value: '14' },
{ label: 'Драма', value: '15' },
{ label: 'Другое', value: '16' },
{ label: 'История', value: '18' },
{ label: 'Мелодрама', value: '21' },
{ label: 'Мистика', value: '22' },
{ label: 'Научная фантастика', value: '23' },
{ label: 'Нон-фикшн', value: '35' },
{ label: 'Подростки и молодежь', value: '26' },
{ label: 'Постапокалипсис', value: '27' },
{ label: 'Поэзия', value: '28' },
{ label: 'Приключения', value: '29' },
{ label: 'Рассказ', value: '34' },
{ label: 'Роман', value: '36' },
{ label: 'Творчество', value: '40' },
{ label: 'Триллер', value: '91' },
{ label: 'Ужасы', value: '90' },
{ label: 'Фантастика', value: '41' },
{ label: 'Фанфик', value: '42' },
{ label: 'Фэнтези', value: '44' },
{ label: 'Эротика', value: '46' },
{ label: 'Юмор', value: '47' },
],
type: FilterTypes.Picker,
},
tags: {
label: "Тэги:",
value: "",
label: 'Тэги:',
value: '',
type: FilterTypes.TextInput,
},
} satisfies Filters;
@@ -401,7 +404,7 @@ interface Price {
deadline: string;
}
type Currency = "NEO";
type Currency = 'NEO';
interface Rating {
value: string;
+64 -53
View File
@@ -1,29 +1,33 @@
import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import dayjs from "dayjs";
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import dayjs from 'dayjs';
class novelOvh implements Plugin.PluginBase {
id = "novelovh";
name = "НовелОВХ";
site = "https://novel.ovh";
version = "1.0.0";
icon = "src/ru/novelovh/icon.png";
id = 'novelovh';
name = 'НовелОВХ';
site = 'https://novel.ovh';
version = '1.0.0';
icon = 'src/ru/novelovh/icon.png';
async popularNovels(
pageNo: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
let url = this.site + "/novel?page=" + (pageNo - 1);
url += "&sort=" +
(showLatestNovels ? "updatedAt" : filters?.sort?.value || "averageRating") + ",desc";
let url = this.site + '/novel?page=' + (pageNo - 1);
url +=
'&sort=' +
(showLatestNovels
? 'updatedAt'
: filters?.sort?.value || 'averageRating') +
',desc';
const result = await fetchApi(url + "&_data=routes/reader/book/index");
const result = await fetchApi(url + '&_data=routes/reader/book/index');
const body = (await result.json()) as { books: BooksEntity[] };
const novels: Plugin.NovelItem[] = [];
body.books.forEach((novel) =>
body.books.forEach(novel =>
novels.push({
name: novel.name.ru,
cover: novel.poster,
@@ -36,7 +40,8 @@ class novelOvh implements Plugin.PluginBase {
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(
this.resolveUrl(novelPath, true) + "?_data=routes/reader/book/$slug/index",
this.resolveUrl(novelPath, true) +
'?_data=routes/reader/book/$slug/index',
);
const json = (await result.json()) as responseNovel;
@@ -44,19 +49,19 @@ class novelOvh implements Plugin.PluginBase {
path: novelPath,
name: json.book.name.ru,
cover: json.book.poster,
genres: json.book.labels?.map?.((label) => label.name).join(","),
genres: json.book.labels?.map?.(label => label.name).join(','),
summary: json.book.description,
status:
json.book.status == "ONGOING"
json.book.status == 'ONGOING'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
json.book.relations?.forEach((person) => {
json.book.relations?.forEach(person => {
switch (person.type) {
case "AUTHOR":
case 'AUTHOR':
novel.author = person.publisher.name;
case "ARTIST":
case 'ARTIST':
novel.artist = person.publisher.name;
}
});
@@ -67,11 +72,14 @@ class novelOvh implements Plugin.PluginBase {
chapters.push({
name:
chapter.title ||
"Том " + (chapter.volume || 0) + " " +
(chapter.name || "Глава " +
'Том ' +
(chapter.volume || 0) +
' ' +
(chapter.name ||
'Глава ' +
(chapter.number || json.chapters.length - chapterIndex)),
path: novelPath + "/" + chapter.id + "/0",
releaseTime: dayjs(chapter.createdAt).format("LLL"),
path: novelPath + '/' + chapter.id + '/0',
releaseTime: dayjs(chapter.createdAt).format('LLL'),
chapterNumber: json.chapters.length - chapterIndex,
}),
);
@@ -82,7 +90,7 @@ class novelOvh implements Plugin.PluginBase {
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(
"https://api.novel.ovh/v2/chapters/" + chapterPath.split("/")[1],
'https://api.novel.ovh/v2/chapters/' + chapterPath.split('/')[1],
);
const book = (await result.json()) as responseChapter;
const image = Object.fromEntries(
@@ -98,11 +106,11 @@ class novelOvh implements Plugin.PluginBase {
page: number | undefined = 1,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/novel?search=${searchTerm}&page=${page - 1}`;
const result = await fetchApi(url + "&_data=routes/reader/book/index");
const result = await fetchApi(url + '&_data=routes/reader/book/index');
const body = (await result.json()) as { books: BooksEntity[] };
const novels: Plugin.NovelItem[] = [];
body.books.forEach((novel) =>
body.books.forEach(novel =>
novels.push({
name: novel.name.ru,
cover: novel.poster,
@@ -116,37 +124,39 @@ class novelOvh implements Plugin.PluginBase {
jsonToHtml = (
json: ContentEntity[],
image: { [key: string]: string },
html: string = "",
html: string = '',
) => {
json.forEach((element) => {
json.forEach(element => {
switch (element.type) {
case "image":
case 'image':
if (element.attrs?.pages?.[0]) {
html += `<img src="${image[element.attrs.pages[0]]}"/>`;
}
break;
case "hardBreak":
html += "<br>";
case 'hardBreak':
html += '<br>';
break;
case "horizontalRule":
case "delimiter":
case 'horizontalRule':
case 'delimiter':
html += '<h2 style="text-align: center">***</h2>';
break;
case "paragraph":
case 'paragraph':
html +=
"<p" +
'<p' +
(element?.attrs?.textAlign
? ` style="text-align: ${element.attrs.textAlign}"`
: "") +
">" +
(element.content ? this.jsonToHtml(element.content, image) : "<br>") +
"</p>";
: '') +
'>' +
(element.content
? this.jsonToHtml(element.content, image)
: '<br>') +
'</p>';
break;
case "text":
case 'text':
html += element.text;
break;
default:
html += JSON.stringify(element, null, "\t"); //maybe I missed something.
html += JSON.stringify(element, null, '\t'); //maybe I missed something.
break;
}
});
@@ -154,20 +164,21 @@ class novelOvh implements Plugin.PluginBase {
};
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) => this.site + "/novel/" + path;
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + '/novel/' + path;
filters = {
sort: {
label: "Сортировка",
value: "averageRating",
label: 'Сортировка',
value: 'averageRating',
options: [
{ label: "Кол-во просмотров", value: "viewsCount" },
{ label: "Кол-во лайков", value: "likesCount" },
{ label: "Кол-во глав", value: "chaptersCount" },
{ label: "Кол-во закладок", value: "bookmarksCount" },
{ label: "Рейтингу", value: "averageRating" },
{ label: "Дате создания", value: "createdAt" },
{ label: "Дате обновления", value: "updatedAt" },
{ label: 'Кол-во просмотров', value: 'viewsCount' },
{ label: 'Кол-во лайков', value: 'likesCount' },
{ label: 'Кол-во глав', value: 'chaptersCount' },
{ label: 'Кол-во закладок', value: 'bookmarksCount' },
{ label: 'Рейтингу', value: 'averageRating' },
{ label: 'Дате создания', value: 'createdAt' },
{ label: 'Дате обновления', value: 'updatedAt' },
],
type: FilterTypes.Picker,
},
+717 -715
View File
File diff suppressed because it is too large Load Diff
+1004 -977
View File
File diff suppressed because it is too large Load Diff
+257 -236
View File
@@ -1,9 +1,9 @@
import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import dayjs from "dayjs";
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
const statusKey: { [key: number]: string } = {
1: NovelStatus.Ongoing,
@@ -13,11 +13,11 @@ const statusKey: { [key: number]: string } = {
};
class RLIB implements Plugin.PluginBase {
id = "RLIB";
name = "RanobeLib";
site = "https://ranobelib.me";
version = "1.0.0";
icon = "src/ru/ranobelib/icon.png";
id = 'RLIB';
name = 'RanobeLib';
site = 'https://ranobelib.me';
version = '1.0.0';
icon = 'src/ru/ranobelib/icon.png';
ui: string | undefined = undefined;
async popularNovels(
@@ -27,63 +27,74 @@ class RLIB implements Plugin.PluginBase {
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.site + "/manga-list?sort=";
url += showLatestNovels ? "last_chapter_at" : filters?.sort?.value || "rate";
url += "&dir=" + (filters?.order?.value || "desc");
url += "&chapters[min]=" + (filters?.require_chapters?.value ? "1" : "0");
let url = this.site + '/manga-list?sort=';
url += showLatestNovels
? 'last_chapter_at'
: filters?.sort?.value || 'rate';
url += '&dir=' + (filters?.order?.value || 'desc');
url += '&chapters[min]=' + (filters?.require_chapters?.value ? '1' : '0');
Object.entries(filters || {}).forEach(([type, { value }]: any) => {
if (value instanceof Array && value.length) {
url += "&" + type + "[]=" + value.join("&" + type + "[]=");
url += '&' + type + '[]=' + value.join('&' + type + '[]=');
}
if (value?.include instanceof Array && value.include.length) {
url += "&" + type + "[include][]=" +
value.include.join("&" + type + "[include][]=");
url +=
'&' +
type +
'[include][]=' +
value.include.join('&' + type + '[include][]=');
}
if (value?.exclude instanceof Array && value.exclude.length) {
url += "&" + type + "[exclude][]=" +
value.exclude.join("&" + type + "[exclude][]=");
url +=
'&' +
type +
'[exclude][]=' +
value.exclude.join('&' + type + '[exclude][]=');
}
});
url += "&page=" + pageNo;
url += '&page=' + pageNo;
const result = await fetchApi(url).then((res) => res.text());
const result = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(result);
this.ui = loadedCheerio("a.header-right-menu__item")
.attr("href")
?.replace?.(/[^0-9]/g, "");
this.ui = loadedCheerio('a.header-right-menu__item')
.attr('href')
?.replace?.(/[^0-9]/g, '');
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".media-card-wrap").each((index, element) => {
const name = loadedCheerio(element).find(".media-card__title").text();
const cover = loadedCheerio(element).find("a.media-card").attr("data-src");
const url = loadedCheerio(element).find("a.media-card").attr("href");
loadedCheerio('.media-card-wrap').each((index, element) => {
const name = loadedCheerio(element).find('.media-card__title').text();
const cover = loadedCheerio(element)
.find('a.media-card')
.attr('data-src');
const url = loadedCheerio(element).find('a.media-card').attr('href');
if (!url) return;
novels.push({ name, cover, path: url.replace(this.site, "") });
novels.push({ name, cover, path: url.replace(this.site, '') });
});
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.resolveUrl(novelPath, true))
.then((res) => res.text());
const body = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".media-name__main").text()?.trim?.() || "",
name: loadedCheerio('.media-name__main').text()?.trim?.() || '',
};
novel.cover = loadedCheerio(".container_responsive img").attr("src");
novel.cover = loadedCheerio('.container_responsive img').attr('src');
novel.summary = loadedCheerio(".media-description__text").text().trim();
novel.summary = loadedCheerio('.media-description__text').text().trim();
novel.genres = loadedCheerio('div[class="media-tags"]')
.text()
.trim()
.replace(/[\n\r]+/g, ", ")
.replace(/ /g, "");
.replace(/[\n\r]+/g, ', ')
.replace(/ /g, '');
loadedCheerio(
'div[class="media-info-list paper"] > [class="media-info-list__item"]',
@@ -92,12 +103,12 @@ class RLIB implements Plugin.PluginBase {
.find('div[class="media-info-list__title"]')
.text();
if (name === "Автор") {
if (name === 'Автор') {
novel.author = loadedCheerio(this)
.find('div[class="media-info-list__value"]')
.text()
.trim();
} else if (name === "Художник") {
} else if (name === 'Художник') {
novel.artist = loadedCheerio(this)
.find('div[class="media-info-list__value"]')
.text()
@@ -105,9 +116,9 @@ class RLIB implements Plugin.PluginBase {
}
});
this.ui = loadedCheerio("a.header-right-menu__item")
.attr("href")
?.replace?.(/[^0-9]/g, "");
this.ui = loadedCheerio('a.header-right-menu__item')
.attr('href')
?.replace?.(/[^0-9]/g, '');
const chaptersRaw = body.match(/window\.__DATA__ = ({.*?});/);
if (chaptersRaw instanceof Array && chaptersRaw.length >= 2) {
@@ -119,7 +130,8 @@ class RLIB implements Plugin.PluginBase {
chaptersJson.manga.engName ||
chaptersJson.manga.name;
}
novel.status = statusKey[chaptersJson.manga.status] || NovelStatus.Unknown;
novel.status =
statusKey[chaptersJson.manga.status] || NovelStatus.Unknown;
this.ui = chaptersJson?.user?.id;
if (!chaptersJson.chapters.list?.length) return novel;
@@ -127,16 +139,19 @@ class RLIB implements Plugin.PluginBase {
const customPage: { [key: number]: string } = {};
const customOrder: { [key: number]: number } = {};
if (chaptersJson.chapters.branches?.length && chaptersJson.chapters.branches.length > 1) {
if (
chaptersJson.chapters.branches?.length &&
chaptersJson.chapters.branches.length > 1
) {
//if the novel is being translated by more than one team
chaptersJson.chapters.branches.forEach(({ teams, id }) => {
if (teams?.length) {
customPage[id || 0] =
teams.find((team) => team.is_active)?.name || teams[0].name;
teams.find(team => team.is_active)?.name || teams[0].name;
}
});
//fixes the chapter's position.
chaptersJson.chapters.list.forEach((chapter) => {
chaptersJson.chapters.list.forEach(chapter => {
chapter.index = customOrder[chapter.branch_id || 0] || 1;
customOrder[chapter.branch_id || 0] =
(customOrder[chapter.branch_id || 0] || 1) + 1;
@@ -147,19 +162,24 @@ class RLIB implements Plugin.PluginBase {
chaptersJson.chapters.list.forEach((chapter, chapterIndex) =>
chapters.push({
name:
"Том " + chapter.chapter_volume +
" Глава " + chapter.chapter_number +
(chapter.chapter_name ? " " + chapter.chapter_name.trim() : ""),
'Том ' +
chapter.chapter_volume +
' Глава ' +
chapter.chapter_number +
(chapter.chapter_name ? ' ' + chapter.chapter_name.trim() : ''),
path:
novelPath +
"/v" + chapter.chapter_volume +
"/c" + chapter.chapter_number +
"?bid=" + (chapter.branch_id || ""),
releaseTime: dayjs(chapter.chapter_created_at).format("LLL"),
'/v' +
chapter.chapter_volume +
'/c' +
chapter.chapter_number +
'?bid=' +
(chapter.branch_id || ''),
releaseTime: dayjs(chapter.chapter_created_at).format('LLL'),
chapterNumber:
customOrder[chapter.branch_id || 0] - (chapter.index || 0) ||
totalChapters - chapterIndex,
page: customPage[chapter.branch_id || 0] || "Основной перевод",
page: customPage[chapter.branch_id || 0] || 'Основной перевод',
}),
);
novel.chapters =
@@ -176,38 +196,39 @@ class RLIB implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.resolveUrl(chapterPath))
.then((res) => res.text());
const result = await fetchApi(this.resolveUrl(chapterPath)).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(result);
loadedCheerio(".reader-container img").each((index, element) => {
loadedCheerio('.reader-container img').each((index, element) => {
const src =
loadedCheerio(element).attr("data-src") ||
loadedCheerio(element).attr("src");
if (!src?.startsWith("http")) {
loadedCheerio(element).attr("src", this.site + src);
loadedCheerio(element).attr('data-src') ||
loadedCheerio(element).attr('src');
if (!src?.startsWith('http')) {
loadedCheerio(element).attr('src', this.site + src);
} else {
loadedCheerio(element).attr("src", src);
loadedCheerio(element).attr('src', src);
}
loadedCheerio(element).removeAttr("data-src");
loadedCheerio(element).removeAttr('data-src');
});
const chapterText = loadedCheerio(".reader-container").html();
return chapterText || "";
const chapterText = loadedCheerio('.reader-container').html();
return chapterText || '';
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const result = await fetchApi(
this.site + "/search?q=" + searchTerm + "&type=manga",
this.site + '/search?q=' + searchTerm + '&type=manga',
);
const body = (await result.json()) as Manga[];
const novels: Plugin.NovelItem[] = [];
body.forEach((novel) =>
body.forEach(novel =>
novels.push({
name: novel.rus_name || novel.name,
cover: novel.coverImage,
path: "/" + novel.slug,
path: '/' + novel.slug,
}),
);
@@ -216,223 +237,223 @@ class RLIB implements Plugin.PluginBase {
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + path + (this.ui ? (isNovel ? "?" : "&") + "ui=" + this.ui : "");
this.site + path + (this.ui ? (isNovel ? '?' : '&') + 'ui=' + this.ui : '');
filters = {
sort: {
label: "Сортировка",
value: "rate",
label: 'Сортировка',
value: 'rate',
options: [
{ label: "Рейтинг", value: "rate" },
{ label: "Имя", value: "name" },
{ label: "Просмотры", value: "views" },
{ label: "Дате добавления", value: "created_at" },
{ label: "Дате обновления", value: "last_chapter_at" },
{ label: "Количество глав", value: "chap_count" },
{ label: 'Рейтинг', value: 'rate' },
{ label: 'Имя', value: 'name' },
{ label: 'Просмотры', value: 'views' },
{ label: 'Дате добавления', value: 'created_at' },
{ label: 'Дате обновления', value: 'last_chapter_at' },
{ label: 'Количество глав', value: 'chap_count' },
],
type: FilterTypes.Picker,
},
order: {
label: "Порядок",
value: "desc",
label: 'Порядок',
value: 'desc',
options: [
{ label: "По убыванию", value: "desc" },
{ label: "По возрастанию", value: "asc" },
{ label: 'По убыванию', value: 'desc' },
{ label: 'По возрастанию', value: 'asc' },
],
type: FilterTypes.Picker,
},
type: {
label: "Тип",
label: 'Тип',
value: [],
options: [
{ label: "Авторский", value: "14" },
{ label: "Английский", value: "13" },
{ label: "Китай", value: "12" },
{ label: "Корея", value: "11" },
{ label: "Фанфик", value: "15" },
{ label: "Япония", value: "10" },
{ label: 'Авторский', value: '14' },
{ label: 'Английский', value: '13' },
{ label: 'Китай', value: '12' },
{ label: 'Корея', value: '11' },
{ label: 'Фанфик', value: '15' },
{ label: 'Япония', value: '10' },
],
type: FilterTypes.CheckboxGroup,
},
format: {
label: "Формат выпуска",
label: 'Формат выпуска',
value: { include: [], exclude: [] },
options: [
{ label: "4-кома (Ёнкома)", value: "1" },
{ label: "В цвете", value: "4" },
{ label: "Веб", value: "6" },
{ label: "Вебтун", value: "7" },
{ label: "Додзинси", value: "3" },
{ label: "Сборник", value: "2" },
{ label: "Сингл", value: "5" },
{ label: '4-кома (Ёнкома)', value: '1' },
{ label: 'В цвете', value: '4' },
{ label: 'Веб', value: '6' },
{ label: 'Вебтун', value: '7' },
{ label: 'Додзинси', value: '3' },
{ label: 'Сборник', value: '2' },
{ label: 'Сингл', value: '5' },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
status: {
label: "Статус перевода",
label: 'Статус перевода',
value: [],
options: [
{ label: "Продолжается", value: "1" },
{ label: "Завершен", value: "2" },
{ label: "Заморожен", value: "3" },
{ label: "Заброшен", value: "4" },
{ label: 'Продолжается', value: '1' },
{ label: 'Завершен', value: '2' },
{ label: 'Заморожен', value: '3' },
{ label: 'Заброшен', value: '4' },
],
type: FilterTypes.CheckboxGroup,
},
manga_status: {
label: "Статус тайтла",
label: 'Статус тайтла',
value: [],
options: [
{ label: "Онгоинг", value: "1" },
{ label: "Завершён", value: "2" },
{ label: "Анонс", value: "3" },
{ label: "Приостановлен", value: "4" },
{ label: "Выпуск прекращён", value: "5" },
{ label: 'Онгоинг', value: '1' },
{ label: 'Завершён', value: '2' },
{ label: 'Анонс', value: '3' },
{ label: 'Приостановлен', value: '4' },
{ label: 'Выпуск прекращён', value: '5' },
],
type: FilterTypes.CheckboxGroup,
},
genres: {
label: "Жанры",
label: 'Жанры',
value: { include: [], exclude: [] },
options: [
{ label: "Арт", value: "32" },
{ label: "Безумие", value: "91" },
{ label: "Боевик", value: "34" },
{ label: "Боевые искусства", value: "35" },
{ label: "Вампиры", value: "36" },
{ label: "Военное", value: "89" },
{ label: "Гарем", value: "37" },
{ label: "Гендерная интрига", value: "38" },
{ label: "Героическое фэнтези", value: "39" },
{ label: "Демоны", value: "81" },
{ label: "Детектив", value: "40" },
{ label: "Детское", value: "88" },
{ label: "Дзёсэй", value: "41" },
{ label: "Драма", value: "43" },
{ label: "Игра", value: "44" },
{ label: "Исекай", value: "79" },
{ label: "История", value: "45" },
{ label: "Киберпанк", value: "46" },
{ label: "Кодомо", value: "76" },
{ label: "Комедия", value: "47" },
{ label: "Космос", value: "83" },
{ label: "Магия", value: "85" },
{ label: "Махо-сёдзё", value: "48" },
{ label: "Машины", value: "90" },
{ label: "Меха", value: "49" },
{ label: "Мистика", value: "50" },
{ label: "Музыка", value: "80" },
{ label: "Научная фантастика", value: "51" },
{ label: "Омегаверс", value: "77" },
{ label: "Пародия", value: "86" },
{ label: "Повседневность", value: "52" },
{ label: "Полиция", value: "82" },
{ label: "Постапокалиптика", value: "53" },
{ label: "Приключения", value: "54" },
{ label: "Психология", value: "55" },
{ label: "Романтика", value: "56" },
{ label: "Самурайский боевик", value: "57" },
{ label: "Сверхъестественное", value: "58" },
{ label: "Сёдзё", value: "59" },
{ label: "Сёдзё-ай", value: "60" },
{ label: "Сёнэн", value: "61" },
{ label: "Сёнэн-ай", value: "62" },
{ label: "Спорт", value: "63" },
{ label: "Супер сила", value: "87" },
{ label: "Сэйнэн", value: "64" },
{ label: "Трагедия", value: "65" },
{ label: "Триллер", value: "66" },
{ label: "Ужасы", value: "67" },
{ label: "Фантастика", value: "68" },
{ label: "Фэнтези", value: "69" },
{ label: "Школа", value: "70" },
{ label: "Эротика", value: "71" },
{ label: "Этти", value: "72" },
{ label: "Юри", value: "73" },
{ label: "Яой", value: "74" },
{ label: 'Арт', value: '32' },
{ label: 'Безумие', value: '91' },
{ label: 'Боевик', value: '34' },
{ label: 'Боевые искусства', value: '35' },
{ label: 'Вампиры', value: '36' },
{ label: 'Военное', value: '89' },
{ label: 'Гарем', value: '37' },
{ label: 'Гендерная интрига', value: '38' },
{ label: 'Героическое фэнтези', value: '39' },
{ label: 'Демоны', value: '81' },
{ label: 'Детектив', value: '40' },
{ label: 'Детское', value: '88' },
{ label: 'Дзёсэй', value: '41' },
{ label: 'Драма', value: '43' },
{ label: 'Игра', value: '44' },
{ label: 'Исекай', value: '79' },
{ label: 'История', value: '45' },
{ label: 'Киберпанк', value: '46' },
{ label: 'Кодомо', value: '76' },
{ label: 'Комедия', value: '47' },
{ label: 'Космос', value: '83' },
{ label: 'Магия', value: '85' },
{ label: 'Махо-сёдзё', value: '48' },
{ label: 'Машины', value: '90' },
{ label: 'Меха', value: '49' },
{ label: 'Мистика', value: '50' },
{ label: 'Музыка', value: '80' },
{ label: 'Научная фантастика', value: '51' },
{ label: 'Омегаверс', value: '77' },
{ label: 'Пародия', value: '86' },
{ label: 'Повседневность', value: '52' },
{ label: 'Полиция', value: '82' },
{ label: 'Постапокалиптика', value: '53' },
{ label: 'Приключения', value: '54' },
{ label: 'Психология', value: '55' },
{ label: 'Романтика', value: '56' },
{ label: 'Самурайский боевик', value: '57' },
{ label: 'Сверхъестественное', value: '58' },
{ label: 'Сёдзё', value: '59' },
{ label: 'Сёдзё-ай', value: '60' },
{ label: 'Сёнэн', value: '61' },
{ label: 'Сёнэн-ай', value: '62' },
{ label: 'Спорт', value: '63' },
{ label: 'Супер сила', value: '87' },
{ label: 'Сэйнэн', value: '64' },
{ label: 'Трагедия', value: '65' },
{ label: 'Триллер', value: '66' },
{ label: 'Ужасы', value: '67' },
{ label: 'Фантастика', value: '68' },
{ label: 'Фэнтези', value: '69' },
{ label: 'Школа', value: '70' },
{ label: 'Эротика', value: '71' },
{ label: 'Этти', value: '72' },
{ label: 'Юри', value: '73' },
{ label: 'Яой', value: '74' },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
tags: {
label: "Теги",
label: 'Теги',
value: { include: [], exclude: [] },
options: [
{ label: "Авантюристы", value: "328" },
{ label: "Антигерой", value: "176" },
{ label: "Бессмертные", value: "333" },
{ label: "Боги", value: "218" },
{ label: "Борьба за власть", value: "309" },
{ label: "Брат и сестра", value: "360" },
{ label: "Ведьма", value: "339" },
{ label: "Видеоигры", value: "204" },
{ label: "Виртуальная реальность", value: "214" },
{ label: "Владыка демонов", value: "349" },
{ label: "Военные", value: "198" },
{ label: "Воспоминания из другого мира", value: "310" },
{ label: "Выживание", value: "212" },
{ label: "ГГ женщина", value: "294" },
{ label: "ГГ имба", value: "292" },
{ label: "ГГ мужчина", value: "295" },
{ label: "ГГ не ояш", value: "325" },
{ label: "ГГ не человек", value: "331" },
{ label: "ГГ ояш", value: "326" },
{ label: "Главный герой бог", value: "324" },
{ label: "Глупый ГГ", value: "298" },
{ label: "Горничные", value: "171" },
{ label: "Гуро", value: "306" },
{ label: "Гяру", value: "197" },
{ label: "Демоны", value: "157" },
{ label: "Драконы", value: "313" },
{ label: "Древний мир", value: "317" },
{ label: "Зверолюди", value: "163" },
{ label: "Зомби", value: "155" },
{ label: "Исторические фигуры", value: "323" },
{ label: "Кулинария", value: "158" },
{ label: "Культивация", value: "161" },
{ label: "ЛГБТ", value: "344" },
{ label: "ЛитРПГ", value: "319" },
{ label: "Лоли", value: "206" },
{ label: "Магия", value: "170" },
{ label: "Машинный перевод", value: "345" },
{ label: "Медицина", value: "159" },
{ label: "Межгалактическая война", value: "330" },
{ label: "Монстр Девушки", value: "207" },
{ label: "Монстры", value: "208" },
{ label: "Мрачный мир", value: "316" },
{ label: "Музыка", value: "358" },
{ label: "Музыка", value: "209" },
{ label: "Ниндзя", value: "199" },
{ label: "Обратный Гарем", value: "210" },
{ label: "Офисные Работники", value: "200" },
{ label: "Пираты", value: "341" },
{ label: "Подземелья", value: "314" },
{ label: "Политика", value: "311" },
{ label: "Полиция", value: "201" },
{ label: "Преступники / Криминал", value: "205" },
{ label: "Призраки / Духи", value: "196" },
{ label: "Призыватели", value: "329" },
{ label: "Прыжки между мирами", value: "321" },
{ label: "Путешествие в другой мир", value: "318" },
{ label: "Путешествие во времени", value: "213" },
{ label: "Рабы", value: "355" },
{ label: "Ранги силы", value: "312" },
{ label: "Реинкарнация", value: "154" },
{ label: "Самураи", value: "202" },
{ label: "Скрытие личности", value: "315" },
{ label: "Средневековье", value: "174" },
{ label: "Традиционные игры", value: "203" },
{ label: "Умный ГГ", value: "303" },
{ label: "Характерный рост", value: "332" },
{ label: "Хикикомори", value: "167" },
{ label: "Эволюция", value: "322" },
{ label: "Элементы РПГ", value: "327" },
{ label: "Эльфы", value: "217" },
{ label: "Якудза", value: "165" },
{ label: 'Авантюристы', value: '328' },
{ label: 'Антигерой', value: '176' },
{ label: 'Бессмертные', value: '333' },
{ label: 'Боги', value: '218' },
{ label: 'Борьба за власть', value: '309' },
{ label: 'Брат и сестра', value: '360' },
{ label: 'Ведьма', value: '339' },
{ label: 'Видеоигры', value: '204' },
{ label: 'Виртуальная реальность', value: '214' },
{ label: 'Владыка демонов', value: '349' },
{ label: 'Военные', value: '198' },
{ label: 'Воспоминания из другого мира', value: '310' },
{ label: 'Выживание', value: '212' },
{ label: 'ГГ женщина', value: '294' },
{ label: 'ГГ имба', value: '292' },
{ label: 'ГГ мужчина', value: '295' },
{ label: 'ГГ не ояш', value: '325' },
{ label: 'ГГ не человек', value: '331' },
{ label: 'ГГ ояш', value: '326' },
{ label: 'Главный герой бог', value: '324' },
{ label: 'Глупый ГГ', value: '298' },
{ label: 'Горничные', value: '171' },
{ label: 'Гуро', value: '306' },
{ label: 'Гяру', value: '197' },
{ label: 'Демоны', value: '157' },
{ label: 'Драконы', value: '313' },
{ label: 'Древний мир', value: '317' },
{ label: 'Зверолюди', value: '163' },
{ label: 'Зомби', value: '155' },
{ label: 'Исторические фигуры', value: '323' },
{ label: 'Кулинария', value: '158' },
{ label: 'Культивация', value: '161' },
{ label: 'ЛГБТ', value: '344' },
{ label: 'ЛитРПГ', value: '319' },
{ label: 'Лоли', value: '206' },
{ label: 'Магия', value: '170' },
{ label: 'Машинный перевод', value: '345' },
{ label: 'Медицина', value: '159' },
{ label: 'Межгалактическая война', value: '330' },
{ label: 'Монстр Девушки', value: '207' },
{ label: 'Монстры', value: '208' },
{ label: 'Мрачный мир', value: '316' },
{ label: 'Музыка', value: '358' },
{ label: 'Музыка', value: '209' },
{ label: 'Ниндзя', value: '199' },
{ label: 'Обратный Гарем', value: '210' },
{ label: 'Офисные Работники', value: '200' },
{ label: 'Пираты', value: '341' },
{ label: 'Подземелья', value: '314' },
{ label: 'Политика', value: '311' },
{ label: 'Полиция', value: '201' },
{ label: 'Преступники / Криминал', value: '205' },
{ label: 'Призраки / Духи', value: '196' },
{ label: 'Призыватели', value: '329' },
{ label: 'Прыжки между мирами', value: '321' },
{ label: 'Путешествие в другой мир', value: '318' },
{ label: 'Путешествие во времени', value: '213' },
{ label: 'Рабы', value: '355' },
{ label: 'Ранги силы', value: '312' },
{ label: 'Реинкарнация', value: '154' },
{ label: 'Самураи', value: '202' },
{ label: 'Скрытие личности', value: '315' },
{ label: 'Средневековье', value: '174' },
{ label: 'Традиционные игры', value: '203' },
{ label: 'Умный ГГ', value: '303' },
{ label: 'Характерный рост', value: '332' },
{ label: 'Хикикомори', value: '167' },
{ label: 'Эволюция', value: '322' },
{ label: 'Элементы РПГ', value: '327' },
{ label: 'Эльфы', value: '217' },
{ label: 'Якудза', value: '165' },
],
type: FilterTypes.ExcludableCheckboxGroup,
},
require_chapters: {
label: "Только проекты с главами",
label: 'Только проекты с главами',
value: true,
type: FilterTypes.Switch,
},
+43 -43
View File
@@ -1,43 +1,43 @@
import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { defaultCover } from "@libs/defaultCover";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import dayjs from "dayjs";
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { defaultCover } from '@libs/defaultCover';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
class RNRF implements Plugin.PluginBase {
id = "RNRF";
name = "РанобэРФ";
site = "https://ранобэ.рф";
version = "1.0.0";
icon = "src/ru/ranoberf/icon.png";
id = 'RNRF';
name = 'РанобэРФ';
site = 'https://ранобэ.рф';
version = '1.0.0';
icon = 'src/ru/ranoberf/icon.png';
async popularNovels(
pageNo: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
let url = this.site + "/books?order=";
let url = this.site + '/books?order=';
url += showLatestNovels
? "lastPublishedChapter"
: filters?.sort?.value || "popular";
url += "&page=" + pageNo;
? 'lastPublishedChapter'
: filters?.sort?.value || 'popular';
url += '&page=' + pageNo;
const result = await fetchApi(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
const json: response = JSON.parse(jsonRaw || "{}");
const jsonRaw = loadedCheerio('#__NEXT_DATA__').html();
const json: response = JSON.parse(jsonRaw || '{}');
const novels: Plugin.NovelItem[] = [];
json.props.pageProps?.totalData?.items?.forEach((novel) =>
json.props.pageProps?.totalData?.items?.forEach(novel =>
novels.push({
name: novel.title,
cover: novel?.verticalImage?.url
? this.site + novel.verticalImage.url
: defaultCover,
path: "/" + novel.slug,
path: '/' + novel.slug,
}),
);
@@ -49,19 +49,19 @@ class RNRF implements Plugin.PluginBase {
const body = await result.text();
const loadedCheerio = parseHTML(body);
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
const book = (JSON.parse(jsonRaw || "{}") as response).props.pageProps.book;
const jsonRaw = loadedCheerio('#__NEXT_DATA__').html();
const book = (JSON.parse(jsonRaw || '{}') as response).props.pageProps.book;
const novel: Plugin.SourceNovel = {
path: novelPath,
name: book?.title || "",
name: book?.title || '',
cover: book?.verticalImage?.url
? this.site + book.verticalImage.url
: defaultCover,
summary: book?.description,
author: book?.author || "",
genres: book?.genres.map((item) => item.title).join(", "),
status: book?.additionalInfo.includes("Активен")
author: book?.author || '',
genres: book?.genres.map(item => item.title).join(', '),
status: book?.additionalInfo.includes('Активен')
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
@@ -73,7 +73,7 @@ class RNRF implements Plugin.PluginBase {
chapters.push({
name: chapter.title,
path: chapter.url,
releaseTime: dayjs(chapter.publishedAt).format("LLL"),
releaseTime: dayjs(chapter.publishedAt).format('LLL'),
chapterNumber: book.chapters.length - chapterIndex,
});
}
@@ -84,22 +84,22 @@ class RNRF implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath).then((res) =>
const result = await fetchApi(this.site + chapterPath).then(res =>
res.text(),
);
let loadedCheerio = parseHTML(result);
const jsonRaw = loadedCheerio("#__NEXT_DATA__").html();
const json: response = JSON.parse(jsonRaw || "{}");
const jsonRaw = loadedCheerio('#__NEXT_DATA__').html();
const json: response = JSON.parse(jsonRaw || '{}');
loadedCheerio = parseHTML(
json.props.pageProps?.chapter?.content?.text || "",
json.props.pageProps?.chapter?.content?.text || '',
);
loadedCheerio("img").each((index, element) => {
if (!loadedCheerio(element).attr("src")?.startsWith("http")) {
const src = loadedCheerio(element).attr("src");
loadedCheerio(element).attr("src", this.site + src);
loadedCheerio('img').each((index, element) => {
if (!loadedCheerio(element).attr('src')?.startsWith('http')) {
const src = loadedCheerio(element).attr('src');
loadedCheerio(element).attr('src', this.site + src);
}
});
@@ -113,13 +113,13 @@ class RNRF implements Plugin.PluginBase {
const body = (await result.json()) as { items: Item[] };
const novels: Plugin.NovelItem[] = [];
body.items.forEach((novel) =>
body.items.forEach(novel =>
novels.push({
name: novel.title,
cover: novel?.verticalImage?.url
? this.site + novel.verticalImage.url
: defaultCover,
path: "/" + novel.slug,
path: '/' + novel.slug,
}),
);
@@ -130,13 +130,13 @@ class RNRF implements Plugin.PluginBase {
filters = {
sort: {
label: "Сортировка",
value: "popular",
label: 'Сортировка',
value: 'popular',
options: [
{ label: "Рейтинг", value: "popular" },
{ label: "Дате добавления", value: "new" },
{ label: "Дате обновления", value: "lastPublishedChapter" },
{ label: "Законченные", value: "completed" },
{ label: 'Рейтинг', value: 'popular' },
{ label: 'Дате добавления', value: 'new' },
{ label: 'Дате обновления', value: 'lastPublishedChapter' },
{ label: 'Законченные', value: 'completed' },
],
type: FilterTypes.Picker,
},
File diff suppressed because it is too large Load Diff
+244 -241
View File
@@ -1,17 +1,17 @@
import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import dayjs from "dayjs";
import qs from "qs";
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
import qs from 'qs';
class Zelluloza implements Plugin.PluginBase {
id = "zelluloza";
name = "Целлюлоза";
site = "https://zelluloza.ru";
version = "1.0.0";
icon = "src/ru/zelluloza/icon.png";
id = 'zelluloza';
name = 'Целлюлоза';
site = 'https://zelluloza.ru';
version = '1.0.0';
icon = 'src/ru/zelluloza/icon.png';
async popularNovels(
pageNo: number,
@@ -20,36 +20,36 @@ class Zelluloza implements Plugin.PluginBase {
showLatestNovels,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
const sort = showLatestNovels ? "0.0" : filters?.sort?.value || "6.0";
const genres = filters?.genres?.value || "";
const sort = showLatestNovels ? '0.0' : filters?.sort?.value || '6.0';
const genres = filters?.genres?.value || '';
const body = await fetchApi(this.site + "/ajaxcall/", {
const body = await fetchApi(this.site + '/ajaxcall/', {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Referer: this.site + "/top/freebooks/",
'Content-Type': 'application/x-www-form-urlencoded',
Referer: this.site + '/top/freebooks/',
Origin: this.site,
},
method: "POST",
method: 'POST',
body: qs.stringify({
op: "morebooks",
par1: "",
op: 'morebooks',
par1: '',
par2: `125:0:${genres}:0.${sort}.0.0.0.0.0.0.0.0.0.1.s.1..:${pageNo}`,
par4: "",
par4: '',
}),
}).then((res) => res.text());
}).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('div[style="display: flex"]').each((index, element) => {
novels.push({
name: loadedCheerio(element).find('span[itemprop="name"]').text() || "",
name: loadedCheerio(element).find('span[itemprop="name"]').text() || '',
cover:
this.site +
loadedCheerio(element).find('img[class="shadow"]').attr("src"),
loadedCheerio(element).find('img[class="shadow"]').attr('src'),
path: (
loadedCheerio(element).find('img[class="shadow"]').attr("onclick") ||
""
).replace(/\D/g, ""),
loadedCheerio(element).find('img[class="shadow"]').attr('onclick') ||
''
).replace(/\D/g, ''),
});
});
@@ -57,20 +57,21 @@ class Zelluloza implements Plugin.PluginBase {
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.resolveUrl(novelPath, true))
.then((res) => res.text());
const body = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio('span[itemprop="name"]').text(),
cover: this.site + loadedCheerio('img[class="shadow"]').attr("src"),
genres: loadedCheerio(".gnres").text()?.split?.(": ")?.[1],
cover: this.site + loadedCheerio('img[class="shadow"]').attr('src'),
genres: loadedCheerio('.gnres').text()?.split?.(': ')?.[1],
summary:
loadedCheerio("#bann_full").text() ||
loadedCheerio("#bann_short").text(),
author: loadedCheerio(".author_link").text(),
status: loadedCheerio(".tech_decription").text().includes("Пишется")
loadedCheerio('#bann_full').text() ||
loadedCheerio('#bann_short').text(),
author: loadedCheerio('.author_link').text(),
status: loadedCheerio('.tech_decription').text().includes('Пишется')
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
@@ -78,7 +79,9 @@ class Zelluloza implements Plugin.PluginBase {
const chapters: Plugin.ChapterItem[] = [];
loadedCheerio('ul[class="g0"] div[class="w800_m"]').each(
(chapterIndex, element) => {
const isFree = loadedCheerio(element).find('div[class="chaptfree"]').length;
const isFree = loadedCheerio(element).find(
'div[class="chaptfree"]',
).length;
if (isFree) {
const chapter = loadedCheerio(element).find('a[class="chptitle"]');
const releaseDate = loadedCheerio(element)
@@ -86,7 +89,7 @@ class Zelluloza implements Plugin.PluginBase {
.text();
chapters.push({
name: chapter.text().trim(),
path: (chapter.attr("href") || "").split("/").slice(2, 4).join("/"),
path: (chapter.attr('href') || '').split('/').slice(2, 4).join('/'),
releaseTime: this.parseDate(releaseDate),
chapterNumber: chapterIndex + 1,
});
@@ -99,33 +102,33 @@ class Zelluloza implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const [bookID, chapterID] = chapterPath.split("/");
const body = await fetchApi(this.site + "/ajaxcall/", {
const [bookID, chapterID] = chapterPath.split('/');
const body = await fetchApi(this.site + '/ajaxcall/', {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
'Content-Type': 'application/x-www-form-urlencoded',
Referer: this.resolveUrl(chapterPath),
Origin: this.site,
},
method: "POST",
method: 'POST',
body: qs.stringify({
op: "getbook",
op: 'getbook',
par1: bookID,
par2: chapterID,
}),
}).then((res) => res.text());
}).then(res => res.text());
const encrypted = body.split("<END>")[0].split("\n");
const encrypted = body.split('<END>')[0].split('\n');
const decrypted = encrypted
.map((str) => decrypt(str))
.join("")
.replace(/\r/g, "")
.map(str => decrypt(str))
.join('')
.replace(/\r/g, '')
.trim();
const chapterText = decrypted //cosmetic filters
.replace(/\[\*]([\s\S]*?)\[\/]/g, "<b>$1</b>")
.replace(/\[_]([\s\S]*?)\[\/]/g, "<u>$1</u>")
.replace(/\[-]([\s\S]*?)\[\/]/g, "<s>$1</s>")
.replace(/\[~]([\s\S]*?)\[\/]/g, "<i>$1</i>"); /*
.replace(/\[\*]([\s\S]*?)\[\/]/g, '<b>$1</b>')
.replace(/\[_]([\s\S]*?)\[\/]/g, '<u>$1</u>')
.replace(/\[-]([\s\S]*?)\[\/]/g, '<s>$1</s>')
.replace(/\[~]([\s\S]*?)\[\/]/g, '<i>$1</i>'); /*
.replace(/\[!]([\s\S]*?)\[\/]/g, '<span style="font-family: Times New Roman">$1</span>')
.replace(/\[ctr]([\s\S]*?)\[\/]/g, "<center>$1</center>")
.replace(/\[rht]([\s\S]*?)\[\/]/g, '<span style="text-align: right">$1</span>')
@@ -143,47 +146,47 @@ class Zelluloza implements Plugin.PluginBase {
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site + "/ajaxcall/", {
const body = await fetchApi(this.site + '/ajaxcall/', {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Referer: this.site + "/search/done/#result",
'Content-Type': 'application/x-www-form-urlencoded',
Referer: this.site + '/search/done/#result',
Origin: this.site,
},
method: "POST",
method: 'POST',
body: qs.stringify({
op: "morebooks",
op: 'morebooks',
par1: searchTerm,
par2: "206:0:0:0.0.0.0.0.0.0.10.0.0.0.0.0..0..:" + pageNo,
par4: "",
par2: '206:0:0:0.0.0.0.0.0.0.10.0.0.0.0.0..0..:' + pageNo,
par4: '',
}),
}).then((res) => res.text());
}).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio('div[style="display: flex"]').each((index, element) => {
novels.push({
name: loadedCheerio(element).find('span[itemprop="name"]').text() || "",
name: loadedCheerio(element).find('span[itemprop="name"]').text() || '',
cover:
this.site +
loadedCheerio(element).find('img[class="shadow"]').attr("src"),
loadedCheerio(element).find('img[class="shadow"]').attr('src'),
path: (
loadedCheerio(element).find('img[class="shadow"]').attr("onclick") ||
""
).replace(/\D/g, ""),
loadedCheerio(element).find('img[class="shadow"]').attr('onclick') ||
''
).replace(/\D/g, ''),
});
});
return novels;
}
parseDate = (dateString: string | undefined = "") => {
parseDate = (dateString: string | undefined = '') => {
const [, day, month, year, hour, minute] =
dateString.match(/(\d{2})\.(\d{2})\.(\d{4})г (\d{2}):(\d{2})/) || [];
if (day && month && year && hour && minute) {
return dayjs(
year + "-" + month + "-" + day + " " + hour + ":" + minute,
).format("LLL");
year + '-' + month + '-' + day + ' ' + hour + ':' + minute,
).format('LLL');
}
return null;
@@ -191,167 +194,167 @@ class Zelluloza implements Plugin.PluginBase {
fetchImage = fetchFile;
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + "/books/" + path;
this.site + '/books/' + path;
filters = {
sort: {
label: "Сортировка",
value: "6.0",
label: 'Сортировка',
value: '6.0',
options: [
{ label: "Топ-недели", value: "6.0" },
{ label: "Бестселеры", value: "5.0" },
{ label: "Популярные у читателей", value: "2.0" },
{ label: "Книжные новинки", value: "0.0" },
{ label: "Законченные книги", value: "0.1" },
{ label: 'Топ-недели', value: '6.0' },
{ label: 'Бестселеры', value: '5.0' },
{ label: 'Популярные у читателей', value: '2.0' },
{ label: 'Книжные новинки', value: '0.0' },
{ label: 'Законченные книги', value: '0.1' },
],
type: FilterTypes.Picker,
},
genres: {
label: "Жанры",
value: "",
label: 'Жанры',
value: '',
options: [
{ label: "Все", value: "" },
{ label: "16+", value: "204" },
{ label: "18+", value: "100" },
{ label: "Авантюра", value: "347" },
{ label: "Альтернативная история", value: "55" },
{ label: "Ангст", value: "463" },
{ label: "Аниме", value: "405" },
{ label: "Антиутопия", value: "21" },
{ label: "Апокалипсис", value: "368" },
{ label: "Аудиокнига", value: "391" },
{ label: "Байки", value: "435" },
{ label: "Бизнес-книги", value: "522" },
{ label: "Биография", value: "52" },
{ label: "Биопанк", value: "379" },
{ label: "Боевая фантастика", value: "512" },
{ label: "Боевик", value: "61" },
{ label: "Боевые искусства", value: "142" },
{ label: "Бояръ-аниме", value: "433" },
{ label: "Вбоквелл", value: "372" },
{ label: "Вестерн", value: "96" },
{ label: "Война миров", value: "137" },
{ label: "Восточное фэнтези", value: "505" },
{ label: "Городское фентези", value: "63" },
{ label: "Городское фэнтези", value: "520" },
{ label: "Готика", value: "83" },
{ label: "Детективы", value: "4" },
{ label: "Детские книги", value: "8" },
{ label: "Документальные книги", value: "351" },
{ label: "Дом и дача", value: "534" },
{ label: "Дорожные истории", value: "41" },
{ label: "Драма", value: "47" },
{ label: "Древняя литература", value: "529" },
{ label: "Женский психологический роман", value: "186" },
{ label: "Женский роман", value: "141" },
{ label: "Зарубежная классика", value: "524" },
{ label: "Здоровье и красота", value: "533" },
{ label: "Изучение языков", value: "537" },
{ label: "Иронический детектив", value: "85" },
{ label: "Исследования", value: "156" },
{ label: "Историческая литература", value: "3" },
{ label: "Исторический любовный роман", value: "518" },
{ label: "Историческое фэнтези", value: "90" },
{ label: "Киберпанк", value: "7" },
{ label: "Классическая литература", value: "9" },
{ label: "Книга-игра", value: "371" },
{ label: "Книги для родителей", value: "535" },
{ label: "Комедия", value: "80" },
{ label: "Конкурсные сборники", value: "349" },
{ label: "Короткий любовный роман", value: "517" },
{ label: "Космическая фантастика", value: "513" },
{ label: "Космоопера", value: "58" },
{ label: "Криминальная проза", value: "35" },
{ label: "Криптоистория", value: "400" },
{ label: "Ксенофантастика", value: "102" },
{ label: "Культура и искусство", value: "536" },
{ label: "Легенды", value: "161" },
{ label: "Лирика", value: "49" },
{ label: "ЛитРПГ", value: "62" },
{ label: "Любовная фантастика", value: "516" },
{ label: "Любовное фэнтези", value: "515" },
{ label: "Любовный роман", value: "46" },
{ label: "Магический реализм", value: "42" },
{ label: "Матриархат", value: "385" },
{ label: "Медицина", value: "57" },
{ label: "Мелодрама", value: "45" },
{ label: "Мемуары", value: "532" },
{ label: "Метафизика", value: "430" },
{ label: "Милитари", value: "126" },
{ label: "Мистика", value: "22" },
{ label: "Морские приключения", value: "211" },
{ label: "Мотивация", value: "135" },
{ label: "Научная фантастика", value: "122" },
{ label: "Научно-популярная", value: "43" },
{ label: "Ненаучная фантастика", value: "387" },
{ label: "Новелла", value: "498" },
{ label: "Нон-фикшн", value: "507" },
{ label: "Нуар", value: "121" },
{ label: "О войне", value: "136" },
{ label: "Оккультизм", value: "424" },
{ label: "Параллельные миры", value: "438" },
{ label: "Пародия", value: "108" },
{ label: "Подростковая фантастика", value: "27" },
{ label: "Подростковый роман", value: "388" },
{ label: "Подростковый ужастик", value: "110" },
{ label: "Постапокалипсис", value: "82" },
{ label: "Постапокалиптика", value: "67" },
{ label: "Поэзия", value: "44" },
{ label: "Превращение", value: "170" },
{ label: "Приключение", value: "497" },
{ label: "Приключения", value: "5" },
{ label: "Природа и животные", value: "538" },
{ label: "Проза", value: "95" },
{ label: "Производственный роман", value: "148" },
{ label: "Психология", value: "31" },
{ label: "Путешествия", value: "383" },
{ label: "Рассказы", value: "101" },
{ label: "Реализм", value: "422" },
{ label: "РеалРПГ", value: "442" },
{ label: "Религия", value: "188" },
{ label: "Робинзонада", value: "434" },
{ label: "Романтика", value: "130" },
{ label: "Романтическая фантастика", value: "501" },
{ label: "Русская классика", value: "523" },
{ label: "Рыцарский роман", value: "112" },
{ label: "Символизм", value: "439" },
{ label: "Сказки", value: "93" },
{ label: "Славянское фэнтези", value: "508" },
{ label: "Сновидения", value: "428" },
{ label: "Современный любовный роман", value: "519" },
{ label: "Социально-психологическая фантастика", value: "98" },
{ label: "Спорт", value: "147" },
{ label: "Справочники", value: "528" },
{ label: "Стимпанк", value: "56" },
{ label: "Субкультура", value: "209" },
{ label: "Сюрреализм", value: "26" },
{ label: "Тёмное фэнтези", value: "437" },
{ label: "Техномагия", value: "159" },
{ label: "Триллер", value: "6" },
{ label: "Трэш", value: "162" },
{ label: "Ужасы", value: "531" },
{ label: "Утопия", value: "191" },
{ label: "Учебник", value: "411" },
{ label: "Фантастика", value: "1" },
{ label: "Фантастический боевик", value: "38" },
{ label: "Фанфик", value: "111" },
{ label: "Философия", value: "461" },
{ label: "Фурри", value: "394" },
{ label: "Фэнтези", value: "2" },
{ label: "Хентай", value: "441" },
{ label: "Хобби и досуг", value: "530" },
{ label: "Хоррор", value: "86" },
{ label: "Чёрный юмор", value: "73" },
{ label: "Эзотерика", value: "30" },
{ label: "Экшн", value: "94" },
{ label: "Эпическое фэнтези", value: "499" },
{ label: "Эротика", value: "19" },
{ label: "Эссе", value: "353" },
{ label: "ЭТТИ", value: "502" },
{ label: "Юмористическая литература", value: "20" },
{ label: "Юмористическая фантастика", value: "514" },
{ label: "Юмористическое фэнтези", value: "521" },
{ label: "EVE - Миры Содружества", value: "99" },
{ label: 'Все', value: '' },
{ label: '16+', value: '204' },
{ label: '18+', value: '100' },
{ label: 'Авантюра', value: '347' },
{ label: 'Альтернативная история', value: '55' },
{ label: 'Ангст', value: '463' },
{ label: 'Аниме', value: '405' },
{ label: 'Антиутопия', value: '21' },
{ label: 'Апокалипсис', value: '368' },
{ label: 'Аудиокнига', value: '391' },
{ label: 'Байки', value: '435' },
{ label: 'Бизнес-книги', value: '522' },
{ label: 'Биография', value: '52' },
{ label: 'Биопанк', value: '379' },
{ label: 'Боевая фантастика', value: '512' },
{ label: 'Боевик', value: '61' },
{ label: 'Боевые искусства', value: '142' },
{ label: 'Бояръ-аниме', value: '433' },
{ label: 'Вбоквелл', value: '372' },
{ label: 'Вестерн', value: '96' },
{ label: 'Война миров', value: '137' },
{ label: 'Восточное фэнтези', value: '505' },
{ label: 'Городское фентези', value: '63' },
{ label: 'Городское фэнтези', value: '520' },
{ label: 'Готика', value: '83' },
{ label: 'Детективы', value: '4' },
{ label: 'Детские книги', value: '8' },
{ label: 'Документальные книги', value: '351' },
{ label: 'Дом и дача', value: '534' },
{ label: 'Дорожные истории', value: '41' },
{ label: 'Драма', value: '47' },
{ label: 'Древняя литература', value: '529' },
{ label: 'Женский психологический роман', value: '186' },
{ label: 'Женский роман', value: '141' },
{ label: 'Зарубежная классика', value: '524' },
{ label: 'Здоровье и красота', value: '533' },
{ label: 'Изучение языков', value: '537' },
{ label: 'Иронический детектив', value: '85' },
{ label: 'Исследования', value: '156' },
{ label: 'Историческая литература', value: '3' },
{ label: 'Исторический любовный роман', value: '518' },
{ label: 'Историческое фэнтези', value: '90' },
{ label: 'Киберпанк', value: '7' },
{ label: 'Классическая литература', value: '9' },
{ label: 'Книга-игра', value: '371' },
{ label: 'Книги для родителей', value: '535' },
{ label: 'Комедия', value: '80' },
{ label: 'Конкурсные сборники', value: '349' },
{ label: 'Короткий любовный роман', value: '517' },
{ label: 'Космическая фантастика', value: '513' },
{ label: 'Космоопера', value: '58' },
{ label: 'Криминальная проза', value: '35' },
{ label: 'Криптоистория', value: '400' },
{ label: 'Ксенофантастика', value: '102' },
{ label: 'Культура и искусство', value: '536' },
{ label: 'Легенды', value: '161' },
{ label: 'Лирика', value: '49' },
{ label: 'ЛитРПГ', value: '62' },
{ label: 'Любовная фантастика', value: '516' },
{ label: 'Любовное фэнтези', value: '515' },
{ label: 'Любовный роман', value: '46' },
{ label: 'Магический реализм', value: '42' },
{ label: 'Матриархат', value: '385' },
{ label: 'Медицина', value: '57' },
{ label: 'Мелодрама', value: '45' },
{ label: 'Мемуары', value: '532' },
{ label: 'Метафизика', value: '430' },
{ label: 'Милитари', value: '126' },
{ label: 'Мистика', value: '22' },
{ label: 'Морские приключения', value: '211' },
{ label: 'Мотивация', value: '135' },
{ label: 'Научная фантастика', value: '122' },
{ label: 'Научно-популярная', value: '43' },
{ label: 'Ненаучная фантастика', value: '387' },
{ label: 'Новелла', value: '498' },
{ label: 'Нон-фикшн', value: '507' },
{ label: 'Нуар', value: '121' },
{ label: 'О войне', value: '136' },
{ label: 'Оккультизм', value: '424' },
{ label: 'Параллельные миры', value: '438' },
{ label: 'Пародия', value: '108' },
{ label: 'Подростковая фантастика', value: '27' },
{ label: 'Подростковый роман', value: '388' },
{ label: 'Подростковый ужастик', value: '110' },
{ label: 'Постапокалипсис', value: '82' },
{ label: 'Постапокалиптика', value: '67' },
{ label: 'Поэзия', value: '44' },
{ label: 'Превращение', value: '170' },
{ label: 'Приключение', value: '497' },
{ label: 'Приключения', value: '5' },
{ label: 'Природа и животные', value: '538' },
{ label: 'Проза', value: '95' },
{ label: 'Производственный роман', value: '148' },
{ label: 'Психология', value: '31' },
{ label: 'Путешествия', value: '383' },
{ label: 'Рассказы', value: '101' },
{ label: 'Реализм', value: '422' },
{ label: 'РеалРПГ', value: '442' },
{ label: 'Религия', value: '188' },
{ label: 'Робинзонада', value: '434' },
{ label: 'Романтика', value: '130' },
{ label: 'Романтическая фантастика', value: '501' },
{ label: 'Русская классика', value: '523' },
{ label: 'Рыцарский роман', value: '112' },
{ label: 'Символизм', value: '439' },
{ label: 'Сказки', value: '93' },
{ label: 'Славянское фэнтези', value: '508' },
{ label: 'Сновидения', value: '428' },
{ label: 'Современный любовный роман', value: '519' },
{ label: 'Социально-психологическая фантастика', value: '98' },
{ label: 'Спорт', value: '147' },
{ label: 'Справочники', value: '528' },
{ label: 'Стимпанк', value: '56' },
{ label: 'Субкультура', value: '209' },
{ label: 'Сюрреализм', value: '26' },
{ label: 'Тёмное фэнтези', value: '437' },
{ label: 'Техномагия', value: '159' },
{ label: 'Триллер', value: '6' },
{ label: 'Трэш', value: '162' },
{ label: 'Ужасы', value: '531' },
{ label: 'Утопия', value: '191' },
{ label: 'Учебник', value: '411' },
{ label: 'Фантастика', value: '1' },
{ label: 'Фантастический боевик', value: '38' },
{ label: 'Фанфик', value: '111' },
{ label: 'Философия', value: '461' },
{ label: 'Фурри', value: '394' },
{ label: 'Фэнтези', value: '2' },
{ label: 'Хентай', value: '441' },
{ label: 'Хобби и досуг', value: '530' },
{ label: 'Хоррор', value: '86' },
{ label: 'Чёрный юмор', value: '73' },
{ label: 'Эзотерика', value: '30' },
{ label: 'Экшн', value: '94' },
{ label: 'Эпическое фэнтези', value: '499' },
{ label: 'Эротика', value: '19' },
{ label: 'Эссе', value: '353' },
{ label: 'ЭТТИ', value: '502' },
{ label: 'Юмористическая литература', value: '20' },
{ label: 'Юмористическая фантастика', value: '514' },
{ label: 'Юмористическое фэнтези', value: '521' },
{ label: 'EVE - Миры Содружества', value: '99' },
],
type: FilterTypes.Picker,
},
@@ -359,26 +362,26 @@ class Zelluloza implements Plugin.PluginBase {
}
const alphabet: { [key: string]: string } = {
"~": "0",
"H": "1",
"^": "2",
"@": "3",
"f": "4",
"0": "5",
"5": "6",
"n": "7",
"r": "8",
"=": "9",
"W": "a",
"L": "b",
"7": "c",
" ": "d",
"u": "e",
"c": "f",
'~': '0',
'H': '1',
'^': '2',
'@': '3',
'f': '4',
'0': '5',
'5': '6',
'n': '7',
'r': '8',
'=': '9',
'W': 'a',
'L': 'b',
'7': 'c',
' ': 'd',
'u': 'e',
'c': 'f',
};
function decrypt(encrypt: string) {
if (!encrypt) return "";
if (!encrypt) return '';
const hexArray = [];
for (let j = 0; j < encrypt.length; j += 2) {
@@ -387,12 +390,12 @@ function decrypt(encrypt: string) {
hexArray.push(alphabet[firstChar] + alphabet[secondChar]);
}
return "<p>" + hexToUtf8(hexArray) + "</p>";
return '<p>' + hexToUtf8(hexArray) + '</p>';
}
function hexToUtf8(hexArray: string[]) {
let index = 0;
let result = "";
let result = '';
while (index < hexArray.length) {
const currentHex = parseInt(hexArray[index], 16) & 0xff;
@@ -401,13 +404,13 @@ function hexToUtf8(hexArray: string[]) {
if (currentHex < 16) {
switch (currentHex) {
case 9:
result += " ";
result += ' ';
break;
case 13:
result += "\r";
result += '\r';
break;
case 10:
result += "\n";
result += '\n';
break;
}
} else {
+38 -33
View File
@@ -1,18 +1,21 @@
import { load as parseHTML } from "cheerio";
import { fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters } from "@libs/filterInputs";
import { load as parseHTML } from 'cheerio';
import { fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters } from '@libs/filterInputs';
class HasulTL implements Plugin.PluginBase {
id = "HasuTL";
name = "Hasu Translations";
icon = "src/es/hasutl/icon.png";
site = "https://hasutl.wordpress.com/";
id = 'HasuTL';
name = 'Hasu Translations';
icon = 'src/es/hasutl/icon.png';
site = 'https://hasutl.wordpress.com/';
filters?: Filters | undefined;
version = "1.0.0";
baseUrl = this.site
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
let url = this.baseUrl + "light-novels-activas/";
version = '1.0.0';
baseUrl = this.site;
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.baseUrl + 'light-novels-activas/';
const result = await fetch(url);
const body = await result.text();
@@ -21,13 +24,13 @@ class HasulTL implements Plugin.PluginBase {
let novels: Plugin.NovelItem[] = [];
loadedCheerio("div.wp-block-columns").each(function () {
const novelName = loadedCheerio(this).find(".wp-block-button").text();
const novelCover = loadedCheerio(this).find("img").attr("src");
loadedCheerio('div.wp-block-columns').each(function () {
const novelName = loadedCheerio(this).find('.wp-block-button').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
let novelUrl = loadedCheerio(this)
.find(".wp-block-button > a")
.attr("href");
.find('.wp-block-button > a')
.attr('href');
if (!novelUrl) return;
@@ -56,23 +59,23 @@ class HasulTL implements Plugin.PluginBase {
novel.url = novelUrl;
novel.name = loadedCheerio(".post-header").text();
novel.name = loadedCheerio('.post-header').text();
novel.cover = loadedCheerio(".featured-media > img").attr("src");
novel.cover = loadedCheerio('.featured-media > img').attr('src');
let novelSummary = loadedCheerio(".post-content").find("p").html()!;
let novelSummary = loadedCheerio('.post-content').find('p').html()!;
novel.summary = novelSummary;
let novelChapters: Plugin.ChapterItem[] = [];
loadedCheerio(".wp-block-media-text__content")
.find("a")
loadedCheerio('.wp-block-media-text__content')
.find('a')
.each(function () {
const chapterName = loadedCheerio(this).text().trim();
const releaseDate = null;
let chapterUrl = loadedCheerio(this).attr("href");
let chapterUrl = loadedCheerio(this).attr('href');
if (!chapterUrl) return;
@@ -97,11 +100,14 @@ class HasulTL implements Plugin.PluginBase {
let loadedCheerio = parseHTML(body);
let chapterText = loadedCheerio(".post-content").html() || '';
let chapterText = loadedCheerio('.post-content').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.baseUrl}?s=${searchTerm}&post_type=wp-manga`;
const result = await fetch(url);
@@ -111,16 +117,16 @@ class HasulTL implements Plugin.PluginBase {
let novels: Plugin.NovelItem[] = [];
loadedCheerio(".post-container").each(function () {
const novelName = loadedCheerio(this).find(".post-header").text();
loadedCheerio('.post-container').each(function () {
const novelName = loadedCheerio(this).find('.post-header').text();
if (
!novelName.includes("Cap") &&
!novelName.includes("Vol") &&
!novelName.includes("Light Novels")
!novelName.includes('Cap') &&
!novelName.includes('Vol') &&
!novelName.includes('Light Novels')
) {
const novelCover = loadedCheerio(this).find("img").attr("src");
const novelCover = loadedCheerio(this).find('img').attr('src');
let novelUrl = loadedCheerio(this).find("a").attr("href");
let novelUrl = loadedCheerio(this).find('a').attr('href');
if (!novelUrl) return;
@@ -139,7 +145,6 @@ class HasulTL implements Plugin.PluginBase {
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new HasulTL();
+53 -50
View File
@@ -1,17 +1,20 @@
import { load as parseHTML } from "cheerio";
import { fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters } from "@libs/filterInputs";
import { load as parseHTML } from 'cheerio';
import { fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters } from '@libs/filterInputs';
class Ligera implements Plugin.PluginBase {
id = "ligera.com";
name = "Novelas Ligera";
icon = "src/es/novelasligera/icon.png";
site = "https://novelasligera.com/";
id = 'ligera.com';
name = 'Novelas Ligera';
icon = 'src/es/novelasligera/icon.png';
site = 'https://novelasligera.com/';
filters?: Filters | undefined;
version = "1.0.0";
version = '1.0.0';
baseUrl = this.site;
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.baseUrl;
const result = await fetch(url);
@@ -21,16 +24,16 @@ class Ligera implements Plugin.PluginBase {
let novels: Plugin.NovelItem[] = [];
loadedCheerio(".elementor-column").each(function () {
loadedCheerio('.elementor-column').each(function () {
const novelName = loadedCheerio(this)
.find(".widget-image-caption.wp-caption-text")
.find('.widget-image-caption.wp-caption-text')
.text();
if (novelName) {
const novelCover = loadedCheerio(this)
.find("a > img")
.attr("data-lazy-src");
.find('a > img')
.attr('data-lazy-src');
let novelUrl = loadedCheerio(this).find("a").attr("href");
let novelUrl = loadedCheerio(this).find('a').attr('href');
if (!novelUrl) return;
const novel = {
name: novelName,
@@ -45,7 +48,7 @@ class Ligera implements Plugin.PluginBase {
return novels;
}
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const url = this.baseUrl + "novela/" + novelUrl;
const url = this.baseUrl + 'novela/' + novelUrl;
// console.log(url);
@@ -56,50 +59,48 @@ class Ligera implements Plugin.PluginBase {
let novel: Plugin.SourceNovel = { url };
novel.name = loadedCheerio("h1").text();
novel.name = loadedCheerio('h1').text();
novel.cover = loadedCheerio(".elementor-widget-container")
.find("img")
.attr("src");
novel.cover = loadedCheerio('.elementor-widget-container')
.find('img')
.attr('src');
loadedCheerio(".elementor-row")
.find("p")
loadedCheerio('.elementor-row')
.find('p')
.each(function () {
if (loadedCheerio(this).text().includes("Autor:")) {
if (loadedCheerio(this).text().includes('Autor:')) {
novel.author = loadedCheerio(this)
.text()
.replace("Autor:", "")
.replace('Autor:', '')
.trim();
}
if (loadedCheerio(this).text().includes("Estado:")) {
if (loadedCheerio(this).text().includes('Estado:')) {
novel.status = loadedCheerio(this)
.text()
.replace("Estado: ", "")
.replace('Estado: ', '')
.trim();
}
if (loadedCheerio(this).text().includes("Género:")) {
loadedCheerio(this).find("span").remove();
novel.genres = loadedCheerio(this)
.text()
.replace(/,\s/g, ",");
if (loadedCheerio(this).text().includes('Género:')) {
loadedCheerio(this).find('span').remove();
novel.genres = loadedCheerio(this).text().replace(/,\s/g, ',');
}
});
novel.summary = loadedCheerio(
".elementor-text-editor.elementor-clearfix"
'.elementor-text-editor.elementor-clearfix',
).text();
let novelChapters: Plugin.ChapterItem[] = [];
loadedCheerio(".elementor-accordion-item").remove();
loadedCheerio('.elementor-accordion-item').remove();
loadedCheerio(".elementor-tab-content")
.find("li")
loadedCheerio('.elementor-tab-content')
.find('li')
.each(function () {
const chapterName = loadedCheerio(this).text();
const releaseDate = null;
const chapterUrl = loadedCheerio(this).find("a").attr("href");
const chapterUrl = loadedCheerio(this).find('a').attr('href');
if (!chapterUrl) return;
const chapter = {
name: chapterName,
@@ -122,17 +123,20 @@ class Ligera implements Plugin.PluginBase {
const body = await result.text();
let loadedCheerio = parseHTML(body);
loadedCheerio(".osny-nightmode.osny-nightmode--left").remove();
loadedCheerio(".code-block.code-block-1").remove();
loadedCheerio(".adsb30").remove();
loadedCheerio(".saboxplugin-wrap").remove();
loadedCheerio(".wp-post-navigation").remove();
loadedCheerio('.osny-nightmode.osny-nightmode--left').remove();
loadedCheerio('.code-block.code-block-1').remove();
loadedCheerio('.adsb30').remove();
loadedCheerio('.saboxplugin-wrap').remove();
loadedCheerio('.wp-post-navigation').remove();
let chapterText = loadedCheerio(".entry-content").html() || '';
let chapterText = loadedCheerio('.entry-content').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const url = this.baseUrl + "?s=" + searchTerm + "&post_type=wp-manga";
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = this.baseUrl + '?s=' + searchTerm + '&post_type=wp-manga';
// console.log(url);
const result = await fetch(url);
@@ -142,19 +146,19 @@ class Ligera implements Plugin.PluginBase {
let novels: Plugin.NovelItem[] = [];
loadedCheerio(".inside-article").each(function () {
const novelCover = loadedCheerio(this).find("img").attr("src");
let novelUrl = loadedCheerio(this).find("a").attr("href");
loadedCheerio('.inside-article').each(function () {
const novelCover = loadedCheerio(this).find('img').attr('src');
let novelUrl = loadedCheerio(this).find('a').attr('href');
let novelName;
if (novelUrl) {
novelName = novelUrl.replace(/-/g, " ").replace(/^./, function (x) {
novelName = novelUrl.replace(/-/g, ' ').replace(/^./, function (x) {
return x.toUpperCase();
});
}
novelUrl += "/";
novelUrl += '/';
if (!novelName || !novelUrl) return;
@@ -174,7 +178,6 @@ class Ligera implements Plugin.PluginBase {
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new Ligera();
+56 -59
View File
@@ -1,25 +1,28 @@
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { load as parseHTML } from "cheerio";
import { defaultCover } from "@libs/defaultCover";
import { Filters } from "@libs/filterInputs";
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { load as parseHTML } from 'cheerio';
import { defaultCover } from '@libs/defaultCover';
import { Filters } from '@libs/filterInputs';
class ReinoWuxia implements Plugin.PluginBase {
id = "awuxia.com";
name = "ReinoWuxia";
icon = "src/es/novelawuxia/icon.png";
id = 'awuxia.com';
name = 'ReinoWuxia';
icon = 'src/es/novelawuxia/icon.png';
filters?: Filters | undefined;
version = "1.0.0";
site = "http://www.reinowuxia.com/"
version = '1.0.0';
site = 'http://www.reinowuxia.com/';
baseUrl = this.site;
getNovelName(y: string | undefined) {
return y?.replace(/-/g, " ").replace(/(?:^|\s)\S/g, (a) => a.toUpperCase());
return y?.replace(/-/g, ' ').replace(/(?:^|\s)\S/g, a => a.toUpperCase());
}
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
let url = this.baseUrl + "p/todas-las-novelas.html";
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.baseUrl + 'p/todas-las-novelas.html';
const result = await fetchApi(url, {
method: "GET",
method: 'GET',
});
const body = await result.text();
@@ -27,18 +30,18 @@ class ReinoWuxia implements Plugin.PluginBase {
let novels: Plugin.NovelItem[] = [];
loadedCheerio(".post-body.entry-content")
.find("a")
.each( (idx, ele) => {
loadedCheerio('.post-body.entry-content')
.find('a')
.each((idx, ele) => {
let novelName = loadedCheerio(ele)
.attr("href")
?.split("/")
.attr('href')
?.split('/')
.pop()
?.replace(".html", "");
?.replace('.html', '');
novelName = this.getNovelName(novelName);
const novelCover = loadedCheerio(ele).find("img").attr("src");
const novelCover = loadedCheerio(ele).find('img').attr('src');
let novelUrl = loadedCheerio(ele).attr("href");
let novelUrl = loadedCheerio(ele).attr('href');
if (!novelName || !novelUrl) return;
@@ -63,68 +66,60 @@ class ReinoWuxia implements Plugin.PluginBase {
let novel: Plugin.SourceNovel = { url };
novel.name = loadedCheerio("h1.post-title").text().trim();
novel.name = loadedCheerio('h1.post-title').text().trim();
novel.cover = loadedCheerio("div.separator").find("a").attr("href");
novel.cover = loadedCheerio('div.separator').find('a').attr('href');
novel.status = "";
novel.status = '';
loadedCheerio("div > b").each(function () {
loadedCheerio('div > b').each(function () {
const detailName = loadedCheerio(this).text();
let detail = loadedCheerio(this)[0].nextSibling;
if (detailName && detail) {
const text = loadedCheerio(detail).text();
if (detailName.includes("Autor")) {
novel.author = text.replace("Autor:", "");
if (detailName.includes('Autor')) {
novel.author = text.replace('Autor:', '');
}
if (detailName.includes("Estatus")) {
novel.status = text.replace("Estatus: ", "");
if (detailName.includes('Estatus')) {
novel.status = text.replace('Estatus: ', '');
}
if (detailName.includes("Géneros:")) {
novel.genres = text
.replace("Géneros: ", "")
.replace(/,\s/g, ",");
if (detailName.includes('Géneros:')) {
novel.genres = text.replace('Géneros: ', '').replace(/,\s/g, ',');
}
}
});
let novelChapters: Plugin.ChapterItem[] = [];
loadedCheerio("div").each(function () {
loadedCheerio('div').each(function () {
const detailName = loadedCheerio(this).text();
if (detailName.includes("Sinopsis")) {
if (detailName.includes('Sinopsis')) {
novel.summary =
loadedCheerio(this).next().text() !== ""
? loadedCheerio(this)
.next()
.text()
.replace("Sinopsis", "")
.trim()
loadedCheerio(this).next().text() !== ''
? loadedCheerio(this).next().text().replace('Sinopsis', '').trim()
: loadedCheerio(this)
.next()
.next()
.text()
.replace("Sinopsis", "")
.replace('Sinopsis', '')
.trim();
}
if (detailName.includes("Lista de Capítulos")) {
if (detailName.includes('Lista de Capítulos')) {
loadedCheerio(this)
.find("a")
.find('a')
.each(function (res) {
const chapterName = loadedCheerio(this).text();
let chapterUrl = loadedCheerio(this).attr("href");
let chapterUrl = loadedCheerio(this).attr('href');
const releaseDate = null;
if (
chapterName &&
chapterUrl &&
!novelChapters.some(
(chap) => chap.name === chapterName
)
!novelChapters.some(chap => chap.name === chapterName)
) {
const chapter = {
name: chapterName,
@@ -150,11 +145,14 @@ class ReinoWuxia implements Plugin.PluginBase {
let loadedCheerio = parseHTML(body);
let chapterText = loadedCheerio(".post-body.entry-content").html() || '';
let chapterText = loadedCheerio('.post-body.entry-content').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.baseUrl}search?q=${searchTerm}`;
const result = await fetchApi(url);
@@ -164,19 +162,19 @@ class ReinoWuxia implements Plugin.PluginBase {
let novels: Plugin.NovelItem[] = [];
loadedCheerio(".date-outer").each((idx, ele) => {
loadedCheerio('.date-outer').each((idx, ele) => {
let novelName = loadedCheerio(ele)
.find("a")
.attr("href")
?.split("/")
.find('a')
.attr('href')
?.split('/')
.pop()
?.replace(/-capitulo(.*?).html/, "");
?.replace(/-capitulo(.*?).html/, '');
const novelUrl = novelName + ".html/";
const novelUrl = novelName + '.html/';
novelName = this.getNovelName(novelName);
const exists = novels.some((novel) => novel.name === novelName);
const exists = novels.some(novel => novel.name === novelName);
if (!exists) {
const novelCover = defaultCover;
@@ -197,7 +195,6 @@ class ReinoWuxia implements Plugin.PluginBase {
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new ReinoWuxia();
+50 -43
View File
@@ -1,24 +1,27 @@
import { fetchFile, fetchApi } from "@libs/fetch";
import { Filters } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import { load as parseHTML } from "cheerio";
import { fetchFile, fetchApi } from '@libs/fetch';
import { Filters } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
import { load as parseHTML } from 'cheerio';
export const id = "oasisTL.wp";
export const name = "Oasis Translations";
export const site = "https://oasistranslations.wordpress.com/";
export const version = "1.0.0";
export const icon = "src/es/oasistranslations/icon.png";
export const id = 'oasisTL.wp';
export const name = 'Oasis Translations';
export const site = 'https://oasistranslations.wordpress.com/';
export const version = '1.0.0';
export const icon = 'src/es/oasistranslations/icon.png';
const baseUrl = site;
class Oasis implements Plugin.PluginBase {
id = "oasisTL.wp";
name = "Oasis Translations";
site = "https://oasistranslations.wordpress.com/";
version = "1.0.0";
id = 'oasisTL.wp';
name = 'Oasis Translations';
site = 'https://oasistranslations.wordpress.com/';
version = '1.0.0';
filters?: Filters | undefined;
icon = "src/es/oasistranslations/icon.png";
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
icon = 'src/es/oasistranslations/icon.png';
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
let url = baseUrl;
const result = await fetchApi(url);
@@ -28,14 +31,14 @@ class Oasis implements Plugin.PluginBase {
let novels: Plugin.NovelItem[] = [];
loadedCheerio(".menu-item-1819")
.find(".sub-menu > li")
loadedCheerio('.menu-item-1819')
.find('.sub-menu > li')
.each(function () {
const novelName = loadedCheerio(this).text();
if (!novelName.match(/Activas|Finalizadas|Dropeadas/)) {
const novelCover = loadedCheerio(this).find("img").attr("src");
const novelCover = loadedCheerio(this).find('img').attr('src');
let novelUrl = loadedCheerio(this).find("a").attr("href");
let novelUrl = loadedCheerio(this).find('a').attr('href');
if (!novelUrl) return;
@@ -61,36 +64,38 @@ class Oasis implements Plugin.PluginBase {
let novel: Plugin.SourceNovel = { url };
novel.name = loadedCheerio("h1.entry-title")
novel.name = loadedCheerio('h1.entry-title')
.text()
.replace(/[\t\n]/g, "")
.replace(/[\t\n]/g, '')
.trim();
novel.cover = loadedCheerio('img[loading="lazy"]').attr("src");
novel.cover = loadedCheerio('img[loading="lazy"]').attr('src');
loadedCheerio(".entry-content > p").each(function (res) {
if (loadedCheerio(this).text().includes("Autor")) {
const details = loadedCheerio(this).html()?.match(/<\/strong>(.|\n)*?<br>/g)?.
map((detail) => detail.replace(/<strong>|<\/strong>|<br>|:\s/g, ""));
loadedCheerio('.entry-content > p').each(function (res) {
if (loadedCheerio(this).text().includes('Autor')) {
const details = loadedCheerio(this)
.html()
?.match(/<\/strong>(.|\n)*?<br>/g)
?.map(detail => detail.replace(/<strong>|<\/strong>|<br>|:\s/g, ''));
novel.genres = "";
if (details){
novel.genres = '';
if (details) {
novel.author = details[2];
novel.genres = details[4].replace(/\s|&nbsp;/g, "");
novel.genres = details[4].replace(/\s|&nbsp;/g, '');
}
}
});
// let novelSummary = $(this).next().html();
novel.summary = "";
novel.summary = '';
let novelChapters: Plugin.ChapterItem[] = [];
// if ($(".entry-content").find("li").length) {
loadedCheerio(".entry-content")
.find("a")
loadedCheerio('.entry-content')
.find('a')
.each(function () {
let chapterUrl = loadedCheerio(this).attr("href");
let chapterUrl = loadedCheerio(this).attr('href');
if (chapterUrl && chapterUrl.includes(baseUrl)) {
const chapterName = loadedCheerio(this).text();
@@ -118,13 +123,16 @@ class Oasis implements Plugin.PluginBase {
let loadedCheerio = parseHTML(body);
loadedCheerio("div#jp-post-flair").remove();
loadedCheerio('div#jp-post-flair').remove();
let chapterText = loadedCheerio(".entry-content").html() || '';
let chapterText = loadedCheerio('.entry-content').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
searchTerm = searchTerm.toLowerCase();
let url = baseUrl;
@@ -135,14 +143,14 @@ class Oasis implements Plugin.PluginBase {
let loadedCheerio = parseHTML(body);
let novels: Plugin.NovelItem[] = [];
loadedCheerio(".menu-item-1819")
.find(".sub-menu > li")
loadedCheerio('.menu-item-1819')
.find('.sub-menu > li')
.each(function () {
const novelName = loadedCheerio(this).text();
if (!novelName.match(/Activas|Finalizadas|Dropeadas/)) {
const novelCover = loadedCheerio(this).find("img").attr("src");
const novelCover = loadedCheerio(this).find('img').attr('src');
let novelUrl = loadedCheerio(this).find("a").attr("href");
let novelUrl = loadedCheerio(this).find('a').attr('href');
if (!novelUrl) return;
@@ -156,8 +164,8 @@ class Oasis implements Plugin.PluginBase {
}
});
novels = novels.filter((novel) =>
novel.name.toLowerCase().includes(searchTerm)
novels = novels.filter(novel =>
novel.name.toLowerCase().includes(searchTerm),
);
return novels;
@@ -165,7 +173,6 @@ class Oasis implements Plugin.PluginBase {
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new Oasis();
+55 -51
View File
@@ -1,31 +1,34 @@
import { fetchApi, fetchFile } from "@libs/fetch";
import { Filters } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import { fetchApi, fetchFile } from '@libs/fetch';
import { Filters } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
class SkyNovels implements Plugin.PluginBase {
id = "skynovels.net";
name = "SkyNovels";
site = "https://www.skynovels.net/";
version = "1.0.0";
icon = "src/es/skynovels/icon.png";
id = 'skynovels.net';
name = 'SkyNovels';
site = 'https://www.skynovels.net/';
version = '1.0.0';
icon = 'src/es/skynovels/icon.png';
baseUrl = this.site;
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
const url = "https://api.skynovels.net/api/novels?&q";
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
const url = 'https://api.skynovels.net/api/novels?&q';
const result = await fetchApi(url);
const body = await result.json() as response;
const body = (await result.json()) as response;
const novels: Plugin.NovelItem[] = [];
body.novels?.forEach((res) => {
body.novels?.forEach(res => {
const novelName = res.nvl_title;
const novelCover =
"https://api.skynovels.net/api/get-image/" +
'https://api.skynovels.net/api/get-image/' +
res.image +
"/novels/false";
'/novels/false';
const novelUrl =
this.baseUrl + "novelas/" + res.id + "/" + res.nvl_name + "/";
this.baseUrl + 'novelas/' + res.id + '/' + res.nvl_name + '/';
const novel = { name: novelName, url: novelUrl, cover: novelCover };
@@ -35,12 +38,12 @@ class SkyNovels implements Plugin.PluginBase {
return novels;
}
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const novelId = novelUrl.split("/")[4];
const novelId = novelUrl.split('/')[4];
const url =
"https://api.skynovels.net/api/novel/" + novelId + "/reading?&q";
'https://api.skynovels.net/api/novel/' + novelId + '/reading?&q';
const result = await fetchApi(url);
const body = await result.json() as responseBook;
const body = (await result.json()) as responseBook;
const item = body?.novel?.[0];
@@ -49,24 +52,24 @@ class SkyNovels implements Plugin.PluginBase {
novel.name = item?.nvl_title;
novel.cover =
"https://api.skynovels.net/api/get-image/" +
'https://api.skynovels.net/api/get-image/' +
item?.image +
"/novels/false";
'/novels/false';
let genres: string[] = [];
item?.genres?.forEach((genre) => genres.push(genre.genre_name));
novel.genres = genres.join(",");
item?.genres?.forEach(genre => genres.push(genre.genre_name));
novel.genres = genres.join(',');
novel.author = item?.nvl_writer;
novel.summary = item?.nvl_content;
novel.status = item?.nvl_status;
let novelChapters: Plugin.ChapterItem[] = [];
item?.volumes?.forEach((volume) => {
volume?.chapters?.forEach((chapter) => {
item?.volumes?.forEach(volume => {
volume?.chapters?.forEach(chapter => {
const chapterName = chapter.chp_index_title;
const releaseDate = new Date(chapter.createdAt).toDateString();
const chapterUrl = novelUrl + chapter.id + "/" + chapter.chp_name;
const chapterUrl = novelUrl + chapter.id + '/' + chapter.chp_name;
const chap = {
name: chapterName,
@@ -83,11 +86,11 @@ class SkyNovels implements Plugin.PluginBase {
return novel;
}
async parseChapter(chapterUrl: string): Promise<string> {
let chapterId: string = chapterUrl.split("/")[6];
let chapterId: string = chapterUrl.split('/')[6];
const url = `https://api.skynovels.net/api/novel-chapter/${chapterId}`;
const result = await fetchApi(url);
const body = await result.json() as responseChapter;
const body = (await result.json()) as responseChapter;
const item = body?.chapter?.[0];
@@ -95,27 +98,30 @@ class SkyNovels implements Plugin.PluginBase {
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
searchTerm = searchTerm.toLowerCase();
const url = "https://api.skynovels.net/api/novels?&q";
const url = 'https://api.skynovels.net/api/novels?&q';
const result = await fetchApi(url);
const body = await result.json() as response;;
const body = (await result.json()) as response;
let results = body?.novels?.filter((novel) =>
novel.nvl_title.toLowerCase().includes(searchTerm)
let results = body?.novels?.filter(novel =>
novel.nvl_title.toLowerCase().includes(searchTerm),
);
const novels: Plugin.NovelItem[] = [];
results?.forEach((res) => {
results?.forEach(res => {
const novelName = res.nvl_title;
const novelCover =
"https://api.skynovels.net/api/get-image/" +
'https://api.skynovels.net/api/get-image/' +
res.image +
"/novels/false";
'/novels/false';
const novelUrl =
this.baseUrl + "novelas/" + res.id + "/" + res.nvl_name + "/";
this.baseUrl + 'novelas/' + res.id + '/' + res.nvl_name + '/';
const novel = { name: novelName, url: novelUrl, cover: novelCover };
@@ -132,7 +138,7 @@ class SkyNovels implements Plugin.PluginBase {
export default new SkyNovels();
interface response {
novels?: (NovelsEntity)[] | null;
novels?: NovelsEntity[] | null;
}
interface NovelsEntity {
id: number;
@@ -154,16 +160,15 @@ interface NovelsEntity {
nvl_last_update: string;
nvl_rating?: number | null;
nvl_ratings_count: number;
genres?: (GenresEntity)[] | null;
genres?: GenresEntity[] | null;
}
interface GenresEntity {
id: number;
genre_name: string;
}
interface responseBook {
novel?: (NovelEntity)[] | null;
novel?: NovelEntity[] | null;
}
interface NovelEntity {
id: number;
@@ -184,11 +189,11 @@ interface NovelEntity {
nvl_chapters: number;
nvl_last_update: string;
nvl_rating: number;
bookmarks?: (BookmarksEntity)[] | null;
volumes?: (VolumesEntity)[] | null;
novel_ratings?: (NovelRatingsEntity)[] | null;
collaborators?: (CollaboratorsEntity)[] | null;
genres?: (GenresEntity)[] | null;
bookmarks?: BookmarksEntity[] | null;
volumes?: VolumesEntity[] | null;
novel_ratings?: NovelRatingsEntity[] | null;
collaborators?: CollaboratorsEntity[] | null;
genres?: GenresEntity[] | null;
}
interface BookmarksEntity {
id: number;
@@ -201,7 +206,7 @@ interface VolumesEntity {
id: number;
nvl_id: number;
user_id?: number | null;
chapters?: (ChaptersEntity)[] | null;
chapters?: ChaptersEntity[] | null;
}
interface ChaptersEntity {
id: number;
@@ -233,9 +238,8 @@ interface CollaboratorsEntity {
user_login: string;
}
interface responseChapter {
chapter?: (ChapterEntity)[] | null;
chapter?: ChapterEntity[] | null;
}
interface ChapterEntity {
id: number;
@@ -256,9 +260,9 @@ interface ChapterEntity {
nvl_name: string;
user_login: string;
reactions_count: number;
comments?: (null)[] | null;
reactions?: (null)[] | null;
total_reactions?: (TotalReactionsEntity)[] | null;
comments?: null[] | null;
reactions?: null[] | null;
total_reactions?: TotalReactionsEntity[] | null;
}
interface TotalReactionsEntity {
reaction_id: number;
+126 -119
View File
@@ -1,19 +1,19 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import { defaultCover } from "@libs/defaultCover";
import { NovelStatus } from "@libs/novelStatus";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
import { defaultCover } from '@libs/defaultCover';
import { NovelStatus } from '@libs/novelStatus';
class TuNovelaLigera implements Plugin.PagePlugin {
id = "tunovelaligera";
name = "TuNovelaLigera";
icon = "src/es/tunovelaligera/icon.png";
site = "https://tunovelaligera.com";
version = "1.0.0";
id = 'tunovelaligera';
name = 'TuNovelaLigera';
icon = 'src/es/tunovelaligera/icon.png';
site = 'https://tunovelaligera.com';
version = '1.0.0';
async sleep (ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
async sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async popularNovels(
pageNo: number,
@@ -22,21 +22,23 @@ class TuNovelaLigera implements Plugin.PagePlugin {
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site
let link = this.site;
link += filters?.genres?.value ? "/genero" + filters.genres.value : "/novelas";
link += filters?.genres?.value
? '/genero' + filters.genres.value
: '/novelas';
link += `/page/${pageNo}?m_orderby=`;
link += (showLatestNovels ? "latest" : filters?.order?.value || "rating");
link += showLatestNovels ? 'latest' : filters?.order?.value || 'rating';
const result = await fetchApi(link).then((res) => res.text());
const result = await fetchApi(link).then(res => res.text());
const loadedCheerio = parseHTML(result);
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".page-item-detail").each((i, el) => {
const name = loadedCheerio(el).find(".h5 > a").text();
const cover = loadedCheerio(el).find("img").attr("src");
const url = loadedCheerio(el).find(".h5 > a").attr("href");
loadedCheerio('.page-item-detail').each((i, el) => {
const name = loadedCheerio(el).find('.h5 > a').text();
const cover = loadedCheerio(el).find('img').attr('src');
const url = loadedCheerio(el).find('.h5 > a').attr('href');
if (!url) return;
novels.push({ name, cover, path: url.replace(this.site, '') });
@@ -47,15 +49,15 @@ class TuNovelaLigera implements Plugin.PagePlugin {
parseChapters(loadedCheerio: CheerioAPI): Plugin.ChapterItem[] {
const chapters: Plugin.ChapterItem[] = [];
loadedCheerio("#lcp_instance_0 li").each((i, el) => {
loadedCheerio('#lcp_instance_0 li').each((i, el) => {
const chapterName = loadedCheerio(el)
.find("a")
.find('a')
.text()
.replace(/[\t\n]/g, "")
.replace(/[\t\n]/g, '')
.trim();
const chapterUrl = loadedCheerio(el).find("a").attr("href");
if(!chapterUrl) return;
const chapterUrl = loadedCheerio(el).find('a').attr('href');
if (!chapterUrl) return;
chapters.push({
name: chapterName,
path: chapterUrl.replace(this.site, ''),
@@ -64,58 +66,60 @@ class TuNovelaLigera implements Plugin.PagePlugin {
return chapters;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & { totalPages: number; }> {
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
const novelUrl = this.site + novelPath;
const result = await fetchApi(novelUrl);
const body = await result.text();
let loadedCheerio = parseHTML(body);
let lastPage = 1;
loadedCheerio('ul.lcp_paginator > li > a').each(function() {
loadedCheerio('ul.lcp_paginator > li > a').each(function () {
const page = Number(this.attribs['title']);
if(page && page > lastPage) lastPage = page;
if (page && page > lastPage) lastPage = page;
});
const novel: Plugin.SourceNovel & { totalPages: number; } = {
const novel: Plugin.SourceNovel & { totalPages: number } = {
path: novelPath,
chapters: [],
totalPages: lastPage,
name: loadedCheerio(".post-title > h1").text().trim()
name: loadedCheerio('.post-title > h1').text().trim(),
};
loadedCheerio(".manga-title-badges").remove();
loadedCheerio('.manga-title-badges').remove();
let novelCover = loadedCheerio(".summary_image > a > img");
let novelCover = loadedCheerio('.summary_image > a > img');
novel.cover =
novelCover.attr("data-src") ||
novelCover.attr("src") ||
novelCover.attr("data-cfsrc") ||
novelCover.attr('data-src') ||
novelCover.attr('src') ||
novelCover.attr('data-cfsrc') ||
defaultCover;
loadedCheerio(".post-content_item").each(function () {
loadedCheerio('.post-content_item').each(function () {
const detailName = loadedCheerio(this)
.find(".summary-heading > h5")
.find('.summary-heading > h5')
.text()
.trim();
const detail = loadedCheerio(this).find(".summary-content").text().trim();
const detail = loadedCheerio(this).find('.summary-content').text().trim();
switch (detailName) {
case "Género(s)":
novel.genres = detail.replace(/, /g, ",");
case 'Género(s)':
novel.genres = detail.replace(/, /g, ',');
break;
case "Autor(es)":
case 'Autor(es)':
novel.author = detail;
break;
case "Estado":
case 'Estado':
novel.status =
detail.includes("OnGoing") || detail.includes("Updating")
detail.includes('OnGoing') || detail.includes('Updating')
? NovelStatus.Ongoing
: NovelStatus.Completed;
break;
}
});
novel.summary = loadedCheerio("div.summary__content > p").text().trim();
novel.summary = loadedCheerio('div.summary__content > p').text().trim();
novel.chapters = this.parseChapters(loadedCheerio);
@@ -130,22 +134,27 @@ class TuNovelaLigera implements Plugin.PagePlugin {
let loadedCheerio = parseHTML(body);
const latestChapterEle = loadedCheerio('#lcp_instance_0 li').first();
const latestChapterUrl = loadedCheerio(latestChapterEle).find("a").attr("href");
const latestChapterName = loadedCheerio(latestChapterEle).find("a")
const latestChapterUrl = loadedCheerio(latestChapterEle)
.find('a')
.attr('href');
const latestChapterName = loadedCheerio(latestChapterEle)
.find('a')
.text()
.replace(/[\t\n]/g, "")
.replace(/[\t\n]/g, '')
.trim();
const latestChapter: Plugin.ChapterItem | undefined = latestChapterUrl ? {
const latestChapter: Plugin.ChapterItem | undefined = latestChapterUrl
? {
path: latestChapterUrl.replace(this.site, ''),
name: latestChapterName
} : undefined
name: latestChapterName,
}
: undefined;
await this.sleep(1000);
const pageText = await fetchApi(pageUrl).then(res => res.text());
const chapters = this.parseChapters(parseHTML(pageText));
return {
chapters,
latestChapter
}
latestChapter,
};
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
@@ -153,14 +162,12 @@ class TuNovelaLigera implements Plugin.PagePlugin {
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio(".c-blog-post.post").html() || "";
const chapterText = loadedCheerio('.c-blog-post.post').html() || '';
return chapterText;
}
async searchNovels(
searchTerm: string,
): Promise<Plugin.NovelItem[]> {
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/?s=${searchTerm}&post_type=wp-manga`;
const result = await fetchApi(url);
@@ -170,10 +177,10 @@ class TuNovelaLigera implements Plugin.PagePlugin {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".c-tabs-item__content").each((i, el) => {
const name = loadedCheerio(el).find(".h4 > a").text();
const cover = loadedCheerio(el).find("img").attr("src");
const url = loadedCheerio(el).find(".h4 > a").attr("href");
loadedCheerio('.c-tabs-item__content').each((i, el) => {
const name = loadedCheerio(el).find('.h4 > a').text();
const cover = loadedCheerio(el).find('img').attr('src');
const url = loadedCheerio(el).find('.h4 > a').attr('href');
if (!url) return;
novels.push({ name, cover, path: url.replace(this.site, '') });
@@ -188,70 +195,70 @@ class TuNovelaLigera implements Plugin.PagePlugin {
filters = {
order: {
value: "rating",
label: "Ordenado por",
value: 'rating',
label: 'Ordenado por',
options: [
{ label: "Lo mas reciente", value: "latest" },
{ label: "A-Z", value: "alphabet" },
{ label: "Clasificación", value: "rating" },
{ label: "Trending", value: "trending" },
{ label: "Mas visto", value: "views" },
{ label: "Nuevo", value: "new-manga" },
{ label: 'Lo mas reciente', value: 'latest' },
{ label: 'A-Z', value: 'alphabet' },
{ label: 'Clasificación', value: 'rating' },
{ label: 'Trending', value: 'trending' },
{ label: 'Mas visto', value: 'views' },
{ label: 'Nuevo', value: 'new-manga' },
],
type: FilterTypes.Picker,
},
genres: {
value: "",
label: "Generos",
value: '',
label: 'Generos',
options: [
{ label: "None", value: "" },
{ label: "Acción", value: "accion" },
{ label: "Adulto", value: "adulto" },
{ label: "Artes Marciales", value: "artes-marciales" },
{ label: "Aventura", value: "aventura" },
{ label: "Ciencia Ficción", value: "ciencia-ficcion" },
{ label: "Comedia", value: "comedia" },
{ label: "Deportes", value: "deportes" },
{ label: "Drama", value: "drama" },
{ label: "Eastern Fantasy", value: "eastern-fantasy" },
{ label: "Ecchi", value: "ecchi" },
{ label: "FanFiction", value: "fan-fiction" },
{ label: "Fantasía", value: "fantasia" },
{ label: "Fantasía oriental", value: "fantasia-oriental" },
{ label: "Ficción Romántica", value: "ficcion-romantica" },
{ label: "Gender Bender", value: "gender-bender" },
{ label: "Harem", value: "harem" },
{ label: "Histórico", value: "historico" },
{ label: "Horror", value: "horror" },
{ label: "Josei", value: "josei" },
{ label: "Maduro", value: "maduro" },
{ label: "Mecha", value: "mecha" },
{ label: "Misterio", value: "misterio" },
{ label: "Novela China", value: "novela-china" },
{ label: "Novela FanFiction", value: "novela-fanfiction" },
{ label: "Novela Japonesa", value: "novela-japonesa" },
{ label: "Novela Koreana", value: "novela-koreana" },
{ label: "Novela ligera", value: "novela-ligera" },
{ label: "Novela original", value: "novela-original" },
{ label: "Novela Web", value: "web-novel" },
{ label: "Psicológico", value: "psicologico" },
{ label: "Realismo Mágico", value: "realismo-magico" },
{ label: "Recuento de vida", value: "recuento-de-vida" },
{ label: "Romance", value: "romance" },
{ label: "Romance contemporáneo", value: "romance-contemporaneo" },
{ label: "Romance Moderno", value: "romance-moderno" },
{ label: "Seinen", value: "seinen" },
{ label: "Shoujo", value: "shoujo" },
{ label: "Shounen", value: "shounen" },
{ label: "Sobrenatural", value: "sobrenatural" },
{ label: "Tragedia", value: "tragedia" },
{ label: "Vampiros", value: "vampiros" },
{ label: "Vida Escolar", value: "vida-escolar" },
{ label: "Western Fantasy", value: "western-fantasy" },
{ label: "Wuxia", value: "wuxia" },
{ label: "Xianxia", value: "xianxia" },
{ label: "Xuanhuan", value: "xuanhuan" },
{ label: "Yaoi", value: "yaoi" },
{ label: 'None', value: '' },
{ label: 'Acción', value: 'accion' },
{ label: 'Adulto', value: 'adulto' },
{ label: 'Artes Marciales', value: 'artes-marciales' },
{ label: 'Aventura', value: 'aventura' },
{ label: 'Ciencia Ficción', value: 'ciencia-ficcion' },
{ label: 'Comedia', value: 'comedia' },
{ label: 'Deportes', value: 'deportes' },
{ label: 'Drama', value: 'drama' },
{ label: 'Eastern Fantasy', value: 'eastern-fantasy' },
{ label: 'Ecchi', value: 'ecchi' },
{ label: 'FanFiction', value: 'fan-fiction' },
{ label: 'Fantasía', value: 'fantasia' },
{ label: 'Fantasía oriental', value: 'fantasia-oriental' },
{ label: 'Ficción Romántica', value: 'ficcion-romantica' },
{ label: 'Gender Bender', value: 'gender-bender' },
{ label: 'Harem', value: 'harem' },
{ label: 'Histórico', value: 'historico' },
{ label: 'Horror', value: 'horror' },
{ label: 'Josei', value: 'josei' },
{ label: 'Maduro', value: 'maduro' },
{ label: 'Mecha', value: 'mecha' },
{ label: 'Misterio', value: 'misterio' },
{ label: 'Novela China', value: 'novela-china' },
{ label: 'Novela FanFiction', value: 'novela-fanfiction' },
{ label: 'Novela Japonesa', value: 'novela-japonesa' },
{ label: 'Novela Koreana', value: 'novela-koreana' },
{ label: 'Novela ligera', value: 'novela-ligera' },
{ label: 'Novela original', value: 'novela-original' },
{ label: 'Novela Web', value: 'web-novel' },
{ label: 'Psicológico', value: 'psicologico' },
{ label: 'Realismo Mágico', value: 'realismo-magico' },
{ label: 'Recuento de vida', value: 'recuento-de-vida' },
{ label: 'Romance', value: 'romance' },
{ label: 'Romance contemporáneo', value: 'romance-contemporaneo' },
{ label: 'Romance Moderno', value: 'romance-moderno' },
{ label: 'Seinen', value: 'seinen' },
{ label: 'Shoujo', value: 'shoujo' },
{ label: 'Shounen', value: 'shounen' },
{ label: 'Sobrenatural', value: 'sobrenatural' },
{ label: 'Tragedia', value: 'tragedia' },
{ label: 'Vampiros', value: 'vampiros' },
{ label: 'Vida Escolar', value: 'vida-escolar' },
{ label: 'Western Fantasy', value: 'western-fantasy' },
{ label: 'Wuxia', value: 'wuxia' },
{ label: 'Xianxia', value: 'xianxia' },
{ label: 'Xuanhuan', value: 'xuanhuan' },
{ label: 'Yaoi', value: 'yaoi' },
],
type: FilterTypes.Picker,
},
+51 -46
View File
@@ -1,26 +1,29 @@
import { fetchApi, fetchFile } from "@libs/fetch";
import { Filters } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import { load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from '@libs/fetch';
import { Filters } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
import { load as parseHTML } from 'cheerio';
export const id = "yuukitls.com";
export const name = "Yuuki Tls";
export const site = "https://yuukitls.com/";
export const version = "1.0.0";
export const icon = "src/es/yuukitls/icon.png";
export const id = 'yuukitls.com';
export const name = 'Yuuki Tls';
export const site = 'https://yuukitls.com/';
export const version = '1.0.0';
export const icon = 'src/es/yuukitls/icon.png';
const baseUrl = site;
class Yuuki implements Plugin.PluginBase {
id = "yuukitls.com";
name = "Yuuki Tls";
icon = "src/es/yuukitls/icon.png";
site = "https://yuukitls.com/";
id = 'yuukitls.com';
name = 'Yuuki Tls';
icon = 'src/es/yuukitls/icon.png';
site = 'https://yuukitls.com/';
filters?: Filters | undefined;
version = "1.0.0";
version = '1.0.0';
baseUrl = this.site;
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
let url = baseUrl;
const result = await fetchApi(url);
@@ -30,15 +33,15 @@ class Yuuki implements Plugin.PluginBase {
let novels: Plugin.NovelItem[] = [];
loadedCheerio(".quadmenu-navbar-collapse ul li:nth-child(2)")
.find("li")
loadedCheerio('.quadmenu-navbar-collapse ul li:nth-child(2)')
.find('li')
.each(function () {
const novelName = loadedCheerio(this)
.text()
.replace(/[\s\n]+/g, " ");
const novelCover = loadedCheerio(this).find("img").attr("src");
.replace(/[\s\n]+/g, ' ');
const novelCover = loadedCheerio(this).find('img').attr('src');
let novelUrl = loadedCheerio(this).find("a").attr("href");
let novelUrl = loadedCheerio(this).find('a').attr('href');
if (!novelUrl) return;
const novel = {
name: novelName,
@@ -63,40 +66,40 @@ class Yuuki implements Plugin.PluginBase {
url,
};
novel.name = loadedCheerio("h1.entry-title")
novel.name = loadedCheerio('h1.entry-title')
.text()
.replace(/[\t\n]/g, "")
.replace(/[\t\n]/g, '')
.trim();
novel.cover = loadedCheerio('img[loading="lazy"]').attr("src");
novel.cover = loadedCheerio('img[loading="lazy"]').attr('src');
loadedCheerio(".entry-content")
.find("div")
loadedCheerio('.entry-content')
.find('div')
.each(function () {
if (loadedCheerio(this).text().includes("Escritor:")) {
if (loadedCheerio(this).text().includes('Escritor:')) {
novel.author = loadedCheerio(this)
.text()
.replace("Escritor: ", "")
.replace('Escritor: ', '')
.trim();
}
if (loadedCheerio(this).text().includes("Género:")) {
if (loadedCheerio(this).text().includes('Género:')) {
novel.genres = loadedCheerio(this)
.text()
.replace(/Género: |\s/g, "");
.replace(/Género: |\s/g, '');
}
if (loadedCheerio(this).text().includes("Sinopsis:")) {
if (loadedCheerio(this).text().includes('Sinopsis:')) {
novel.summary = loadedCheerio(this).next().text();
}
});
let novelChapters: Plugin.ChapterItem[] = [];
if (loadedCheerio(".entry-content").find("li").length) {
loadedCheerio(".entry-content")
.find("li")
if (loadedCheerio('.entry-content').find('li').length) {
loadedCheerio('.entry-content')
.find('li')
.each(function () {
let chapterUrl = loadedCheerio(this).find("a").attr("href");
let chapterUrl = loadedCheerio(this).find('a').attr('href');
if (chapterUrl && chapterUrl.includes(baseUrl)) {
const chapterName = loadedCheerio(this).text();
@@ -112,10 +115,10 @@ class Yuuki implements Plugin.PluginBase {
}
});
} else {
loadedCheerio(".entry-content")
.find("p")
loadedCheerio('.entry-content')
.find('p')
.each(function () {
let chapterUrl = loadedCheerio(this).find("a").attr("href");
let chapterUrl = loadedCheerio(this).find('a').attr('href');
if (chapterUrl && chapterUrl.includes(baseUrl)) {
const chapterName = loadedCheerio(this).text();
@@ -143,11 +146,14 @@ class Yuuki implements Plugin.PluginBase {
const body = await result.text();
let loadedCheerio = parseHTML(body);
let chapterText = loadedCheerio(".entry-content").html() || '';
let chapterText = loadedCheerio('.entry-content').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
searchTerm = searchTerm.toLowerCase();
let url = baseUrl;
@@ -159,13 +165,13 @@ class Yuuki implements Plugin.PluginBase {
let novels: Plugin.NovelItem[] = [];
loadedCheerio(".menu-item-2869")
.find(".menu-item.menu-item-type-post_type.menu-item-object-post")
loadedCheerio('.menu-item-2869')
.find('.menu-item.menu-item-type-post_type.menu-item-object-post')
.each(function () {
const novelName = loadedCheerio(this).text();
const novelCover = loadedCheerio(this).find("img").attr("src");
const novelCover = loadedCheerio(this).find('img').attr('src');
let novelUrl = loadedCheerio(this).find("a").attr("href");
let novelUrl = loadedCheerio(this).find('a').attr('href');
if (!novelUrl) return;
const novel = {
@@ -177,8 +183,8 @@ class Yuuki implements Plugin.PluginBase {
novels.push(novel);
});
novels = novels.filter((novel) =>
novel.name.toLowerCase().includes(searchTerm)
novels = novels.filter(novel =>
novel.name.toLowerCase().includes(searchTerm),
);
return novels;
@@ -186,7 +192,6 @@ class Yuuki implements Plugin.PluginBase {
fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new Yuuki();
+46 -45
View File
@@ -1,17 +1,20 @@
import { load as parseHTML } from "cheerio";
import { fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters } from "@libs/filterInputs";
import { load as parseHTML } from 'cheerio';
import { fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters } from '@libs/filterInputs';
class EpikNovel implements Plugin.PluginBase {
id = "epiknovel.com";
name = "EpikNovel";
icon = "src/tr/epiknovel/icon.png";
site = "https://www.epiknovel.com/";
version = "1.0.0";
id = 'epiknovel.com';
name = 'EpikNovel';
icon = 'src/tr/epiknovel/icon.png';
site = 'https://www.epiknovel.com/';
version = '1.0.0';
baseUrl = this.site;
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
let url = this.baseUrl + "seri-listesi?Sayfa=" + pageNo;
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.baseUrl + 'seri-listesi?Sayfa=' + pageNo;
const result = await fetch(url);
const body = await result.text();
@@ -20,10 +23,10 @@ class EpikNovel implements Plugin.PluginBase {
let novels: Plugin.NovelItem[] = [];
loadedCheerio("div.col-lg-12.col-md-12").each(function () {
const novelName = loadedCheerio(this).find("h3").text();
const novelCover = loadedCheerio(this).find("img").attr("data-src");
const novelUrl = loadedCheerio(this).find("h3 > a").attr("href");
loadedCheerio('div.col-lg-12.col-md-12').each(function () {
const novelName = loadedCheerio(this).find('h3').text();
const novelCover = loadedCheerio(this).find('img').attr('data-src');
const novelUrl = loadedCheerio(this).find('h3 > a').attr('href');
if (!novelUrl) return;
@@ -51,52 +54,47 @@ class EpikNovel implements Plugin.PluginBase {
let novel: Plugin.SourceNovel = { url };
novel.name = loadedCheerio("h1#tables").text().trim();
novel.name = loadedCheerio('h1#tables').text().trim();
novel.cover = loadedCheerio("img.manga-cover").attr("src");
novel.cover = loadedCheerio('img.manga-cover').attr('src');
novel.summary = loadedCheerio(
"#wrapper > div.row > div.col-md-9 > div:nth-child(6) > p:nth-child(3)"
'#wrapper > div.row > div.col-md-9 > div:nth-child(6) > p:nth-child(3)',
)
.text()
.trim();
novel.status = loadedCheerio(
"#wrapper > div.row > div.col-md-9 > div.row > div.col-md-9 > h4:nth-child(3) > a"
'#wrapper > div.row > div.col-md-9 > div.row > div.col-md-9 > h4:nth-child(3) > a',
)
.text()
.trim();
novel.author = loadedCheerio("#NovelInfo > p:nth-child(4)")
novel.author = loadedCheerio('#NovelInfo > p:nth-child(4)')
.text()
.replace(/Publisher:|\s/g, "")
.replace(/Publisher:|\s/g, '')
.trim();
let novelChapters: Plugin.ChapterItem[] = [];
loadedCheerio("table").find("tr").first().remove();
loadedCheerio('table').find('tr').first().remove();
loadedCheerio("table")
.find("tr")
loadedCheerio('table')
.find('tr')
.each(function () {
const releaseDate = loadedCheerio(this)
.find("td:nth-child(3)")
.text();
const releaseDate = loadedCheerio(this).find('td:nth-child(3)').text();
let chapterName = loadedCheerio(this)
.find("td:nth-child(1) > a")
.find('td:nth-child(1) > a')
.text();
if (
loadedCheerio(this).find("td:nth-child(1) > span").length >
0
) {
chapterName = "🔒 " + chapterName;
if (loadedCheerio(this).find('td:nth-child(1) > span').length > 0) {
chapterName = '🔒 ' + chapterName;
}
const chapterUrl = loadedCheerio(this)
.find(" td:nth-child(1) > a")
.attr("href");
.find(' td:nth-child(1) > a')
.attr('href');
if (!chapterUrl) return;
@@ -124,16 +122,20 @@ class EpikNovel implements Plugin.PluginBase {
let chapterText = '';
if (result.url === "https://www.epiknovel.com/login") {
chapterText = "Premium Chapter";
if (result.url === 'https://www.epiknovel.com/login') {
chapterText = 'Premium Chapter';
} else {
chapterText = loadedCheerio("div#icerik").html() || '';
chapterText = loadedCheerio('div#icerik').html() || '';
}
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
const url = this.baseUrl + "seri-listesi?q=" + searchTerm + '&Sayfa=' + pageNo;
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url =
this.baseUrl + 'seri-listesi?q=' + searchTerm + '&Sayfa=' + pageNo;
const result = await fetch(url);
const body = await result.text();
@@ -142,10 +144,10 @@ class EpikNovel implements Plugin.PluginBase {
let novels: Plugin.NovelItem[] = [];
loadedCheerio("div.col-lg-12.col-md-12").each(function () {
const novelName = loadedCheerio(this).find("h3").text();
const novelCover = loadedCheerio(this).find("img").attr("data-src");
const novelUrl = loadedCheerio(this).find("h3 > a").attr("href");
loadedCheerio('div.col-lg-12.col-md-12').each(function () {
const novelName = loadedCheerio(this).find('h3').text();
const novelCover = loadedCheerio(this).find('img').attr('data-src');
const novelUrl = loadedCheerio(this).find('h3 > a').attr('href');
if (!novelUrl) return;
@@ -163,7 +165,6 @@ class EpikNovel implements Plugin.PluginBase {
fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new EpikNovel();
+100 -111
View File
@@ -1,47 +1,47 @@
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 LSHNovel implements Plugin.PluginBase {
id = "lshnovel";
name = "Liebe Schnee Hiver Novel";
icon = "multisrc/wpmangastream/icons/lshnovel.png";
site = "https://lshnovel.com/";
version = "1.0.0";
id = 'lshnovel';
name = 'Liebe Schnee Hiver Novel';
icon = 'multisrc/wpmangastream/icons/lshnovel.png';
site = 'https://lshnovel.com/';
version = '1.0.0';
async popularNovels(
pageNo: number,
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = `${this.site}series/?page=${pageNo}`;
if (filters.genres.value.length) {
link += filters.genres.value.map((i) => `&genre[]=${i}`).join("");
link += filters.genres.value.map(i => `&genre[]=${i}`).join('');
}
if (filters.type.value.length)
link += filters.type.value.map((i) => `&lang[]=${i}`).join("");
link += filters.type.value.map(i => `&lang[]=${i}`).join('');
link += "&status=" + filters.status;
link += '&status=' + filters.status;
link += "&order=" + filters.order;
link += '&order=' + filters.order;
const headers = new Headers();
const body = await fetchApi(link, { headers }).then((result) =>
result.text()
const body = await fetchApi(link, { headers }).then(result =>
result.text(),
);
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio("article.bs").each(function () {
const novelName = loadedCheerio(this).find(".ntitle").text().trim();
let image = loadedCheerio(this).find("img");
const novelCover = image.attr("data-src") || image.attr("src");
loadedCheerio('article.bs').each(function () {
const novelName = loadedCheerio(this).find('.ntitle').text().trim();
let image = loadedCheerio(this).find('img');
const novelCover = image.attr('data-src') || image.attr('src');
const novelUrl = loadedCheerio(this).find("a").attr("href");
const novelUrl = loadedCheerio(this).find('a').attr('href');
if (!novelUrl) return;
@@ -70,59 +70,48 @@ class LSHNovel implements Plugin.PluginBase {
chapters: [],
};
novel.name = loadedCheerio(".entry-title").text();
novel.name = loadedCheerio('.entry-title').text();
novel.cover =
loadedCheerio("img.wp-post-image").attr("data-src") ||
loadedCheerio("img.wp-post-image").attr("src");
loadedCheerio('img.wp-post-image').attr('data-src') ||
loadedCheerio('img.wp-post-image').attr('src');
loadedCheerio("div.spe > span").each(function () {
const detailName = loadedCheerio(this).find("b").text().trim();
const detail = loadedCheerio(this)
.find("b")
.remove()
.end()
.text()
.trim();
loadedCheerio('div.spe > span').each(function () {
const detailName = loadedCheerio(this).find('b').text().trim();
const detail = loadedCheerio(this).find('b').remove().end().text().trim();
switch (detailName) {
case "Yazar:":
case 'Yazar:':
novel.author = detail;
break;
case "Seviye:":
case 'Seviye:':
novel.status = detail;
break;
}
});
novel.genres = loadedCheerio(".genxed")
.text()
.trim()
.replace(/\s/g, ",");
novel.genres = loadedCheerio('.genxed').text().trim().replace(/\s/g, ',');
loadedCheerio('div[itemprop="description"] h3,p.a,strong').remove();
novel.summary = loadedCheerio('div[itemprop="description"]')
.find("br")
.replaceWith("\n")
.find('br')
.replaceWith('\n')
.end()
.text();
let chapter: Plugin.ChapterItem[] = [];
loadedCheerio(".eplister")
.find("li")
loadedCheerio('.eplister')
.find('li')
.each(function () {
const chapterName =
loadedCheerio(this).find(".epl-num").text() +
" - " +
loadedCheerio(this).find(".epl-title").text();
loadedCheerio(this).find('.epl-num').text() +
' - ' +
loadedCheerio(this).find('.epl-title').text();
const releaseDate = loadedCheerio(this)
.find(".epl-date")
.text()
.trim();
const releaseDate = loadedCheerio(this).find('.epl-date').text().trim();
const chapterUrl = loadedCheerio(this).find("a").attr("href");
const chapterUrl = loadedCheerio(this).find('a').attr('href');
if (!chapterUrl) return;
@@ -144,14 +133,14 @@ class LSHNovel implements Plugin.PluginBase {
const loadedCheerio = parseHTML(body);
let chapterText = loadedCheerio(".epcontent").html() || "";
let chapterText = loadedCheerio('.epcontent').html() || '';
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo?: number | undefined
pageNo?: number | undefined,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}?s=${searchTerm}`;
const headers = new Headers();
@@ -162,10 +151,10 @@ class LSHNovel implements Plugin.PluginBase {
const novels: Plugin.NovelItem[] = [];
loadedCheerio("article.bs").each(function () {
const novelName = loadedCheerio(this).find(".ntitle").text().trim();
const novelCover = loadedCheerio(this).find("img").attr("src");
const novelUrl = loadedCheerio(this).find("a").attr("href");
loadedCheerio('article.bs').each(function () {
const novelName = loadedCheerio(this).find('.ntitle').text().trim();
const novelCover = loadedCheerio(this).find('img').attr('src');
const novelUrl = loadedCheerio(this).find('a').attr('href');
if (!novelUrl) return;
novels.push({
@@ -183,124 +172,124 @@ class LSHNovel implements Plugin.PluginBase {
filters = {
order: {
value: "popular",
label: "Önerilen",
value: 'popular',
label: 'Önerilen',
options: [
{ label: "Varsayılan", value: "" },
{ label: 'Varsayılan', value: '' },
{ label: "A-Z", value: "title" },
{ label: 'A-Z', value: 'title' },
{ label: "Z-A", value: "titlereverse" },
{ label: 'Z-A', value: 'titlereverse' },
{ label: "Son Yüklemeler", value: "update" },
{ label: 'Son Yüklemeler', value: 'update' },
{ label: "Son Eklenenler", value: "latest" },
{ label: 'Son Eklenenler', value: 'latest' },
{ label: "Bestimiz", value: "popular" },
{ label: 'Bestimiz', value: 'popular' },
],
type: FilterTypes.Picker,
},
status: {
label: "Statü",
value: "",
label: 'Statü',
value: '',
options: [
{ label: "Tümü", value: "" },
{ label: 'Tümü', value: '' },
{ label: "Ongoing", value: "ongoing" },
{ label: 'Ongoing', value: 'ongoing' },
{ label: "Hiatus", value: "hiatus" },
{ label: 'Hiatus', value: 'hiatus' },
{ label: "Completed", value: "completed" },
{ label: 'Completed', value: 'completed' },
],
type: FilterTypes.Picker,
},
type: {
value: [],
label: "Tür",
label: 'Tür',
options: [
{ label: "Çeviri Novel", value: "ceviri-novel" },
{ label: 'Çeviri Novel', value: 'ceviri-novel' },
{ label: "Liz-Chan", value: "liz-chan" },
{ label: 'Liz-Chan', value: 'liz-chan' },
{ label: "Manhwa", value: "manhwa" },
{ label: 'Manhwa', value: 'manhwa' },
{ label: "Orijinal Novel", value: "orijinal-novel" },
{ label: 'Orijinal Novel', value: 'orijinal-novel' },
{ label: "Web Novel", value: "web-novel" },
{ label: 'Web Novel', value: 'web-novel' },
],
type: FilterTypes.CheckboxGroup,
},
genres: {
value: [],
label: "Kategori",
label: 'Kategori',
options: [
{ label: "+18", value: "18" },
{ label: '+18', value: '18' },
{ label: "Action", value: "action" },
{ label: 'Action', value: 'action' },
{ label: "Adult", value: "adult" },
{ label: 'Adult', value: 'adult' },
{ label: "Aksiyon", value: "aksiyon" },
{ label: 'Aksiyon', value: 'aksiyon' },
{ label: "BL", value: "bl" },
{ label: 'BL', value: 'bl' },
{ label: "Comedy", value: "comedy" },
{ label: 'Comedy', value: 'comedy' },
{ label: "Doğaüstü", value: "dogaustu" },
{ label: 'Doğaüstü', value: 'dogaustu' },
{ label: "Dram", value: "dram" },
{ label: 'Dram', value: 'dram' },
{ label: "Drama", value: "drama" },
{ label: 'Drama', value: 'drama' },
{ label: "Ecchi", value: "ecchi" },
{ label: 'Ecchi', value: 'ecchi' },
{ label: "Fantastik", value: "fantastik" },
{ label: 'Fantastik', value: 'fantastik' },
{ label: "Fantasy", value: "fantasy" },
{ label: 'Fantasy', value: 'fantasy' },
{ label: "Gizem", value: "gizem" },
{ label: 'Gizem', value: 'gizem' },
{ label: "Harem", value: "harem" },
{ label: 'Harem', value: 'harem' },
{ label: "Historical", value: "historical" },
{ label: 'Historical', value: 'historical' },
{ label: "Josei", value: "josei" },
{ label: 'Josei', value: 'josei' },
{ label: "Macera", value: "macera" },
{ label: 'Macera', value: 'macera' },
{ label: "Manhwa", value: "manhwa" },
{ label: 'Manhwa', value: 'manhwa' },
{ label: "Martial Arts", value: "martial-arts" },
{ label: 'Martial Arts', value: 'martial-arts' },
{ label: "Mature", value: "mature" },
{ label: 'Mature', value: 'mature' },
{ label: "Novel", value: "novel" },
{ label: 'Novel', value: 'novel' },
{ label: "Okul", value: "okul" },
{ label: 'Okul', value: 'okul' },
{ label: "Psikolojik", value: "psikolojik" },
{ label: 'Psikolojik', value: 'psikolojik' },
{ label: "Psychological", value: "psychological" },
{ label: 'Psychological', value: 'psychological' },
{ label: "Reverse Harem", value: "reverse-harem" },
{ label: 'Reverse Harem', value: 'reverse-harem' },
{ label: "Romance", value: "romance" },
{ label: 'Romance', value: 'romance' },
{ label: "Romantik", value: "romantik" },
{ label: 'Romantik', value: 'romantik' },
{ label: "Shoujo", value: "shoujo" },
{ label: 'Shoujo', value: 'shoujo' },
{ label: "Slice Of Life", value: "slice-of-life" },
{ label: 'Slice Of Life', value: 'slice-of-life' },
{ label: "Smut", value: "smut" },
{ label: 'Smut', value: 'smut' },
{ label: "Supernatural", value: "supernatural" },
{ label: 'Supernatural', value: 'supernatural' },
{ label: "Tarihi", value: "tarihi" },
{ label: 'Tarihi', value: 'tarihi' },
{ label: "Tragedy", value: "tragedy" },
{ label: 'Tragedy', value: 'tragedy' },
{ label: "Yaoi", value: "yaoi" },
{ label: 'Yaoi', value: 'yaoi' },
],
type: FilterTypes.CheckboxGroup,
},
+50 -48
View File
@@ -1,32 +1,32 @@
import { Plugin } from "@typings/plugin";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import dayjs from "dayjs";
import { Plugin } from '@typings/plugin';
import { fetchApi, fetchFile } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
import dayjs from 'dayjs';
class Smakolykytl implements Plugin.PluginBase {
id = "smakolykytl";
name = "Смаколики";
site = "https://smakolykytl.site/";
version = "1.0.0";
icon = "src/uk/smakolykytl/icon.png";
id = 'smakolykytl';
name = 'Смаколики';
site = 'https://smakolykytl.site/';
version = '1.0.0';
icon = 'src/uk/smakolykytl/icon.png';
async popularNovels(
pageNo: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
const url = showLatestNovels
? "https://api.smakolykytl.site/api/user/updates"
: "https://api.smakolykytl.site/api/user/projects";
? 'https://api.smakolykytl.site/api/user/updates'
: 'https://api.smakolykytl.site/api/user/projects';
const result = await fetchApi(url);
const json = (await result.json()) as response;
const novels: Plugin.NovelItem[] = [];
(json?.projects || json?.updates)?.forEach((novel) =>
(json?.projects || json?.updates)?.forEach(novel =>
novels.push({
name: novel.title,
cover: novel.image.url,
path: "titles/" + novel.id,
path: 'titles/' + novel.id,
}),
);
@@ -34,42 +34,42 @@ class Smakolykytl implements Plugin.PluginBase {
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const id = novelPath.split("/").pop();
const id = novelPath.split('/').pop();
const result = await fetchApi(
"https://api.smakolykytl.site/api/user/projects/" + id,
'https://api.smakolykytl.site/api/user/projects/' + id,
);
const book = (await result.json()) as response;
const novel: Plugin.SourceNovel = {
path: novelPath,
name: book?.project?.title || "",
name: book?.project?.title || '',
cover: book?.project?.image?.url,
summary: book?.project?.description,
author: book?.project?.author,
status: book?.project?.status_translate.includes("Триває")
status: book?.project?.status_translate.includes('Триває')
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
const tags = [book?.project?.genres, book?.project?.tags]
.flat()
.map((tags) => tags?.title)
.filter((tags) => tags);
.map(tags => tags?.title)
.filter(tags => tags);
if (tags.length) {
novel.genres = tags.join(", ");
novel.genres = tags.join(', ');
}
const chapters: Plugin.ChapterItem[] = [];
const res = await fetchApi(
"https://api.smakolykytl.site/api/user/projects/" + id + "/books",
'https://api.smakolykytl.site/api/user/projects/' + id + '/books',
);
const data = (await res.json()) as response;
data?.books?.forEach((volume) =>
volume?.chapters?.forEach((chapter) =>
data?.books?.forEach(volume =>
volume?.chapters?.forEach(chapter =>
chapters.push({
name: volume.title + " " + chapter.title,
path: "read/" + chapter.id,
releaseTime: dayjs(chapter.modifiedAt).format("LLL"),
name: volume.title + ' ' + chapter.title,
path: 'read/' + chapter.id,
releaseTime: dayjs(chapter.modifiedAt).format('LLL'),
chapterNumber: chapters.length + 1,
}),
),
@@ -80,19 +80,21 @@ class Smakolykytl implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
const id = chapterPath.split("/").pop();
const id = chapterPath.split('/').pop();
const result = await fetchApi(
"https://api.smakolykytl.site/api/user/chapters/" + id,
'https://api.smakolykytl.site/api/user/chapters/' + id,
);
const json = (await result.json()) as response;
const chapterRaw: HTML[] = JSON.parse(json?.chapter?.content || "[]");
const chapterRaw: HTML[] = JSON.parse(json?.chapter?.content || '[]');
const chapterText = jsonToHtml(chapterRaw);
return chapterText;
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const result = await fetchApi("https://api.smakolykytl.site/api/user/projects");
const result = await fetchApi(
'https://api.smakolykytl.site/api/user/projects',
);
const json = (await result.json()) as response;
const searchTitle = searchTerm.toLowerCase();
const novels: Plugin.NovelItem[] = [];
@@ -102,11 +104,11 @@ class Smakolykytl implements Plugin.PluginBase {
({ title, id }) =>
title.toLowerCase().includes(searchTitle) || String(id) == searchTerm,
)
?.forEach((novel) =>
?.forEach(novel =>
novels.push({
name: novel.title,
cover: novel.image.url,
path: "titles/" + novel.id,
path: 'titles/' + novel.id,
}),
);
return novels;
@@ -117,34 +119,34 @@ class Smakolykytl implements Plugin.PluginBase {
export default new Smakolykytl();
function jsonToHtml(json: HTML[], html: string = "") {
json.forEach((element) => {
function jsonToHtml(json: HTML[], html: string = '') {
json.forEach(element => {
switch (element.type) {
case "hardBreak":
html += "<br>";
case 'hardBreak':
html += '<br>';
break;
case "horizontalRule":
html += "<hr>";
case 'horizontalRule':
html += '<hr>';
break;
case "image":
case 'image':
if (element.attrs) {
const attrs = Object.entries(element.attrs)
.filter((attr) => attr?.[1])
.map((attr) => `${attr[0]}="${attr[1]}"`);
html += "<img " + attrs.join("; ") + ">";
.filter(attr => attr?.[1])
.map(attr => `${attr[0]}="${attr[1]}"`);
html += '<img ' + attrs.join('; ') + '>';
}
break;
case "paragraph":
case 'paragraph':
html +=
"<p>" +
(element.content ? jsonToHtml(element.content) : "<br>") +
"</p>";
'<p>' +
(element.content ? jsonToHtml(element.content) : '<br>') +
'</p>';
break;
case "text":
case 'text':
html += element.text;
break;
default:
html += JSON.stringify(element, null, "\t"); //maybe I missed something.
html += JSON.stringify(element, null, '\t'); //maybe I missed something.
break;
}
});
+124 -114
View File
@@ -1,31 +1,26 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { isUrlAbsolute } from "@libs/isAbsoluteUrl";
import { NovelStatus } from "@libs/novelStatus";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
import { NovelStatus } from '@libs/novelStatus';
import { FilterTypes, Filters } from '@libs/filterInputs';
class HakoPlugin implements Plugin.PluginBase {
id = "ln.hako";
name = "Hako";
icon = "src/vi/hakolightnovel/icon.png";
site = "https://ln.hako.vn";
version = "1.0.1";
id = 'ln.hako';
name = 'Hako';
icon = 'src/vi/hakolightnovel/icon.png';
site = 'https://ln.hako.vn';
version = '1.0.1';
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio("#mainpart .row .thumb-item-flow").each((index, ele) => {
loadedCheerio('#mainpart .row .thumb-item-flow').each((index, ele) => {
let url = loadedCheerio(ele)
.find("div.thumb_attr.series-title > a")
.attr("href");
.find('div.thumb_attr.series-title > a')
.attr('href');
if (url) {
const name = loadedCheerio(ele)
.find(".series-title")
.text()
.trim();
let cover = loadedCheerio(ele)
.find(".img-in-ratio")
.attr("data-bg");
const name = loadedCheerio(ele).find('.series-title').text().trim();
let cover = loadedCheerio(ele).find('.img-in-ratio').attr('data-bg');
if (cover && !isUrlAbsolute(cover)) {
cover = this.site + cover;
@@ -40,21 +35,24 @@ class HakoPlugin implements Plugin.PluginBase {
}
async popularNovels(
pageNo: number,
{showLatestNovels, filters}: Plugin.PopularNovelsOptions<typeof this.filters>
{
showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site + "/danh-sach"
if(filters.alphabet.value){
link += "/" + filters.alphabet.value;
let link = this.site + '/danh-sach';
if (filters.alphabet.value) {
link += '/' + filters.alphabet.value;
}
const params = new URLSearchParams();
for(const novelType of filters.type.value){
params.append(novelType, "1");
for (const novelType of filters.type.value) {
params.append(novelType, '1');
}
for(const status of filters.status.value){
params.append(status, "1");
for (const status of filters.status.value) {
params.append(status, '1');
}
params.append("sapxep", filters.sort.value);
link += '?' + params.toString() + '&page=' + pageNo
params.append('sapxep', filters.sort.value);
link += '?' + params.toString() + '&page=' + pageNo;
const result = await fetch(link);
const body = await result.text();
const loadedCheerio = parseHTML(body);
@@ -70,37 +68,43 @@ class HakoPlugin implements Plugin.PluginBase {
let loadedCheerio = parseHTML(body);
novel.name = loadedCheerio(".series-name").text().trim();
novel.name = loadedCheerio('.series-name').text().trim();
const background =
loadedCheerio(".series-cover > .a6-ratio > div").attr("style") ||
"";
loadedCheerio('.series-cover > .a6-ratio > div').attr('style') || '';
const novelCover = background.substring(
background.indexOf("http"),
background.length - 2
background.indexOf('http'),
background.length - 2,
);
novel.cover = novelCover
? isUrlAbsolute(novelCover)
? novelCover
: this.site + novelCover
: "";
: '';
novel.summary = loadedCheerio(".summary-content").text().trim();
novel.summary = loadedCheerio('.summary-content').text().trim();
novel.genres = loadedCheerio(".series-information > div:nth-child(1)")
novel.genres = loadedCheerio('.series-information > div:nth-child(1)')
.text()
.trim()
.split(/\n[\s\n]*/).join(',');
.split(/\n[\s\n]*/)
.join(',');
novel.author = loadedCheerio('.series-information > div:nth-child(2) > .info-value')
novel.author = loadedCheerio(
'.series-information > div:nth-child(2) > .info-value',
)
.text()
.trim();
novel.artist = loadedCheerio('.series-information > div:nth-child(3) > .info-value')
novel.artist = loadedCheerio(
'.series-information > div:nth-child(3) > .info-value',
)
.text()
.trim();
novel.status = loadedCheerio('.series-information > div:nth-child(4) > .info-value')
novel.status = loadedCheerio(
'.series-information > div:nth-child(4) > .info-value',
)
.text()
.trim();
@@ -121,36 +125,41 @@ class HakoPlugin implements Plugin.PluginBase {
novel.chapters = [];
loadedCheerio('.volume-list').each((idx, ele) => {
const customPage = loadedCheerio(ele).find('.sect-title').text().trim();
let num = 0, part = 1;
loadedCheerio(ele).find('.list-chapters > li').each((idx, chapterEle) => {
const path = loadedCheerio(chapterEle).find("a").attr("href");
let num = 0,
part = 1;
loadedCheerio(ele)
.find('.list-chapters > li')
.each((idx, chapterEle) => {
const path = loadedCheerio(chapterEle).find('a').attr('href');
const chapterName = loadedCheerio(chapterEle)
.find(".chapter-name")
.find('.chapter-name')
.text()
.trim();
let chapterNumber = Number(chapterName.match(/Chương\s*(\d+)/i)?.[1]);
if(chapterNumber){
if (chapterNumber) {
num = chapterNumber;
part = 1;
}else{
} else {
chapterNumber = num + part / 10;
part ++;
part++;
}
const chapterTime = loadedCheerio(chapterEle)
.find(".chapter-time")
.find('.chapter-time')
.text()
.split('/')
.map(x => Number(x));
if(path){
novel.chapters?.push(
{
if (path) {
novel.chapters?.push({
page: customPage,
path,
name: chapterName,
releaseTime: new Date(chapterTime[2], chapterTime[1], chapterTime[0]).toISOString(),
releaseTime: new Date(
chapterTime[2],
chapterTime[1],
chapterTime[0],
).toISOString(),
chapterNumber: chapterNumber,
}
);
});
}
});
});
@@ -162,15 +171,16 @@ class HakoPlugin implements Plugin.PluginBase {
const loadedCheerio = parseHTML(body);
const chapterText = loadedCheerio("#chapter-content").html() || "";
const chapterText = loadedCheerio('#chapter-content').html() || '';
return chapterText;
}
async searchNovels(
searchTerm: string,
pageNo: number
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = this.site + "/tim-kiem?keywords=" + searchTerm + '&page=' + pageNo;
const url =
this.site + '/tim-kiem?keywords=' + searchTerm + '&page=' + pageNo;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = parseHTML(body);
@@ -178,81 +188,81 @@ class HakoPlugin implements Plugin.PluginBase {
}
async fetchImage(url: string): Promise<string | undefined> {
const headers = {
Referer: "https://ln.hako.vn",
Referer: 'https://ln.hako.vn',
};
return await fetchFile(url, { headers: headers });
}
filters = {
alphabet: {
type: FilterTypes.Picker,
value: "",
label: "Chữ cái",
value: '',
label: 'Chữ cái',
options: [
{label: 'Tất cả', value: ''},
{label: 'Khác', value: 'khac'},
{label: 'A', value: 'a'},
{label: 'B', value: 'b'},
{label: 'C', value: 'c'},
{label: 'D', value: 'd'},
{label: 'E', value: 'e'},
{label: 'F', value: 'f'},
{label: 'G', value: 'g'},
{label: 'H', value: 'h'},
{label: 'I', value: 'i'},
{label: 'J', value: 'j'},
{label: 'K', value: 'k'},
{label: 'L', value: 'l'},
{label: 'M', value: 'm'},
{label: 'N', value: 'n'},
{label: 'O', value: 'o'},
{label: 'P', value: 'p'},
{label: 'Q', value: 'q'},
{label: 'R', value: 'r'},
{label: 'S', value: 's'},
{label: 'T', value: 't'},
{label: 'U', value: 'u'},
{label: 'V', value: 'v'},
{label: 'W', value: 'w'},
{label: 'X', value: 'x'},
{label: 'Y', value: 'y'},
{label: 'Z', value: 'z'},
]
{ label: 'Tất cả', value: '' },
{ label: 'Khác', value: 'khac' },
{ label: 'A', value: 'a' },
{ label: 'B', value: 'b' },
{ label: 'C', value: 'c' },
{ label: 'D', value: 'd' },
{ label: 'E', value: 'e' },
{ label: 'F', value: 'f' },
{ label: 'G', value: 'g' },
{ label: 'H', value: 'h' },
{ label: 'I', value: 'i' },
{ label: 'J', value: 'j' },
{ label: 'K', value: 'k' },
{ label: 'L', value: 'l' },
{ label: 'M', value: 'm' },
{ label: 'N', value: 'n' },
{ label: 'O', value: 'o' },
{ label: 'P', value: 'p' },
{ label: 'Q', value: 'q' },
{ label: 'R', value: 'r' },
{ label: 'S', value: 's' },
{ label: 'T', value: 't' },
{ label: 'U', value: 'u' },
{ label: 'V', value: 'v' },
{ label: 'W', value: 'w' },
{ label: 'X', value: 'x' },
{ label: 'Y', value: 'y' },
{ label: 'Z', value: 'z' },
],
},
type: {
type: FilterTypes.CheckboxGroup,
label: "Phân loại",
label: 'Phân loại',
value: [],
options: [
{label: "Truyện dịch", value: "truyendich"},
{label: "Truyện sáng tác", value: "sangtac"},
{label: "Convert", value: "convert"}
]
{ label: 'Truyện dịch', value: 'truyendich' },
{ label: 'Truyện sáng tác', value: 'sangtac' },
{ label: 'Convert', value: 'convert' },
],
},
status: {
type: FilterTypes.CheckboxGroup,
label: "Tình trạng",
label: 'Tình trạng',
value: [],
options: [
{label: "Đang tiến hành", value: "dangtienhanh"},
{label: "Tạm ngưng", value: "tamngung"},
{label: "Đã hoàn thành", value: "hoanthanh"}
]
{ label: 'Đang tiến hành', value: 'dangtienhanh' },
{ label: 'Tạm ngưng', value: 'tamngung' },
{ label: 'Đã hoàn thành', value: 'hoanthanh' },
],
},
sort: {
type: FilterTypes.Picker,
label: "Sắp xếp",
value: "top",
label: 'Sắp xếp',
value: 'top',
options: [
{label: "A-Z", value: "tentruyen"},
{label: "Z-A", value: "tentruyenza"},
{label: "Mới cập nhật", value: "capnhat"},
{label: "Truyện mới", value: "truyenmoi"},
{label: "Theo dõi", value: "theodoi"},
{label: "Top toàn thời gian", value: "top"},
{label: "Top tháng", value: "topthang"},
{label: "Số từ", value: "sotu"},
]
}
{ label: 'A-Z', value: 'tentruyen' },
{ label: 'Z-A', value: 'tentruyenza' },
{ label: 'Mới cập nhật', value: 'capnhat' },
{ label: 'Truyện mới', value: 'truyenmoi' },
{ label: 'Theo dõi', value: 'theodoi' },
{ label: 'Top toàn thời gian', value: 'top' },
{ label: 'Top tháng', value: 'topthang' },
{ label: 'Số từ', value: 'sotu' },
],
},
} satisfies Filters;
}
+135 -123
View File
@@ -1,14 +1,13 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { NovelStatus } from "@libs/novelStatus";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { NovelStatus } from '@libs/novelStatus';
interface VolumeData {
story: string;
navigation: string;
value: string
value: string;
}
interface ChapterSelect {
@@ -17,44 +16,52 @@ interface ChapterSelect {
}
interface VolumeList {
chapters: Plugin.ChapterItem[],
volumes?: VolumeData[]
chapters: Plugin.ChapterItem[];
volumes?: VolumeData[];
}
class TruyenConect implements Plugin.PagePlugin {
id = "truyenconect";
name = "Truyen Conect";
icon = "src/vi/truyenconect/icon.png";
site = "https://truyenconect.com";
version = "1.0.0";
async sleep (ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
id = 'truyenconect';
name = 'Truyen Conect';
icon = 'src/vi/truyenconect/icon.png';
site = 'https://truyenconect.com';
version = '1.0.0';
async sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
parseNovels(loadedCheerio: CheerioAPI, selector: string){
parseNovels(loadedCheerio: CheerioAPI, selector: string) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(selector).each((idx, ele) => {
const url = loadedCheerio(ele).find('a').attr('href');
if(url){
if (url) {
novels.push({
name: loadedCheerio(ele).find('img').attr('alt') || '',
path: url.replace(this.site, ''),
cover: loadedCheerio(ele).find('img').attr('data-src'),
})
});
}
});
return novels;
}
async popularNovels(pageNo: number, {filters, showLatestNovels}: Plugin.PopularNovelsOptions<typeof this.filters>): Promise<Plugin.NovelItem[]> {
async popularNovels(
pageNo: number,
{
filters,
showLatestNovels,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let link = this.site;
let selector = '.c-page__content > .grid9.block .item-thumb.c-image-hover';
if(showLatestNovels){
selector = '.c-page__content .page-content-listing.item-big_thumbnail .item-thumb.c-image-hover';
}else if(filters.category.value){
if (showLatestNovels) {
selector =
'.c-page__content .page-content-listing.item-big_thumbnail .item-thumb.c-image-hover';
} else if (filters.category.value) {
link += '/' + filters.category.value;
selector = 'table.manga-shortcodes.manga-chapters-listing td[width="10%"]';
if(filters.category.value === 'the-loai'){
selector = '.item-thumb.hover-details.c-image-hover'
selector =
'table.manga-shortcodes.manga-chapters-listing td[width="10%"]';
if (filters.category.value === 'the-loai') {
selector = '.item-thumb.hover-details.c-image-hover';
link += '/' + filters.genre.value;
}
link += '?page=' + pageNo;
@@ -68,18 +75,18 @@ class TruyenConect implements Plugin.PagePlugin {
const chapters: Plugin.ChapterItem[] = [];
loadedCheerio('option').each((idx, ele) => {
let url = ele.attribs['value'];
if(!url) return;
if (!url) return;
const chapterId = url.match(/\/(\d+)-/)?.[1];
if(chapterId){
url = url.replace(chapterId+'-', '') + '-' + chapterId;
if (chapterId) {
url = url.replace(chapterId + '-', '') + '-' + chapterId;
}
let num = url.match(/chuong-(\d+)/)?.[1];
chapters.push({
path: url.replace(this.site, ''),
name: loadedCheerio(ele).text().trim(),
chapterNumber: Number(num) || undefined,
})
})
});
});
return chapters.reverse();
}
parseVolumes(loadedCheerio: CheerioAPI) {
@@ -89,91 +96,90 @@ class TruyenConect implements Plugin.PagePlugin {
story: ele.attribs['data-story'],
navigation: ele.attribs['data-navigation'],
value: ele.attribs['value'],
})
})
});
});
return volumes;
}
async getVolumes (firstChapterUrl: string) {
async getVolumes(firstChapterUrl: string) {
const chapterId = firstChapterUrl.match(/-(\d+)$/)?.[1];
if(!chapterId) throw new Error('Không tìm thấy chương');
if (!chapterId) throw new Error('Không tìm thấy chương');
const url = this.site + '/truyen/get-chap-selector?chap=' + chapterId;
const data: ChapterSelect = await fetch(url).then(r => r.json());
const volumeList: VolumeList = {
chapters: this.parseChapters(parseHTML(data.chap_selector))
chapters: this.parseChapters(parseHTML(data.chap_selector)),
};
if (data.eps_selector) {
volumeList.volumes = this.parseVolumes(
parseHTML(data.eps_selector as string),
);
}
if(data.eps_selector){
volumeList.volumes = this.parseVolumes(parseHTML(data.eps_selector as string))
return volumeList;
}
return volumeList
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & { totalPages: number; }> {
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
const url = this.site + novelPath;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel & { totalPages: number; }= {
name: loadedCheerio(".post-title > h1").text().trim(),
const novel: Plugin.SourceNovel & { totalPages: number } = {
name: loadedCheerio('.post-title > h1').text().trim(),
path: novelPath,
chapters: [],
totalPages: 1
totalPages: 1,
};
novel.cover = loadedCheerio(".summary_image > a > img").attr(
"data-src"
);
novel.cover = loadedCheerio('.summary_image > a > img').attr('data-src');
loadedCheerio(".post-content_item").each(function () {
loadedCheerio('.post-content_item').each(function () {
const detailName = loadedCheerio(this)
.find(".summary-heading > h5")
.find('.summary-heading > h5')
.text()
.trim();
const detail = loadedCheerio(this).find(".summary-content").html();
const detail = loadedCheerio(this).find('.summary-content').html();
if (!detail) return;
switch (detailName) {
case "Thể loại":
case 'Thể loại':
novel.genres = loadedCheerio(detail)
.children("a")
.children('a')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
.join(',');
break;
case "Tác giả":
case 'Tác giả':
novel.author = loadedCheerio(detail).text().trim();
break;
case "Hoạ sĩ":
case 'Hoạ sĩ':
novel.artist = loadedCheerio(detail).text().trim();
break;
case "Trạng thái":
case 'Trạng thái':
const status = detail.trim();
if(status === 'Full') novel.status = NovelStatus.Completed;
else if(status === 'Tạm ngưng') novel.status = NovelStatus.OnHiatus;
else if(status === 'Đang tiến hành') novel.status = NovelStatus.Ongoing
if (status === 'Full') novel.status = NovelStatus.Completed;
else if (status === 'Tạm ngưng') novel.status = NovelStatus.OnHiatus;
else if (status === 'Đang tiến hành')
novel.status = NovelStatus.Ongoing;
else novel.status = NovelStatus.Unknown;
break;
}
});
loadedCheerio(
".description-summary > div.summary__content > div"
).remove();
loadedCheerio('.description-summary > div.summary__content > div').remove();
novel.summary = loadedCheerio(
".description-summary > div.summary__content"
)
novel.summary = loadedCheerio('.description-summary > div.summary__content')
.text()
.trim();
const firstChapLink = loadedCheerio('#init-links > a').first().attr('href');
if(!firstChapLink) throw new Error('Không tìm thấy truyện');
if (!firstChapLink) throw new Error('Không tìm thấy truyện');
await this.sleep(1000);
const volumeList = await this.getVolumes(firstChapLink);
if(volumeList.volumes) {
if (volumeList.volumes) {
novel.totalPages = volumeList.volumes.length;
novel.chapters = volumeList.chapters
}else{
novel.chapters = volumeList.chapters;
} else {
novel.totalPages = 1;
novel.chapters = volumeList.chapters;
}
@@ -185,32 +191,38 @@ class TruyenConect implements Plugin.PagePlugin {
const body = await result.text();
let loadedCheerio = parseHTML(body);
const firstChapLink = loadedCheerio('#init-links > a').first().attr('href');
if(!firstChapLink) throw new Error('Không tìm thấy truyện');
if (!firstChapLink) throw new Error('Không tìm thấy truyện');
await this.sleep(1000);
const volumeList = await this.getVolumes(firstChapLink);
const volumeIndex = Number(page) - 1;
if(!volumeList.volumes) throw new Error("Không tìm thấy truyện");
if(volumeIndex >= volumeList.volumes.length) throw new Error("Không tìm thấy volume");
if (!volumeList.volumes) throw new Error('Không tìm thấy truyện');
if (volumeIndex >= volumeList.volumes.length)
throw new Error('Không tìm thấy volume');
const volume = volumeList.volumes[volumeIndex];
const query = `dataEpisodes=${volume.value}&datastory=${volume.story}&dataNavigation=${encodeURIComponent(volume.navigation)}`;
const chaptersUrl = `${this.site}/truyen/getreadingchapAction?${query}`;
await this.sleep(1000);
const res: {err: number, html: string} = await fetch(chaptersUrl).then(r => r.json());
if(res.err) throw new Error(res.html);
const res: { err: number; html: string } = await fetch(chaptersUrl).then(
r => r.json(),
);
if (res.err) throw new Error(res.html);
return {
chapters: this.parseChapters(parseHTML(res.html))
}
chapters: this.parseChapters(parseHTML(res.html)),
};
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetch(this.site + chapterPath);
const body = await result.text();
const loadedCheerio = parseHTML(body);
let chapterText = loadedCheerio(".reading-content").html() || '';
let chapterText = loadedCheerio('.reading-content').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}?key=${searchTerm}&page=${pageNo}`;
const result = await fetch(url);
const body = await result.text();
@@ -219,7 +231,7 @@ class TruyenConect implements Plugin.PagePlugin {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".tab-thumb.c-image-hover > a").each((idx, ele) => {
loadedCheerio('.tab-thumb.c-image-hover > a').each((idx, ele) => {
const novelName = ele.attribs['title'];
const novelCover = loadedCheerio(ele).find('img').first().attr('src');
const novelUrl = ele.attribs['href'];
@@ -240,59 +252,59 @@ class TruyenConect implements Plugin.PagePlugin {
}
filters = {
category: {
label: "Lọc theo",
label: 'Lọc theo',
value: '',
type: FilterTypes.Picker,
options: [
{label: '', value: ''},
{label: 'Thể loại', value: 'the-loai'},
{label: 'Truyện dịch', value: 'truyen-dich'},
{label: 'Truyện convert', value: 'convert'}
]
{ label: '', value: '' },
{ label: 'Thể loại', value: 'the-loai' },
{ label: 'Truyện dịch', value: 'truyen-dich' },
{ label: 'Truyện convert', value: 'convert' },
],
},
genre: {
label: 'Thể loại',
value: 'action',
type: FilterTypes.Picker,
options: [
{label: 'Action', value: 'action'},
{label: 'Adult', value: 'adult'},
{label: 'Adventure', value: 'adventure'},
{label: 'Chinese novel', value: 'chinese-novel'},
{label: 'Chuyển Sinh', value: 'chuyen-sinh'},
{label: 'English Novel', value: 'english-novel'},
{label: 'Harem', value: 'harem'},
{label: 'Ecchi', value: 'ecchi'},
{label: 'Fantasy', value: 'fantasy'},
{label: 'Drama', value: 'drama'},
{label: 'Game', value: 'game'},
{label: 'Tiên hiệp', value: 'tien-hiep'},
{label: 'Kiếm Hiệp', value: 'kiem-hiep'},
{label: 'Ngôn Tình', value: 'ngon-tinh'},
{label: 'Isekai', value: 'isekai'},
{label: 'Lịch Sử', value: 'lich-su'},
{label: 'Web Novel', value: 'web-novel'},
{label: 'Xuyên không', value: 'xuyen-khong'},
{label: 'Trọng sinh', value: 'trong-sinh'},
{label: 'Trinh thám', value: 'trinh-tham'},
{label: 'Dị giới', value: 'di-gioi'},
{label: 'Huyền ảo', value: 'huyen-ao'},
{label: 'Sắc Hiệp', value: 'sac-hiep'},
{label: 'Dị năng', value: 'di-nang'},
{label: 'Linh dị', value: 'linh-di'},
{label: 'Đô thị', value: 'do-thi'},
{label: 'Comedy', value: 'comedy'},
{label: 'School Life', value: 'school-life'},
{label: 'Romance', value: 'romance'},
{label: 'Martial-arts', value: 'martial-arts'},
{label: 'Light Novel', value: 'light-novel'},
{label: 'Huyền huyễn', value: 'huyen-huyen'},
{label: 'Kỳ Huyễn', value: 'ky-huyen'},
{label: 'Khoa Huyễn', value: 'khoa-huyen'},
{label: 'Võng Du', value: 'vong-du'},
{label: 'Đồng Nhân', value: 'dong-nhan'},
]
}
{ label: 'Action', value: 'action' },
{ label: 'Adult', value: 'adult' },
{ label: 'Adventure', value: 'adventure' },
{ label: 'Chinese novel', value: 'chinese-novel' },
{ label: 'Chuyển Sinh', value: 'chuyen-sinh' },
{ label: 'English Novel', value: 'english-novel' },
{ label: 'Harem', value: 'harem' },
{ label: 'Ecchi', value: 'ecchi' },
{ label: 'Fantasy', value: 'fantasy' },
{ label: 'Drama', value: 'drama' },
{ label: 'Game', value: 'game' },
{ label: 'Tiên hiệp', value: 'tien-hiep' },
{ label: 'Kiếm Hiệp', value: 'kiem-hiep' },
{ label: 'Ngôn Tình', value: 'ngon-tinh' },
{ label: 'Isekai', value: 'isekai' },
{ label: 'Lịch Sử', value: 'lich-su' },
{ label: 'Web Novel', value: 'web-novel' },
{ label: 'Xuyên không', value: 'xuyen-khong' },
{ label: 'Trọng sinh', value: 'trong-sinh' },
{ label: 'Trinh thám', value: 'trinh-tham' },
{ label: 'Dị giới', value: 'di-gioi' },
{ label: 'Huyền ảo', value: 'huyen-ao' },
{ label: 'Sắc Hiệp', value: 'sac-hiep' },
{ label: 'Dị năng', value: 'di-nang' },
{ label: 'Linh dị', value: 'linh-di' },
{ label: 'Đô thị', value: 'do-thi' },
{ label: 'Comedy', value: 'comedy' },
{ label: 'School Life', value: 'school-life' },
{ label: 'Romance', value: 'romance' },
{ label: 'Martial-arts', value: 'martial-arts' },
{ label: 'Light Novel', value: 'light-novel' },
{ label: 'Huyền huyễn', value: 'huyen-huyen' },
{ label: 'Kỳ Huyễn', value: 'ky-huyen' },
{ label: 'Khoa Huyễn', value: 'khoa-huyen' },
{ label: 'Võng Du', value: 'vong-du' },
{ label: 'Đồng Nhân', value: 'dong-nhan' },
],
},
} satisfies Filters;
}
+72 -55
View File
@@ -1,9 +1,9 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters } from "@libs/filterInputs";
import { NovelStatus } from "@libs/novelStatus";
import { NovelItem } from "../../test_web/static/js";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters } from '@libs/filterInputs';
import { NovelStatus } from '@libs/novelStatus';
import { NovelItem } from '../../test_web/static/js';
interface SearchedNovel {
name: string;
@@ -11,90 +11,104 @@ interface SearchedNovel {
coverUrl: string;
}
interface SearchedResult {
data?: SearchedNovel[]
data?: SearchedNovel[];
}
class LightNovelVN implements Plugin.PagePlugin {
id = "lightnovel.vn";
name = "Light Novel VN";
version = "1.0.0";
icon = "src/vi/lightnovelvn/icon.png";
id = 'lightnovel.vn';
name = 'Light Novel VN';
version = '1.0.0';
icon = 'src/vi/lightnovelvn/icon.png';
filters?: Filters | undefined;
site = "https://lightnovel.vn";
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
site = 'https://lightnovel.vn';
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/truyen-hot-ds?page=${pageNo}`;
const body = await fetch(url).then((r) => r.text());
const body = await fetch(url).then(r => r.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".flex.flex-col[itemtype='https://schema.org/Book']").each((idx, ele) => {
const novelName = loadedCheerio(ele).find('h3[itemprop="name"] > a').text().trim();
loadedCheerio(".flex.flex-col[itemtype='https://schema.org/Book']").each(
(idx, ele) => {
const novelName = loadedCheerio(ele)
.find('h3[itemprop="name"] > a')
.text()
.trim();
const img = loadedCheerio(ele).find('noscript').html();
const novelCover = img?.match(/srcSet="([^\s]+)/)?.[1];
const novelUrl = loadedCheerio(ele).find('h3[itemprop="name"] > a').attr('href');
if(novelUrl)
const novelUrl = loadedCheerio(ele)
.find('h3[itemprop="name"] > a')
.attr('href');
if (novelUrl)
novels.push({
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ''),
});
});
},
);
return novels;
}
parseChapters(loadedCheerio: CheerioAPI){
parseChapters(loadedCheerio: CheerioAPI) {
const chapters: Plugin.ChapterItem[] = [];
loadedCheerio('ul.chapter-list > li').each((idx, ele) => {
const chNum = Number(loadedCheerio(ele).find('div').first().text());
const chapterUrl = loadedCheerio(ele).find('a').attr('href');
const name = loadedCheerio(ele).find('a').text().trim();
if(chapterUrl){
if (chapterUrl) {
chapters.push({
path: chapterUrl.replace(this.site, ''),
name,
chapterNumber: chNum
})
chapterNumber: chNum,
});
}
})
});
return chapters;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & { totalPages: number; }> {
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
const url = this.site + novelPath;
const body = await fetch(url).then((r) => r.text());
const body = await fetch(url).then(r => r.text());
let loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel & { totalPages: number; } = {
const novel: Plugin.SourceNovel & { totalPages: number } = {
path: novelPath,
chapters: [],
name: 'Không có tiêu đề',
totalPages: 1
totalPages: 1,
};
novel.name = loadedCheerio('h1[itemprop="name"]').text().trim();
novel.cover = loadedCheerio('header div:nth-child(2) img').attr('srcset')?.split(/\s+/)[0]
novel.cover = loadedCheerio('header div:nth-child(2) img')
.attr('srcset')
?.split(/\s+/)[0];
const genres: string[] = [];
loadedCheerio('a[itemprop="genre"]').each(function() {
loadedCheerio('a[itemprop="genre"]').each(function () {
genres.push(loadedCheerio(this).text());
})
});
novel.genres = genres.join(',');
novel.status = loadedCheerio('span.font-bold.text-size22:last').text();
if(novel.status === 'Đang ra'){
if (novel.status === 'Đang ra') {
novel.status = NovelStatus.Ongoing;
}else if(novel.status === 'Hoàn thành'){
} else if (novel.status === 'Hoàn thành') {
novel.status = NovelStatus.Completed;
}else {
} else {
novel.status = NovelStatus.Unknown;
}
novel.author = loadedCheerio('a[itemprop="author"] > span').text();
novel.summary = loadedCheerio('#bookIntro').text().replace(/\s+/g, ' ');
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
await delay(1000);
const chapterListUrl = url + '/danh-sach-chuong';
@@ -103,13 +117,13 @@ class LightNovelVN implements Plugin.PagePlugin {
novel.chapters = this.parseChapters(loadedChapterList);
loadedChapterList('nav[aria-label="Pagination"] a').each((index, ele) => {
const href = ele.attribs['href'];
if(href){
if (href) {
const page = Number(href.match(/\?page=(\d+)/)?.[1]);
if(page && page > novel.totalPages){
if (page && page > novel.totalPages) {
novel.totalPages = page;
}
}
})
});
return novel;
}
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
@@ -117,45 +131,48 @@ class LightNovelVN implements Plugin.PagePlugin {
const chapterListBody = await fetchApi(url).then(r => r.text());
const chapters = this.parseChapters(parseHTML(chapterListBody));
return {
chapters
}
chapters,
};
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then((r) => r.text());
const body = await fetchApi(this.site + chapterPath).then(r => r.text());
const loadedCheerio = parseHTML(body);
let chapterText = loadedCheerio("div.chapter-content").html() || '';
let chapterText = loadedCheerio('div.chapter-content').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
if(pageNo > 1) return [];
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
if (pageNo > 1) return [];
const url = `${this.site}/api/book-search`;
let formData = new FormData();
formData.append("keyword", searchTerm);
formData.append('keyword', searchTerm);
const result: SearchedResult = await fetchApi(
url,
{
method: "POST",
const result: SearchedResult = await fetchApi(url, {
method: 'POST',
body: formData,
},
).then((r) => r.json());
}).then(r => r.json());
const novels: NovelItem[] = result.data?.map(item => {
const novels: NovelItem[] =
result.data?.map(item => {
return {
name: item.name,
path: '/truyen/' + item.slug,
cover: (this.site + item.coverUrl).replace('default.jpg', '150.jpg?w=256&q=')
}
cover: (this.site + item.coverUrl).replace(
'default.jpg',
'150.jpg?w=256&q=',
),
};
}) || [];
return novels;
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new LightNovelVN();
+53 -35
View File
@@ -1,8 +1,8 @@
import { fetchFile } from "@libs/fetch";
import { Filters } from "@libs/filterInputs";
import { NovelStatus } from "@libs/novelStatus";
import { Plugin } from "@typings/plugin";
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchFile } from '@libs/fetch';
import { Filters } from '@libs/filterInputs';
import { NovelStatus } from '@libs/novelStatus';
import { Plugin } from '@typings/plugin';
import { CheerioAPI, load as parseHTML } from 'cheerio';
class Nettruyen implements Plugin.PagePlugin {
id = 'nettruyen';
@@ -10,87 +10,102 @@ class Nettruyen implements Plugin.PagePlugin {
icon = 'src/vi/nettruyen/icon.png';
site = 'https://nettruyen.com.vn';
version = '1.0.0';
parseNovels(loadedCheerio: CheerioAPI){
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio('#listchuong > ul > li > a.thumb').each((idx, ele) => {
const path = ele.attribs['href'];
if(!path) return;
if (!path) return;
const name = ele.attribs['title'];
const cover = this.site + '/' + loadedCheerio(ele).find('img').first().attr('src');
const cover =
this.site + '/' + loadedCheerio(ele).find('img').first().attr('src');
novels.push({
path: '/' + path,
name,
cover
})
})
cover,
});
});
return novels;
}
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/xem-nhieu/trang-${pageNo}.html`;
const body = await fetch(url).then(r => r.text());
return this.parseNovels(parseHTML(body));
}
parseChapters(loadedCheerio: CheerioAPI){
parseChapters(loadedCheerio: CheerioAPI) {
const chapters: Plugin.ChapterItem[] = [];
loadedCheerio('ul.list-unstyled > li > a').each((idx, ele) => {
const path = ele.attribs['href'];
if(!path) return;
if (!path) return;
const name = ele.attribs['title'];
chapters.push({
path: '/' + path,
name,
chapterNumber: Number(name.match(/Chương (\d+)/)?.[1])
})
})
chapterNumber: Number(name.match(/Chương (\d+)/)?.[1]),
});
});
return chapters;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & { totalPages: number; }> {
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
const url = this.site + novelPath;
const body = await fetch(url).then(r => r.text());
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel & { totalPages: number; } = {
const novel: Plugin.SourceNovel & { totalPages: number } = {
path: novelPath,
name: loadedCheerio('.gioithieutruyen.profile-info > h1').text().trim(),
chapters: [],
totalPages: Number(loadedCheerio('.phantrang > span:nth-child(2)').text()) || 1,
}
novel.cover = this.site + '/' + loadedCheerio('.img-thumb > img').attr('src');
totalPages:
Number(loadedCheerio('.phantrang > span:nth-child(2)').text()) || 1,
};
novel.cover =
this.site + '/' + loadedCheerio('.img-thumb > img').attr('src');
loadedCheerio('.thongtintruyen.note.note-info').each((idx, ele) => {
const strong = loadedCheerio(ele).find('strong').text().trim();
switch(strong){
switch (strong) {
case 'Tác giả':
novel.author = loadedCheerio(ele).text().replace(/Tác giả\s+\n?:/, '')
novel.author = loadedCheerio(ele)
.text()
.replace(/Tác giả\s+\n?:/, '');
break;
case 'Trạng thái':
const text = loadedCheerio(ele).text();
if(text.includes('Đang ra')){
if (text.includes('Đang ra')) {
novel.status = NovelStatus.Ongoing;
}else if(text.includes('Hoàn thành')){
} else if (text.includes('Hoàn thành')) {
novel.status = NovelStatus.Completed;
}else {
} else {
novel.status = NovelStatus.Unknown;
}
break;
case 'Thể loại':
novel.genres = loadedCheerio('a > span').toArray().map((gEle) => loadedCheerio(gEle).text()).join(',');
novel.genres = loadedCheerio('a > span')
.toArray()
.map(gEle => loadedCheerio(gEle).text())
.join(',');
break;
default:
break;
}
});
novel.summary = loadedCheerio('.gioithieutruyen > .gioithieutruyen').text().trim()
novel.summary = loadedCheerio('.gioithieutruyen > .gioithieutruyen')
.text()
.trim();
novel.chapters = this.parseChapters(loadedCheerio);
return novel;
}
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
const id = novelPath.match(/-(\d+)\//)?.[1];
if(!id) throw new Error("Cant parse page");
const url = `${this.site}/ajax.chuong.php?id=${id}&page=${page}&url=${novelPath.replace(/\//g,'')}&loai=truyendich`;
if (!id) throw new Error('Cant parse page');
const url = `${this.site}/ajax.chuong.php?id=${id}&page=${page}&url=${novelPath.replace(/\//g, '')}&loai=truyendich`;
const body = await fetch(url).then(r => r.text());
const chapters = this.parseChapters(parseHTML(body));
return {
chapters
}
chapters,
};
}
async parseChapter(chapterPath: string): Promise<string> {
const url = this.site + chapterPath;
@@ -98,8 +113,11 @@ class Nettruyen implements Plugin.PagePlugin {
const loadedCheerio = parseHTML(body);
return loadedCheerio('#noidungchap').html() || '';
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
throw new Error("Method not implemented.");
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
throw new Error('Method not implemented.');
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
+57 -45
View File
@@ -1,58 +1,64 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters } from "@libs/filterInputs";
import { NovelStatus } from "@libs/novelStatus";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters } from '@libs/filterInputs';
import { NovelStatus } from '@libs/novelStatus';
class TruyenFull implements Plugin.PagePlugin {
id = "truyenchu";
name = "Truyện Chữ";
icon = "src/vi/truyenchu/icon.png";
site = "https://truyenchu.vn";
version = "1.0.0";
id = 'truyenchu';
name = 'Truyện Chữ';
icon = 'src/vi/truyenchu/icon.png';
site = 'https://truyenchu.vn';
version = '1.0.0';
parseNovels(loadedCheerio: CheerioAPI){
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".list-truyen .row").each((idx, ele) => {
const novelName = loadedCheerio(ele)
.find("h3.truyen-title > a")
.text();
loadedCheerio('.list-truyen .row').each((idx, ele) => {
const novelName = loadedCheerio(ele).find('h3.truyen-title > a').text();
const novelCover = this.site + loadedCheerio(ele).find("div[data-classname='cover']").attr("data-image");
const novelCover =
this.site +
loadedCheerio(ele)
.find("div[data-classname='cover']")
.attr('data-image');
const novelUrl = loadedCheerio(ele)
.find("h3.truyen-title > a")
.attr("href");
if(novelUrl){
.find('h3.truyen-title > a')
.attr('href');
if (novelUrl) {
novels.push({
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, '')
path: novelUrl.replace(this.site, ''),
});
}
});
return novels;
}
parseChapters(html: string){
const listChapterHTML =
html.match(/("list_chapter":\s?\\{0}"(.+)\\{0}"),/)?.[1];
if(!listChapterHTML) throw new Error("Không tải được chương");
parseChapters(html: string) {
const listChapterHTML = html.match(
/("list_chapter":\s?\\{0}"(.+)\\{0}"),/,
)?.[1];
if (!listChapterHTML) throw new Error('Không tải được chương');
const listChapter = JSON.parse(`{${listChapterHTML}}`);
const loadedChapterList = parseHTML(listChapter.list_chapter);
const chapters: Plugin.ChapterItem[] = [];
loadedChapterList('ul > li > a').each((idx, ele) => {
const path = ele.attribs['href'].replace(this.site, '');
if(path){
if (path) {
chapters.push({
name: ele.attribs['title'],
path,
chapterNumber: Number(path.match(/\/chuong-(\d+)/)?.[1])
})
chapterNumber: Number(path.match(/\/chuong-(\d+)/)?.[1]),
});
}
});
return chapters;
}
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/danh-sach/truyen-hot?page=${pageNo}`;
const result = await fetch(url);
const body = await result.text();
@@ -60,7 +66,9 @@ class TruyenFull implements Plugin.PagePlugin {
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
const url = this.site + novelPath;
const result = await fetch(url);
@@ -71,39 +79,39 @@ class TruyenFull implements Plugin.PagePlugin {
const regex = new RegExp(`${novelPath}\\?page=\\d+`, 'g');
body.match(regex)?.forEach(v => {
const page = Number(v.match(/\?page=(\d+)/)?.[1]);
if(page && page > lastPage){
if (page && page > lastPage) {
lastPage = page;
}
});
const novel: Plugin.SourceNovel & {totalPages: number} = {
const novel: Plugin.SourceNovel & { totalPages: number } = {
path: novelPath,
name: loadedCheerio("div.book > img").attr("alt") || 'Không có tiêu đề',
name: loadedCheerio('div.book > img').attr('alt') || 'Không có tiêu đề',
chapters: [],
totalPages: lastPage,
};
novel.cover = this.site + loadedCheerio("div.book > img").attr("src");
novel.cover = this.site + loadedCheerio('div.book > img').attr('src');
novel.summary = loadedCheerio("div.desc-text").text().trim();
novel.summary = loadedCheerio('div.desc-text').text().trim();
novel.author = loadedCheerio('h3:contains("Tác giả:")')
.parent()
.contents()
.text()
.replace("Tác giả:", "");
.replace('Tác giả:', '');
novel.genres = loadedCheerio('h3:contains("Thể loại")')
.siblings()
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
.join(',');
novel.status = loadedCheerio('h3:contains("Trạng thái")').next().text();
if(novel.status === "Full"){
if (novel.status === 'Full') {
novel.status = NovelStatus.Completed;
}else if(novel.status === 'Đang ra'){
} else if (novel.status === 'Đang ra') {
novel.status = NovelStatus.Ongoing;
}else{
} else {
novel.status = NovelStatus.Unknown;
}
novel.chapters = this.parseChapters(body);
@@ -116,8 +124,8 @@ class TruyenFull implements Plugin.PagePlugin {
const chapters = this.parseChapters(body);
return {
chapters
}
chapters,
};
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetch(this.site + chapterPath);
@@ -125,23 +133,27 @@ class TruyenFull implements Plugin.PagePlugin {
const loadedCheerio = parseHTML(body);
let chapterText = (loadedCheerio(".chapter-title").html() || '') + (loadedCheerio('#chapter-c').html() || '');
let chapterText =
(loadedCheerio('.chapter-title').html() || '') +
(loadedCheerio('#chapter-c').html() || '');
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const searchUrl = `${this.site}/tim-kiem?tukhoa=${searchTerm}&page=${pageNo}`;
const result = await fetch(searchUrl);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio)
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new TruyenFull();
+55 -44
View File
@@ -1,49 +1,54 @@
import { CheerioAPI, load as parseHTML } from "cheerio";
import { fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { Filters } from "@libs/filterInputs";
import { NovelStatus } from "@libs/novelStatus";
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { Filters } from '@libs/filterInputs';
import { NovelStatus } from '@libs/novelStatus';
class TruyenFull implements Plugin.PagePlugin {
id = "truyenfull";
name = "Truyện Full";
icon = "src/vi/truyenfull/icon.png";
site = "https://truyenfull.vn";
version = "1.0.0";
id = 'truyenfull';
name = 'Truyện Full';
icon = 'src/vi/truyenfull/icon.png';
site = 'https://truyenfull.vn';
version = '1.0.0';
parseNovels(loadedCheerio: CheerioAPI){
parseNovels(loadedCheerio: CheerioAPI) {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".list-truyen .row").each((idx, ele) => {
const novelName = loadedCheerio(ele)
.find("h3.truyen-title > a")
.text();
loadedCheerio('.list-truyen .row').each((idx, ele) => {
const novelName = loadedCheerio(ele).find('h3.truyen-title > a').text();
const novelCover = loadedCheerio(ele).find("div[data-classname='cover']").attr("data-image");
const novelCover = loadedCheerio(ele)
.find("div[data-classname='cover']")
.attr('data-image');
const novelUrl = loadedCheerio(ele)
.find("h3.truyen-title > a")
.attr("href");
if(novelUrl){
.find('h3.truyen-title > a')
.attr('href');
if (novelUrl) {
novels.push({
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, '')
path: novelUrl.replace(this.site, ''),
});
}
});
return novels;
}
parseChapters(loadedCheerio: CheerioAPI): Plugin.ChapterItem[]{
return loadedCheerio('ul.list-chapter > li > a').toArray().map((ele) => {
parseChapters(loadedCheerio: CheerioAPI): Plugin.ChapterItem[] {
return loadedCheerio('ul.list-chapter > li > a')
.toArray()
.map(ele => {
const path = ele.attribs['href'].replace(this.site, '');
return {
name: loadedCheerio(ele).text().trim(),
path,
chapterNumber: Number(path.match(/\/chuong-(\d+)\//)?.[1])
chapterNumber: Number(path.match(/\/chuong-(\d+)\//)?.[1]),
};
});
}
})
}
async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions<Filters>): Promise<Plugin.NovelItem[]> {
async popularNovels(
pageNo: number,
options: Plugin.PopularNovelsOptions<Filters>,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/danh-sach/truyen-hot/trang-${pageNo}/`;
const result = await fetch(url);
@@ -53,7 +58,9 @@ class TruyenFull implements Plugin.PagePlugin {
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel & {totalPages: number}> {
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
const url = this.site + novelPath;
const result = await fetch(url);
@@ -61,40 +68,40 @@ class TruyenFull implements Plugin.PagePlugin {
let loadedCheerio = parseHTML(body);
let lastPage = 1;
loadedCheerio('ul.pagination.pagination-sm > li > a').each(function() {
loadedCheerio('ul.pagination.pagination-sm > li > a').each(function () {
const page = Number(this.attribs['href'].match(/\/trang-(\d+)\//)?.[1]);
if(page && page > lastPage) lastPage = page;
if (page && page > lastPage) lastPage = page;
});
const novel: Plugin.SourceNovel & {totalPages: number} = {
const novel: Plugin.SourceNovel & { totalPages: number } = {
path: novelPath,
name: loadedCheerio("div.book > img").attr("alt") || 'Không có tiêu đề',
name: loadedCheerio('div.book > img').attr('alt') || 'Không có tiêu đề',
chapters: [],
totalPages: lastPage,
};
novel.cover = loadedCheerio("div.book > img").attr("src");
novel.cover = loadedCheerio('div.book > img').attr('src');
novel.summary = loadedCheerio("div.desc-text").text().trim();
novel.summary = loadedCheerio('div.desc-text').text().trim();
novel.author = loadedCheerio('h3:contains("Tác giả:")')
.parent()
.contents()
.text()
.replace("Tác giả:", "");
.replace('Tác giả:', '');
novel.genres = loadedCheerio('h3:contains("Thể loại")')
.siblings()
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(",");
.join(',');
novel.status = loadedCheerio('h3:contains("Trạng thái")').next().text();
if(novel.status === "Full"){
if (novel.status === 'Full') {
novel.status = NovelStatus.Completed;
}else if(novel.status === 'Đang ra'){
} else if (novel.status === 'Đang ra') {
novel.status = NovelStatus.Ongoing;
}else{
} else {
novel.status = NovelStatus.Unknown;
}
novel.chapters = this.parseChapters(loadedCheerio);
@@ -108,8 +115,8 @@ class TruyenFull implements Plugin.PagePlugin {
const loadedCheerio = parseHTML(body);
const chapters = this.parseChapters(loadedCheerio);
return {
chapters
}
chapters,
};
}
async parseChapter(chapterPath: string): Promise<string> {
const result = await fetch(this.site + chapterPath);
@@ -117,23 +124,27 @@ class TruyenFull implements Plugin.PagePlugin {
const loadedCheerio = parseHTML(body);
let chapterText = (loadedCheerio(".chapter-title").html() || '') + (loadedCheerio('#chapter-c').html() || '');
let chapterText =
(loadedCheerio('.chapter-title').html() || '') +
(loadedCheerio('#chapter-c').html() || '');
return chapterText;
}
async searchNovels(searchTerm: string, pageNo: number): Promise<Plugin.NovelItem[]> {
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const searchUrl = `${this.site}/tim-kiem?tukhoa=${searchTerm}&page=${pageNo}`;
const result = await fetch(searchUrl);
const body = await result.text();
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio)
return this.parseNovels(loadedCheerio);
}
async fetchImage(url: string): Promise<string | undefined> {
return fetchFile(url);
}
}
export default new TruyenFull();
+36 -36
View File
@@ -1,27 +1,27 @@
require("module-alias/register");
require('module-alias/register');
import * as fs from "fs";
import { languages } from "@libs/languages";
import * as path from "path";
import { Plugin } from "@typings/plugin";
import { minify } from "./terser";
import * as fs from 'fs';
import { languages } from '@libs/languages';
import * as path from 'path';
import { Plugin } from '@typings/plugin';
import { minify } from './terser';
const root = path.dirname(__dirname);
const outRoot = path.join(root, "..");
const { execSync } = require("child_process");
const REMOTE = execSync("git remote get-url origin")
const outRoot = path.join(root, '..');
const { execSync } = require('child_process');
const REMOTE = execSync('git remote get-url origin')
.toString()
.replace(/[\s\n]/g, "");
let CURRENT_BRANCH = "dist";
.replace(/[\s\n]/g, '');
let CURRENT_BRANCH = 'dist';
try {
CURRENT_BRANCH = execSync("git rev-parse --abbrev-ref HEAD")
CURRENT_BRANCH = execSync('git rev-parse --abbrev-ref HEAD')
.toString()
.replace(/[\s\n]/g, "");
.replace(/[\s\n]/g, '');
} catch {
//
}
const matched = REMOTE.match(/([^:/]+?)\/([^/.]+)(\.git)?$/);
if (!matched) throw Error("Cant parse git url");
if (!matched) throw Error('Cant parse git url');
const USERNAME = matched[1];
const REPO = matched[2];
const USER_CONTENT_LINK = `https://raw.githubusercontent.com/${USERNAME}/${REPO}/${CURRENT_BRANCH}`;
@@ -40,29 +40,29 @@ const json: {
iconUrl: string;
}[];
} = {};
if (!fs.existsSync(path.join(outRoot, ".dist"))) {
fs.mkdirSync(path.join(outRoot, ".dist"));
if (!fs.existsSync(path.join(outRoot, '.dist'))) {
fs.mkdirSync(path.join(outRoot, '.dist'));
}
const jsonPath = path.join(outRoot, ".dist", "plugins.json");
const jsonMinPath = path.join(outRoot, ".dist", "plugins.min.json");
const jsonPath = path.join(outRoot, '.dist', 'plugins.json');
const jsonMinPath = path.join(outRoot, '.dist', 'plugins.min.json');
const pluginSet = new Set();
let totalPlugins = 0;
for (let language in languages) {
// language with English name
const langPath = path.join(root, "plugins", language.toLowerCase());
const langPath = path.join(root, 'plugins', language.toLowerCase());
if (!fs.existsSync(langPath)) continue;
const plugins = fs.readdirSync(langPath);
json[language] = [];
plugins.forEach((plugin) => {
if (plugin.startsWith(".")) return;
plugins.forEach(plugin => {
if (plugin.startsWith('.')) return;
minify(path.join(langPath, plugin));
const instance: Plugin.PluginBase = require(
`../plugins/${language.toLowerCase()}/${plugin.split(".")[0]}`,
`../plugins/${language.toLowerCase()}/${plugin.split('.')[0]}`,
).default;
const { id, name, site, version, icon } = instance;
const normalisedName = name.replace(/\[.*\]/, "");
const normalisedName = name.replace(/\[.*\]/, '');
const info = {
id,
@@ -76,44 +76,44 @@ for (let language in languages) {
if (pluginSet.has(id)) {
console.log("There's already a plugin with id:", id);
throw new Error("2 or more plugins have the same id");
throw new Error('2 or more plugins have the same id');
} else {
pluginSet.add(id);
}
json[language].push(info);
totalPlugins += 1;
console.log(name, "✅");
console.log(name, '✅');
});
}
for (let lang in json) json[lang].sort((a, b) => a.id.localeCompare(b.id));
fs.writeFileSync(jsonMinPath, JSON.stringify(json));
fs.writeFileSync(jsonPath, JSON.stringify(json, null, "\t"));
if(CURRENT_BRANCH === 'dist'){
fs.writeFileSync(jsonPath, JSON.stringify(json, null, '\t'));
if (CURRENT_BRANCH === 'dist') {
fetch(`https://img.shields.io/badge/Plugins-${totalPlugins}-blue`)
.then(res => res.text())
.then(data => {
fs.writeFileSync('total.svg', data, {encoding: 'utf-8'});
})
fs.writeFileSync('total.svg', data, { encoding: 'utf-8' });
});
}
// check for broken plugins
for (let language in languages) {
const tsFiles = fs.readdirSync(
path.join(root, "..", "plugins", language.toLocaleLowerCase())
path.join(root, '..', 'plugins', language.toLocaleLowerCase()),
);
tsFiles
.filter((f) => f.endsWith(".broken.ts"))
.forEach((fn) => {
.filter(f => f.endsWith('.broken.ts'))
.forEach(fn => {
console.error(
language.toLocaleLowerCase() +
"/" +
fn.replace(".broken.ts", "") +
" ❌"
'/' +
fn.replace('.broken.ts', '') +
' ❌',
);
});
}
console.log(jsonPath);
console.log("Done ✅");
console.log('Done ✅');
+14 -14
View File
@@ -1,5 +1,5 @@
import path from "path";
import fs from "fs";
import path from 'path';
import fs from 'fs';
type GeneratedScript = {
lang: string;
@@ -10,34 +10,32 @@ type GeneratedScript = {
export type ScrpitGeneratorFunction = (name: string) => GeneratedScript[];
const isScriptGenerator = (s: unknown): s is ScrpitGeneratorFunction => {
return !!s && typeof s === "function";
return !!s && typeof s === 'function';
};
const generate = async (name: string): Promise<boolean> => {
try {
const generateAll: ScrpitGeneratorFunction | unknown =
require(`./${name}/generator`).generateAll;
const generateAll: ScrpitGeneratorFunction | unknown = require(
`./${name}/generator`,
).generateAll;
if (!isScriptGenerator(generateAll)) return false;
const sources = generateAll(name);
for (let source of sources) {
const { lang, filename, pluginScript } = source;
if (!lang || !filename || !pluginScript) {
console.warn(
name,
": lang, filename, pluginScript are required!"
);
console.warn(name, ': lang, filename, pluginScript are required!');
continue;
}
const pluginsDir = path.join(
path.dirname(path.dirname(__dirname)),
"plugins"
'plugins',
);
const filePath = path.join(
pluginsDir,
lang.toLowerCase(),
filename.replace(/[\s-\.]+/g, "") + `[${name}].ts`
filename.replace(/[\s-\.]+/g, '') + `[${name}].ts`,
);
fs.writeFileSync(filePath, pluginScript, { encoding: "utf-8" });
fs.writeFileSync(filePath, pluginScript, { encoding: 'utf-8' });
}
return true;
} catch (e) {
@@ -49,8 +47,10 @@ const generate = async (name: string): Promise<boolean> => {
const run = async () => {
const sources = fs
.readdirSync(__dirname)
.filter((name) =>
fs.lstatSync(path.join(__dirname, name)).isDirectory() && !name.endsWith('.broken')
.filter(
name =>
fs.lstatSync(path.join(__dirname, name)).isDirectory() &&
!name.endsWith('.broken'),
);
for (let name of sources) {
+24 -24
View File
@@ -1,43 +1,43 @@
import * as fs from "fs";
import * as cheerio from "cheerio";
import * as path from "path";
import list from "./sources.json";
import { HotNovelPubMetadata } from "./template";
import * as fs from 'fs';
import * as cheerio from 'cheerio';
import * as path from 'path';
import list from './sources.json';
import { HotNovelPubMetadata } from './template';
async function getFilters(sources: HotNovelPubMetadata) {
const filters: any = {
sort: {
type: "Picker",
label: "Order",
value: "hot",
type: 'Picker',
label: 'Order',
value: 'hot',
options: [],
},
category: {
type: "Picker",
label: "category",
value: "",
options: [{ label: "NONE", value: "" }],
type: 'Picker',
label: 'category',
value: '',
options: [{ label: 'NONE', value: '' }],
},
};
const body = await fetch(sources.sourceSite).then((res) => res.text());
const body = await fetch(sources.sourceSite).then(res => res.text());
const $: cheerio.CheerioAPI = cheerio.load(body);
$(".new-update").remove();
$('.new-update').remove();
$("section > div").each((i, el) => {
const id = $(el).find('a[class="see-all"]').attr("href");
$('section > div').each((i, el) => {
const id = $(el).find('a[class="see-all"]').attr('href');
if (id) {
filters.sort.options.push({
label: $(el).find('[class="section-title"]').text().trim(),
value: id.split("/").pop(),
value: id.split('/').pop(),
});
}
});
filters.category.label = $(".category-title").text().trim();
const apiSite = sources.sourceSite.replace("://", "://api.");
const jsonRaw = await fetch(apiSite + "/categories", {
filters.category.label = $('.category-title').text().trim();
const apiSite = sources.sourceSite.replace('://', '://api.');
const jsonRaw = await fetch(apiSite + '/categories', {
headers: {
lang: sources.options?.lang || "en",
lang: sources.options?.lang || 'en',
},
});
const json = (await jsonRaw.json()) as response;
@@ -45,7 +45,7 @@ async function getFilters(sources: HotNovelPubMetadata) {
if (json.data?.length) {
json.data
.sort((a, b) => a.name.localeCompare(b.name))
.forEach((category) =>
.forEach(category =>
filters.category.options.push({
label: category.name,
value: category.slug,
@@ -69,13 +69,13 @@ interface DataEntity {
async function start() {
const result = [];
for (const sources of list) {
console.log("updating the filters in", sources.sourceName);
console.log('updating the filters in', sources.sourceName);
const NewFilters = await getFilters(sources as any);
sources.filters = NewFilters;
result.push(sources);
}
fs.writeFileSync(
path.join(__dirname, "sources.json"),
path.join(__dirname, 'sources.json'),
JSON.stringify(result, null, 2),
);
}
+16 -15
View File
@@ -1,21 +1,21 @@
import { FilterTypes, Filters } from "../../../libs/filterInputs";
import { ScrpitGeneratorFunction } from "../generate";
import list from "./sources.json";
import { HotNovelPubMetadata } from "./template";
import { readFileSync } from "fs";
import path from "path";
import { FilterTypes, Filters } from '../../../libs/filterInputs';
import { ScrpitGeneratorFunction } from '../generate';
import list from './sources.json';
import { HotNovelPubMetadata } from './template';
import { readFileSync } from 'fs';
import path from 'path';
const lang: { [key: string]: string } = {
en: "english",
ru: "russian",
es: "spanish",
pt: "portuguese",
th: "turkish",
en: 'english',
ru: 'russian',
es: 'spanish',
pt: 'portuguese',
th: 'turkish',
};
export const generateAll: ScrpitGeneratorFunction = function (name) {
return list
.map<HotNovelPubMetadata>((p) => {
.map<HotNovelPubMetadata>(p => {
const filters: Filters = {};
for (const k in p.filters) {
const f = p.filters[k as keyof typeof p.filters];
@@ -36,8 +36,9 @@ export const generateAll: ScrpitGeneratorFunction = function (name) {
const generator = function generator(metadata: HotNovelPubMetadata) {
const HotNovelPubTemplate = readFileSync(
path.join(__dirname, "template.ts"), {
encoding: "utf-8",
path.join(__dirname, 'template.ts'),
{
encoding: 'utf-8',
},
);
@@ -51,7 +52,7 @@ export default plugin;
`.trim();
return {
lang: lang[metadata?.options?.lang || "en"] || "english",
lang: lang[metadata?.options?.lang || 'en'] || 'english',
filename: metadata.sourceName,
pluginScript,
};
+34 -32
View File
@@ -1,9 +1,9 @@
import { fetchFile, fetchApi } from "@libs/fetch";
import { Filters, FilterTypes } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import dayjs from "dayjs";
import { fetchFile, fetchApi } from '@libs/fetch';
import { Filters, FilterTypes } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
export interface HotNovelPubMetadata {
id: string;
@@ -32,23 +32,23 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
this.name = metadata.sourceName;
this.icon = `multisrc/hotnovelpub/icons/${metadata.id}.png`;
this.site = metadata.sourceSite;
this.apiSite = metadata.sourceSite.replace("://", "://api.");
this.version = "1.0.0";
this.apiSite = metadata.sourceSite.replace('://', '://api.');
this.version = '1.0.0';
this.filters = metadata.filters;
this.lang = metadata.options?.lang || "en";
this.lang = metadata.options?.lang || 'en';
}
async popularNovels(
pageNo: number,
{ filters, showLatestNovels }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
let url = this.apiSite + "/books/";
url += showLatestNovels ? "new" : filters?.sort?.value || "hot";
let url = this.apiSite + '/books/';
url += showLatestNovels ? 'new' : filters?.sort?.value || 'hot';
if (filters?.category?.value) {
url = this.apiSite + "/category/" + filters.category.value;
url = this.apiSite + '/category/' + filters.category.value;
}
url += "/?page=" + (pageNo - 1) + "&limit=20";
url += '/?page=' + (pageNo - 1) + '&limit=20';
const result = await fetchApi(url, {
headers: {
@@ -59,11 +59,11 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
const novels: Plugin.NovelItem[] = [];
if (json.status && json.data.books.data?.length) {
json.data.books.data.forEach((novel) =>
json.data.books.data.forEach(novel =>
novels.push({
name: novel.name,
cover: this.site + novel.image,
path: "/" + novel.slug,
path: '/' + novel.slug,
}),
);
}
@@ -71,7 +71,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.apiSite + "/book" + novelPath, {
const result = await fetchApi(this.apiSite + '/book' + novelPath, {
headers: {
lang: this.lang,
},
@@ -85,13 +85,13 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
summary: json.data.book.authorize.description,
author: json.data.book.authorize.name,
status:
json.data.book.status === "updating"
json.data.book.status === 'updating'
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
if (json.data.tags.tags_name?.length) {
novel.genres = json.data.tags.tags_name.join(",");
novel.genres = json.data.tags.tags_name.join(',');
}
if (json.data.chapters?.length) {
@@ -99,7 +99,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
json.data.chapters.forEach((chapter, chapterIndex) =>
chapters.push({
name: chapter.title,
path: "/" + chapter.slug,
path: '/' + chapter.slug,
releaseTime: undefined,
chapterNumber: (chapter.index || chapterIndex) + 1,
}),
@@ -111,50 +111,52 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
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);
let chapterText = loadedCheerio("#content-item").html() || "";
let chapterText = loadedCheerio('#content-item').html() || '';
if (chapterText) {
const result = await fetchApi(
this.site + "/server/getContent?slug=" + chapterPath,
this.site + '/server/getContent?slug=' + chapterPath,
);
const json = (await result.json()) as ChapterType;
if (json.data) {
chapterText += json.data
.map((item) => "<p>" + item + "</p>")
.join("")
.replace(/\n/g, "</p><p>")
.replace(/\s/g, " ");
.map(item => '<p>' + item + '</p>')
.join('')
.replace(/\n/g, '</p><p>')
.replace(/\s/g, ' ');
}
}
return chapterText.replace(/\.copy right hot novel pub/g, "");
return chapterText.replace(/\.copy right hot novel pub/g, '');
}
async searchNovels(
searchTerm: string,
pageNo: number,
): Promise<Plugin.NovelItem[]> {
const result = await fetchApi(this.apiSite + "/search", {
const result = await fetchApi(this.apiSite + '/search', {
headers: {
"Content-Type": "application/json;charset=utf-8",
'Content-Type': 'application/json;charset=utf-8',
Referer: this.site,
Origin: this.site,
lang: this.lang,
},
method: "POST",
method: 'POST',
body: JSON.stringify({ key_search: searchTerm }),
});
const json = (await result.json()) as responseSearch;
const novels: Plugin.NovelItem[] = [];
if (json.status && json.data.books?.length) {
json.data.books.forEach((novel) =>
json.data.books.forEach(novel =>
novels.push({
name: novel.name,
cover: undefined,
path: "/" + novel.slug,
path: '/' + novel.slug,
}),
);
}
+10 -10
View File
@@ -1,10 +1,10 @@
import { Filters } from "../../../libs/filterInputs";
import { ScrpitGeneratorFunction } from "../generate";
import list from "./sources.json";
import defaultSettings from "./settings.json";
import { IfreedomMetadata } from "./template";
import { readFileSync } from "fs";
import path from "path";
import { Filters } from '../../../libs/filterInputs';
import { ScrpitGeneratorFunction } from '../generate';
import list from './sources.json';
import defaultSettings from './settings.json';
import { IfreedomMetadata } from './template';
import { readFileSync } from 'fs';
import path from 'path';
export const generateAll: ScrpitGeneratorFunction = function (name) {
return list.map((metadata: IfreedomMetadata) => {
@@ -15,8 +15,8 @@ export const generateAll: ScrpitGeneratorFunction = function (name) {
};
const generator = function generator(metadata: IfreedomMetadata) {
const IfreedomTemplate = readFileSync(path.join(__dirname, "template.ts"), {
encoding: "utf-8",
const IfreedomTemplate = readFileSync(path.join(__dirname, 'template.ts'), {
encoding: 'utf-8',
});
const pluginScript = `
@@ -29,7 +29,7 @@ export default plugin;
`.trim();
return {
lang: "russian",
lang: 'russian',
filename: metadata.sourceName,
pluginScript,
};
+71 -60
View File
@@ -1,9 +1,9 @@
import { fetchFile, fetchApi } from "@libs/fetch";
import { Filters, FilterTypes } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import dayjs from "dayjs";
import { fetchFile, fetchApi } from '@libs/fetch';
import { Filters, FilterTypes } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
export interface IfreedomMetadata {
id: string;
@@ -25,7 +25,7 @@ class IfreedomPlugin implements Plugin.PluginBase {
this.name = metadata.sourceName;
this.icon = `multisrc/ifreedom/icons/${metadata.id}.png`;
this.site = metadata.sourceSite;
this.version = "1.0.0";
this.version = '1.0.0';
this.filters = metadata.filters;
}
@@ -36,87 +36,95 @@ class IfreedomPlugin implements Plugin.PluginBase {
showLatestNovels,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.site + "/vse-knigi/?sort=" +
(showLatestNovels ? "По дате обновления" : filters?.sort?.value || "По рейтингу");
let url =
this.site +
'/vse-knigi/?sort=' +
(showLatestNovels
? 'По дате обновления'
: filters?.sort?.value || 'По рейтингу');
Object.entries(filters || {}).forEach(([type, { value }]) => {
if (value instanceof Array && value.length) {
url += "&" + type + "[]=" + value.join("&" + type + "[]=");
url += '&' + type + '[]=' + value.join('&' + type + '[]=');
}
});
url += "&bpage=" + page;
url += '&bpage=' + page;
const body = await fetchApi(url).then((res) => res.text());
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = loadedCheerio(
"div.one-book-home > div.img-home a",
'div.one-book-home > div.img-home a',
)
.map((index, element) => ({
name: loadedCheerio(element).attr("title") || "",
cover: loadedCheerio(element).find("img").attr("src"),
name: loadedCheerio(element).attr('title') || '',
cover: loadedCheerio(element).find('img').attr('src'),
path:
loadedCheerio(element).attr("href")?.replace?.(this.site, "") || "",
loadedCheerio(element).attr('href')?.replace?.(this.site, '') || '',
}))
.get()
.filter((novel) => novel.name && novel.path);
.filter(novel => novel.name && novel.path);
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then((res) => res.text());
const body = await fetchApi(this.site + novelPath).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio(".entry-title").text(),
cover: loadedCheerio(".img-ranobe > img").attr("src"),
summary: loadedCheerio('meta[name="description"]').attr("content"),
name: loadedCheerio('.entry-title').text(),
cover: loadedCheerio('.img-ranobe > img').attr('src'),
summary: loadedCheerio('meta[name="description"]').attr('content'),
};
loadedCheerio("div.data-ranobe").each(function () {
switch (loadedCheerio(this).find("b").text()) {
case "Автор":
loadedCheerio('div.data-ranobe').each(function () {
switch (loadedCheerio(this).find('b').text()) {
case 'Автор':
novel.author = loadedCheerio(this)
.find("div.data-value")
.find('div.data-value')
.text()
.trim();
break;
case "Жанры":
novel.genres = loadedCheerio("div.data-value > a")
case 'Жанры':
novel.genres = loadedCheerio('div.data-value > a')
.map((index, element) => loadedCheerio(element).text()?.trim())
.get()
.join(",");
.join(',');
break;
case "Статус книги":
novel.status = loadedCheerio("div.data-value")
case 'Статус книги':
novel.status = loadedCheerio('div.data-value')
.text()
.includes("активен")
.includes('активен')
? NovelStatus.Ongoing
: NovelStatus.Completed;
break;
}
});
if (novel.author == "Не указан") delete novel.author;
if (novel.author == 'Не указан') delete novel.author;
const chapters: Plugin.ChapterItem[] = [];
const totalChapters = loadedCheerio("div.li-ranobe").length;
const totalChapters = loadedCheerio('div.li-ranobe').length;
loadedCheerio("div.li-ranobe").each((chapterIndex, element) => {
const name = loadedCheerio(element).find("a").text();
const url = loadedCheerio(element).find("a").attr("href");
if (!loadedCheerio(element).find("label.buy-ranobe").length && name && url) {
loadedCheerio('div.li-ranobe').each((chapterIndex, element) => {
const name = loadedCheerio(element).find('a').text();
const url = loadedCheerio(element).find('a').attr('href');
if (
!loadedCheerio(element).find('label.buy-ranobe').length &&
name &&
url
) {
const releaseDate = loadedCheerio(element)
.find("div.li-col2-ranobe")
.find('div.li-col2-ranobe')
.text()
.trim();
chapters.push({
name,
path: url.replace(this.site, ""),
path: url.replace(this.site, ''),
releaseTime: this.parseDate(releaseDate),
chapterNumber: totalChapters - chapterIndex,
});
@@ -128,21 +136,23 @@ class IfreedomPlugin implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
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);
loadedCheerio(".entry-content img").each((index, element) => {
const srcset = loadedCheerio(element).attr("srcset")?.split?.(" ");
loadedCheerio('.entry-content img').each((index, element) => {
const srcset = loadedCheerio(element).attr('srcset')?.split?.(' ');
if (srcset?.length) {
loadedCheerio(element).removeAttr("srcset");
const bestlink: string[] = srcset.filter((url) => url.startsWith("http"));
loadedCheerio(element).removeAttr('srcset');
const bestlink: string[] = srcset.filter(url => url.startsWith('http'));
if (bestlink[bestlink.length - 1]) {
loadedCheerio(element).attr("src", bestlink[bestlink.length - 1]);
loadedCheerio(element).attr('src', bestlink[bestlink.length - 1]);
}
}
});
const chapterText = loadedCheerio(".entry-content").html() || "";
const chapterText = loadedCheerio('.entry-content').html() || '';
return chapterText;
}
@@ -151,25 +161,26 @@ class IfreedomPlugin implements Plugin.PluginBase {
page: number | undefined = 1,
): Promise<Plugin.NovelItem[]> {
const url =
this.site + "/vse-knigi/?searchname=" + searchTerm + "&bpage=" + page;
const result = await fetchApi(url).then((res) => res.text());
this.site + '/vse-knigi/?searchname=' + searchTerm + '&bpage=' + page;
const result = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(result);
const novels: Plugin.NovelItem[] = loadedCheerio(
"div.one-book-home > div.img-home a",
'div.one-book-home > div.img-home a',
)
.map((index, element) => ({
name: loadedCheerio(element).attr("title") || "",
cover: loadedCheerio(element).find("img").attr("src"),
path: loadedCheerio(element).attr("href")?.replace?.(this.site, "") || "",
name: loadedCheerio(element).attr('title') || '',
cover: loadedCheerio(element).find('img').attr('src'),
path:
loadedCheerio(element).attr('href')?.replace?.(this.site, '') || '',
}))
.get()
.filter((novel) => novel.name && novel.path);
.filter(novel => novel.name && novel.path);
return novels;
}
parseDate = (dateString: string | undefined = "") => {
parseDate = (dateString: string | undefined = '') => {
const months: { [key: string]: number } = {
января: 1,
февраля: 2,
@@ -185,16 +196,16 @@ class IfreedomPlugin implements Plugin.PluginBase {
декабря: 12,
};
if (dateString.includes(".")) {
const [day, month, year] = dateString.split(".");
if (dateString.includes('.')) {
const [day, month, year] = dateString.split('.');
if (day && month && year) {
return dayjs(year + "-" + month + "-" + day).format("LL");
return dayjs(year + '-' + month + '-' + day).format('LL');
}
} else if (dateString.includes(" ")) {
const [day, month] = dateString.split(" ");
} else if (dateString.includes(' ')) {
const [day, month] = dateString.split(' ');
if (day && months[month]) {
const year = new Date().getFullYear();
return dayjs(year + "-" + months[month] + "-" + day).format("LL");
return dayjs(year + '-' + months[month] + '-' + day).format('LL');
}
}
return dateString || null;
+15 -10
View File
@@ -1,8 +1,8 @@
import { ScrpitGeneratorFunction } from "../generate";
import { LightNovelWPMetadata } from "./template";
import list from "./sources.json";
import { readFileSync } from "fs";
import path from "path";
import { ScrpitGeneratorFunction } from '../generate';
import { LightNovelWPMetadata } from './template';
import list from './sources.json';
import { readFileSync } from 'fs';
import path from 'path';
export const generateAll: ScrpitGeneratorFunction = function (name) {
return list.map((source: LightNovelWPMetadata) => {
@@ -11,15 +11,20 @@ export const generateAll: ScrpitGeneratorFunction = function (name) {
filters = require(`./filters/${source.id}`);
source.filters = filters.filters;
} catch (e) {}
console.log(`[${name}] Generating: ${source.id}${" ".repeat(20 - source.id.length)} ${source.filters ? "🔎with filters🔍" : "🚫no filters🚫"}`);
console.log(
`[${name}] Generating: ${source.id}${' '.repeat(20 - source.id.length)} ${source.filters ? '🔎with filters🔍' : '🚫no filters🚫'}`,
);
return generator(source);
});
};
const generator = function generator(source: LightNovelWPMetadata) {
const LightNovelWPTemplate = readFileSync(path.join(__dirname, "template.ts"),{
encoding: "utf-8",
});
const LightNovelWPTemplate = readFileSync(
path.join(__dirname, 'template.ts'),
{
encoding: 'utf-8',
},
);
const pluginScript = `
${LightNovelWPTemplate}
@@ -28,7 +33,7 @@ export default plugin;
`.trim();
return {
lang: source.options?.lang || "English",
lang: source.options?.lang || 'English',
filename: source.sourceName,
pluginScript,
};
+70 -46
View File
@@ -3,99 +3,123 @@ import * as cheerio from 'cheerio';
import * as path from 'path';
function getFilters(name: string, html: string) {
const $:cheerio.CheerioAPI = cheerio.load(html);
const filters: any = {filters: {
"genre[]": {
type: "Checkbox",
label: "Genre",
const $: cheerio.CheerioAPI = cheerio.load(html);
const filters: any = {
filters: {
'genre[]': {
type: 'Checkbox',
label: 'Genre',
value: [],
options: []
options: [],
},
"type[]": {
type: "Checkbox",
label: "Type",
'type[]': {
type: 'Checkbox',
label: 'Type',
value: [],
options: []
options: [],
},
"status": {
type: "Picker",
label: "Status",
value: "",
options: []
'status': {
type: 'Picker',
label: 'Status',
value: '',
options: [],
},
"order": {
type: "Picker",
label: "Order",
value: "",
options: []
'order': {
type: 'Picker',
label: 'Order',
value: '',
options: [],
},
}};
},
};
const filtersContainer = $('div.quickfilter').find("ul");
const filtersContainer = $('div.quickfilter').find('ul');
filtersContainer.each((i, el) => {
const filterName = Object.keys(filters.filters)[i];
filters.filters[filterName].label = $(el).prev().contents().first().text().trim();
$(el).find("li").each((j, li) => {
filters.filters[filterName].label = $(el)
.prev()
.contents()
.first()
.text()
.trim();
$(el)
.find('li')
.each((j, li) => {
filters.filters[filterName].options.push({
label: $(li).text().trim(),
value: decodeURI($(li).find("input").attr("value") || "")
value: decodeURI($(li).find('input').attr('value') || ''),
});
});
});
fs.writeFileSync(
path.join(__dirname, 'filters', name + ".json"),
JSON.stringify(filters, null, 2)
path.join(__dirname, 'filters', name + '.json'),
JSON.stringify(filters, null, 2),
);
}
async function getFiltersFromURL(name: string, url: string) {
const response = await fetch(url + "/series/");
const response = await fetch(url + '/series/');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}, while fetching ${response.url}`);
throw new Error(
`HTTP error! status: ${response.status}, while fetching ${response.url}`,
);
}
const html = await response.text();
try {
getFilters(name, html);
} catch (e) {
console.error("Error while getting filters from", url);
console.error('Error while getting filters from', url);
}
}
async function askGetFilter(){
async function askGetFilter() {
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
output: process.stdout,
});
const EREASE_PREV_LINE = '\x1b[1A\r\x1b[2K';
await readline.question('Enter the id of the site (same one as in sources.json): ', async (name: string) => {
await readline.question(EREASE_PREV_LINE + 'Do you want to get the filters from a URL or the html text? (if url dosen\'t work try html) (url/html): ', async (method: string) => {
if (method.toLowerCase() === "url") {
await readline.question(EREASE_PREV_LINE + 'Enter the URL (same one as in sources.json): ', async (url: string) => {
await readline.question(
'Enter the id of the site (same one as in sources.json): ',
async (name: string) => {
await readline.question(
EREASE_PREV_LINE +
"Do you want to get the filters from a URL or the html text? (if url dosen't work try html) (url/html): ",
async (method: string) => {
if (method.toLowerCase() === 'url') {
await readline.question(
EREASE_PREV_LINE +
'Enter the URL (same one as in sources.json): ',
async (url: string) => {
try {
await getFiltersFromURL(name, url);
} catch (e: any) {
console.error("Error while getting filters from", url);
console.error('Error while getting filters from', url);
console.log(e.message || e);
}
readline.close();
});
},
);
} else {
process.stdout.write(EREASE_PREV_LINE + `Enter the html text from the page at {sourceSite}/series (at the end press ENTER then press CTRL+C)
(to make it faster you can run \`$(\"div.quickfilter\").parent().html()\` in the console to get only the important html part): `)
let html = "";
process.stdout.write(
EREASE_PREV_LINE +
`Enter the html text from the page at {sourceSite}/series (at the end press ENTER then press CTRL+C)
(to make it faster you can run \`$(\"div.quickfilter\").parent().html()\` in the console to get only the important html part): `,
);
let html = '';
readline.on('SIGINT', () => {
console.log("Creating filters file");
console.log('Creating filters file');
getFilters(name, html);
readline.close();
});
readline.on('line', (line: string) => {
html += line + "\n";
html += line + '\n';
});
}
});
});
},
);
},
);
}
askGetFilter();
+133 -94
View File
@@ -1,9 +1,9 @@
import { CheerioAPI, load } from "cheerio";
import { fetchApi, fetchFile } from "@libs/fetch";
import { Plugin } from "@typings/plugin";
import { NovelStatus } from "@libs/novelStatus";
import { defaultCover } from "@libs/defaultCover";
import { Filters } from "@libs/filterInputs";
import { CheerioAPI, load } from 'cheerio';
import { fetchApi, fetchFile } from '@libs/fetch';
import { Plugin } from '@typings/plugin';
import { NovelStatus } from '@libs/novelStatus';
import { defaultCover } from '@libs/defaultCover';
import { Filters } from '@libs/filterInputs';
interface LightNovelWPOptions {
reverseChapters?: boolean;
@@ -40,50 +40,64 @@ class LightNovelWPPlugin implements Plugin.PluginBase {
}
getHostname(url: string): string {
return url.split("/")[2];
return url.split('/')[2];
}
async getCheerio(url: string, search: boolean): Promise<CheerioAPI> {
const r = await fetchApi(url);
if (!r.ok && search != true) throw new Error("Could not reach site (" + r.status + ") try to open in webview.");
if (!r.ok && search != true)
throw new Error(
'Could not reach site (' + r.status + ') try to open in webview.',
);
const $ = load(await r.text());
const title = $("title").text().trim();
if (this.getHostname(url) != this.getHostname(r.url) ||
title == "Bot Verification" || title == "You are being redirected..." || title == "Un instant..." || title == "Just a moment..." || title == "Redirecting...")
throw new Error("Captcha error, please open in webview");
const title = $('title').text().trim();
if (
this.getHostname(url) != this.getHostname(r.url) ||
title == 'Bot Verification' ||
title == 'You are being redirected...' ||
title == 'Un instant...' ||
title == 'Just a moment...' ||
title == 'Redirecting...'
)
throw new Error('Captcha error, please open in webview');
return ($);
return $;
}
parseNovels($: CheerioAPI): Plugin.NovelItem[] {
const novels: Plugin.NovelItem[] = [];
$("div.listupd > article").each((i, elem) => {
const novelName = $(elem).find("h2").text();
const image = $(elem).find("img");
const novelUrl = $(elem).find("a").attr("href");
$('div.listupd > article').each((i, elem) => {
const novelName = $(elem).find('h2').text();
const image = $(elem).find('img');
const novelUrl = $(elem).find('a').attr('href');
if (novelUrl) {
novels.push({
name: novelName,
cover: image.attr("data-src") || image.attr("src") || defaultCover,
path: novelUrl.replace(this.site, ""),
cover: image.attr('data-src') || image.attr('src') || defaultCover,
path: novelUrl.replace(this.site, ''),
});
}
});
return novels;
}
async popularNovels(pageNo: number, { filters, showLatestNovels }: Plugin.PopularNovelsOptions <typeof this.filters>): Promise<Plugin.NovelItem[]> {
let url = this.site + "/series/?page=" + pageNo;
async popularNovels(
pageNo: number,
{
filters,
showLatestNovels,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.site + '/series/?page=' + pageNo;
if (!filters) filters = {};
if (showLatestNovels) url += "&order=latest";
if (showLatestNovels) url += '&order=latest';
for (const key in filters) {
if (typeof filters[key].value === "object")
if (typeof filters[key].value === 'object')
for (const value of filters[key].value as string[])
url += `&${key}=${value}`;
else if (filters[key].value)
url += `&${key}=${filters[key].value}`;
else if (filters[key].value) url += `&${key}=${filters[key].value}`;
}
const $ = await this.getCheerio(url, false);
return this.parseNovels($);
@@ -92,19 +106,22 @@ class LightNovelWPPlugin implements Plugin.PluginBase {
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const $ = await this.getCheerio(this.site + novelPath, false);
const novel: Plugin.SourceNovel = { path: novelPath.replace(this.site, ""), name: "Untitled" };
const novel: Plugin.SourceNovel = {
path: novelPath.replace(this.site, ''),
name: 'Untitled',
};
novel.name = $("h1.entry-title").text().trim();
const image = $("img.wp-post-image");
novel.cover = image.attr("data-src") || image.attr("src") || defaultCover;
switch ($("div.sertostat > span").attr("class")?.toLowerCase() || "") {
case "completed":
novel.name = $('h1.entry-title').text().trim();
const image = $('img.wp-post-image');
novel.cover = image.attr('data-src') || image.attr('src') || defaultCover;
switch ($('div.sertostat > span').attr('class')?.toLowerCase() || '') {
case 'completed':
novel.status = NovelStatus.Completed;
break;
case "ongoing":
case 'ongoing':
novel.status = NovelStatus.Ongoing;
break;
case "hiatus":
case 'hiatus':
novel.status = NovelStatus.OnHiatus;
break;
default:
@@ -112,49 +129,55 @@ class LightNovelWPPlugin implements Plugin.PluginBase {
break;
}
let details = $("div.serl");
if (!details.length) details = $("div.spe > span");
let details = $('div.serl');
if (!details.length) details = $('div.spe > span');
details.each(function () {
const detailName = $(this).contents().first().text().replace(":", "").trim().toLowerCase();
const detailName = $(this)
.contents()
.first()
.text()
.replace(':', '')
.trim()
.toLowerCase();
const detail = $(this).contents().last().text().trim().toLowerCase();
switch (detailName) {
case "الكاتب":
case "author":
case "auteur":
case "autor":
case "yazar":
case 'الكاتب':
case 'author':
case 'auteur':
case 'autor':
case 'yazar':
novel.author = detail;
break;
case "الحالة":
case "status":
case "statut":
case "estado":
case "durum":
case 'الحالة':
case 'status':
case 'statut':
case 'estado':
case 'durum':
switch (detail) {
case "مكتملة":
case "completed":
case "complété":
case "completo":
case "completado":
case "tamamlandı":
case 'مكتملة':
case 'completed':
case 'complété':
case 'completo':
case 'completado':
case 'tamamlandı':
novel.status = NovelStatus.Completed;
break;
case "مستمرة":
case "ongoing":
case "en cours":
case "em andamento":
case "en progreso":
case "devam ediyor":
case 'مستمرة':
case 'ongoing':
case 'en cours':
case 'em andamento':
case 'en progreso':
case 'devam ediyor':
novel.status = NovelStatus.Ongoing;
break;
case "متوقفة":
case "hiatus":
case "en pause":
case "hiato":
case "pausa":
case "pausado":
case "duraklatıldı":
case 'متوقفة':
case 'hiatus':
case 'en pause':
case 'hiato':
case 'pausa':
case 'pausado':
case 'duraklatıldı':
novel.status = NovelStatus.OnHiatus;
break;
default:
@@ -162,40 +185,48 @@ class LightNovelWPPlugin implements Plugin.PluginBase {
break;
}
break;
case "الفنان":
case "artist":
case "artiste":
case "artista":
case "çizer":
case 'الفنان':
case 'artist':
case 'artiste':
case 'artista':
case 'çizer':
novel.artist = detail;
break;
}
});
let genres = $(".sertogenre")
if (!genres.length) genres = $(".genxed")
let genres = $('.sertogenre');
if (!genres.length) genres = $('.genxed');
novel.genres = genres
.children("a")
.children('a')
.map((i, el) => $(el).text())
.toArray()
.join(",");
.join(',');
let summary = $(".sersys > p").map((i, el) => $(el).text().trim()).toArray();
if (!summary.length) summary = $(".entry-content > p").map((i, el) => $(el).text().trim()).toArray();
novel.summary = summary.join("\n");
let summary = $('.sersys > p')
.map((i, el) => $(el).text().trim())
.toArray();
if (!summary.length)
summary = $('.entry-content > p')
.map((i, el) => $(el).text().trim())
.toArray();
novel.summary = summary.join('\n');
const chapters: Plugin.ChapterItem[] = [];
$(".eplister li").each((i, elem) => {
const chapterName = $(elem).find(".epl-num").text() + " " + $(elem).find(".epl-title").text();
const chapterUrl = $(elem).find("a").attr("href") || "";
const releaseTime = $(elem).find(".epl-date").text().trim();
$('.eplister li').each((i, elem) => {
const chapterName =
$(elem).find('.epl-num').text() +
' ' +
$(elem).find('.epl-title').text();
const chapterUrl = $(elem).find('a').attr('href') || '';
const releaseTime = $(elem).find('.epl-date').text().trim();
let isFreeChapter: boolean;
switch ($(elem).find(".epl-price").text().trim().toLowerCase()) {
case "free":
case "gratuit":
case "مجاني":
case "livre":
case "":
switch ($(elem).find('.epl-price').text().trim().toLowerCase()) {
case 'free':
case 'gratuit':
case 'مجاني':
case 'livre':
case '':
isFreeChapter = true;
break;
default:
@@ -205,7 +236,7 @@ class LightNovelWPPlugin implements Plugin.PluginBase {
if (isFreeChapter)
chapters.push({
name: chapterName,
path: chapterUrl.replace(this.site, ""),
path: chapterUrl.replace(this.site, ''),
releaseTime: releaseTime,
});
});
@@ -218,16 +249,24 @@ class LightNovelWPPlugin implements Plugin.PluginBase {
async parseChapter(chapterPath: string): Promise<string> {
const $ = await this.getCheerio(this.site + chapterPath, false);
if (this.id == "kolnovel") {
let ignore = $("article > style").text().trim().split(",");
if (this.id == 'kolnovel') {
let ignore = $('article > style').text().trim().split(',');
ignore.push(...(ignore.pop()?.match(/^\.\w+/) || []));
ignore.map((tag) => $(`p${tag}`).remove());
ignore.map(tag => $(`p${tag}`).remove());
}
return $(".epcontent p").map((i, el) => $(el)).toArray().join("\n") || "";
return (
$('.epcontent p')
.map((i, el) => $(el))
.toArray()
.join('\n') || ''
);
}
async searchNovels(searchTerm: string, page: number): Promise<Plugin.NovelItem[]> {
const url = this.site + "page/" + page + "/?s=" + searchTerm;
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const url = this.site + 'page/' + page + '/?s=' + searchTerm;
const $ = await this.getCheerio(url, true);
return this.parseNovels($);
}
+13 -12
View File
@@ -1,9 +1,9 @@
import { FilterTypes, Filters } from "../../../libs/filterInputs";
import { ScrpitGeneratorFunction } from "../generate";
import list from "./sources.json";
import { MadaraMetadata } from "./template";
import { readFileSync } from "fs";
import path from "path";
import { FilterTypes, Filters } from '../../../libs/filterInputs';
import { ScrpitGeneratorFunction } from '../generate';
import list from './sources.json';
import { MadaraMetadata } from './template';
import { readFileSync } from 'fs';
import path from 'path';
export const generateAll: ScrpitGeneratorFunction = function (name) {
return list.map((metadata: MadaraMetadata) => {
@@ -11,16 +11,17 @@ export const generateAll: ScrpitGeneratorFunction = function (name) {
try {
filters = require(`./filters/${metadata.id}`);
metadata.filters = filters.filters;
}
catch (e) {}
console.log(`[${name}] Generating: ${metadata.id}${" ".repeat(20 - metadata.id.length)} ${metadata.filters ? "🔎with filters🔍" : "🚫no filters🚫"}`);
} catch (e) {}
console.log(
`[${name}] Generating: ${metadata.id}${' '.repeat(20 - metadata.id.length)} ${metadata.filters ? '🔎with filters🔍' : '🚫no filters🚫'}`,
);
return generator(metadata);
});
};
const generator = function generator(metadata: MadaraMetadata) {
const madaraTemplate = readFileSync(path.join(__dirname, "template.ts"), {
encoding: "utf-8",
const madaraTemplate = readFileSync(path.join(__dirname, 'template.ts'), {
encoding: 'utf-8',
});
const pluginScript = `
@@ -29,7 +30,7 @@ const plugin = new MadaraPlugin(${JSON.stringify(metadata)});
export default plugin;
`.trim();
return {
lang: metadata.options?.lang || "English",
lang: metadata.options?.lang || 'English',
filename: metadata.sourceName,
pluginScript,
};
+119 -88
View File
@@ -3,177 +3,208 @@ import * as cheerio from 'cheerio';
import * as path from 'path';
function getFilters(name: string, html: string) {
const $:cheerio.CheerioAPI = cheerio.load(html);
const filters: any = {filters: {
"genre[]": {
type: "Checkbox",
label: "Genre",
const $: cheerio.CheerioAPI = cheerio.load(html);
const filters: any = {
filters: {
'genre[]': {
type: 'Checkbox',
label: 'Genre',
value: [],
options: []
},
"op": {
type: "Switch",
label: "Having ALL selected genres",
value: false
},
"author": {
type: "Text",
label: "Author",
value: ""
},
"artist": {
type: "Text",
label: "Artist",
value: ""
},
"release": {
type: "Text",
label: "Year of Released",
value: ""
},
"adult": {
type: "Picker",
label: "Adult content",
value: "",
options: [],
},
"status[]": {
type: "Checkbox",
label: "Status",
'op': {
type: 'Switch',
label: 'Having ALL selected genres',
value: false,
},
'author': {
type: 'Text',
label: 'Author',
value: '',
},
'artist': {
type: 'Text',
label: 'Artist',
value: '',
},
'release': {
type: 'Text',
label: 'Year of Released',
value: '',
},
'adult': {
type: 'Picker',
label: 'Adult content',
value: '',
options: [],
},
'status[]': {
type: 'Checkbox',
label: 'Status',
value: [],
options: []
options: [],
},
"m_orderby": {
type: "Picker",
label: "Order by",
value: "",
options: []
'm_orderby': {
type: 'Picker',
label: 'Order by',
value: '',
options: [],
},
}};
},
};
const divs = $("form.search-advanced-form > div");
const divs = $('form.search-advanced-form > div');
// ==================== Genre ====================
const genreDiv = divs.eq(0);
genreDiv.find("label").each((i, el) => {
genreDiv.find('label').each((i, el) => {
const option = {
label: $(el).text().trim(),
value: decodeURI($(el).attr("for") || "")
value: decodeURI($(el).attr('for') || ''),
};
filters.filters["genre[]"].options.push(option);
filters.filters['genre[]'].options.push(option);
});
// ===================== Op ======================
const statusDiv = divs.eq(1);
filters.filters["op"].label = statusDiv.find("option").eq(1)
.text().replace("AND", "").replace("(", "").replace(")", "").trim();
filters.filters['op'].label = statusDiv
.find('option')
.eq(1)
.text()
.replace('AND', '')
.replace('(', '')
.replace(')', '')
.trim();
// ==================== Author ====================
const authorDiv = divs.eq(2);
filters.filters["author"].label = authorDiv.find("span").text().trim();
filters.filters['author'].label = authorDiv.find('span').text().trim();
// ==================== Artist ====================
const artistDiv = divs.eq(3);
filters.filters["artist"].label = artistDiv.find("span").text().trim();
filters.filters['artist'].label = artistDiv.find('span').text().trim();
// ==================== Release ====================
const releaseDiv = divs.eq(4);
filters.filters["release"].label = releaseDiv.find("span").text().trim();
filters.filters['release'].label = releaseDiv.find('span').text().trim();
// ==================== Adult ====================
const adultDiv = divs.eq(5);
filters.filters["adult"].label = adultDiv.find("span").text().trim();
adultDiv.find("option").each((i, el) => {
filters.filters['adult'].label = adultDiv.find('span').text().trim();
adultDiv.find('option').each((i, el) => {
const option = {
label: $(el).text().trim(),
value: decodeURI($(el).attr("value") || "")
value: decodeURI($(el).attr('value') || ''),
};
filters.filters["adult"].options.push(option);
filters.filters['adult'].options.push(option);
});
// ==================== Status ====================
const statusDiv2 = divs.eq(6);
filters.filters["status[]"].label = statusDiv2.find("span").text().trim();
statusDiv2.find("label").each((i, el) => {
filters.filters['status[]'].label = statusDiv2.find('span').text().trim();
statusDiv2.find('label').each((i, el) => {
const option = {
label: $(el).text().trim(),
value: $(el).attr("for")
value: $(el).attr('for'),
};
filters.filters["status[]"].options.push(option);
filters.filters['status[]'].options.push(option);
});
// ==================== Order by ====================
const orderByDiv = $("div.c-nav-tabs").eq(0);
filters.filters["m_orderby"].label = orderByDiv.find("span").text().trim();
orderByDiv.find("a").each((i, el) => {
const orderByDiv = $('div.c-nav-tabs').eq(0);
filters.filters['m_orderby'].label = orderByDiv.find('span').text().trim();
orderByDiv.find('a').each((i, el) => {
const option = {
label: $(el).text().trim(),
value: decodeURI(($(el).attr("href")?.split("=").length == 3 ? $(el).attr("href")?.split("=")[2] : "") || "")
value: decodeURI(
($(el).attr('href')?.split('=').length == 3
? $(el).attr('href')?.split('=')[2]
: '') || '',
),
};
filters.filters["m_orderby"].options.push(option);
filters.filters['m_orderby'].options.push(option);
});
if (filters.filters["m_orderby"].options.length == 0 ||
filters.filters["status[]"].options.length == 0 ||
filters.filters["adult"].options.length == 0 ||
filters.filters["genre[]"].options.length == 0) {
console.error(`🚨Error in filters for ${name} please fix manually (${path.join(__dirname, 'filters', name + ".json")})🚨`);
if (
filters.filters['m_orderby'].options.length == 0 ||
filters.filters['status[]'].options.length == 0 ||
filters.filters['adult'].options.length == 0 ||
filters.filters['genre[]'].options.length == 0
) {
console.error(
`🚨Error in filters for ${name} please fix manually (${path.join(__dirname, 'filters', name + '.json')})🚨`,
);
}
fs.writeFileSync(
path.join(__dirname, 'filters', name + ".json"),
JSON.stringify(filters, null, 2)
path.join(__dirname, 'filters', name + '.json'),
JSON.stringify(filters, null, 2),
);
console.log(`✅Filters created successfully for ${name}✅`);
}
async function getFiltersFromURL(name: string, url: string) {
const response = await fetch(url + "/?s=&post_type=wp-manga");
const response = await fetch(url + '/?s=&post_type=wp-manga');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}, while fetching ${response.url}`);
throw new Error(
`HTTP error! status: ${response.status}, while fetching ${response.url}`,
);
}
const html = await response.text();
try {
getFilters(name, html);
} catch (e) {
console.error("Error while getting filters from", url);
console.error('Error while getting filters from', url);
}
}
async function askGetFilter(){
async function askGetFilter() {
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
output: process.stdout,
});
const EREASE_PREV_LINE = '\x1b[1A\r\x1b[2K';
await readline.question('Enter the id of the site (same one as in sources.json): ', async (name: string) => {
await readline.question(EREASE_PREV_LINE + 'Do you want to get the filters from a URL or the html text? (if url dosen\'t work try html) (url/html): ', async (method: string) => {
if (method.toLowerCase() === "url") {
await readline.question(EREASE_PREV_LINE + 'Enter the URL (same one as in sources.json): ', async (url: string) => {
await readline.question(
'Enter the id of the site (same one as in sources.json): ',
async (name: string) => {
await readline.question(
EREASE_PREV_LINE +
"Do you want to get the filters from a URL or the html text? (if url dosen't work try html) (url/html): ",
async (method: string) => {
if (method.toLowerCase() === 'url') {
await readline.question(
EREASE_PREV_LINE +
'Enter the URL (same one as in sources.json): ',
async (url: string) => {
try {
await getFiltersFromURL(name, url);
} catch (e: any) {
console.error("Error while getting filters from", url);
console.error('Error while getting filters from', url);
console.log(e.message || e);
}
readline.close();
});
},
);
} else {
process.stdout.write(EREASE_PREV_LINE + `Enter the html text from the page at {sourceSite}/?s=&post_type=wp-manga (at the end press ENTER then press CTRL+C): `)
let html = "";
process.stdout.write(
EREASE_PREV_LINE +
`Enter the html text from the page at {sourceSite}/?s=&post_type=wp-manga (at the end press ENTER then press CTRL+C): `,
);
let html = '';
readline.on('SIGINT', () => {
console.log("Stopeed reading input, creating filters file");
console.log('Stopeed reading input, creating filters file');
getFilters(name, html);
readline.close();
});
readline.on('line', (line: string) => {
html += line + "\n";
html += line + '\n';
});
}
});
});
},
);
},
);
}
askGetFilter();
+154 -122
View File
@@ -1,13 +1,13 @@
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 { defaultCover } from "@libs/defaultCover";
import { NovelStatus } from "@libs/novelStatus";
import dayjs from "dayjs";
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 { defaultCover } from '@libs/defaultCover';
import { NovelStatus } from '@libs/novelStatus';
import dayjs from 'dayjs';
const includesAny = (str: string, keywords: string[]) =>
new RegExp(keywords.join("|")).test(str);
new RegExp(keywords.join('|')).test(str);
interface MadaraOptions {
useNewChapterEndpoint?: boolean;
@@ -45,157 +45,178 @@ class MadaraPlugin implements Plugin.PluginBase {
}
getHostname(url: string): string {
return url.split("/")[2];
return url.split('/')[2];
}
async getCheerio(url: string): Promise<CheerioAPI> {
const r = await fetchApi(url);
if (!r.ok) throw new Error("Could not reach site (" + r.status + ") try to open in webview.");
if (!r.ok)
throw new Error(
'Could not reach site (' + r.status + ') try to open in webview.',
);
const $ = parseHTML(await r.text());
const title = $("title").text().trim();
if (this.getHostname(url) != this.getHostname(r.url) ||
title == "Bot Verification" || title == "You are being redirected..." || title == "Un instant..." || title == "Just a moment..." || title == "Redirecting...")
throw new Error("Captcha error, please open in webview");
return ($);
const title = $('title').text().trim();
if (
this.getHostname(url) != this.getHostname(r.url) ||
title == 'Bot Verification' ||
title == 'You are being redirected...' ||
title == 'Un instant...' ||
title == 'Just a moment...' ||
title == 'Redirecting...'
)
throw new Error('Captcha error, please open in webview');
return $;
}
parseNovels(loadedCheerio: CheerioAPI): Plugin.NovelItem[] {
const novels: Plugin.NovelItem[] = [];
loadedCheerio(".manga-title-badges").remove();
loadedCheerio('.manga-title-badges').remove();
loadedCheerio(".page-item-detail, .c-tabs-item__content").each((index, element) => {
const novelName = loadedCheerio(element).find(".post-title").text().trim();
const novelUrl = loadedCheerio(element).find(".post-title").find("a").attr("href") || "";
loadedCheerio('.page-item-detail, .c-tabs-item__content').each(
(index, element) => {
const novelName = loadedCheerio(element)
.find('.post-title')
.text()
.trim();
const novelUrl =
loadedCheerio(element).find('.post-title').find('a').attr('href') ||
'';
if (!novelName || !novelUrl) return;
const image = loadedCheerio(element).find("img");
const novelCover = image.attr("data-src") || image.attr("src");
const image = loadedCheerio(element).find('img');
const novelCover = image.attr('data-src') || image.attr('src');
const novel: Plugin.NovelItem = {
name: novelName,
cover: novelCover,
path: novelUrl.replace(this.site, ""),
path: novelUrl.replace(this.site, ''),
};
novels.push(novel);
});
},
);
return novels;
}
async popularNovels(pageNo: number,{ filters, showLatestNovels }: Plugin.PopularNovelsOptions<typeof this.filters>): Promise<Plugin.NovelItem[]> {
let url = this.site + "/page/" + pageNo + "/?s=&post_type=wp-manga";
async popularNovels(
pageNo: number,
{
filters,
showLatestNovels,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
let url = this.site + '/page/' + pageNo + '/?s=&post_type=wp-manga';
if (!filters) filters = {};
if (showLatestNovels) url += "&m_orderby=latest";
if (showLatestNovels) url += '&m_orderby=latest';
for (const key in filters) {
if (typeof filters[key].value === "object")
if (typeof filters[key].value === 'object')
for (const value of filters[key].value as string[])
url += `&${key}=${value}`;
else if (filters[key].value)
url += `&${key}=${filters[key].value}`;
else if (filters[key].value) url += `&${key}=${filters[key].value}`;
}
const loadedCheerio = await this.getCheerio(url);
return this.parseNovels(loadedCheerio);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then((res) => res.text());
const body = await fetchApi(this.site + novelPath).then(res => res.text());
let loadedCheerio = parseHTML(body);
loadedCheerio(".manga-title-badges, #manga-title span").remove();
loadedCheerio('.manga-title-badges, #manga-title span').remove();
const novel: Plugin.SourceNovel = {
path: novelPath,
name:
loadedCheerio(".post-title h1").text().trim() ||
loadedCheerio("#manga-title h1").text(),
loadedCheerio('.post-title h1').text().trim() ||
loadedCheerio('#manga-title h1').text(),
};
novel.cover =
loadedCheerio(".summary_image > a > img").attr("data-lazy-src") ||
loadedCheerio(".summary_image > a > img").attr("data-src") ||
loadedCheerio(".summary_image > a > img").attr("src") ||
loadedCheerio(".summary_image > a > img").attr("src") ||
loadedCheerio('.summary_image > a > img').attr('data-lazy-src') ||
loadedCheerio('.summary_image > a > img').attr('data-src') ||
loadedCheerio('.summary_image > a > img').attr('src') ||
loadedCheerio('.summary_image > a > img').attr('src') ||
defaultCover;
loadedCheerio(".post-content_item, .post-content").each(function () {
const detailName = loadedCheerio(this).find("h5").text().trim();
const detail = loadedCheerio(this).find(".summary-content").text().trim();
loadedCheerio('.post-content_item, .post-content').each(function () {
const detailName = loadedCheerio(this).find('h5').text().trim();
const detail = loadedCheerio(this).find('.summary-content').text().trim();
switch (detailName) {
case "Genre(s)":
case "Género(s)":
case "التصنيفات":
case 'Genre(s)':
case 'Género(s)':
case 'التصنيفات':
novel.genres = detail;
break;
case "Author(s)":
case "Autor(es)":
case "المؤلف":
case "المؤلف (ين)":
case 'Author(s)':
case 'Autor(es)':
case 'المؤلف':
case 'المؤلف (ين)':
novel.author = detail;
break;
case "Status":
case "Estado":
case 'Status':
case 'Estado':
novel.status =
detail.includes("OnGoing") || detail.includes("مستمرة")
detail.includes('OnGoing') || detail.includes('مستمرة')
? NovelStatus.Ongoing
: NovelStatus.Completed;
break;
}
});
loadedCheerio("div.summary__content .code-block,script").remove();
loadedCheerio('div.summary__content .code-block,script').remove();
novel.summary =
loadedCheerio("div.summary__content").text().trim() ||
loadedCheerio("#tab-manga-about").text().trim() ||
loadedCheerio('div.summary__content').text().trim() ||
loadedCheerio('#tab-manga-about').text().trim() ||
loadedCheerio('.post-content_item h5:contains("Summary")')
.next()
.text()
.trim();
const chapters: Plugin.ChapterItem[] = [];
let html = "";
let html = '';
if (this.options?.useNewChapterEndpoint) {
html = await fetchApi(this.site + novelPath + "ajax/chapters/", {
method: "POST",
}).then((res) => res.text());
html = await fetchApi(this.site + novelPath + 'ajax/chapters/', {
method: 'POST',
}).then(res => res.text());
} else {
const novelId =
loadedCheerio(".rating-post-id").attr("value") ||
loadedCheerio("#manga-chapters-holder").attr("data-id") ||
"";
loadedCheerio('.rating-post-id').attr('value') ||
loadedCheerio('#manga-chapters-holder').attr('data-id') ||
'';
const formData = new FormData();
formData.append("action", "manga_get_chapters");
formData.append("manga", novelId);
formData.append('action', 'manga_get_chapters');
formData.append('manga', novelId);
html = await fetchApi(this.site + "wp-admin/admin-ajax.php", {
method: "POST",
html = await fetchApi(this.site + 'wp-admin/admin-ajax.php', {
method: 'POST',
body: formData,
}).then((res) => res.text());
}).then(res => res.text());
}
if (html !== "0") {
if (html !== '0') {
loadedCheerio = parseHTML(html);
}
const totalChapters = loadedCheerio(".wp-manga-chapter").length;
loadedCheerio(".wp-manga-chapter").each((chapterIndex, element) => {
const chapterName = loadedCheerio(element).find("a").text().trim();
const totalChapters = loadedCheerio('.wp-manga-chapter').length;
loadedCheerio('.wp-manga-chapter').each((chapterIndex, element) => {
const chapterName = loadedCheerio(element).find('a').text().trim();
let releaseDate = loadedCheerio(element)
.find("span.chapter-release-date")
.find('span.chapter-release-date')
.text()
.trim();
if (releaseDate) {
releaseDate = this.parseData(releaseDate);
} else {
releaseDate = dayjs().format("LL");
releaseDate = dayjs().format('LL');
}
let chapterUrl = loadedCheerio(element).find("a").attr("href") || "";
let chapterUrl = loadedCheerio(element).find('a').attr('href') || '';
chapters.push({
name: chapterName,
path: chapterUrl.replace(this.site, ""),
path: chapterUrl.replace(this.site, ''),
releaseTime: releaseDate || null,
chapterNumber: totalChapters - chapterIndex,
});
@@ -206,21 +227,32 @@ class MadaraPlugin implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
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(".text-left").html() ||
loadedCheerio(".text-right").html() ||
loadedCheerio(".entry-content").html() ||
loadedCheerio(".c-blog-post > div > div:nth-child(2)").html() ||
"";
loadedCheerio('.text-left').html() ||
loadedCheerio('.text-right').html() ||
loadedCheerio('.entry-content').html() ||
loadedCheerio('.c-blog-post > div > div:nth-child(2)').html() ||
'';
return chapterText;
}
async searchNovels(searchTerm: string, pageNo?: number | undefined): Promise<Plugin.NovelItem[]> {
const url = this.site + "page/" + pageNo + "/?s=" + searchTerm + "&post_type=wp-manga";
async searchNovels(
searchTerm: string,
pageNo?: number | undefined,
): Promise<Plugin.NovelItem[]> {
const url =
this.site +
'page/' +
pageNo +
'/?s=' +
searchTerm +
'&post_type=wp-manga';
const loadedCheerio = await this.getCheerio(url);
return this.parseNovels(loadedCheerio);
}
@@ -229,66 +261,66 @@ class MadaraPlugin implements Plugin.PluginBase {
parseData = (date: string) => {
const dayJSDate = dayjs(); // today
const timeAgo = date.match(/\d+/)?.[0] || "";
const timeAgo = date.match(/\d+/)?.[0] || '';
const timeAgoInt = parseInt(timeAgo, 10);
if (!timeAgo) return date; // there is no number!
if (includesAny(date, ["detik", "segundo", "second", "วินาที"])) {
dayJSDate.subtract(timeAgoInt, "second"); // go back N seconds
if (includesAny(date, ['detik', 'segundo', 'second', 'วินาที'])) {
dayJSDate.subtract(timeAgoInt, 'second'); // go back N seconds
} else if (
includesAny(date, [
"menit",
"dakika",
"min",
"minute",
"minuto",
"นาที",
"دقائق",
'menit',
'dakika',
'min',
'minute',
'minuto',
'นาที',
'دقائق',
])
) {
dayJSDate.subtract(timeAgoInt, "minute"); // go back N minute
dayJSDate.subtract(timeAgoInt, 'minute'); // go back N minute
} else if (
includesAny(date, [
"jam",
"saat",
"heure",
"hora",
"hour",
"ชั่วโมง",
"giờ",
"ore",
"ساعة",
"小时",
'jam',
'saat',
'heure',
'hora',
'hour',
'ชั่วโมง',
'giờ',
'ore',
'ساعة',
'小时',
])
) {
dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours
dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours
} else if (
includesAny(date, [
"hari",
"gün",
"jour",
"día",
"dia",
"day",
"วัน",
"ngày",
"giorni",
"أيام",
"天",
'hari',
'gün',
'jour',
'día',
'dia',
'day',
'วัน',
'ngày',
'giorni',
'أيام',
'天',
])
) {
dayJSDate.subtract(timeAgoInt, "days"); // go back N days
} else if (includesAny(date, ["week", "semana"])) {
dayJSDate.subtract(timeAgoInt, "week"); // go back N a week
} else if (includesAny(date, ["month", "mes"])) {
dayJSDate.subtract(timeAgoInt, "month"); // go back N months
} else if (includesAny(date, ["year", "año"])) {
dayJSDate.subtract(timeAgoInt, "year"); // go back N years
dayJSDate.subtract(timeAgoInt, 'days'); // go back N days
} else if (includesAny(date, ['week', 'semana'])) {
dayJSDate.subtract(timeAgoInt, 'week'); // go back N a week
} else if (includesAny(date, ['month', 'mes'])) {
dayJSDate.subtract(timeAgoInt, 'month'); // go back N months
} else if (includesAny(date, ['year', 'año'])) {
dayJSDate.subtract(timeAgoInt, 'year'); // go back N years
} else {
return date;
}
return dayJSDate.format("LL");
return dayJSDate.format('LL');
};
}
+18 -15
View File
@@ -1,16 +1,18 @@
import { FilterTypes, Filters } from "../../../libs/filterInputs";
import { ScrpitGeneratorFunction } from "../generate";
import list from "./sources.json";
import defaultSettings from "./settings.json";
import { ReadwnMetadata } from "./template";
import { readFileSync } from "fs";
import path from "path";
import { FilterTypes, Filters } from '../../../libs/filterInputs';
import { ScrpitGeneratorFunction } from '../generate';
import list from './sources.json';
import defaultSettings from './settings.json';
import { ReadwnMetadata } from './template';
import { readFileSync } from 'fs';
import path from 'path';
export const generateAll: ScrpitGeneratorFunction = function (name) {
return list
.map<ReadwnMetadata>((p) => {
.map<ReadwnMetadata>(p => {
p.filters.tags.options.unshift(...defaultSettings.filters.tags.options);
p.filters.genres.options.unshift(...defaultSettings.filters.genres.options);
p.filters.genres.options.unshift(
...defaultSettings.filters.genres.options,
);
p.filters = Object.assign(defaultSettings.filters, p.filters);
const d = false;
@@ -33,8 +35,8 @@ export const generateAll: ScrpitGeneratorFunction = function (name) {
};
const generator = function generator(metadata: ReadwnMetadata) {
const readwnTemplate = readFileSync(path.join(__dirname, "template.ts"), {
encoding: "utf-8",
const readwnTemplate = readFileSync(path.join(__dirname, 'template.ts'), {
encoding: 'utf-8',
});
//metadata.filters = {
@@ -45,14 +47,15 @@ const generator = function generator(metadata: ReadwnMetadata) {
const pluginScript = `
${readwnTemplate}
const plugin = new ReadwnPlugin(${
JSON.stringify(metadata).replace(/"type":"([^"]+)"/g, '"type":FilterTypes.$1')
});
const plugin = new ReadwnPlugin(${JSON.stringify(metadata).replace(
/"type":"([^"]+)"/g,
'"type":FilterTypes.$1',
)});
export default plugin;
`.trim();
return {
lang: "english",
lang: 'english',
filename: metadata.sourceName,
pluginScript,
};
+74 -68
View File
@@ -1,10 +1,10 @@
import { fetchFile, fetchApi } from "@libs/fetch";
import { Filters, FilterTypes } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import dayjs from "dayjs";
import qs from "qs";
import { fetchFile, fetchApi } from '@libs/fetch';
import { Filters, FilterTypes } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
import qs from 'qs';
export interface ReadwnMetadata {
id: string;
@@ -26,7 +26,7 @@ class ReadwnPlugin implements Plugin.PluginBase {
this.name = metadata.sourceName;
this.icon = `multisrc/readwn/icons/${metadata.id}.png`;
this.site = metadata.sourceSite;
this.version = "1.0.0";
this.version = '1.0.0';
this.filters = metadata.filters;
}
@@ -34,115 +34,115 @@ class ReadwnPlugin implements Plugin.PluginBase {
pageNo: number,
{ filters, showLatestNovels }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
let url = this.site + "/list/";
url += (filters?.genres?.value || "all") + "/";
url += (filters?.status?.value || "all") + "-";
url += showLatestNovels ? "lastdotime" : filters?.sort?.value || "newstime";
url += "-" + (pageNo - 1) + ".html";
let url = this.site + '/list/';
url += (filters?.genres?.value || 'all') + '/';
url += (filters?.status?.value || 'all') + '-';
url += showLatestNovels ? 'lastdotime' : filters?.sort?.value || 'newstime';
url += '-' + (pageNo - 1) + '.html';
if (filters?.tags?.value) {
//only 1 page
url = this.site + "/tags/" + filters.tags.value + "-0.html";
url = this.site + '/tags/' + filters.tags.value + '-0.html';
}
const body = await fetchApi(url).then((res) => res.text());
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novels: Plugin.NovelItem[] = loadedCheerio("li.novel-item")
const novels: Plugin.NovelItem[] = loadedCheerio('li.novel-item')
.map((index, element) => ({
name: loadedCheerio(element).find("h4").text() || "",
name: loadedCheerio(element).find('h4').text() || '',
cover:
this.site +
loadedCheerio(element).find(".novel-cover > img").attr("data-src"),
path: loadedCheerio(element).find("a").attr("href") || "",
loadedCheerio(element).find('.novel-cover > img').attr('data-src'),
path: loadedCheerio(element).find('a').attr('href') || '',
}))
.get()
.filter((novel) => novel.name && novel.path);
.filter(novel => novel.name && novel.path);
return novels;
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + novelPath).then((res) => res.text());
const body = await fetchApi(this.site + novelPath).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: loadedCheerio("h1.novel-title").text() || "",
name: loadedCheerio('h1.novel-title').text() || '',
};
novel.author = loadedCheerio("span[itemprop=author]").text();
novel.author = loadedCheerio('span[itemprop=author]').text();
novel.cover =
this.site + loadedCheerio("figure.cover > img").attr("data-src");
this.site + loadedCheerio('figure.cover > img').attr('data-src');
novel.summary = loadedCheerio(".summary")
novel.summary = loadedCheerio('.summary')
.text()
.replace("Summary", "")
.replace('Summary', '')
.trim();
novel.genres = loadedCheerio("div.categories > ul > li")
novel.genres = loadedCheerio('div.categories > ul > li')
.map((index, element) => loadedCheerio(element).text()?.trim())
.get()
.join(",");
.join(',');
loadedCheerio("div.header-stats > span").each(function () {
if (loadedCheerio(this).find("small").text() === "Status") {
loadedCheerio('div.header-stats > span').each(function () {
if (loadedCheerio(this).find('small').text() === 'Status') {
novel.status =
loadedCheerio(this).find("strong").text() === "Ongoing"
loadedCheerio(this).find('strong').text() === 'Ongoing'
? NovelStatus.Ongoing
: NovelStatus.Completed;
}
});
const latestChapterNo = parseInt(
loadedCheerio(".header-stats")
.find("span > strong")
loadedCheerio('.header-stats')
.find('span > strong')
.first()
.text()
.trim(),
);
const chapters: Plugin.ChapterItem[] = loadedCheerio(".chapter-list li")
const chapters: Plugin.ChapterItem[] = loadedCheerio('.chapter-list li')
.map((chapterIndex, element) => {
const name = loadedCheerio(element)
.find("a .chapter-title")
.find('a .chapter-title')
.text()
.trim();
const path = loadedCheerio(element).find("a").attr("href")?.trim();
const path = loadedCheerio(element).find('a').attr('href')?.trim();
if (!name || !path) return null;
let releaseTime = loadedCheerio(element)
.find("a .chapter-update")
.find('a .chapter-update')
.text()
.trim();
if (releaseTime?.includes?.("ago")) {
const timeAgo = releaseTime.match(/\d+/)?.[0] || "0";
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")
releaseTime.includes('hours ago') ||
releaseTime.includes('hour ago')
) {
dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours
dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours
}
if (
releaseTime.includes("days ago") ||
releaseTime.includes("day ago")
releaseTime.includes('days ago') ||
releaseTime.includes('day ago')
) {
dayJSDate.subtract(timeAgoInt, "days"); // go back N days
dayJSDate.subtract(timeAgoInt, 'days'); // go back N days
}
if (
releaseTime.includes("months ago") ||
releaseTime.includes("month ago")
releaseTime.includes('months ago') ||
releaseTime.includes('month ago')
) {
dayJSDate.subtract(timeAgoInt, "months"); // go back N months
dayJSDate.subtract(timeAgoInt, 'months'); // go back N months
}
releaseTime = dayJSDate.format("LL");
releaseTime = dayJSDate.format('LL');
}
}
@@ -154,18 +154,22 @@ class ReadwnPlugin implements Plugin.PluginBase {
};
})
.get()
.filter((chapter) => chapter);
.filter(chapter => chapter);
if (latestChapterNo > chapters.length) {
const lastChapterNo = parseInt(
chapters[chapters.length - 1].path.match(/_(\d+)\.html/)?.[1] || "",
chapters[chapters.length - 1].path.match(/_(\d+)\.html/)?.[1] || '',
10,
);
for (let i = (lastChapterNo || chapters.length) + 1; i <= latestChapterNo; i++) {
for (
let i = (lastChapterNo || chapters.length) + 1;
i <= latestChapterNo;
i++
) {
chapters.push({
name: "Chapter " + i,
path: novelPath.replace(".html", "_" + i + ".html"),
name: 'Chapter ' + i,
path: novelPath.replace('.html', '_' + i + '.html'),
releaseTime: null,
chapterNumber: i,
});
@@ -177,38 +181,40 @@ class ReadwnPlugin implements Plugin.PluginBase {
}
async parseChapter(chapterPath: string): Promise<string> {
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(".chapter-content").html() || "";
const chapterText = loadedCheerio('.chapter-content').html() || '';
return chapterText;
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const result = await fetchApi(this.site + "/e/search/index.php", {
const result = await fetchApi(this.site + '/e/search/index.php', {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Referer: this.site + "/search.html",
'Content-Type': 'application/x-www-form-urlencoded',
Referer: this.site + '/search.html',
Origin: this.site,
},
method: "POST",
method: 'POST',
body: qs.stringify({
show: "title",
show: 'title',
tempid: 1,
tbname: "news",
tbname: 'news',
keyboard: searchTerm,
}),
}).then((res) => res.text());
}).then(res => res.text());
const loadedCheerio = parseHTML(result);
const novels: Plugin.NovelItem[] = loadedCheerio("li.novel-item")
const novels: Plugin.NovelItem[] = loadedCheerio('li.novel-item')
.map((index, element) => ({
name: loadedCheerio(element).find("h4").text() || "",
cover: this.site + loadedCheerio(element).find("img").attr("data-src"),
path: loadedCheerio(element).find("a").attr("href") || "",
name: loadedCheerio(element).find('h4').text() || '',
cover: this.site + loadedCheerio(element).find('img').attr('data-src'),
path: loadedCheerio(element).find('a').attr('href') || '',
}))
.get()
.filter((novel) => novel.name && novel.path);
.filter(novel => novel.name && novel.path);
return novels;
}
+13 -13
View File
@@ -1,14 +1,14 @@
import { Filters, FilterTypes } from "../../../libs/filterInputs";
import { ScrpitGeneratorFunction } from "../generate";
import list from "./sources.json";
import defaultSettings from "./settings.json";
import { RulateMetadata } from "./template";
import { readFileSync } from "fs";
import path from "path";
import { Filters, FilterTypes } from '../../../libs/filterInputs';
import { ScrpitGeneratorFunction } from '../generate';
import list from './sources.json';
import defaultSettings from './settings.json';
import { RulateMetadata } from './template';
import { readFileSync } from 'fs';
import path from 'path';
export const generateAll: ScrpitGeneratorFunction = function (name) {
return list
.map<RulateMetadata>((p) => {
.map<RulateMetadata>(p => {
if (p.filters) {
p.filters.cat.options.unshift(...defaultSettings.filters.cat.options);
}
@@ -34,21 +34,21 @@ export const generateAll: ScrpitGeneratorFunction = function (name) {
};
const generator = function generator(metadata: RulateMetadata) {
const rulateTemplate = readFileSync(path.join(__dirname, "template.ts"), {
encoding: "utf-8",
const rulateTemplate = readFileSync(path.join(__dirname, 'template.ts'), {
encoding: 'utf-8',
});
const pluginScript = `
${rulateTemplate}
const plugin = new RulatePlugin(${JSON.stringify(metadata)
.replace(/"type":"([^"]+)"/g, '"type":FilterTypes.$1')
.replace(/\.XCheckbox/g, ".ExcludableCheckboxGroup") //remember to redo
.replace(/\.Checkbox/g, ".CheckboxGroup")});
.replace(/\.XCheckbox/g, '.ExcludableCheckboxGroup') //remember to redo
.replace(/\.Checkbox/g, '.CheckboxGroup')});
export default plugin;
`.trim();
return {
lang: "russian",
lang: 'russian',
filename: metadata.sourceName,
pluginScript,
};
+78 -74
View File
@@ -1,9 +1,9 @@
import { fetchFile, fetchApi } from "@libs/fetch";
import { Filters, FilterTypes } from "@libs/filterInputs";
import { Plugin } from "@typings/plugin";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import dayjs from "dayjs";
import { fetchFile, fetchApi } from '@libs/fetch';
import { Filters, FilterTypes } from '@libs/filterInputs';
import { Plugin } from '@typings/plugin';
import { NovelStatus } from '@libs/novelStatus';
import { load as parseHTML } from 'cheerio';
import dayjs from 'dayjs';
export interface RulateMetadata {
id: string;
@@ -25,7 +25,7 @@ class RulatePlugin implements Plugin.PluginBase {
this.name = metadata.sourceName;
this.icon = `multisrc/rulate/icons/${metadata.id}.png`;
this.site = metadata.sourceSite;
this.version = "1.0.0";
this.version = '1.0.0';
this.filters = metadata.filters;
}
@@ -34,35 +34,37 @@ class RulatePlugin implements Plugin.PluginBase {
{ filters, showLatestNovels }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = [];
let url = this.site + "/search?t=";
url += "&cat=" + (filters?.cat?.value || "0");
url += "&s_lang=" + (filters?.s_lang?.value || "0");
url += "&t_lang=" + (filters?.t_lang?.value || "0");
url += "&type=" + (filters?.type?.value || "0");
url += "&sort=" + (showLatestNovels ? "4" : filters?.sort?.value || "6");
url += "&atmosphere=" + (filters?.atmosphere?.value || "0");
url += "&adult=" + (filters?.adult?.value || "0");
let url = this.site + '/search?t=';
url += '&cat=' + (filters?.cat?.value || '0');
url += '&s_lang=' + (filters?.s_lang?.value || '0');
url += '&t_lang=' + (filters?.t_lang?.value || '0');
url += '&type=' + (filters?.type?.value || '0');
url += '&sort=' + (showLatestNovels ? '4' : filters?.sort?.value || '6');
url += '&atmosphere=' + (filters?.atmosphere?.value || '0');
url += '&adult=' + (filters?.adult?.value || '0');
Object.entries(filters || {}).forEach(([type, { value }]) => {
if (value instanceof Array && value.length) {
url += "&" + value
.map((val) => (type == "extra" ? val + "=1" : type + "[]=" + val))
.join("&");
url +=
'&' +
value
.map(val => (type == 'extra' ? val + '=1' : type + '[]=' + val))
.join('&');
}
});
url += "&Book_page=" + pageNo;
url += '&Book_page=' + pageNo;
const body = await fetchApi(url).then((res) => res.text());
const body = await fetchApi(url).then(res => res.text());
const loadedCheerio = parseHTML(body);
loadedCheerio(
'ul[class="search-results"] > li:not([class="ad_type_catalog"])',
).each((index, element) => {
loadedCheerio(element).find("p > a").text();
const name = loadedCheerio(element).find("p > a").text();
const cover = loadedCheerio(element).find("img").attr("src");
const path = loadedCheerio(element).find("p > a").attr("href");
loadedCheerio(element).find('p > a').text();
const name = loadedCheerio(element).find('p > a').text();
const cover = loadedCheerio(element).find('img').attr('src');
const path = loadedCheerio(element).find('p > a').attr('href');
if (!name || !path) return;
novels.push({ name, cover: this.site + cover, path });
@@ -73,13 +75,13 @@ class RulatePlugin implements Plugin.PluginBase {
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
let result = await fetchApi(this.site + novelPath);
if (result.url.includes("mature?path=")) {
if (result.url.includes('mature?path=')) {
const formData = new FormData();
formData.append("path", novelPath);
formData.append("ok", "Да");
formData.append('path', novelPath);
formData.append('ok', 'Да');
await fetchApi(result.url, {
method: "POST",
method: 'POST',
body: formData,
});
@@ -96,41 +98,41 @@ class RulatePlugin implements Plugin.PluginBase {
.text()
.trim(),
};
if (novel.name?.includes?.("[")) {
novel.name = novel.name.split("[")[0].trim();
if (novel.name?.includes?.('[')) {
novel.name = novel.name.split('[')[0].trim();
}
novel.cover =
this.site + loadedCheerio('div[class="images"] > div img').attr("src");
novel.summary = loadedCheerio("#Info > div:nth-child(3), .book-description")
this.site + loadedCheerio('div[class="images"] > div img').attr('src');
novel.summary = loadedCheerio('#Info > div:nth-child(3), .book-description')
.text()
.trim();
novel.author = loadedCheerio(
".book-stats-icons_author > span:nth-child(2) > a:nth-child(1)",
'.book-stats-icons_author > span:nth-child(2) > a:nth-child(1)',
).text();
const genres: string[] = [];
loadedCheerio("div.span5 > p, .span5 > div:nth-child(2) > p").each(
loadedCheerio('div.span5 > p, .span5 > div:nth-child(2) > p').each(
function () {
switch (loadedCheerio(this).find("strong").text()) {
case "Автор:":
novel.author = loadedCheerio(this).find("em > a").text().trim();
switch (loadedCheerio(this).find('strong').text()) {
case 'Автор:':
novel.author = loadedCheerio(this).find('em > a').text().trim();
break;
case "Выпуск:":
case 'Выпуск:':
novel.status =
loadedCheerio(this).find("em").text().trim() === "продолжается"
loadedCheerio(this).find('em').text().trim() === 'продолжается'
? NovelStatus.Ongoing
: NovelStatus.Completed;
break;
case "Тэги:":
case 'Тэги:':
loadedCheerio(this)
.find("em > a")
.find('em > a')
.each(function () {
genres.push(loadedCheerio(this).text());
});
break;
case "Жанры:":
case 'Жанры:':
loadedCheerio(this)
.find("em > a")
.find('em > a')
.each(function () {
genres.push(loadedCheerio(this).text());
});
@@ -140,23 +142,23 @@ class RulatePlugin implements Plugin.PluginBase {
);
if (genres.length) {
novel.genres = genres.reverse().join(",");
novel.genres = genres.reverse().join(',');
}
const chapters: Plugin.ChapterItem[] = [];
loadedCheerio("table > tbody > tr.chapter_row").each(
loadedCheerio('table > tbody > tr.chapter_row').each(
(chapterIndex, element) => {
const chapterName = loadedCheerio(element)
.find('td[class="t"] > a')
.text()
.trim();
const releaseDate = loadedCheerio(element)
.find("td > span")
.attr("title")
.find('td > span')
.attr('title')
?.trim();
const chapterUrl = loadedCheerio(element)
.find('td[class="t"] > a')
.attr("href");
.attr('href');
if (
!loadedCheerio(element).find('td > span[class="disabled"]').length &&
@@ -179,12 +181,12 @@ class RulatePlugin implements Plugin.PluginBase {
async parseChapter(chapterPath: string): Promise<string> {
let result = await fetchApi(this.site + chapterPath);
if (result.url.includes("mature?path=")) {
if (result.url.includes('mature?path=')) {
const formData = new FormData();
formData.append("ok", "Да");
formData.append('ok', 'Да');
await fetchApi(result.url, {
method: "POST",
method: 'POST',
body: formData,
});
@@ -193,26 +195,26 @@ class RulatePlugin implements Plugin.PluginBase {
const body = await result.text();
const loadedCheerio = parseHTML(body);
loadedCheerio(".content-text img").each((index, element) => {
if (!loadedCheerio(element).attr("src")?.startsWith("http")) {
const src = loadedCheerio(element).attr("src");
loadedCheerio(element).attr("src", this.site + src);
loadedCheerio('.content-text img').each((index, element) => {
if (!loadedCheerio(element).attr('src')?.startsWith('http')) {
const src = loadedCheerio(element).attr('src');
loadedCheerio(element).attr('src', this.site + src);
}
});
const chapterText = loadedCheerio(".content-text").html();
return chapterText || "";
const chapterText = loadedCheerio('.content-text').html();
return chapterText || '';
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = [];
const result = await fetchApi(
this.site + "/search/autocomplete?query=" + searchTerm,
this.site + '/search/autocomplete?query=' + searchTerm,
);
const json = (await result.json()) as response[];
json.forEach((novel) => {
const name = novel.title_one + " / " + novel.title_two;
json.forEach(novel => {
const name = novel.title_one + ' / ' + novel.title_two;
if (!novel.url) return;
novels.push({
@@ -225,24 +227,26 @@ class RulatePlugin implements Plugin.PluginBase {
return novels;
}
parseDate = (dateString: string | undefined = "") => {
parseDate = (dateString: string | undefined = '') => {
const months: { [key: string]: number } = {
"янв.": 1,
"февр.": 2,
"мар.": 3,
"апр.": 4,
'янв.': 1,
'февр.': 2,
'мар.': 3,
'апр.': 4,
мая: 5,
"июн.": 6,
"июл.": 7,
"авг.": 8,
"сент.": 9,
"окт.": 10,
"нояб.": 11,
"дек.": 12,
'июн.': 6,
'июл.': 7,
'авг.': 8,
'сент.': 9,
'окт.': 10,
'нояб.': 11,
'дек.': 12,
};
const [day, month, year, , time] = dateString.split(" ");
const [day, month, year, , time] = dateString.split(' ');
if (day && months[month] && year && time) {
return dayjs(year + "-" + months[month] + "-" + day + " " + time).format("LLL");
return dayjs(year + '-' + months[month] + '-' + day + ' ' + time).format(
'LLL',
);
}
return dateString || null;
};
+2 -2
View File
@@ -1,5 +1,5 @@
const { minify_sync } = require("terser");
import * as fs from "fs";
const { minify_sync } = require('terser');
import * as fs from 'fs';
export const minify = function (path: string) {
const code = fs.readFileSync(path).toString();
+26 -28
View File
@@ -1,10 +1,10 @@
import fs from "fs";
import { languages } from "@libs/languages";
import path from "path";
import { PluginList } from "@typings/types";
import { Plugin } from "@typings/plugin";
import { Filters } from "@libs/filterInputs";
const root = path.dirname(require?.main?.filename || "");
import fs from 'fs';
import { languages } from '@libs/languages';
import path from 'path';
import { PluginList } from '@typings/types';
import { Plugin } from '@typings/plugin';
import { Filters } from '@libs/filterInputs';
const root = path.dirname(require?.main?.filename || '');
export const fetchHeaders: Record<string, string> = {};
@@ -12,20 +12,15 @@ export const all_plugins = (): PluginList => {
const res: PluginList = {};
for (let languageEnglish in languages) {
//language with English name
const languageNative =
languages[languageEnglish as keyof typeof languages];
const langPath = path.join(
root,
"plugins",
languageEnglish.toLowerCase()
);
const languageNative = languages[languageEnglish as keyof typeof languages];
const langPath = path.join(root, 'plugins', languageEnglish.toLowerCase());
if (!fs.existsSync(langPath)) continue;
const plugins = fs.readdirSync(langPath);
res[languageEnglish] = [];
plugins.forEach((plugin) => {
if (plugin.startsWith(".")) return;
plugins.forEach(plugin => {
if (plugin.startsWith('.')) return;
const requirePath = `@plugins/${languageEnglish.toLowerCase()}/${
plugin.split(".")[0]
plugin.split('.')[0]
}`;
const instance = require(requirePath).default;
const { id, name, version, icon } = instance;
@@ -44,7 +39,7 @@ export const all_plugins = (): PluginList => {
};
const getPlugin = async (
requirePath: string
requirePath: string,
): Promise<Plugin.PluginBase | Plugin.PagePlugin | null> => {
const plugin = await require(requirePath).default;
return plugin;
@@ -56,33 +51,36 @@ export const getFilter = async (pluginRequirePath: string) =>
export const popularNovels = async (
pluginRequirePath: string,
page: number,
options: Plugin.PopularNovelsOptions<Filters>
options: Plugin.PopularNovelsOptions<Filters>,
) => (await getPlugin(pluginRequirePath))?.popularNovels(page, options);
export const searchNovels = async (
pluginRequirePath: string,
page: number,
searchTerm: string
searchTerm: string,
) => (await getPlugin(pluginRequirePath))?.searchNovels(searchTerm, page);
export const parseNovel = async (
pluginRequirePath: string,
novelPath: string
novelPath: string,
) => (await getPlugin(pluginRequirePath))?.parseNovel(novelPath);
export const hasParsePage = async (
pluginRequirePath: string,
) => (await getPlugin(pluginRequirePath) as Plugin.PagePlugin | null)?.parsePage;
export const hasParsePage = async (pluginRequirePath: string) =>
((await getPlugin(pluginRequirePath)) as Plugin.PagePlugin | null)?.parsePage;
export const parsePage = async(
export const parsePage = async (
pluginRequirePath: string,
novelPath: string,
page: string
) => (await getPlugin(pluginRequirePath) as Plugin.PagePlugin | null)?.parsePage(novelPath, page);
page: string,
) =>
((await getPlugin(pluginRequirePath)) as Plugin.PagePlugin | null)?.parsePage(
novelPath,
page,
);
export const parseChapter = async (
pluginRequirePath: string,
chapterPath: string
chapterPath: string,
) => (await getPlugin(pluginRequirePath))?.parseChapter(chapterPath);
export const fetchImage = async (pluginRequirePath: string, url: string) =>

Some files were not shown because too many files have changed in this diff Show More