fix: linter issues
This commit is contained in:
+9
-1
@@ -165,10 +165,11 @@ export default tseslint.config(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.{ts,tsx,mts,cts,js}'],
|
files: ['**/*.{ts,tsx,mts,cts,js,cjs}'],
|
||||||
ignores: ['./plugins/*/*.ts', './plugins/multisrc/*/template.ts'],
|
ignores: ['./plugins/*/*.ts', './plugins/multisrc/*/template.ts'],
|
||||||
rules: {
|
rules: {
|
||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': 'warn',
|
'@typescript-eslint/no-unused-vars': 'warn',
|
||||||
'@typescript-eslint/no-namespace': 'off',
|
'@typescript-eslint/no-namespace': 'off',
|
||||||
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
|
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
|
||||||
@@ -191,7 +192,14 @@ export default tseslint.config(
|
|||||||
globals: {
|
globals: {
|
||||||
...globals.serviceworker,
|
...globals.serviceworker,
|
||||||
...globals.browser,
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['**/fictioneer/custom/*/*.js'],
|
||||||
|
rules: {
|
||||||
|
'no-undef': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Generated
+28
-26
@@ -5462,9 +5462,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -5556,9 +5556,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.6",
|
"version": "1.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -5566,6 +5566,7 @@
|
|||||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
},
|
},
|
||||||
@@ -7605,9 +7606,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.3",
|
"version": "8.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -7624,7 +7625,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.8",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -7690,11 +7691,12 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/protobufjs": {
|
"node_modules/protobufjs": {
|
||||||
"version": "7.4.0",
|
"version": "7.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz",
|
||||||
"integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
|
"integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@protobufjs/aspromise": "^1.1.2",
|
"@protobufjs/aspromise": "^1.1.2",
|
||||||
"@protobufjs/base64": "^1.1.2",
|
"@protobufjs/base64": "^1.1.2",
|
||||||
@@ -12900,9 +12902,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"fast-uri": {
|
"fast-uri": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"fastq": {
|
"fastq": {
|
||||||
@@ -12966,9 +12968,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"follow-redirects": {
|
"follow-redirects": {
|
||||||
"version": "1.15.6",
|
"version": "1.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"for-each": {
|
"for-each": {
|
||||||
@@ -14298,11 +14300,11 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"version": "8.5.3",
|
"version": "8.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"nanoid": "^3.3.8",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
}
|
}
|
||||||
@@ -14350,9 +14352,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"protobufjs": {
|
"protobufjs": {
|
||||||
"version": "7.4.0",
|
"version": "7.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz",
|
||||||
"integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
|
"integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@protobufjs/aspromise": "^1.1.2",
|
"@protobufjs/aspromise": "^1.1.2",
|
||||||
|
|||||||
+32
-22
@@ -1,13 +1,13 @@
|
|||||||
import { CheerioAPI, load as parseHTML } from 'cheerio';
|
import { load as parseHTML } from 'cheerio';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
import { Filters } from '@libs/filterInputs';
|
||||||
import { defaultCover } from '@libs/defaultCover';
|
import { defaultCover } from '@libs/defaultCover';
|
||||||
|
|
||||||
class dilartube implements Plugin.PluginBase {
|
class dilartube implements Plugin.PluginBase {
|
||||||
id = 'dilartube';
|
id = 'dilartube';
|
||||||
name = 'dilar tube';
|
name = 'dilar tube';
|
||||||
version = '1.0.1';
|
version = '1.0.2';
|
||||||
icon = 'src/ar/dilartube/icon.png';
|
icon = 'src/ar/dilartube/icon.png';
|
||||||
site = 'https://golden.rest/';
|
site = 'https://golden.rest/';
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ class dilartube implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
page: number,
|
page: number,
|
||||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<Filters>,
|
{ showLatestNovels }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
let link = `${this.site}api/releases?page=${page}`;
|
let link = `${this.site}api/releases?page=${page}`;
|
||||||
if (showLatestNovels) {
|
if (showLatestNovels) {
|
||||||
@@ -82,14 +82,14 @@ class dilartube implements Plugin.PluginBase {
|
|||||||
const chapterItems: Plugin.ChapterItem[] = [];
|
const chapterItems: Plugin.ChapterItem[] = [];
|
||||||
const fullUrl = this.site + 'api/' + novelUrl;
|
const fullUrl = this.site + 'api/' + novelUrl;
|
||||||
const chapterUrl = this.site + 'api/' + novelUrl + '/releases';
|
const chapterUrl = this.site + 'api/' + novelUrl + '/releases';
|
||||||
const manga = await fetchApi(fullUrl).then(r => r.json());
|
const manga: MangaResponse = await fetchApi(fullUrl).then(r => r.json());
|
||||||
const chapters = await fetchApi(chapterUrl).then(r => r.json());
|
const chapters = await fetchApi(chapterUrl).then(r => r.json());
|
||||||
const mangaData = manga.mangaData;
|
const mangaData = manga.mangaData;
|
||||||
const chapterData = chapters.releases;
|
const chapterData = chapters.releases;
|
||||||
|
|
||||||
const novel: Plugin.SourceNovel = {
|
const novel: Plugin.SourceNovel = {
|
||||||
path: novelUrl,
|
path: novelUrl,
|
||||||
name: mangaData.arabic_title || 'Untitled',
|
name: mangaData.arabic_title ?? mangaData.title ?? 'Untitled',
|
||||||
author:
|
author:
|
||||||
(mangaData.authors.length > 0 ? mangaData.authors[0].name : '') ||
|
(mangaData.authors.length > 0 ? mangaData.authors[0].name : '') ||
|
||||||
'Unknown',
|
'Unknown',
|
||||||
@@ -97,7 +97,7 @@ class dilartube implements Plugin.PluginBase {
|
|||||||
cover: `${this.site}uploads/manga/cover/${mangaData.id}/${mangaData.cover}`,
|
cover: `${this.site}uploads/manga/cover/${mangaData.id}/${mangaData.cover}`,
|
||||||
chapters: [],
|
chapters: [],
|
||||||
};
|
};
|
||||||
const translationStatusId: string = mangaData.translation_status;
|
const translationStatusId = mangaData.translation_status.toString();
|
||||||
const translationText =
|
const translationText =
|
||||||
{
|
{
|
||||||
'1': 'مستمره',
|
'1': 'مستمره',
|
||||||
@@ -105,13 +105,15 @@ class dilartube implements Plugin.PluginBase {
|
|||||||
'2': 'متوقفة',
|
'2': 'متوقفة',
|
||||||
'3': 'غير مترجمه',
|
'3': 'غير مترجمه',
|
||||||
}[translationStatusId] || 'غير معروف';
|
}[translationStatusId] || 'غير معروف';
|
||||||
const statusWords = new Set(['مكتمل', 'جديد', 'مستمر']);
|
// const statusWords = new Set(['مكتمل', 'جديد', 'مستمر']);
|
||||||
const mainGenres = mangaData.categories
|
const mainGenres = Array.from(
|
||||||
.map((category: { name: any }) => category.name)
|
new Set(mangaData.categories.map(g => g.name)),
|
||||||
|
)
|
||||||
|
.filter(Boolean)
|
||||||
.join(',');
|
.join(',');
|
||||||
novel.genres = `${translationText},${mainGenres}`;
|
novel.genres = `${translationText},${mainGenres}`;
|
||||||
|
|
||||||
const statusId: string = mangaData.story_status;
|
const statusId = mangaData.story_status.toString();
|
||||||
const statusText =
|
const statusText =
|
||||||
{
|
{
|
||||||
'2': 'Ongoing',
|
'2': 'Ongoing',
|
||||||
@@ -138,12 +140,18 @@ class dilartube implements Plugin.PluginBase {
|
|||||||
const parsedData = JSON.parse(jsonData as string);
|
const parsedData = JSON.parse(jsonData as string);
|
||||||
|
|
||||||
const chapterText = parsedData.readerDataAction.readerData.release.content;
|
const chapterText = parsedData.readerDataAction.readerData.release.content;
|
||||||
return chapterText;
|
// return html with p tags
|
||||||
|
return chapterText
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line: string) => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((line: string) => `<p>${line}</p>`)
|
||||||
|
.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchNovels(
|
async searchNovels(
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
page: number,
|
// page: number,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('query', searchTerm);
|
formData.append('query', searchTerm);
|
||||||
@@ -156,6 +164,7 @@ class dilartube implements Plugin.PluginBase {
|
|||||||
return this.parseNovels(data);
|
return this.parseNovels(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filters: Filters | undefined = undefined;
|
||||||
// filters = {
|
// filters = {
|
||||||
// types: {
|
// types: {
|
||||||
// value: [],
|
// value: [],
|
||||||
@@ -245,8 +254,8 @@ type Manga = {
|
|||||||
publisher_name: string | null;
|
publisher_name: string | null;
|
||||||
discord_url: string | null;
|
discord_url: string | null;
|
||||||
mobile_exclusive: boolean;
|
mobile_exclusive: boolean;
|
||||||
authors: any[];
|
// authors: any[];
|
||||||
artists: any[];
|
// artists: any[];
|
||||||
categories: Category[];
|
categories: Category[];
|
||||||
type: Type;
|
type: Type;
|
||||||
};
|
};
|
||||||
@@ -327,7 +336,7 @@ type MangaData = {
|
|||||||
uniq_visitors_count: number;
|
uniq_visitors_count: number;
|
||||||
publisher_id: number | null;
|
publisher_id: number | null;
|
||||||
publisher_name: string | null;
|
publisher_name: string | null;
|
||||||
arabic_title: string;
|
arabic_title: string | null;
|
||||||
english: string;
|
english: string;
|
||||||
synonyms: string;
|
synonyms: string;
|
||||||
japanese: string;
|
japanese: string;
|
||||||
@@ -360,11 +369,12 @@ type MangaData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type MangaResponse = {
|
type MangaResponse = {
|
||||||
membersMentioning: any[];
|
// membersMentioning: any[];
|
||||||
memberRates: any | null;
|
// memberRates: any | null;
|
||||||
mangaLogs: Record<string, any>;
|
// mangaLogs: Record<string, any>;
|
||||||
mangaData: MangaData;
|
mangaData: MangaData;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ChapterRelease = {
|
type ChapterRelease = {
|
||||||
id: number;
|
id: number;
|
||||||
manga_id: number;
|
manga_id: number;
|
||||||
@@ -385,7 +395,7 @@ type ChapterRelease = {
|
|||||||
has_rev_link: boolean;
|
has_rev_link: boolean;
|
||||||
};
|
};
|
||||||
type searchManga = {
|
type searchManga = {
|
||||||
filter: any;
|
// filter: any;
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
@@ -417,8 +427,8 @@ type searchManga = {
|
|||||||
publisher_name: string | null;
|
publisher_name: string | null;
|
||||||
discord_url: string | null;
|
discord_url: string | null;
|
||||||
mobile_exclusive: boolean;
|
mobile_exclusive: boolean;
|
||||||
authors: any[];
|
// authors: any[];
|
||||||
artists: any[];
|
// artists: any[];
|
||||||
categories: Category[];
|
categories: Category[];
|
||||||
type: Type;
|
type: Type;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CheerioAPI, load as parseHTML } from 'cheerio';
|
import { load as parseHTML } from 'cheerio';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||||
@@ -7,7 +7,7 @@ import { defaultCover } from '@libs/defaultCover';
|
|||||||
class RewayatClub implements Plugin.PagePlugin {
|
class RewayatClub implements Plugin.PagePlugin {
|
||||||
id = 'rewayatclub';
|
id = 'rewayatclub';
|
||||||
name = 'Rewayat Club';
|
name = 'Rewayat Club';
|
||||||
version = '1.0.2';
|
version = '1.0.3';
|
||||||
icon = 'src/ar/rewayatclub/icon.png';
|
icon = 'src/ar/rewayatclub/icon.png';
|
||||||
site = 'https://rewayat.club/';
|
site = 'https://rewayat.club/';
|
||||||
|
|
||||||
@@ -34,7 +34,10 @@ class RewayatClub implements Plugin.PagePlugin {
|
|||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
page: number,
|
page: number,
|
||||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<Filters>,
|
{
|
||||||
|
showLatestNovels,
|
||||||
|
filters,
|
||||||
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
let link = `https://api.rewayat.club/api/novels/`;
|
let link = `https://api.rewayat.club/api/novels/`;
|
||||||
let body: NovelData = {
|
let body: NovelData = {
|
||||||
@@ -253,7 +256,7 @@ type ChapterEntry = {
|
|||||||
hits: number;
|
hits: number;
|
||||||
id: number;
|
id: number;
|
||||||
};
|
};
|
||||||
read: any[];
|
// read: any[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ChapterData = {
|
type ChapterData = {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { defaultCover } from '@libs/defaultCover';
|
|||||||
class Sunovels implements Plugin.PagePlugin {
|
class Sunovels implements Plugin.PagePlugin {
|
||||||
id = 'sunovels';
|
id = 'sunovels';
|
||||||
name = 'Sunovels';
|
name = 'Sunovels';
|
||||||
version = '1.0.0';
|
version = '1.0.1';
|
||||||
icon = 'src/ar/sunovels/icon.png';
|
icon = 'src/ar/sunovels/icon.png';
|
||||||
site = 'https://sunovels.com/';
|
site = 'https://sunovels.com/';
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class Sunovels implements Plugin.PagePlugin {
|
|||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
page: number,
|
page: number,
|
||||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<Filters>,
|
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const pageCorrected = page - 1;
|
const pageCorrected = page - 1;
|
||||||
let link = `${this.site}library?`;
|
let link = `${this.site}library?`;
|
||||||
@@ -137,7 +137,7 @@ class Sunovels implements Plugin.PagePlugin {
|
|||||||
name: item.chapterName,
|
name: item.chapterName,
|
||||||
releaseTime: new Date(item.releaseTime).toISOString(),
|
releaseTime: new Date(item.releaseTime).toISOString(),
|
||||||
path: item.chapterUrl,
|
path: item.chapterUrl,
|
||||||
chapterNumber: item.chapterNumber,
|
chapterNumber: Number(item.chapterNumber),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return chapter;
|
return chapter;
|
||||||
@@ -169,8 +169,7 @@ class Sunovels implements Plugin.PagePlugin {
|
|||||||
const dateAttr = loadedCheerio(el)
|
const dateAttr = loadedCheerio(el)
|
||||||
.find('time.chapter-update')
|
.find('time.chapter-update')
|
||||||
.attr('datetime');
|
.attr('datetime');
|
||||||
const date = new Date(dateAttr);
|
const releaseTime = dateAttr ? new Date(dateAttr).toISOString() : '';
|
||||||
const releaseTime = date.toISOString();
|
|
||||||
const chapternumber = loadedCheerio(el)
|
const chapternumber = loadedCheerio(el)
|
||||||
.find('strong.chapter-title')
|
.find('strong.chapter-title')
|
||||||
.text()
|
.text()
|
||||||
|
|||||||
+11
-11
@@ -17,17 +17,17 @@ const parseUrl = (url?: string): URL | undefined => {
|
|||||||
const getStandardNovelPath = (url?: string): string | undefined => {
|
const getStandardNovelPath = (url?: string): string | undefined => {
|
||||||
const parsedUrl = parseUrl(url);
|
const parsedUrl = parseUrl(url);
|
||||||
if (!parsedUrl) return undefined;
|
if (!parsedUrl) return undefined;
|
||||||
const match = parsedUrl.pathname.match(/^(\/amp)?(\/n\/[^\/]+\/)/);
|
const match = parsedUrl.pathname.match(/^(\/amp)?(\/n\/[^/]+\/)/);
|
||||||
return match?.[2]?.replace(/^\//, '');
|
return match?.[2]?.replace(/^\//, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getChapterFileName = (url?: string): string | undefined => {
|
// const getChapterFileName = (url?: string): string | undefined => {
|
||||||
const parsedUrl = parseUrl(url);
|
// const parsedUrl = parseUrl(url);
|
||||||
if (!parsedUrl) return undefined;
|
// if (!parsedUrl) return undefined;
|
||||||
const fileName = parsedUrl.pathname.split('/').pop();
|
// const fileName = parsedUrl.pathname.split('/').pop();
|
||||||
if (fileName && /^\d+\.html$/.test(fileName)) return fileName;
|
// if (fileName && /^\d+\.html$/.test(fileName)) return fileName;
|
||||||
return undefined;
|
// return undefined;
|
||||||
};
|
// };
|
||||||
|
|
||||||
const makeAbsolute = (
|
const makeAbsolute = (
|
||||||
relativeUrl?: string,
|
relativeUrl?: string,
|
||||||
@@ -47,7 +47,7 @@ class QuanbenPlugin implements Plugin.PluginBase {
|
|||||||
id = 'quanben';
|
id = 'quanben';
|
||||||
name = 'Quanben';
|
name = 'Quanben';
|
||||||
site = 'https://www.quanben.io/';
|
site = 'https://www.quanben.io/';
|
||||||
version = '1.1.0';
|
version = '1.1.1';
|
||||||
icon = 'src/cn/quanben/icon.png';
|
icon = 'src/cn/quanben/icon.png';
|
||||||
defaultCover = defaultCover;
|
defaultCover = defaultCover;
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ class QuanbenPlugin implements Plugin.PluginBase {
|
|||||||
async parseChapterList(novelPath: string): Promise<Plugin.ChapterItem[]> {
|
async parseChapterList(novelPath: string): Promise<Plugin.ChapterItem[]> {
|
||||||
if (!novelPath.startsWith('n/') || !novelPath.endsWith('/')) return [];
|
if (!novelPath.startsWith('n/') || !novelPath.endsWith('/')) return [];
|
||||||
|
|
||||||
const novelSlug = novelPath.match(/^n\/([^\/]+)\//)?.[1];
|
const novelSlug = novelPath.match(/^n\/([^/]+)\//)?.[1];
|
||||||
if (!novelSlug) return [];
|
if (!novelSlug) return [];
|
||||||
|
|
||||||
const mirrorUrl = `https://quanben5.com/n/${novelSlug}/xiaoshuo.html`;
|
const mirrorUrl = `https://quanben5.com/n/${novelSlug}/xiaoshuo.html`;
|
||||||
@@ -230,7 +230,7 @@ class QuanbenPlugin implements Plugin.PluginBase {
|
|||||||
// Helper function to extract and clean chapter content from HTML body
|
// Helper function to extract and clean chapter content from HTML body
|
||||||
private extractChapterContent(body: string): string {
|
private extractChapterContent(body: string): string {
|
||||||
const $ = parseHTML(body);
|
const $ = parseHTML(body);
|
||||||
let $content = $('#contentbody, #content, .content').first();
|
const $content = $('#contentbody, #content, .content').first();
|
||||||
if (!$content.length) return 'Error: Chapter content not found.';
|
if (!$content.length) return 'Error: Chapter content not found.';
|
||||||
|
|
||||||
$content
|
$content
|
||||||
|
|||||||
+23
-10
@@ -29,7 +29,7 @@ class ixdzs8Plugin implements Plugin.PluginBase {
|
|||||||
id = 'ixdzs8';
|
id = 'ixdzs8';
|
||||||
name = '爱下电子书';
|
name = '爱下电子书';
|
||||||
site = 'https://ixdzs8.com/';
|
site = 'https://ixdzs8.com/';
|
||||||
version = '2.2.8';
|
version = '2.2.9';
|
||||||
icon = 'src/cn/ixdzs8/favicon.png';
|
icon = 'src/cn/ixdzs8/favicon.png';
|
||||||
|
|
||||||
imageRequestInit = {
|
imageRequestInit = {
|
||||||
@@ -49,14 +49,18 @@ class ixdzs8Plugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
$('ul.u-list > li.burl').each((_i, el) => {
|
$('ul.u-list > li.burl').each((_i, el) => {
|
||||||
const $el = $(el);
|
const $el = $(el);
|
||||||
let novelPath: string | undefined;
|
|
||||||
let novelName: string | undefined;
|
|
||||||
let novelCover: string | undefined;
|
|
||||||
|
|
||||||
const $link = $el.find('.l-info h3 a');
|
const $link = $el.find('.l-info h3 a');
|
||||||
novelPath = $link.attr('href')?.trim();
|
const novelPath: string | undefined = $link.attr('href')?.trim();
|
||||||
novelName = ($link.attr('title') || $link.text() || '').trim();
|
const novelName: string | undefined = (
|
||||||
novelCover = $el.find('.l-img img').attr('src')?.trim();
|
$link.attr('title') ||
|
||||||
|
$link.text() ||
|
||||||
|
''
|
||||||
|
).trim();
|
||||||
|
const novelCover: string | undefined = $el
|
||||||
|
.find('.l-img img')
|
||||||
|
.attr('src')
|
||||||
|
?.trim();
|
||||||
|
|
||||||
if (novelPath && novelName) {
|
if (novelPath && novelName) {
|
||||||
novels.push({
|
novels.push({
|
||||||
@@ -159,14 +163,14 @@ class ixdzs8Plugin implements Plugin.PluginBase {
|
|||||||
throw new Error(`Failed to fetch chapters for bid=${bookId}`);
|
throw new Error(`Failed to fetch chapters for bid=${bookId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await res.json();
|
const json: ChapterJSON = await res.json();
|
||||||
|
|
||||||
if (json.rs !== 200 || !Array.isArray(json.data)) {
|
if (json.rs !== 200 || !Array.isArray(json.data)) {
|
||||||
throw new Error('Invalid response format from chapter list');
|
throw new Error('Invalid response format from chapter list');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build chapters array
|
// Build chapters array
|
||||||
const chapters: Plugin.ChapterItem[] = json.data.map((ch: any) => {
|
const chapters: Plugin.ChapterItem[] = json.data.map(ch => {
|
||||||
return {
|
return {
|
||||||
name: ch.title,
|
name: ch.title,
|
||||||
path: ch.ctype === '0' ? `read/${bookId}/p${ch.ordernum}.html` : '', // only normal chapters get link
|
path: ch.ctype === '0' ? `read/${bookId}/p${ch.ordernum}.html` : '', // only normal chapters get link
|
||||||
@@ -254,7 +258,7 @@ class ixdzs8Plugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async searchNovels(
|
async searchNovels(
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
pageNo: number,
|
// pageNo: number,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const searchUrl = `${this.site}bsearch?q=${encodeURIComponent(searchTerm)}`;
|
const searchUrl = `${this.site}bsearch?q=${encodeURIComponent(searchTerm)}`;
|
||||||
let body = '';
|
let body = '';
|
||||||
@@ -298,3 +302,12 @@ class ixdzs8Plugin implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default new ixdzs8Plugin();
|
export default new ixdzs8Plugin();
|
||||||
|
|
||||||
|
type ChapterJSON = {
|
||||||
|
rs: number;
|
||||||
|
data?: {
|
||||||
|
ctype: string;
|
||||||
|
ordernum: string;
|
||||||
|
title: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class LinovelPlugin implements Plugin.PluginBase {
|
|||||||
name = 'linovel';
|
name = 'linovel';
|
||||||
icon = 'src/cn/linovel/icon.png';
|
icon = 'src/cn/linovel/icon.png';
|
||||||
site = 'https://www.linovel.net';
|
site = 'https://www.linovel.net';
|
||||||
version = '1.0.0';
|
version = '1.0.1';
|
||||||
filters: Filters | undefined = undefined;
|
filters: Filters | undefined = undefined;
|
||||||
imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined;
|
imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined;
|
||||||
|
|
||||||
@@ -27,13 +27,12 @@ class LinovelPlugin implements Plugin.PluginBase {
|
|||||||
return parseHTML(body);
|
return parseHTML(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels() // pageNo: number,
|
||||||
pageNo: number,
|
// {
|
||||||
{
|
// showLatestNovels,
|
||||||
showLatestNovels,
|
// filters,
|
||||||
filters,
|
// }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
: Promise<Plugin.NovelItem[]> {
|
||||||
): Promise<Plugin.NovelItem[]> {
|
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
const loadedCheerio = await this.fetchHTML(this.site);
|
const loadedCheerio = await this.fetchHTML(this.site);
|
||||||
@@ -139,7 +138,7 @@ class LinovelPlugin implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
async searchNovels(
|
async searchNovels(
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
pageNo: number,
|
// pageNo: number,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const loadedCheerio = await this.fetchHTML(
|
const loadedCheerio = await this.fetchHTML(
|
||||||
this.site + '/search/?kw=' + searchTerm,
|
this.site + '/search/?kw=' + searchTerm,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class Linovelib implements Plugin.PluginBase {
|
|||||||
name = 'Linovelib';
|
name = 'Linovelib';
|
||||||
icon = 'src/cn/linovelib/icon.png';
|
icon = 'src/cn/linovelib/icon.png';
|
||||||
site = 'https://www.bilinovel.com';
|
site = 'https://www.bilinovel.com';
|
||||||
version = '1.2.0';
|
version = '1.2.1';
|
||||||
imageRequestInit?: Plugin.ImageRequestInit | undefined = {
|
imageRequestInit?: Plugin.ImageRequestInit | undefined = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -29,7 +29,7 @@ class Linovelib implements Plugin.PluginBase {
|
|||||||
type: 'Text',
|
type: 'Text',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
serverUrl = storage.get('host') || 'http://localhost:5301';
|
serverUrl: string = storage.get('host') || 'http://localhost:5301';
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
@@ -82,7 +82,7 @@ class Linovelib implements Plugin.PluginBase {
|
|||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
// move major logic to LDS
|
// move major logic to LDS
|
||||||
const lastFetchChapterTime =
|
const lastFetchChapterTime =
|
||||||
storage.get('lastFetchChapterTime_' + chapterPath) || 0;
|
Number(storage.get('lastFetchChapterTime_' + chapterPath)) || 0;
|
||||||
if (Date.now() - lastFetchChapterTime < 10000) {
|
if (Date.now() - lastFetchChapterTime < 10000) {
|
||||||
return storage.get('chapterContent_' + chapterPath) || '';
|
return storage.get('chapterContent_' + chapterPath) || '';
|
||||||
}
|
}
|
||||||
@@ -97,10 +97,11 @@ class Linovelib implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async searchNovels(
|
async searchNovels(
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
pageNo: number,
|
// pageNo: number,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
// move major logic to LDS
|
// move major logic to LDS
|
||||||
const lastSearchTime = storage.get('lastSearchTime_' + this.id) || 0;
|
const lastSearchTime =
|
||||||
|
Number(storage.get('lastSearchTime_' + this.id)) || 0;
|
||||||
if (Date.now() - lastSearchTime < 5000) {
|
if (Date.now() - lastSearchTime < 5000) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class Linovelib_tw implements Plugin.PluginBase {
|
|||||||
name = 'Linovelib(繁體)';
|
name = 'Linovelib(繁體)';
|
||||||
icon = 'src/cn/linovelib/icon.png';
|
icon = 'src/cn/linovelib/icon.png';
|
||||||
site = 'https://tw.linovelib.com/';
|
site = 'https://tw.linovelib.com/';
|
||||||
version = '1.0.0';
|
version = '1.0.1';
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
@@ -368,7 +368,7 @@ class Linovelib_tw implements Plugin.PluginBase {
|
|||||||
pageCheerio('div.cgo, center').remove();
|
pageCheerio('div.cgo, center').remove();
|
||||||
|
|
||||||
// Load lazyloaded images
|
// Load lazyloaded images
|
||||||
pageCheerio('#acontentl img.imagecontent').each((i, el) => {
|
pageCheerio('[id*=acontent] img.imagecontent').each((i, el) => {
|
||||||
// Sometimes images are either in data-src or src
|
// Sometimes images are either in data-src or src
|
||||||
const imgSrc =
|
const imgSrc =
|
||||||
pageCheerio(el).attr('data-src') || pageCheerio(el).attr('src');
|
pageCheerio(el).attr('data-src') || pageCheerio(el).attr('src');
|
||||||
@@ -382,7 +382,7 @@ class Linovelib_tw implements Plugin.PluginBase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Recover the original character
|
// Recover the original character
|
||||||
pageText = pageCheerio('#acontentl').html() || '';
|
pageText = pageCheerio('[id*=acontent]').html() || '';
|
||||||
pageText = pageText.replace(/./g, char => skillgg[char] || char);
|
pageText = pageText.replace(/./g, char => skillgg[char] || char);
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CheerioAPI, load as parseHTML } from 'cheerio';
|
import { CheerioAPI, load as parseHTML } from 'cheerio';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi, FetchInit } from '@libs/fetch';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||||
|
|
||||||
@@ -8,11 +8,11 @@ class NovelsOnline implements Plugin.PluginBase {
|
|||||||
name = 'novelsOnline';
|
name = 'novelsOnline';
|
||||||
site = 'https://novelsonline.org';
|
site = 'https://novelsonline.org';
|
||||||
icon = 'src/en/novelsonline/icon.png';
|
icon = 'src/en/novelsonline/icon.png';
|
||||||
version = '1.0.1';
|
version = '1.0.2';
|
||||||
|
|
||||||
async safeFetch(
|
async safeFetch(
|
||||||
url: string,
|
url: string,
|
||||||
init: any | undefined = undefined,
|
init: FetchInit | undefined = undefined,
|
||||||
): Promise<CheerioAPI> {
|
): Promise<CheerioAPI> {
|
||||||
const r = await fetchApi(url, init);
|
const r = await fetchApi(url, init);
|
||||||
if (!r.ok)
|
if (!r.ok)
|
||||||
@@ -131,12 +131,14 @@ class NovelsOnline implements Plugin.PluginBase {
|
|||||||
.join(', ');
|
.join(', ');
|
||||||
break;
|
break;
|
||||||
case 'Artist(s)':
|
case 'Artist(s)':
|
||||||
const artist = detail
|
{
|
||||||
.find('li')
|
const artist = detail
|
||||||
.map((_, el) => $(el).text())
|
.find('li')
|
||||||
.get()
|
.map((_, el) => $(el).text())
|
||||||
.join(', ');
|
.get()
|
||||||
if (artist && artist != 'N/A') novel.artist = artist;
|
.join(', ');
|
||||||
|
if (artist && artist != 'N/A') novel.artist = artist;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'Status':
|
case 'Status':
|
||||||
novel.status = detail.text().trim();
|
novel.status = detail.text().trim();
|
||||||
|
|||||||
@@ -1,14 +1,36 @@
|
|||||||
import { CheerioAPI, load } from 'cheerio';
|
import { CheerioAPI, load } from 'cheerio';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { fetchApi, fetchText } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
|
import dayjs, { ManipulateType } from 'dayjs';
|
||||||
|
|
||||||
|
type NovelJSON = {
|
||||||
|
success: boolean;
|
||||||
|
data: {
|
||||||
|
posts: {
|
||||||
|
title: string;
|
||||||
|
thumbnail: string;
|
||||||
|
permalink: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChapterJSON = {
|
||||||
|
success: boolean;
|
||||||
|
data: {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
slug: string;
|
||||||
|
date: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
class StorySeedlingPlugin implements Plugin.PluginBase {
|
class StorySeedlingPlugin implements Plugin.PluginBase {
|
||||||
id = 'storyseedling';
|
id = 'storyseedling';
|
||||||
name = 'StorySeedling';
|
name = 'StorySeedling';
|
||||||
icon = 'src/en/storyseedling/icon.png';
|
icon = 'src/en/storyseedling/icon.png';
|
||||||
site = 'https://storyseedling.com/';
|
site = 'https://storyseedling.com/';
|
||||||
version = '1.0.4';
|
version = '1.0.6';
|
||||||
nonce: string | undefined;
|
nonce: string | undefined;
|
||||||
|
|
||||||
async getCheerio(url: string, search: boolean): Promise<CheerioAPI> {
|
async getCheerio(url: string, search: boolean): Promise<CheerioAPI> {
|
||||||
@@ -22,8 +44,40 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
|||||||
return $;
|
return $;
|
||||||
}
|
}
|
||||||
|
|
||||||
async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> {
|
private parseAgoDate(date: string | undefined) {
|
||||||
const novels: Plugin.NovelItem[] = [];
|
//parseMadaraDate
|
||||||
|
const parsed = dayjs(date);
|
||||||
|
if (date && parsed.isValid()) {
|
||||||
|
return parsed.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [amt, time, ago] = date?.toLowerCase().trim().split(/\s+/) || [];
|
||||||
|
const decade = time?.includes('decade'); // dayjs no support, but just in case
|
||||||
|
const amount = (amt === 'a' || amt === 'an' ? 1 : +amt) * (decade ? 10 : 1);
|
||||||
|
const unit = (decade ? 'year' : time) as ManipulateType;
|
||||||
|
|
||||||
|
const validUnits = [
|
||||||
|
'millisecond', // waow
|
||||||
|
'second',
|
||||||
|
'minute',
|
||||||
|
'hour',
|
||||||
|
'day',
|
||||||
|
'week',
|
||||||
|
'month',
|
||||||
|
'year',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (ago !== 'ago' || isNaN(amount) || !validUnits.includes(unit)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dayjs().subtract(amount, unit).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getNovels(
|
||||||
|
pageNo: number,
|
||||||
|
searchTerm = '',
|
||||||
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const body = await fetchApi(this.site + 'browse').then(r => r.text());
|
const body = await fetchApi(this.site + 'browse').then(r => r.text());
|
||||||
const loadedCheerio = load(body);
|
const loadedCheerio = load(body);
|
||||||
|
|
||||||
@@ -33,31 +87,32 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
|||||||
.replace("')", '') as string;
|
.replace("')", '') as string;
|
||||||
|
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append('search', '');
|
data.append('search', searchTerm);
|
||||||
data.append('orderBy', 'recent');
|
data.append('orderBy', 'recent');
|
||||||
data.append('curpage', pageNo.toString());
|
data.append('curpage', pageNo.toString());
|
||||||
data.append('post', postValue);
|
data.append('post', postValue);
|
||||||
data.append('action', 'fetch_browse');
|
data.append('action', 'fetch_browse');
|
||||||
|
|
||||||
const response: any = await fetchApi(this.site + 'ajax', {
|
const response = (await fetchApi(this.site + 'ajax', {
|
||||||
body: data,
|
body: data,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
}).then(res => res.json());
|
}).then(res => res.json())) as NovelJSON;
|
||||||
|
|
||||||
response.data.posts.forEach((element: any) =>
|
const novels: Plugin.NovelItem[] = response.data.posts.map(element => ({
|
||||||
novels.push({
|
name: element.title,
|
||||||
name: element.title,
|
cover: element.thumbnail,
|
||||||
cover: element.thumbnail,
|
path: element.permalink.replace(this.site, ''),
|
||||||
path: element.permalink.replace(this.site, ''),
|
}));
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||||
|
return this.getNovels(pageNo);
|
||||||
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
const $ = await this.getCheerio(this.site + novelPath, false);
|
const $ = await this.getCheerio(this.site + novelPath, false);
|
||||||
const baseUrl = this.site;
|
|
||||||
|
|
||||||
const novel: Partial<Plugin.SourceNovel> = {
|
const novel: Partial<Plugin.SourceNovel> = {
|
||||||
path: novelPath,
|
path: novelPath,
|
||||||
@@ -67,7 +122,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
|||||||
const coverUrl = $('img[x-ref="art"].w-full.rounded.shadow-md').attr('src');
|
const coverUrl = $('img[x-ref="art"].w-full.rounded.shadow-md').attr('src');
|
||||||
|
|
||||||
if (coverUrl) {
|
if (coverUrl) {
|
||||||
novel.cover = new URL(coverUrl, baseUrl).href;
|
novel.cover = new URL(coverUrl, this.site).href;
|
||||||
}
|
}
|
||||||
|
|
||||||
const genres: string[] = [];
|
const genres: string[] = [];
|
||||||
@@ -122,7 +177,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
|||||||
const chaptersUrl = `${this.site}${chapterListing}`;
|
const chaptersUrl = `${this.site}${chapterListing}`;
|
||||||
const refererUrl = `${this.site}${novel.path}`;
|
const refererUrl = `${this.site}${novel.path}`;
|
||||||
|
|
||||||
let results = await fetchApi(chaptersUrl, {
|
const results: ChapterJSON = await fetchApi(chaptersUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
referrer: refererUrl,
|
referrer: refererUrl,
|
||||||
referrerPolicy: 'origin',
|
referrerPolicy: 'origin',
|
||||||
@@ -136,20 +191,18 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (results.data) {
|
if (results.data) {
|
||||||
results = results.data;
|
results.data.forEach(chap => {
|
||||||
results.forEach(function (chap: any) {
|
|
||||||
if (chap.url == null) {
|
if (chap.url == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const name = chap.title;
|
const name = chap.title;
|
||||||
const url = chap.url as string;
|
const url = chap.url as string;
|
||||||
const releaseTime = chap.date;
|
|
||||||
const chapterNumber = chap.slug;
|
const chapterNumber = chap.slug;
|
||||||
|
|
||||||
chapters.push({
|
chapters.push({
|
||||||
name: name,
|
name: name,
|
||||||
path: url.replace(baseUrl, ''),
|
path: url.replace(this.site, ''),
|
||||||
releaseTime,
|
releaseTime: this.parseAgoDate(chap.date),
|
||||||
chapterNumber: parseInt(chapterNumber),
|
chapterNumber: parseInt(chapterNumber),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -166,7 +219,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
|||||||
const $ = await this.getCheerio(this.site + chapterPath, false);
|
const $ = await this.getCheerio(this.site + chapterPath, false);
|
||||||
this.nonce = $('div.mb-4:has(h1.text-xl) > div')
|
this.nonce = $('div.mb-4:has(h1.text-xl) > div')
|
||||||
.attr('x-data')
|
.attr('x-data')
|
||||||
?.match(/loadChapter\('.+?', '(.+?)'\)/)[1];
|
?.match(/loadChapter\('.+?', '(.+?)'\)/)![1];
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
@@ -200,7 +253,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let html = text
|
const html = text
|
||||||
.replace(/cls[a-f0-9]+/g, '')
|
.replace(/cls[a-f0-9]+/g, '')
|
||||||
.split('')
|
.split('')
|
||||||
.map(char => {
|
.map(char => {
|
||||||
@@ -212,7 +265,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
|||||||
: char;
|
: char;
|
||||||
})
|
})
|
||||||
.join('');
|
.join('');
|
||||||
let $ = load(html);
|
const $ = load(html);
|
||||||
|
|
||||||
$('span').text((_, txt) =>
|
$('span').text((_, txt) =>
|
||||||
txt.toLowerCase().includes('storyseedling') ||
|
txt.toLowerCase().includes('storyseedling') ||
|
||||||
@@ -228,37 +281,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase {
|
|||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const novels: Plugin.NovelItem[] = [];
|
return this.getNovels(pageNo, searchTerm);
|
||||||
|
|
||||||
const body = await fetchApi(this.site + 'browse').then(r => r.text());
|
|
||||||
const loadedCheerio = load(body);
|
|
||||||
|
|
||||||
const postValue = loadedCheerio('div[ax-load][x-data]')
|
|
||||||
.attr('x-data')
|
|
||||||
?.replace("browse('", '')
|
|
||||||
.replace("')", '') as string;
|
|
||||||
|
|
||||||
const data = new FormData();
|
|
||||||
data.append('search', searchTerm);
|
|
||||||
data.append('orderBy', 'recent');
|
|
||||||
data.append('curpage', pageNo.toString());
|
|
||||||
data.append('post', postValue);
|
|
||||||
data.append('action', 'fetch_browse');
|
|
||||||
|
|
||||||
const response: any = await fetchApi(this.site + 'ajax', {
|
|
||||||
body: data,
|
|
||||||
method: 'POST',
|
|
||||||
}).then(res => res.json());
|
|
||||||
|
|
||||||
response.data.posts.forEach((element: any) =>
|
|
||||||
novels.push({
|
|
||||||
name: element.title,
|
|
||||||
cover: element.thumbnail,
|
|
||||||
path: element.permalink.replace(this.site, ''),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return novels;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+74
-53
@@ -7,7 +7,7 @@ import { defaultCover } from '@libs/defaultCover';
|
|||||||
class ArchiveOfOurOwn implements Plugin.PluginBase {
|
class ArchiveOfOurOwn implements Plugin.PluginBase {
|
||||||
id = 'archiveofourown';
|
id = 'archiveofourown';
|
||||||
name = 'Archive Of Our Own';
|
name = 'Archive Of Our Own';
|
||||||
version = '1.0.3';
|
version = '1.0.4';
|
||||||
icon = 'src/en/ao3/icon.png';
|
icon = 'src/en/ao3/icon.png';
|
||||||
site = 'https://archiveofourown.org/';
|
site = 'https://archiveofourown.org/';
|
||||||
|
|
||||||
@@ -42,57 +42,73 @@ class ArchiveOfOurOwn implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
page: number,
|
page: number,
|
||||||
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions<Filters>,
|
{
|
||||||
|
showLatestNovels,
|
||||||
|
filters,
|
||||||
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
// Base URL and common parameters
|
const params = new URLSearchParams({
|
||||||
let link = `${this.site}works/search?page=${page}&work_search%5Blanguage_id%5D=en`;
|
commit: 'Search',
|
||||||
|
page: page.toString(),
|
||||||
|
'work_search[language_id]': filters.language.value,
|
||||||
|
});
|
||||||
|
|
||||||
// Apply sorting based on showLatestNovels
|
|
||||||
if (showLatestNovels) {
|
if (showLatestNovels) {
|
||||||
link += `&work_search%5Bsort_column%5D=revised_at&work_search%5Bsort_direction%5D=${filters.sortdir.value}`;
|
params.set('work_search[sort_column]', 'revised_at');
|
||||||
} else if (filters) {
|
} else {
|
||||||
link += `&work_search%5Bsort_column%5D=${filters.sort.value}&work_search%5Bsort_direction%5D=${filters.sortdir.value}`;
|
params.set('work_search[sort_column]', filters.sort.value);
|
||||||
}
|
}
|
||||||
|
params.set('work_search[sort_direction]', filters.sortdir.value);
|
||||||
// Apply additional filters
|
|
||||||
if (filters) {
|
// we could send in the entire thing without checking for blanks
|
||||||
// if (filters.genre.value !== '') link += `&work_search%5Bfandom_names%5D=${filters.genre.value}`;
|
if (filters.completion.value !== '') {
|
||||||
if (filters.completion.value !== '')
|
params.set('work_search[complete]', filters.completion.value);
|
||||||
link += `&work_search%5Bcomplete%5D=${filters.completion.value}`;
|
}
|
||||||
if (filters.crossover.value !== '')
|
if (filters.crossover.value !== '') {
|
||||||
link += `&work_search%5Bcrossover%5D=${filters.crossover.value}`;
|
params.set('work_search[crossover]', filters.crossover.value);
|
||||||
if (filters.categories.value.length > 0) {
|
}
|
||||||
filters.categories.value.forEach((category: string) => {
|
if (filters.categories.value.length > 0) {
|
||||||
link += `&work_search%5Bcategory_ids%5D%5B%5D=${category}`;
|
filters.categories.value.forEach((category: string) => {
|
||||||
});
|
params.append('work_search[category_ids][]', category);
|
||||||
}
|
});
|
||||||
if (filters.warningsFilter.value.length > 0) {
|
}
|
||||||
filters.warningsFilter.value.forEach((warning: string) => {
|
if (filters.warningsFilter.value.length > 0) {
|
||||||
link += `&work_search%5Barchive_warning_ids%5D%5B%5D=${warning}`;
|
filters.warningsFilter.value.forEach((warning: string) => {
|
||||||
});
|
params.append('work_search[archive_warning_ids][]', warning);
|
||||||
}
|
});
|
||||||
if (filters.singlechap.value !== '')
|
}
|
||||||
link += `&work_search%5Bsingle_chapter%5D=${filters.singlechap.value}`;
|
if (filters.singlechap.value) {
|
||||||
if (filters.author.value !== '')
|
params.set('work_search[single_chapter]', '1');
|
||||||
link += `&work_search%5Bcreators%5D=${filters.author.value}`;
|
}
|
||||||
if (
|
if (filters.author.value !== '') {
|
||||||
filters.dateFilter.value !== '' &&
|
params.set('work_search[creators]', filters.author.value);
|
||||||
filters.dateIncrements.value !== ''
|
}
|
||||||
) {
|
if (
|
||||||
link += `&work_search%5Brevised_at%5D=${filters.dateFilter.value}+${filters.dateIncrements.value}`;
|
filters.dateFilter.value !== '' &&
|
||||||
}
|
filters.dateIncrements.value !== ''
|
||||||
if (filters.words.value !== '')
|
) {
|
||||||
link += `&work_search%5Bword_count%5D=${filters.words.value}`;
|
params.set(
|
||||||
if (filters.hits.value !== '')
|
'work_search[revised_at]',
|
||||||
link += `&work_search%5Bhits%5D=${filters.hits.value}`;
|
`${filters.dateFilter.value} ${filters.dateIncrements.value}`,
|
||||||
if (filters.bookmarks.value !== '')
|
);
|
||||||
link += `&work_search%5Bbookmarks_count%5D=${filters.bookmarks.value}`;
|
}
|
||||||
if (filters.comments.value !== '')
|
if (filters.words.value !== '') {
|
||||||
link += `&work_search%5Bcomments_count%5D=${filters.comments.value}`;
|
params.set('work_search[word_count]', filters.words.value);
|
||||||
if (filters.kudos.value !== '')
|
}
|
||||||
link += `&work_search%5Bkudos_count%5D=${filters.kudos.value}`;
|
if (filters.hits.value !== '') {
|
||||||
|
params.set('work_search[hits]', filters.hits.value);
|
||||||
|
}
|
||||||
|
if (filters.bookmarks.value !== '') {
|
||||||
|
params.set('work_search[bookmarks_count]', filters.bookmarks.value);
|
||||||
|
}
|
||||||
|
if (filters.comments.value !== '') {
|
||||||
|
params.set('work_search[comments_count]', filters.comments.value);
|
||||||
|
}
|
||||||
|
if (filters.kudos.value !== '') {
|
||||||
|
params.set('work_search[kudos_count]', filters.kudos.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const link = `${this.site}works/search?${params.toString()}`;
|
||||||
const body = await fetchApi(link).then(r => r.text());
|
const body = await fetchApi(link).then(r => r.text());
|
||||||
const loadedCheerio = parseHTML(body);
|
const loadedCheerio = parseHTML(body);
|
||||||
return this.parseNovels(loadedCheerio);
|
return this.parseNovels(loadedCheerio);
|
||||||
@@ -149,12 +165,12 @@ class ArchiveOfOurOwn implements Plugin.PluginBase {
|
|||||||
novel.summary = `Fandom:\n${fandom}\n\nRating:\n${rating}\n\nWarning:\n${warning}\n\nSummary:\n${summary}\n\nSeries:\n${series}\n\nRelationships:\n${relation}\n\nCharacters:\n${character}\n\nStats:\n${stats}`;
|
novel.summary = `Fandom:\n${fandom}\n\nRating:\n${rating}\n\nWarning:\n${warning}\n\nSummary:\n${summary}\n\nSeries:\n${series}\n\nRelationships:\n${relation}\n\nCharacters:\n${character}\n\nStats:\n${stats}`;
|
||||||
const chapterItems: Plugin.ChapterItem[] = [];
|
const chapterItems: Plugin.ChapterItem[] = [];
|
||||||
const longReleaseDate: string[] = [];
|
const longReleaseDate: string[] = [];
|
||||||
let match: RegExpExecArray | null;
|
// let match: RegExpExecArray | null;
|
||||||
chapterlistload('ol.index').each((i, ele) => {
|
chapterlistload('ol.index').each((i, ele) => {
|
||||||
chapterlistload(ele)
|
chapterlistload(ele)
|
||||||
.find('li')
|
.find('li')
|
||||||
.each((i, el) => {
|
.each((i, el) => {
|
||||||
const chapterNameMatch = chapterlistload(el).find('a').text().trim();
|
// const chapterNameMatch = chapterlistload(el).find('a').text().trim();
|
||||||
const releaseTimeText = chapterlistload(el)
|
const releaseTimeText = chapterlistload(el)
|
||||||
.find('span.datetime')
|
.find('span.datetime')
|
||||||
.text()
|
.text()
|
||||||
@@ -282,7 +298,13 @@ class ArchiveOfOurOwn implements Plugin.PluginBase {
|
|||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
page: number,
|
page: number,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const searchUrl = `${this.site}works/search?page=${page}&work_search%5Blanguage_id%5D=en&work_search%5Bquery%5D=${encodeURIComponent(searchTerm)}`;
|
const params = new URLSearchParams({
|
||||||
|
commit: 'Search',
|
||||||
|
page: page.toString(),
|
||||||
|
'work_search[language_id]': 'en',
|
||||||
|
'work_search[query]': searchTerm,
|
||||||
|
});
|
||||||
|
const searchUrl = `${this.site}works/search?${params.toString()}`;
|
||||||
|
|
||||||
const result = await fetchApi(searchUrl);
|
const result = await fetchApi(searchUrl);
|
||||||
const body = await result.text();
|
const body = await result.text();
|
||||||
@@ -521,10 +543,9 @@ class ArchiveOfOurOwn implements Plugin.PluginBase {
|
|||||||
type: FilterTypes.CheckboxGroup,
|
type: FilterTypes.CheckboxGroup,
|
||||||
},
|
},
|
||||||
singlechap: {
|
singlechap: {
|
||||||
value: '',
|
value: false,
|
||||||
label: 'Single Chapter Stories',
|
label: 'Single Chapter Stories',
|
||||||
options: [{ label: 'Single Chapter', value: '1' }],
|
type: FilterTypes.Switch,
|
||||||
type: FilterTypes.Picker,
|
|
||||||
},
|
},
|
||||||
author: {
|
author: {
|
||||||
value: '',
|
value: '',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class Chrysanthemumgarden implements Plugin.PluginBase {
|
|||||||
name = 'Chrysanthemum Garden';
|
name = 'Chrysanthemum Garden';
|
||||||
icon = 'src/en/chrysanthemumgarden/icon.png';
|
icon = 'src/en/chrysanthemumgarden/icon.png';
|
||||||
site = 'https://chrysanthemumgarden.com';
|
site = 'https://chrysanthemumgarden.com';
|
||||||
version = '1.0.1';
|
version = '1.0.3';
|
||||||
filters: Filters | undefined = undefined;
|
filters: Filters | undefined = undefined;
|
||||||
imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined;
|
imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined;
|
||||||
|
|
||||||
@@ -18,10 +18,10 @@ class Chrysanthemumgarden implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
{
|
// {
|
||||||
showLatestNovels,
|
// showLatestNovels,
|
||||||
filters,
|
// filters,
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
// }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const req = await fetchApi(
|
const req = await fetchApi(
|
||||||
this.site + (pageNo === 1 ? '/books' : '/books/page/' + pageNo) + '/',
|
this.site + (pageNo === 1 ? '/books' : '/books/page/' + pageNo) + '/',
|
||||||
@@ -136,7 +136,9 @@ class Chrysanthemumgarden implements Plugin.PluginBase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllNovels() {}
|
async getAllNovels() {
|
||||||
|
// linter? what is the purpose of this
|
||||||
|
}
|
||||||
|
|
||||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||||
this.site + (isNovel ? '/book/' : '/chapter/') + path;
|
this.site + (isNovel ? '/book/' : '/chapter/') + path;
|
||||||
|
|||||||
@@ -1,26 +1,42 @@
|
|||||||
import { CheerioAPI, load as parseHTML } from 'cheerio';
|
import { CheerioAPI, load as parseHTML } from 'cheerio';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { FilterTypes, Filters } from '@libs/filterInputs';
|
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { storage } from '@libs/storage';
|
import { storage } from '@libs/storage';
|
||||||
import dayjs from 'dayjs';
|
import { defaultCover } from '@libs/defaultCover';
|
||||||
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
|
|
||||||
enum APIAction {
|
enum APIAction {
|
||||||
novels = 'load_novels',
|
novels = 'load_novels',
|
||||||
search = 'live_novel_search',
|
search = 'live_novel_search',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface APIParams {
|
type APIParams = {
|
||||||
action: APIAction;
|
action: APIAction;
|
||||||
params: Record<string, string | number>;
|
params: Record<string, string | number>;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
type ChapterJSON = {
|
||||||
|
items: ChapterItem[];
|
||||||
|
total: number;
|
||||||
|
total_pages?: number;
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
order?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChapterItem = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
locked: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
class CrimsonScrollsPlugin implements Plugin.PluginBase {
|
class CrimsonScrollsPlugin implements Plugin.PluginBase {
|
||||||
id = 'crimsonscrolls';
|
id = 'crimsonscrolls';
|
||||||
name = 'Crimson Scrolls';
|
name = 'Crimson Scrolls';
|
||||||
icon = 'src/en/crimsonscrolls/icon.png';
|
icon = 'src/en/crimsonscrolls/icon.png';
|
||||||
site = 'https://crimsonscrolls.net';
|
site = 'https://crimsonscrolls.net';
|
||||||
version = '1.0.0';
|
version = '1.0.1';
|
||||||
|
|
||||||
hideLocked = storage.get('hideLocked');
|
hideLocked = storage.get('hideLocked');
|
||||||
pluginSettings = {
|
pluginSettings = {
|
||||||
@@ -35,7 +51,7 @@ class CrimsonScrollsPlugin implements Plugin.PluginBase {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('action', query.action);
|
formData.append('action', query.action);
|
||||||
for (const [key, value] of Object.entries(query.params))
|
for (const [key, value] of Object.entries(query.params))
|
||||||
formData.append(key, value);
|
formData.append(key, value.toString());
|
||||||
|
|
||||||
const result = await fetchApi(`${this.site}/wp-admin/admin-ajax.php`, {
|
const result = await fetchApi(`${this.site}/wp-admin/admin-ajax.php`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -45,17 +61,28 @@ class CrimsonScrollsPlugin implements Plugin.PluginBase {
|
|||||||
return parseHTML(result.html);
|
return parseHTML(result.html);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchChapters(id: number, page?: number | undefined) {
|
async fetchChapters(
|
||||||
const chapter: any[] = [];
|
id: number,
|
||||||
const url = `${this.site}/wp-json/cs/v1/novels/${id}/chapters?per_page=75&order=asc`; //page=${page}
|
page?: number | undefined,
|
||||||
const data = await fetchApi(`${url}&page=${page || 1}`).then(r => r.json());
|
): Promise<ChapterItem[]> {
|
||||||
const locked = data.items.some(e => e.locked);
|
const url = `${this.site}/wp-json/cs/v1/novels/${id}/chapters?per_page=75&order=asc`;
|
||||||
|
const data: ChapterJSON = await fetchApi(`${url}&page=${page ?? 1}`).then(
|
||||||
|
r => r.json(),
|
||||||
|
);
|
||||||
|
|
||||||
if (data.page < data.total_pages && !(locked && this.hideLocked))
|
const items = data.items || [];
|
||||||
return data.items.concat(
|
const locked = items.some(e => e.locked);
|
||||||
await this.fetchChapters(id, parseInt(data.page) + 1),
|
|
||||||
);
|
if (
|
||||||
else return data.items;
|
data.total_pages &&
|
||||||
|
(data.page ?? 1) < data.total_pages &&
|
||||||
|
!(locked && this.hideLocked)
|
||||||
|
) {
|
||||||
|
const nextItems = await this.fetchChapters(id, (data.page ?? 0) + 1);
|
||||||
|
return items.concat(nextItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseNovels(loadedCheerio: CheerioAPI) {
|
parseNovels(loadedCheerio: CheerioAPI) {
|
||||||
@@ -83,7 +110,9 @@ class CrimsonScrollsPlugin implements Plugin.PluginBase {
|
|||||||
.filter(e => e.length > 0)
|
.filter(e => e.length > 0)
|
||||||
.join(' '),
|
.join(' '),
|
||||||
cover: novelCover,
|
cover: novelCover,
|
||||||
path: novelUrl.replace(this.site, '').split('/').at(2),
|
path: novelUrl
|
||||||
|
? new URL(novelUrl, this.site).pathname.substring(1)
|
||||||
|
: defaultCover,
|
||||||
};
|
};
|
||||||
novels.push(novel);
|
novels.push(novel);
|
||||||
},
|
},
|
||||||
@@ -94,59 +123,61 @@ class CrimsonScrollsPlugin implements Plugin.PluginBase {
|
|||||||
async popularNovels(page: number): Promise<Plugin.NovelItem[]> {
|
async popularNovels(page: number): Promise<Plugin.NovelItem[]> {
|
||||||
const loadedCheerio = await this.queryAPI({
|
const loadedCheerio = await this.queryAPI({
|
||||||
action: APIAction.novels,
|
action: APIAction.novels,
|
||||||
params: { page: page as string },
|
params: { page: page.toString() },
|
||||||
});
|
});
|
||||||
return this.parseNovels(loadedCheerio);
|
return this.parseNovels(loadedCheerio);
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
const result = await fetchApi(`${this.site}/novel/${novelPath}`).then(r =>
|
const result = await fetchApi(`${this.site}/${novelPath}`).then(r =>
|
||||||
r.text(),
|
r.text(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let loadedCheerio = parseHTML(result);
|
const loadedCheerio = parseHTML(result);
|
||||||
let novelInfo = loadedCheerio('#single-novel-content-wrapper');
|
const novelInfo = loadedCheerio('#single-novel-content-wrapper');
|
||||||
|
|
||||||
const novel: Plugin.SourceNovel = {
|
const novel: Plugin.SourceNovel = {
|
||||||
path: novelPath,
|
path: novelPath,
|
||||||
name: novelInfo.find('h1.chapter-title').text().trim() || 'Untitled',
|
name: novelInfo.find('h1').text().trim() ?? 'Untitled',
|
||||||
cover: novelInfo.find('.single-novel-cover > img').data('src'),
|
cover:
|
||||||
|
novelInfo.find('img:first').data('src')?.toString() ?? defaultCover,
|
||||||
summary: novelInfo.find('#synopsis-full').text().trim(),
|
summary: novelInfo.find('#synopsis-full').text().trim(),
|
||||||
author: novelInfo
|
author: novelInfo.find('strong:first').next().text().trim(),
|
||||||
.find('.single-novel-meta strong')
|
|
||||||
.filter(
|
|
||||||
(i, el) =>
|
|
||||||
loadedCheerio(el).text().toLowerCase().search('author') >= 0,
|
|
||||||
)[0]
|
|
||||||
.next.data.trim(),
|
|
||||||
chapters: [],
|
chapters: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
novel.genres = novelInfo
|
novel.genres = novelInfo
|
||||||
.find('.single-novel-meta strong')
|
.find('.cs-genre-chip')
|
||||||
.filter(
|
.map((_, el) => loadedCheerio(el).text().trim())
|
||||||
(i, el) => loadedCheerio(el).text().toLowerCase().search('genre') >= 0,
|
.toArray()
|
||||||
)[0]
|
|
||||||
.next.data.split(',')
|
|
||||||
.map(e => e.trim())
|
|
||||||
.join(',');
|
.join(',');
|
||||||
|
|
||||||
novel.status = 'Unknown';
|
const rawStatus = novelInfo.find('.cs-nsb-badge').text().trim();
|
||||||
const id = loadedCheerio('#chapter-list').data('novel');
|
const map: Record<string, string> = {
|
||||||
const chapters = await this.fetchChapters(id);
|
ongoing: NovelStatus.Ongoing,
|
||||||
|
hiatus: NovelStatus.OnHiatus,
|
||||||
|
dropped: NovelStatus.Cancelled,
|
||||||
|
cancelled: NovelStatus.Cancelled,
|
||||||
|
completed: NovelStatus.Completed,
|
||||||
|
};
|
||||||
|
novel.status = map[rawStatus.toLowerCase()] ?? NovelStatus.Unknown;
|
||||||
|
|
||||||
novel.chapters = [];
|
const id = loadedCheerio('#chapter-list').data('novel');
|
||||||
for (const idx in chapters) {
|
const chapters = await this.fetchChapters(Number(id));
|
||||||
if (!(chapters[idx].locked && this.hideLocked)) {
|
|
||||||
novel.chapters.push({
|
const novelChapters: Plugin.ChapterItem[] = [];
|
||||||
name: chapters[idx].locked
|
chapters.forEach((chapter, index) => {
|
||||||
? `🔒 ${chapters[idx].title}`
|
if (!(chapter.locked && this.hideLocked)) {
|
||||||
: chapters[idx].title,
|
novelChapters.push({
|
||||||
path: chapters[idx].url.replace(this.site, '').split('/').at(2),
|
name: chapter.locked ? `🔒 ${chapter.title}` : chapter.title,
|
||||||
chapterNumber: parseInt(idx) + 1,
|
path: chapter.url
|
||||||
|
? new URL(chapter.url, this.site).pathname.split('/')[2]
|
||||||
|
: '',
|
||||||
|
chapterNumber: index + 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
novel.chapters = novelChapters;
|
||||||
|
|
||||||
return novel;
|
return novel;
|
||||||
}
|
}
|
||||||
@@ -176,8 +207,9 @@ class CrimsonScrollsPlugin implements Plugin.PluginBase {
|
|||||||
return this.parseNovels(loadedCheerio);
|
return this.parseNovels(loadedCheerio);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
// not sure purpose of this, commented out
|
||||||
this.site + '/novel/' + path;
|
// resolveUrl = (path: string, isNovel?: boolean) =>
|
||||||
|
// this.site + '/novel/' + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new CrimsonScrollsPlugin();
|
export default new CrimsonScrollsPlugin();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Filters, FilterTypes, FilterValueWithType } from '@libs/filterInputs';
|
|||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { load as parseHTML } from 'cheerio';
|
import { load as parseHTML } from 'cheerio';
|
||||||
|
|
||||||
//TODO: this looks similar to fictioneer source? maybe use multisrc someday
|
// TODO: change layoput, layout has updated
|
||||||
|
|
||||||
class DDLPlugin implements Plugin.PluginBase {
|
class DDLPlugin implements Plugin.PluginBase {
|
||||||
id = 'DDL.com';
|
id = 'DDL.com';
|
||||||
@@ -291,10 +291,7 @@ class DDLPlugin implements Plugin.PluginBase {
|
|||||||
name.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()),
|
name.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()),
|
||||||
)
|
)
|
||||||
.map(([, , path]) => path);
|
.map(([, , path]) => path);
|
||||||
return await this.asyncMap(
|
return await this.asyncMap(foundNovels, this.grabCachedNovel.bind(this));
|
||||||
await foundNovels,
|
|
||||||
this.grabCachedNovel.bind(this),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class EarlyNovelPlugin implements Plugin.PagePlugin {
|
|||||||
|
|
||||||
link += `?page=${pageNo}`;
|
link += `?page=${pageNo}`;
|
||||||
|
|
||||||
const body = await fetchApi(link).then(res => res.text());
|
const body = await fetchApi(link).then((res: Response) => res.text());
|
||||||
|
|
||||||
const loadedCheerio = parseHTML(body);
|
const loadedCheerio = parseHTML(body);
|
||||||
return this.parseNovels(loadedCheerio);
|
return this.parseNovels(loadedCheerio);
|
||||||
@@ -110,7 +110,7 @@ class EarlyNovelPlugin implements Plugin.PagePlugin {
|
|||||||
|
|
||||||
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
||||||
const url = this.site + novelPath + '?page=' + page;
|
const url = this.site + novelPath + '?page=' + page;
|
||||||
const body = await fetchApi(url).then(res => res.text());
|
const body = await fetchApi(url).then((res: Response) => res.text());
|
||||||
const loadedCheerio = parseHTML(body);
|
const loadedCheerio = parseHTML(body);
|
||||||
const chapters = this.parseChapters(loadedCheerio);
|
const chapters = this.parseChapters(loadedCheerio);
|
||||||
return { chapters };
|
return { chapters };
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { Node } from 'domhandler';
|
|
||||||
import { load as loadCheerio } from 'cheerio';
|
import { load as loadCheerio } from 'cheerio';
|
||||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||||
import { storage } from '@libs/storage';
|
import { storage } from '@libs/storage';
|
||||||
@@ -13,6 +12,10 @@ type APINovel = {
|
|||||||
description: string;
|
description: string;
|
||||||
status: string;
|
status: string;
|
||||||
genres: { name: string }[];
|
genres: { name: string }[];
|
||||||
|
user?: {
|
||||||
|
username?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type APIChapter = {
|
type APIChapter = {
|
||||||
@@ -35,12 +38,44 @@ type ChapterInfo = {
|
|||||||
chapterNumber: number;
|
chapterNumber: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Chapter = {
|
||||||
|
type: string;
|
||||||
|
content: {
|
||||||
|
type: string;
|
||||||
|
attrs?: Attrs;
|
||||||
|
content: {
|
||||||
|
type: string;
|
||||||
|
text?: string;
|
||||||
|
marks?: {
|
||||||
|
type: string;
|
||||||
|
attrs?: Attrs;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Attrs = {
|
||||||
|
textAlign?: string;
|
||||||
|
href?: string;
|
||||||
|
level?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Nodes = {
|
||||||
|
type: string;
|
||||||
|
nodes?: {
|
||||||
|
type: string;
|
||||||
|
// couldnt find the sveltekit so disable eslint here
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
data: any[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
class FenrirRealmPlugin implements Plugin.PluginBase {
|
class FenrirRealmPlugin implements Plugin.PluginBase {
|
||||||
id = 'fenrir';
|
id = 'fenrir';
|
||||||
name = 'Fenrir Realm';
|
name = 'Fenrir Realm';
|
||||||
icon = 'src/en/fenrirrealm/icon.png';
|
icon = 'src/en/fenrirrealm/icon.png';
|
||||||
site = 'https://fenrirealm.com';
|
site = 'https://fenrirealm.com';
|
||||||
version = '1.0.13';
|
version = '1.1.0';
|
||||||
imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined;
|
imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined;
|
||||||
|
|
||||||
hideLocked = storage.get('hideLocked');
|
hideLocked = storage.get('hideLocked');
|
||||||
@@ -62,14 +97,16 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
|||||||
filters,
|
filters,
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
// let sort = "updated";
|
const params = new URLSearchParams({
|
||||||
let sort = filters.sort.value;
|
page: pageNo.toString(),
|
||||||
if (showLatestNovels) sort = 'latest';
|
per_page: '20',
|
||||||
const genresFilter = filters.genres.value
|
status: filters.status.value,
|
||||||
.map(g => '&genres%5B%5D=' + g)
|
order: showLatestNovels ? 'latest' : filters.sort.value,
|
||||||
.join('');
|
});
|
||||||
|
filters.genres.value.forEach(g => params.append('genres[]', g));
|
||||||
|
|
||||||
const res = await fetchApi(
|
const res = await fetchApi(
|
||||||
`${this.site}/api/series/filter?page=${pageNo}&per_page=20&status=${filters.status.value}&order=${sort}${genresFilter}`,
|
`${this.site}/api/series/filter?${params.toString()}`,
|
||||||
).then(r =>
|
).then(r =>
|
||||||
r.json().catch(() => {
|
r.json().catch(() => {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -90,7 +127,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
if (!apiRes.ok) {
|
if (!apiRes.ok) {
|
||||||
const slugMatch = novelPath.match(/^\d+-(.+)$/);
|
const slugMatch = novelPath.match(/^\d+-(.+)$/);
|
||||||
let searchSlug = slugMatch ? slugMatch[1] : novelPath;
|
const searchSlug = slugMatch ? slugMatch[1] : novelPath;
|
||||||
apiRes = await fetchApi(
|
apiRes = await fetchApi(
|
||||||
`${this.site}/api/new/v2/series/${searchSlug}/chapters`,
|
`${this.site}/api/new/v2/series/${searchSlug}/chapters`,
|
||||||
{},
|
{},
|
||||||
@@ -120,7 +157,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const seriesData = await fetchApi(
|
const seriesData: APINovel = await fetchApi(
|
||||||
`${this.site}/api/new/v2/series/${cleanNovelPath}`,
|
`${this.site}/api/new/v2/series/${cleanNovelPath}`,
|
||||||
).then(r => r.json());
|
).then(r => r.json());
|
||||||
const summaryCheerio = loadCheerio(seriesData.description || '');
|
const summaryCheerio = loadCheerio(seriesData.description || '');
|
||||||
@@ -131,7 +168,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
|||||||
summary:
|
summary:
|
||||||
summaryCheerio('p').length > 0
|
summaryCheerio('p').length > 0
|
||||||
? summaryCheerio('p')
|
? summaryCheerio('p')
|
||||||
.map((i, el) => loadCheerio(el).text())
|
.map((_, el) => loadCheerio(el).text())
|
||||||
.get()
|
.get()
|
||||||
.join('\n\n')
|
.join('\n\n')
|
||||||
: summaryCheerio.text() || '',
|
: summaryCheerio.text() || '',
|
||||||
@@ -139,7 +176,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
|||||||
cover: seriesData.cover
|
cover: seriesData.cover
|
||||||
? this.site + '/' + seriesData.cover
|
? this.site + '/' + seriesData.cover
|
||||||
: defaultCover,
|
: defaultCover,
|
||||||
genres: (seriesData.genres || []).map((g: any) => g.name).join(','),
|
genres: (seriesData.genres || []).map(g => g.name).join(','),
|
||||||
status: seriesData.status || 'Unknown',
|
status: seriesData.status || 'Unknown',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -184,14 +221,14 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
|||||||
const content = json.content;
|
const content = json.content;
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
const parsedContent = JSON.parse(content);
|
const parsedContent: Chapter = JSON.parse(content);
|
||||||
if (parsedContent.type === 'doc') {
|
if (parsedContent.type === 'doc') {
|
||||||
return parsedContent.content
|
return parsedContent.content
|
||||||
.map((node: any) => {
|
.map(node => {
|
||||||
if (node.type === 'paragraph') {
|
if (node.type === 'paragraph') {
|
||||||
const innerHtml =
|
const innerHtml =
|
||||||
node.content
|
node.content
|
||||||
?.map((c: any) => {
|
?.map(c => {
|
||||||
if (c.type === 'text') {
|
if (c.type === 'text') {
|
||||||
let text = c.text;
|
let text = c.text;
|
||||||
if (c.marks) {
|
if (c.marks) {
|
||||||
@@ -216,8 +253,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
if (node.type === 'heading') {
|
if (node.type === 'heading') {
|
||||||
const level = node.attrs?.level || 1;
|
const level = node.attrs?.level || 1;
|
||||||
const innerHtml =
|
const innerHtml = node.content?.map(c => c.text).join('') || '';
|
||||||
node.content?.map((c: any) => c.text).join('') || '';
|
|
||||||
return `<h${level}>${innerHtml}</h${level}>`;
|
return `<h${level}>${innerHtml}</h${level}>`;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
@@ -235,7 +271,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
|||||||
const loadedCheerio = loadCheerio(body);
|
const loadedCheerio = loadCheerio(body);
|
||||||
|
|
||||||
let chapterText = loadedCheerio('div.content-area p')
|
let chapterText = loadedCheerio('div.content-area p')
|
||||||
.map((i, el) => `<p>${loadCheerio(el).html()}</p>`)
|
.map((_, el) => `<p>${loadCheerio(el).html()}</p>`)
|
||||||
.get()
|
.get()
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
@@ -247,24 +283,24 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
|||||||
try {
|
try {
|
||||||
const jsonUrl = `${this.site}/series/${chapterPath.split('~~')[0]}/__data.json?x-sveltekit-invalidated=001`;
|
const jsonUrl = `${this.site}/series/${chapterPath.split('~~')[0]}/__data.json?x-sveltekit-invalidated=001`;
|
||||||
const jsonRes = await fetchApi(jsonUrl);
|
const jsonRes = await fetchApi(jsonUrl);
|
||||||
const json = await jsonRes.json();
|
const json: Nodes = await jsonRes.json();
|
||||||
|
|
||||||
const nodes = json.nodes;
|
const nodes = json.nodes;
|
||||||
const data = nodes?.find((n: any) => n.type === 'data')?.data;
|
const data = nodes?.find(n => n.type === 'data')?.data;
|
||||||
if (data) {
|
if (data) {
|
||||||
const contentStr = data.find(
|
const contentStr = data.find(
|
||||||
(d: any) => typeof d === 'string' && d.includes('{"type":"doc"'),
|
d => typeof d === 'string' && d.includes('{"type":"doc"'),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (contentStr) {
|
if (contentStr) {
|
||||||
const contentJson = JSON.parse(contentStr);
|
const contentJson: Chapter = JSON.parse(contentStr);
|
||||||
if (contentJson.type === 'doc') {
|
if (contentJson.type === 'doc') {
|
||||||
chapterText = contentJson.content
|
chapterText = contentJson.content
|
||||||
.map((node: any) => {
|
.map(node => {
|
||||||
if (node.type === 'paragraph') {
|
if (node.type === 'paragraph') {
|
||||||
const innerHtml =
|
const innerHtml =
|
||||||
node.content
|
node.content
|
||||||
?.map((c: any) => {
|
?.map(c => {
|
||||||
if (c.type === 'text') {
|
if (c.type === 'text') {
|
||||||
let text = c.text;
|
let text = c.text;
|
||||||
if (c.marks) {
|
if (c.marks) {
|
||||||
@@ -330,8 +366,8 @@ class FenrirRealmPlugin implements Plugin.PluginBase {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
// resolveUrl = (path: string, isNovel?: boolean) =>
|
||||||
this.site + '/series/' + path.split('~~')[0];
|
// this.site + '/series/' + path.split('~~')[0];
|
||||||
|
|
||||||
filters = {
|
filters = {
|
||||||
status: {
|
status: {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
|||||||
pageNo: number,
|
pageNo: number,
|
||||||
{
|
{
|
||||||
showLatestNovels,
|
showLatestNovels,
|
||||||
filters,
|
// filters,
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
return await this.getPage(
|
return await this.getPage(
|
||||||
@@ -23,7 +23,8 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getData(url: string) {
|
// TODO: Fix URLs, current URL leads to 404
|
||||||
|
async getData<T>(url: string): Promise<Response<T>> {
|
||||||
return await fetchApi(this.site + '/api/__api_party/fictionzone', {
|
return await fetchApi(this.site + '/api/__api_party/fictionzone', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -41,20 +42,25 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
|||||||
}).then(r => r.json());
|
}).then(r => r.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPage(url: string) {
|
async getPage(url: string): Promise<Plugin.NovelItem[]> {
|
||||||
const data = await this.getData(url);
|
const data = await this.getData<{ novels: NovelItem[] }>(url);
|
||||||
|
|
||||||
return data.data.novels.map((n: any) => ({
|
return data.data.novels.map(n => ({
|
||||||
name: n.title,
|
name: n.title,
|
||||||
cover: `https://cdn.fictionzone.net/insecure/rs:fill:165:250/${n.image}.webp`,
|
cover: `https://cdn.fictionzone.net/insecure/rs:fill:165:250/${n.image}.webp`,
|
||||||
path: `novel/${n.slug}`,
|
path: `novel/${n.slug}`,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChapterPage(id: string, novelPath: string) {
|
async getChapterPage(
|
||||||
const data = await this.getData('/platform/chapter-lists?novel_id=' + id);
|
id: string,
|
||||||
|
novelPath: string,
|
||||||
|
): Promise<Plugin.ChapterItem[]> {
|
||||||
|
const data = await this.getData<{ chapters: ChapterItem[] }>(
|
||||||
|
'/platform/chapter-lists?novel_id=' + id,
|
||||||
|
);
|
||||||
|
|
||||||
return data.data.chapters.map((n: any) => ({
|
return data.data.chapters.map(n => ({
|
||||||
name: n.title,
|
name: n.title,
|
||||||
number: n.chapter_number,
|
number: n.chapter_number,
|
||||||
date: n.published_date
|
date: n.published_date
|
||||||
@@ -66,7 +72,7 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
const novelSlug = novelPath.replace('novel/', '');
|
const novelSlug = novelPath.replace('novel/', '');
|
||||||
const data = await this.getData(
|
const data = await this.getData<NovelDetails>(
|
||||||
`/platform/novel-details?slug=${novelSlug}`,
|
`/platform/novel-details?slug=${novelSlug}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -75,8 +81,8 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
|||||||
name: data.data.title,
|
name: data.data.title,
|
||||||
cover: `https://cdn.fictionzone.net/insecure/rs:fill:165:250/${data.data.image}.webp`,
|
cover: `https://cdn.fictionzone.net/insecure/rs:fill:165:250/${data.data.image}.webp`,
|
||||||
genres: [
|
genres: [
|
||||||
...data.data.genres.map((g: any) => g.name),
|
...data.data.genres.map(g => g.name),
|
||||||
...data.data.tags.map((g: any) => g.name),
|
...data.data.tags.map(g => g.name),
|
||||||
].join(','),
|
].join(','),
|
||||||
status:
|
status:
|
||||||
data.data.status == 1
|
data.data.status == 1
|
||||||
@@ -85,7 +91,7 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
|||||||
? NovelStatus.Completed
|
? NovelStatus.Completed
|
||||||
: NovelStatus.Unknown,
|
: NovelStatus.Unknown,
|
||||||
author:
|
author:
|
||||||
data.data.contributors.filter((c: any) => c.role == 'author')[0]
|
data.data.contributors.filter(c => c.role == 'author')[0]
|
||||||
?.display_name || '',
|
?.display_name || '',
|
||||||
summary: data.data.synopsis,
|
summary: data.data.synopsis,
|
||||||
chapters: await this.getChapterPage(data.data.id, novelPath),
|
chapters: await this.getChapterPage(data.data.id, novelPath),
|
||||||
@@ -93,8 +99,10 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
const data = await this.getData(chapterPath.split('|')[1]);
|
const data = await this.getData<{ content: string }>(
|
||||||
return '<p>' + data.data.content.replaceAll('\n', '</p><p>') + '</p>';
|
chapterPath.split('|')[1],
|
||||||
|
);
|
||||||
|
return '<p>' + data.data.content.split('\n').join('</p><p>') + '</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchNovels(
|
async searchNovels(
|
||||||
@@ -106,8 +114,36 @@ class FictionZonePlugin implements Plugin.PluginBase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
// resolveUrl = (path: string, isNovel?: boolean) =>
|
||||||
this.site + '/' + path.split('|')[0];
|
// this.site + '/' + path.split('|')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new FictionZonePlugin();
|
export default new FictionZonePlugin();
|
||||||
|
|
||||||
|
type Response<T> = {
|
||||||
|
data: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NovelItem = {
|
||||||
|
title: string;
|
||||||
|
image: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChapterItem = {
|
||||||
|
title: string;
|
||||||
|
chapter_number: number;
|
||||||
|
published_date: string;
|
||||||
|
chapter_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NovelDetails = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
image: string;
|
||||||
|
genres: { name: string }[];
|
||||||
|
tags: { name: string }[];
|
||||||
|
status: number;
|
||||||
|
contributors: { role: string; display_name: string }[];
|
||||||
|
synopsis: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ import { Plugin } from '@/types/plugin';
|
|||||||
import { Parser } from 'htmlparser2';
|
import { Parser } from 'htmlparser2';
|
||||||
import { FilterTypes, Filters } from '@libs/filterInputs';
|
import { FilterTypes, Filters } from '@libs/filterInputs';
|
||||||
import { defaultCover } from '@libs/defaultCover';
|
import { defaultCover } from '@libs/defaultCover';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi, FetchInit } from '@libs/fetch';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
|
|
||||||
class Foxteller implements Plugin.PluginBase {
|
class Foxteller implements Plugin.PluginBase {
|
||||||
id = 'foxteller';
|
id = 'foxteller';
|
||||||
name = 'Foxteller';
|
name = 'Foxteller';
|
||||||
site = 'https://www.foxteller.com';
|
site = 'https://www.foxteller.com';
|
||||||
version = '1.0.2';
|
version = '1.0.3';
|
||||||
icon = 'src/en/foxteller/icon.png';
|
icon = 'src/en/foxteller/icon.png';
|
||||||
|
|
||||||
async safeFecth(url: string, init: any = {}): Promise<string> {
|
async safeFecth(url: string, init?: FetchInit): Promise<string> {
|
||||||
const r = await fetchApi(url, init);
|
const r = await fetchApi(url, init);
|
||||||
if (!r.ok)
|
if (!r.ok)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -221,8 +221,8 @@ class Foxteller implements Plugin.PluginBase {
|
|||||||
Accept: 'application/json, text/plain, */*',
|
Accept: 'application/json, text/plain, */*',
|
||||||
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
|
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
|
||||||
'Content-Type': 'application/json;charset=utf-8',
|
'Content-Type': 'application/json;charset=utf-8',
|
||||||
|
Referer: this.resolveUrl(chapterPath),
|
||||||
},
|
},
|
||||||
Referer: this.resolveUrl(chapterPath),
|
|
||||||
body: JSON.stringify({ 'x1': novelID, 'x2': chapterID }),
|
body: JSON.stringify({ 'x1': novelID, 'x2': chapterID }),
|
||||||
}).then(res => res.json());
|
}).then(res => res.json());
|
||||||
|
|
||||||
@@ -259,8 +259,8 @@ class Foxteller implements Plugin.PluginBase {
|
|||||||
Accept: 'application/json, text/plain, */*',
|
Accept: 'application/json, text/plain, */*',
|
||||||
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
|
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
|
||||||
'Content-Type': 'application/json;charset=utf-8',
|
'Content-Type': 'application/json;charset=utf-8',
|
||||||
|
Referer: this.site,
|
||||||
},
|
},
|
||||||
Referer: this.site,
|
|
||||||
body: JSON.stringify({ query: searchTerm }),
|
body: JSON.stringify({ query: searchTerm }),
|
||||||
});
|
});
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|||||||
+138
-74
@@ -1,9 +1,10 @@
|
|||||||
import { CheerioAPI, load } from 'cheerio';
|
import { load } from 'cheerio';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
import { storage } from '@libs/storage';
|
import { storage } from '@libs/storage';
|
||||||
|
import { defaultCover } from '@libs/defaultCover';
|
||||||
|
|
||||||
class Genesis implements Plugin.PluginBase {
|
class Genesis implements Plugin.PluginBase {
|
||||||
id = 'genesistudio';
|
id = 'genesistudio';
|
||||||
@@ -12,7 +13,7 @@ class Genesis implements Plugin.PluginBase {
|
|||||||
customCSS = 'src/en/genesis/customCSS.css';
|
customCSS = 'src/en/genesis/customCSS.css';
|
||||||
site = 'https://genesistudio.com';
|
site = 'https://genesistudio.com';
|
||||||
api = 'https://api.genesistudio.com';
|
api = 'https://api.genesistudio.com';
|
||||||
version = '2.0.0';
|
version = '2.0.1';
|
||||||
|
|
||||||
hideLocked = storage.get('hideLocked');
|
hideLocked = storage.get('hideLocked');
|
||||||
pluginSettings = {
|
pluginSettings = {
|
||||||
@@ -23,14 +24,27 @@ class Genesis implements Plugin.PluginBase {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
imageRequestInit?: Plugin.ImageRequestInit | undefined = {
|
imageRequestInit?: Plugin.ImageRequestInit = {
|
||||||
headers: {
|
headers: {
|
||||||
'referrer': this.site,
|
'referrer': this.site,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async parseNovelJSON(json: any[]): Promise<Plugin.SourceNovel[]> {
|
async parseNovelJSON(): Promise<Plugin.SourceNovel[]> {
|
||||||
return json.map((novel: any) => ({
|
// Thought about caching this,
|
||||||
|
// but not sure what would happen if a new novel were to be
|
||||||
|
// added to the library, so, fetch everytime it is
|
||||||
|
//
|
||||||
|
// fields param literally gives you the JSON you want
|
||||||
|
// maybe TODO: add filtering
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
status: 'published',
|
||||||
|
fields: '["id","novel_title","cover","abbreviation"]',
|
||||||
|
limit: '-1',
|
||||||
|
});
|
||||||
|
const link = `${this.site}/api/directus/novels?${params.toString()}`;
|
||||||
|
const json: NovelJSON[] = await fetchApi(link).then(r => r.json());
|
||||||
|
return json.map(novel => ({
|
||||||
name: novel.novel_title,
|
name: novel.novel_title,
|
||||||
path: `/novels/${novel.abbreviation}`.trim(),
|
path: `/novels/${novel.abbreviation}`.trim(),
|
||||||
cover: `${this.api}/storage/v1/object/public/directus/${novel.cover}.png`,
|
cover: `${this.api}/storage/v1/object/public/directus/${novel.cover}.png`,
|
||||||
@@ -40,11 +54,19 @@ class Genesis implements Plugin.PluginBase {
|
|||||||
async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> {
|
async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||||
// There is only one page of results, and no known page function, so do not try
|
// There is only one page of results, and no known page function, so do not try
|
||||||
if (pageNo !== 1) return [];
|
if (pageNo !== 1) return [];
|
||||||
// Only 14 results, no use in sorting or status
|
return this.parseNovelJSON();
|
||||||
// Also all novels are Ongoing with no Completed, can't test status filter
|
}
|
||||||
const link = `${this.site}/api/directus/novels?status=published&fields=["cover","novel_title","cover","abbreviation"]&limit=-1`;
|
|
||||||
const json = await fetchApi(link).then(r => r.json());
|
async getCoverUrl(coverId: string): Promise<string> {
|
||||||
return this.parseNovelJSON(json);
|
// genesis doesn't actually use jpegs but just in case
|
||||||
|
const ext = await fetchApi(`${this.site}/api/directus-file/${coverId}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data =>
|
||||||
|
data.type ? data.type.split('/')[1].replace('jpeg', 'jpg') : 'png',
|
||||||
|
)
|
||||||
|
.catch(() => 'png');
|
||||||
|
|
||||||
|
return `${this.api}/storage/v1/object/public/directus/${coverId}.${ext}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
@@ -53,13 +75,20 @@ class Genesis implements Plugin.PluginBase {
|
|||||||
|
|
||||||
// Fetch the novel's data in JSON format
|
// Fetch the novel's data in JSON format
|
||||||
const raw = await fetchApi(url);
|
const raw = await fetchApi(url);
|
||||||
const json = await raw.json();
|
const json: NovelJSON = await raw.json();
|
||||||
|
|
||||||
|
const novel: Plugin.SourceNovel = {
|
||||||
|
name: json.novel_title,
|
||||||
|
path: novelPath,
|
||||||
|
summary: json.synopsis,
|
||||||
|
author: json.author,
|
||||||
|
cover: json.cover ? await this.getCoverUrl(json.cover) : defaultCover,
|
||||||
|
genres: json.genres
|
||||||
|
?.map(g => g.genres_id?.label)
|
||||||
|
.filter(l => l)
|
||||||
|
.join(','),
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize the novel object with default values
|
|
||||||
const parse = await this.parseNovelJSON([json]);
|
|
||||||
const novel: Plugin.SourceNovel = parse[0];
|
|
||||||
novel.summary = json.synopsis;
|
|
||||||
novel.author = json.author;
|
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
ongoing: NovelStatus.Ongoing,
|
ongoing: NovelStatus.Ongoing,
|
||||||
hiatus: NovelStatus.OnHiatus,
|
hiatus: NovelStatus.OnHiatus,
|
||||||
@@ -68,21 +97,8 @@ class Genesis implements Plugin.PluginBase {
|
|||||||
completed: NovelStatus.Completed,
|
completed: NovelStatus.Completed,
|
||||||
unknown: NovelStatus.Unknown,
|
unknown: NovelStatus.Unknown,
|
||||||
};
|
};
|
||||||
novel.status = map[json.serialization.toLowerCase()] ?? NovelStatus.Unknown;
|
novel.status =
|
||||||
if (json.cover) {
|
map[json.serialization?.toLowerCase() || ''] ?? NovelStatus.Unknown;
|
||||||
const url = `${this.site}/api/directus-file/${json.cover}`;
|
|
||||||
const imgJson = await (await fetchApi(url)).json();
|
|
||||||
console.log(imgJson.type);
|
|
||||||
novel.cover = `${this.api}/storage/v1/object/public/directus/${json.cover}.png`;
|
|
||||||
if (imgJson.type == 'image/gif') {
|
|
||||||
novel.cover = novel.cover?.replace('.png', '.gif');
|
|
||||||
} else if (imgJson.type !== 'image/png') {
|
|
||||||
novel.cover = novel.cover?.replace(
|
|
||||||
'.png',
|
|
||||||
'.' + imgJson.type.toString().split('/')[1],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the chapters if available and assign them to the novel object
|
// Parse the chapters if available and assign them to the novel object
|
||||||
novel.chapters = await this.extractChapters(json.id);
|
novel.chapters = await this.extractChapters(json.id);
|
||||||
@@ -91,12 +107,12 @@ class Genesis implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to extract and format chapters
|
// Helper function to extract and format chapters
|
||||||
async extractChapters(id: string): Plugin.ChapterItem[] {
|
async extractChapters(id: string): Promise<Plugin.ChapterItem[]> {
|
||||||
const url = `${this.site}/api/novels-chapter/${id}`;
|
const url = `${this.site}/api/novels-chapter/${id}`;
|
||||||
|
|
||||||
// Fetch the chapter data in JSON format
|
// Fetch the chapter data in JSON format
|
||||||
const raw = await fetchApi(url);
|
const raw = await fetchApi(url);
|
||||||
const json = await raw.json();
|
const json: ChapterJSON = await raw.json();
|
||||||
|
|
||||||
// Format each chapter and add only valid ones
|
// Format each chapter and add only valid ones
|
||||||
const chapters = json.data.chapters
|
const chapters = json.data.chapters
|
||||||
@@ -116,7 +132,7 @@ class Genesis implements Plugin.PluginBase {
|
|||||||
chapterNumber: Number(chapterNum),
|
chapterNumber: Number(chapterNum),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(chapter => chapter !== null) as Plugin.ChapterItem[];
|
.filter(chapter => chapter !== null);
|
||||||
|
|
||||||
return chapters;
|
return chapters;
|
||||||
}
|
}
|
||||||
@@ -131,21 +147,17 @@ class Genesis implements Plugin.PluginBase {
|
|||||||
let external_api;
|
let external_api;
|
||||||
let apikey;
|
let apikey;
|
||||||
|
|
||||||
let URLs = [];
|
const URLs: string[] = [];
|
||||||
let code;
|
let code;
|
||||||
|
|
||||||
// Find URL with API Key
|
$('head script[src]').each((_, el) => {
|
||||||
const srcs = $('head')
|
const src = $(el).attr('src')!;
|
||||||
.find('script')
|
if (!URLs.includes(src)) {
|
||||||
.map(function () {
|
|
||||||
const src = $(this).attr('src');
|
|
||||||
if (src in URLs) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
URLs.push(src);
|
URLs.push(src);
|
||||||
})
|
}
|
||||||
.toArray();
|
});
|
||||||
for (let src of URLs) {
|
|
||||||
|
for (const src of URLs) {
|
||||||
const script = await fetchApi(`${this.site}${src}`);
|
const script = await fetchApi(`${this.site}${src}`);
|
||||||
const raw = await script.text();
|
const raw = await script.text();
|
||||||
if (raw.includes('sb_publishable')) {
|
if (raw.includes('sb_publishable')) {
|
||||||
@@ -176,9 +188,14 @@ class Genesis implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = `${external_api}/rest/v1/chapters?select=id,chapter_title,chapter_number,chapter_content,status,novel&id=eq.${id}&status=eq.released`;
|
const path = `${external_api}/rest/v1/chapters`;
|
||||||
|
const search = new URLSearchParams({
|
||||||
|
select: 'id,chapter_title,chapter_number,chapter_content,status,novel',
|
||||||
|
id: `eq.${id}`,
|
||||||
|
status: 'eq.released',
|
||||||
|
});
|
||||||
|
|
||||||
const chQuery = await fetchApi(path, {
|
const chQuery = await fetchApi(`${path}?${search}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
// Cookie: 'csrftoken=' + csrftoken,
|
// Cookie: 'csrftoken=' + csrftoken,
|
||||||
@@ -197,12 +214,21 @@ class Genesis implements Plugin.PluginBase {
|
|||||||
pageNo: number,
|
pageNo: number,
|
||||||
): Promise<Plugin.SourceNovel[]> {
|
): Promise<Plugin.SourceNovel[]> {
|
||||||
if (pageNo !== 1) return [];
|
if (pageNo !== 1) return [];
|
||||||
// TODO: Figure out how to search
|
// Since only 26 novels, fetch all the novels
|
||||||
const url = `${this.site}/api/novels/search?title=${encodeURIComponent(searchTerm)}`;
|
// then filter out the novels which match the criteria
|
||||||
const json = await fetchApi(url).then(r => r.json());
|
const novels = await this.parseNovelJSON();
|
||||||
return this.parseNovelJSON(json);
|
const query = this.normalize(searchTerm);
|
||||||
|
|
||||||
|
return novels.filter(novel => this.normalize(novel.name).includes(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// grabbed from Witch Cult Translations
|
||||||
|
private normalize(str: string) {
|
||||||
|
return str.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// due to the low amount of novels, using filters kinda overkill
|
||||||
|
// unless we apply filters to cached results
|
||||||
filters = {
|
filters = {
|
||||||
sort: {
|
sort: {
|
||||||
label: 'Sort Results By',
|
label: 'Sort Results By',
|
||||||
@@ -213,32 +239,43 @@ class Genesis implements Plugin.PluginBase {
|
|||||||
],
|
],
|
||||||
type: FilterTypes.Picker,
|
type: FilterTypes.Picker,
|
||||||
},
|
},
|
||||||
storyStatus: {
|
// storyStatus: {
|
||||||
label: 'Status',
|
// label: 'Status',
|
||||||
value: 'All',
|
// value: 'All',
|
||||||
options: [
|
// options: [
|
||||||
{ label: 'All', value: 'All' },
|
// { label: 'All', value: 'All' },
|
||||||
{ label: 'Ongoing', value: 'Ongoing' },
|
// { label: 'Ongoing', value: 'Ongoing' },
|
||||||
{ label: 'Completed', value: 'Completed' },
|
// { label: 'Completed', value: 'Completed' },
|
||||||
],
|
// ],
|
||||||
type: FilterTypes.Picker,
|
// type: FilterTypes.Picker,
|
||||||
},
|
// },
|
||||||
genres: {
|
genres: {
|
||||||
label: 'Genres',
|
label: 'Genres',
|
||||||
value: [],
|
value: [],
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Action', value: 'Action' },
|
{ 'label': 'Academy', 'value': '21' },
|
||||||
{ label: 'Comedy', value: 'Comedy' },
|
{ 'label': 'Action', 'value': '1' },
|
||||||
{ label: 'Drama', value: 'Drama' },
|
{ 'label': 'Adventure', 'value': '15' },
|
||||||
{ label: 'Fantasy', value: 'Fantasy' },
|
{ 'label': 'Calm Protagonist', 'value': '22' },
|
||||||
{ label: 'Harem', value: 'Harem' },
|
{ 'label': 'Comedy', 'value': '2' },
|
||||||
{ label: 'Martial Arts', value: 'Martial Arts' },
|
{ 'label': 'Cultivation', 'value': '25' },
|
||||||
{ label: 'Modern', value: 'Modern' },
|
{ 'label': 'Drama', 'value': '3' },
|
||||||
{ label: 'Mystery', value: 'Mystery' },
|
{ 'label': 'Fantasy', 'value': '5' },
|
||||||
{ label: 'Psychological', value: 'Psychological' },
|
{ 'label': 'Harem', 'value': '11' },
|
||||||
{ label: 'Romance', value: 'Romance' },
|
{ 'label': 'Idol', 'value': '20' },
|
||||||
{ label: 'Slice of life', value: 'Slice of Life' },
|
{ 'label': 'Martial Arts', 'value': '6' },
|
||||||
{ label: 'Tragedy', value: 'Tragedy' },
|
{ 'label': 'Modern', 'value': '4' },
|
||||||
|
{ 'label': 'Modern Fantasy', 'value': '27' },
|
||||||
|
{ 'label': 'Mystery', 'value': '8' },
|
||||||
|
{ 'label': 'Psychological', 'value': '10' },
|
||||||
|
{ 'label': 'Romance', 'value': '9' },
|
||||||
|
{ 'label': 'School Life', 'value': '13' },
|
||||||
|
{ 'label': 'Sci-fi', 'value': '24' },
|
||||||
|
{ 'label': 'Slice of Life', 'value': '7' },
|
||||||
|
{ 'label': 'Supernatural', 'value': '14' },
|
||||||
|
{ 'label': 'Tragedy', 'value': '12' },
|
||||||
|
{ 'label': 'Transmigration', 'value': '23' },
|
||||||
|
{ 'label': 'Yandere', 'value': '26' },
|
||||||
],
|
],
|
||||||
type: FilterTypes.CheckboxGroup,
|
type: FilterTypes.CheckboxGroup,
|
||||||
},
|
},
|
||||||
@@ -246,3 +283,30 @@ class Genesis implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default new Genesis();
|
export default new Genesis();
|
||||||
|
|
||||||
|
type NovelJSON = {
|
||||||
|
id: string;
|
||||||
|
novel_title: string;
|
||||||
|
abbreviation: string;
|
||||||
|
cover: string;
|
||||||
|
synopsis?: string;
|
||||||
|
author?: string;
|
||||||
|
serialization?: string;
|
||||||
|
genres?: {
|
||||||
|
genres_id?: {
|
||||||
|
id?: number;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChapterJSON = {
|
||||||
|
data: {
|
||||||
|
chapters: {
|
||||||
|
id: string;
|
||||||
|
chapter_number: number;
|
||||||
|
chapter_title: string;
|
||||||
|
isUnlocked: boolean;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ class IndraTranslations implements Plugin.PluginBase {
|
|||||||
id = 'indratranslations';
|
id = 'indratranslations';
|
||||||
name = 'Indra Translations';
|
name = 'Indra Translations';
|
||||||
site = 'https://indratranslations.com';
|
site = 'https://indratranslations.com';
|
||||||
version = '1.2.0';
|
version = '1.2.1';
|
||||||
// icon = 'src/en/indratranslations/icon.png';
|
icon = 'src/en/indratranslations/icon.png';
|
||||||
// customCSS = 'src/en/indratranslations/customCSS.css';
|
// customCSS = 'src/en/indratranslations/customCSS.css';
|
||||||
// (optional) Add these files to the repo and uncomment the lines above if you want an icon/custom CSS.
|
// (optional) Add these files to the repo and uncomment the lines above if you want an icon/custom CSS.
|
||||||
|
|
||||||
@@ -101,12 +101,12 @@ class IndraTranslations implements Plugin.PluginBase {
|
|||||||
$(el).find('a[href*="/series/"]').first() ||
|
$(el).find('a[href*="/series/"]').first() ||
|
||||||
$(el).find('.tab-thumb a[href*="/series/"]').first();
|
$(el).find('.tab-thumb a[href*="/series/"]').first();
|
||||||
|
|
||||||
const href = (a as any).attr?.('href') || '';
|
const href = a.attr?.('href') || '';
|
||||||
const title =
|
const title =
|
||||||
this.clean($(el).find('.post-title a').text()) ||
|
this.clean($(el).find('.post-title a').text()) ||
|
||||||
this.clean($(el).find('.tab-summary .post-title a').text()) ||
|
this.clean($(el).find('.tab-summary .post-title a').text()) ||
|
||||||
this.clean((a as any).attr?.('title')) ||
|
this.clean(a.attr?.('title')) ||
|
||||||
this.clean((a as any).text?.());
|
this.clean(a.text?.());
|
||||||
|
|
||||||
const img =
|
const img =
|
||||||
$(el).find('img').attr('data-src') ||
|
$(el).find('img').attr('data-src') ||
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class InkittPlugin implements Plugin.PluginBase {
|
|||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
{
|
{
|
||||||
showLatestNovels,
|
// showLatestNovels,
|
||||||
filters,
|
filters,
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
@@ -40,7 +40,7 @@ class InkittPlugin implements Plugin.PluginBase {
|
|||||||
throw new Error('Failed to load novels, try opening in webview.');
|
throw new Error('Failed to load novels, try opening in webview.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.stories.map((novel: any) => {
|
return data.stories.map((novel: InkittNovel) => {
|
||||||
return {
|
return {
|
||||||
name: novel.title,
|
name: novel.title,
|
||||||
path: this.getPath(novel),
|
path: this.getPath(novel),
|
||||||
@@ -52,7 +52,7 @@ class InkittPlugin implements Plugin.PluginBase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getPath(novel: any) {
|
getPath(novel: InkittNovel) {
|
||||||
return (novel.category_one || novel.genres[0]) + '/' + novel.id;
|
return (novel.category_one || novel.genres[0]) + '/' + novel.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ class InkittPlugin implements Plugin.PluginBase {
|
|||||||
novel.author = loadedCheerio('dl > dd > a.author-link').text();
|
novel.author = loadedCheerio('dl > dd > a.author-link').text();
|
||||||
|
|
||||||
novel.genres = loadedCheerio('dd.genres > a')
|
novel.genres = loadedCheerio('dd.genres > a')
|
||||||
.map((i, el) => loadedCheerio(el).text())
|
.map((_, el) => loadedCheerio(el).text())
|
||||||
.toArray()
|
.toArray()
|
||||||
.join(', ');
|
.join(', ');
|
||||||
const status = loadedCheerio('div.dlc > dl:has(dt:contains("Status")) > dd')
|
const status = loadedCheerio('div.dlc > dl:has(dt:contains("Status")) > dd')
|
||||||
@@ -81,12 +81,12 @@ class InkittPlugin implements Plugin.PluginBase {
|
|||||||
const apiReq = await fetchApi(
|
const apiReq = await fetchApi(
|
||||||
this.site + `/api/stories/${novelPath.split('/')[1]}`,
|
this.site + `/api/stories/${novelPath.split('/')[1]}`,
|
||||||
);
|
);
|
||||||
const apiData = await apiReq.json();
|
const apiData = (await apiReq.json()) as InkittApi;
|
||||||
novel.cover = apiData.vertical_cover.url;
|
novel.cover = apiData.vertical_cover.url;
|
||||||
|
|
||||||
novel.summary = loadedCheerio('p.story-summary').text();
|
novel.summary = loadedCheerio('p.story-summary').text();
|
||||||
|
|
||||||
novel.chapters = apiData.chapters.map((c: any) => {
|
novel.chapters = apiData.chapters.map((c: InkittChapter) => {
|
||||||
return {
|
return {
|
||||||
name: c.name,
|
name: c.name,
|
||||||
path: novelPath + '/chapters/' + c.chapter_number,
|
path: novelPath + '/chapters/' + c.chapter_number,
|
||||||
@@ -119,7 +119,7 @@ class InkittPlugin implements Plugin.PluginBase {
|
|||||||
throw new Error('Failed to search novels, try opening in webview.');
|
throw new Error('Failed to search novels, try opening in webview.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.stories.map((novel: any) => {
|
return data.stories.map((novel: InkittNovel) => {
|
||||||
return {
|
return {
|
||||||
name: novel.title,
|
name: novel.title,
|
||||||
path: this.getPath(novel),
|
path: this.getPath(novel),
|
||||||
@@ -131,8 +131,8 @@ class InkittPlugin implements Plugin.PluginBase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
// resolveUrl = (path: string, isNovel?: boolean) =>
|
||||||
this.site + '/stories/' + path;
|
// this.site + '/stories/' + path;
|
||||||
|
|
||||||
filters = {
|
filters = {
|
||||||
genres: {
|
genres: {
|
||||||
@@ -160,3 +160,31 @@ class InkittPlugin implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default new InkittPlugin();
|
export default new InkittPlugin();
|
||||||
|
|
||||||
|
// Typings inferred from usage, no actual investigation done
|
||||||
|
// TODO: change layout
|
||||||
|
type InkittNovel = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
category_one?: string;
|
||||||
|
genres: string[];
|
||||||
|
cover: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
vertical_cover: {
|
||||||
|
url: string;
|
||||||
|
iphone: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type InkittChapter = {
|
||||||
|
name: string;
|
||||||
|
chapter_number: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type InkittApi = {
|
||||||
|
vertical_cover: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
chapters: InkittChapter[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
name = 'iNovelTranslation';
|
name = 'iNovelTranslation';
|
||||||
icon = 'src/en/inoveltranslation/icon.png';
|
icon = 'src/en/inoveltranslation/icon.png';
|
||||||
site = 'https://inoveltranslation.com';
|
site = 'https://inoveltranslation.com';
|
||||||
version = '1.0.1';
|
version = '1.0.2';
|
||||||
filters: Filters | undefined = undefined;
|
filters: Filters | undefined = undefined;
|
||||||
|
|
||||||
pluginSettings = {
|
pluginSettings = {
|
||||||
@@ -22,7 +22,6 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Optimized stealth headers to mirror a real browser environment
|
|
||||||
private readonly HEADERS = {
|
private readonly HEADERS = {
|
||||||
'Accept':
|
'Accept':
|
||||||
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
||||||
@@ -33,20 +32,16 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
'Sec-Fetch-Site': 'same-origin',
|
'Sec-Fetch-Site': 'same-origin',
|
||||||
};
|
};
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||||
pageNo: number,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
options: Plugin.PopularNovelsOptions<typeof this.filters>,
|
|
||||||
): Promise<Plugin.NovelItem[]> {
|
|
||||||
const url = `${this.site}/api/novels?limit=50&page=${pageNo}`;
|
const url = `${this.site}/api/novels?limit=50&page=${pageNo}`;
|
||||||
const result = await fetchApi(url, { headers: this.HEADERS }).then(r =>
|
const result: ApiResponse<NovelData> = await fetchApi(url, {
|
||||||
r.json(),
|
headers: this.HEADERS,
|
||||||
);
|
}).then(r => r.json());
|
||||||
|
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
if (result.docs) {
|
if (result.docs) {
|
||||||
result.docs.forEach((doc: any) => {
|
result.docs.forEach(doc => {
|
||||||
novels.push({
|
novels.push({
|
||||||
name: doc.title,
|
name: doc.title,
|
||||||
path: `/novels/${doc.id}`,
|
path: `/novels/${doc.id}`,
|
||||||
@@ -61,27 +56,24 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
const id = novelPath.split('/').pop();
|
const id = novelPath.split('/').pop();
|
||||||
const novelUrl = `${this.site}/api/novels/${id}?depth=1`;
|
const novelUrl = `${this.site}/api/novels/${id}?depth=1`;
|
||||||
const novelData = await fetchApi(novelUrl, { headers: this.HEADERS }).then(
|
const novelData: NovelData = await fetchApi(novelUrl, {
|
||||||
r => r.json(),
|
headers: this.HEADERS,
|
||||||
);
|
}).then(r => r.json());
|
||||||
|
|
||||||
const chaptersUrl = `${this.site}/api/chapters?where[novel][equals]=${id}&limit=999&depth=0`;
|
const chaptersUrl = `${this.site}/api/chapters?where[novel][equals]=${id}&limit=999&depth=0`;
|
||||||
const chaptersData = await fetchApi(chaptersUrl, {
|
const chaptersData: ApiResponse<ChapterData> = await fetchApi(chaptersUrl, {
|
||||||
headers: this.HEADERS,
|
headers: this.HEADERS,
|
||||||
}).then(r => r.json());
|
}).then(r => r.json());
|
||||||
|
|
||||||
// Extract status
|
|
||||||
const status =
|
const status =
|
||||||
novelData.publication === 'completed'
|
novelData.publication === 'completed'
|
||||||
? NovelStatus.Completed
|
? NovelStatus.Completed
|
||||||
: NovelStatus.Ongoing;
|
: NovelStatus.Ongoing;
|
||||||
|
|
||||||
// Extract genres (tags)
|
|
||||||
const genres = novelData.tags
|
const genres = novelData.tags
|
||||||
? novelData.tags.map((tag: any) => tag.name).join(', ')
|
? novelData.tags.map(tag => tag.name).join(', ')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
// Process summary (Lexical JSON from API 'sypnosis') - use Plain Text for App Synopsis
|
|
||||||
let summary = '';
|
let summary = '';
|
||||||
if (novelData.sypnosis && novelData.sypnosis.root) {
|
if (novelData.sypnosis && novelData.sypnosis.root) {
|
||||||
summary = this.lexicalToText(novelData.sypnosis.root);
|
summary = this.lexicalToText(novelData.sypnosis.root);
|
||||||
@@ -103,7 +95,7 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
const hideLocked = storage.get('hideLocked');
|
const hideLocked = storage.get('hideLocked');
|
||||||
|
|
||||||
if (chaptersData.docs) {
|
if (chaptersData.docs) {
|
||||||
chaptersData.docs.forEach((doc: any) => {
|
chaptersData.docs.forEach(doc => {
|
||||||
const isLocked = doc.tier !== null;
|
const isLocked = doc.tier !== null;
|
||||||
if (isLocked && hideLocked) {
|
if (isLocked && hideLocked) {
|
||||||
return;
|
return;
|
||||||
@@ -121,7 +113,6 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure chapters are sorted numerically
|
|
||||||
novel.chapters = chapters.sort(
|
novel.chapters = chapters.sort(
|
||||||
(a, b) => (a.chapterNumber || 0) - (b.chapterNumber || 0),
|
(a, b) => (a.chapterNumber || 0) - (b.chapterNumber || 0),
|
||||||
);
|
);
|
||||||
@@ -129,7 +120,6 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
// Artificial delay to prevent aggressive rate limiting
|
|
||||||
await new Promise(res => setTimeout(res, 1500));
|
await new Promise(res => setTimeout(res, 1500));
|
||||||
|
|
||||||
const rscHeader = { ...this.HEADERS, rsc: '1' };
|
const rscHeader = { ...this.HEADERS, rsc: '1' };
|
||||||
@@ -139,8 +129,8 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
response = await fetchApi(this.site + chapterPath, {
|
response = await fetchApi(this.site + chapterPath, {
|
||||||
headers: rscHeader,
|
headers: rscHeader,
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
throw new Error(`Network error: ${e.message}`);
|
throw new Error(`Network error: ${(e as Error).message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
@@ -155,7 +145,6 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
throw new Error('Server returned empty data.');
|
throw new Error('Server returned empty data.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Proactive Cloudflare Detection
|
|
||||||
if (
|
if (
|
||||||
rscText.includes('cf-browser-verification') ||
|
rscText.includes('cf-browser-verification') ||
|
||||||
rscText.includes('cf-challenge') ||
|
rscText.includes('cf-challenge') ||
|
||||||
@@ -192,7 +181,12 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
let startIndex = rscText.lastIndexOf('{', sigIndex);
|
let startIndex = rscText.lastIndexOf('{', sigIndex);
|
||||||
|
|
||||||
// Check for "content" or "root" before to find the start of the relevant object
|
// Check for "content" or "root" before to find the start of the relevant object
|
||||||
const contextKeys = ['"content"', '\\"content\\"', '"root"', '\\"root\\"'];
|
const contextKeys = [
|
||||||
|
'"content"',
|
||||||
|
'\\"content\\"',
|
||||||
|
'"root"',
|
||||||
|
'\\"root\\"',
|
||||||
|
];
|
||||||
for (const key of contextKeys) {
|
for (const key of contextKeys) {
|
||||||
const keyIndex = rscText.lastIndexOf(key, sigIndex);
|
const keyIndex = rscText.lastIndexOf(key, sigIndex);
|
||||||
if (keyIndex !== -1 && keyIndex > startIndex - 50) {
|
if (keyIndex !== -1 && keyIndex > startIndex - 50) {
|
||||||
@@ -236,8 +230,8 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
|
|
||||||
if (jsonStr) {
|
if (jsonStr) {
|
||||||
try {
|
try {
|
||||||
// Attempt to parse directly (if it's unescaped RSC)
|
// eslint-disable-next-line no-control-regex
|
||||||
let safeJson = jsonStr.replace(/[\x00-\x1F\x7F-\x9F]/g, '');
|
const safeJson = jsonStr.replace(/[\x00-\x1F\x7F-\x9F]/g, '');
|
||||||
let parsedData;
|
let parsedData;
|
||||||
try {
|
try {
|
||||||
parsedData = JSON.parse(safeJson);
|
parsedData = JSON.parse(safeJson);
|
||||||
@@ -246,18 +240,22 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
const cleanJson = jsonStr
|
const cleanJson = jsonStr
|
||||||
.replace(/\\"/g, '"')
|
.replace(/\\"/g, '"')
|
||||||
.replace(/\\\\/g, '\\')
|
.replace(/\\\\/g, '\\')
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
.replace(/[\x00-\x1F\x7F-\x9F]/g, '');
|
.replace(/[\x00-\x1F\x7F-\x9F]/g, '');
|
||||||
parsedData = JSON.parse(cleanJson);
|
parsedData = JSON.parse(cleanJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
let lexicalRoot = parsedData.root || parsedData.content?.root || parsedData;
|
const lexicalRoot =
|
||||||
|
parsedData.root || parsedData.content?.root || parsedData;
|
||||||
if (lexicalRoot && lexicalRoot.children) {
|
if (lexicalRoot && lexicalRoot.children) {
|
||||||
return this.lexicalToHtml(lexicalRoot);
|
return this.lexicalToHtml(lexicalRoot);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
// Fallback to regex text extraction if JSON parsing fails
|
// Fallback to regex text extraction if JSON parsing fails
|
||||||
let fallbackHtml = '';
|
let fallbackHtml = '';
|
||||||
const textMatches = jsonStr.match(/\\?"text\\?"\s*:\s*\\?"(.*?)\\?"/g);
|
const textMatches = jsonStr.match(
|
||||||
|
/\\?"text\\?"\s*:\s*\\?"(.*?)\\?"/g,
|
||||||
|
);
|
||||||
if (textMatches && textMatches.length > 0) {
|
if (textMatches && textMatches.length > 0) {
|
||||||
textMatches.forEach(m => {
|
textMatches.forEach(m => {
|
||||||
let text = m.match(/: ?"?(.*?)"?$/)?.[1] || '';
|
let text = m.match(/: ?"?(.*?)"?$/)?.[1] || '';
|
||||||
@@ -296,11 +294,7 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private lexicalToHtml(node: LexicalNode): string {
|
||||||
* Recursively converts Lexical JSON nodes to HTML strings.
|
|
||||||
* Suitable for Chapter Content.
|
|
||||||
*/
|
|
||||||
private lexicalToHtml(node: any): string {
|
|
||||||
let html = '';
|
let html = '';
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
for (const child of node.children) {
|
for (const child of node.children) {
|
||||||
@@ -308,8 +302,8 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
html += `<p>${this.lexicalToHtml(child)}</p>`;
|
html += `<p>${this.lexicalToHtml(child)}</p>`;
|
||||||
} else if (child.type === 'text') {
|
} else if (child.type === 'text') {
|
||||||
let text = child.text || '';
|
let text = child.text || '';
|
||||||
if (child.format & 1) text = `<b>${text}</b>`;
|
if (child.format && child.format & 1) text = `<b>${text}</b>`;
|
||||||
if (child.format & 2) text = `<i>${text}</i>`;
|
if (child.format && child.format & 2) text = `<i>${text}</i>`;
|
||||||
html += text;
|
html += text;
|
||||||
} else if (child.type === 'list') {
|
} else if (child.type === 'list') {
|
||||||
const tag = child.listType === 'number' ? 'ol' : 'ul';
|
const tag = child.listType === 'number' ? 'ol' : 'ul';
|
||||||
@@ -327,11 +321,7 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private lexicalToText(node: LexicalNode): string {
|
||||||
* Recursively converts Lexical JSON nodes to Plain Text with newlines.
|
|
||||||
* Suitable for Novel Synopsis in the app.
|
|
||||||
*/
|
|
||||||
private lexicalToText(node: any): string {
|
|
||||||
let textOut = '';
|
let textOut = '';
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
for (const child of node.children) {
|
for (const child of node.children) {
|
||||||
@@ -356,14 +346,14 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
const url = `${this.site}/api/novels?where[title][contains]=${encodeURIComponent(
|
const url = `${this.site}/api/novels?where[title][contains]=${encodeURIComponent(
|
||||||
searchTerm,
|
searchTerm,
|
||||||
)}&limit=50&page=${pageNo}`;
|
)}&limit=50&page=${pageNo}`;
|
||||||
const result = await fetchApi(url, { headers: this.HEADERS }).then(r =>
|
const result: ApiResponse<NovelData> = await fetchApi(url, {
|
||||||
r.json(),
|
headers: this.HEADERS,
|
||||||
);
|
}).then(r => r.json());
|
||||||
|
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
if (result.docs) {
|
if (result.docs) {
|
||||||
result.docs.forEach((doc: any) => {
|
result.docs.forEach(doc => {
|
||||||
novels.push({
|
novels.push({
|
||||||
name: doc.title,
|
name: doc.title,
|
||||||
path: `/novels/${doc.id}`,
|
path: `/novels/${doc.id}`,
|
||||||
@@ -374,8 +364,43 @@ class INovelTranslation implements Plugin.PluginBase {
|
|||||||
|
|
||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveUrl = (path: string, isNovel?: boolean) => this.site + path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new INovelTranslation();
|
export default new INovelTranslation();
|
||||||
|
|
||||||
|
type LexicalNode = {
|
||||||
|
type: string;
|
||||||
|
text?: string;
|
||||||
|
children?: LexicalNode[];
|
||||||
|
format?: number;
|
||||||
|
listType?: string;
|
||||||
|
tag?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NovelData = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
cover?: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
author?: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
publication?: string;
|
||||||
|
tags?: { name: string }[];
|
||||||
|
sypnosis?: {
|
||||||
|
root: LexicalNode;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChapterData = {
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
chapter: number;
|
||||||
|
tier: string | null;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApiResponse<T> = {
|
||||||
|
docs: T[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class LeafStudio implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
page: number,
|
page: number,
|
||||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
// { filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
let link = this.site + 'novels';
|
let link = this.site + 'novels';
|
||||||
if (page > 1) {
|
if (page > 1) {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { fetchApi } from '@libs/fetch';
|
|||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { Filters } from '@libs/filterInputs';
|
import { Filters } from '@libs/filterInputs';
|
||||||
import { load as loadCheerio } from 'cheerio';
|
import { load as loadCheerio } from 'cheerio';
|
||||||
import { defaultCover } from '@libs/defaultCover';
|
// import { defaultCover } from '@libs/defaultCover';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
|
// import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
|
||||||
// import { storage, localStorage, sessionStorage } from '@libs/storage';
|
// import { storage, localStorage, sessionStorage } from '@libs/storage';
|
||||||
// import { encode, decode } from 'urlencode';
|
// import { encode, decode } from 'urlencode';
|
||||||
// import dayjs from 'dayjs';
|
// import dayjs from 'dayjs';
|
||||||
import { Parser } from 'htmlparser2';
|
// import { Parser } from 'htmlparser2';
|
||||||
|
|
||||||
class LNTPlugin implements Plugin.PluginBase {
|
class LNTPlugin implements Plugin.PluginBase {
|
||||||
id = 'lightnoveltranslations';
|
id = 'lightnoveltranslations';
|
||||||
@@ -26,7 +26,7 @@ class LNTPlugin implements Plugin.PluginBase {
|
|||||||
pageNo: number,
|
pageNo: number,
|
||||||
{
|
{
|
||||||
showLatestNovels,
|
showLatestNovels,
|
||||||
filters,
|
// filters,
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
let link = this.site + 'read/';
|
let link = this.site + 'read/';
|
||||||
@@ -193,11 +193,11 @@ class LNTPlugin implements Plugin.PluginBase {
|
|||||||
novels.push(tempNovel);
|
novels.push(tempNovel);
|
||||||
});
|
});
|
||||||
|
|
||||||
type SearchEntry = {
|
// type SearchEntry = {
|
||||||
title: string;
|
// title: string;
|
||||||
thumbnail: string;
|
// thumbnail: string;
|
||||||
permalink: string;
|
// permalink: string;
|
||||||
};
|
// };
|
||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class LnMTLPlugin implements Plugin.PagePlugin {
|
|||||||
name = 'LnMTL';
|
name = 'LnMTL';
|
||||||
icon = 'src/en/lnmtl/icon.png';
|
icon = 'src/en/lnmtl/icon.png';
|
||||||
site = 'https://lnmtl.com/';
|
site = 'https://lnmtl.com/';
|
||||||
version = '2.1.0';
|
version = '2.1.1';
|
||||||
|
|
||||||
async sleep(ms: number) {
|
async sleep(ms: number) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
@@ -126,10 +126,12 @@ class LnMTLPlugin implements Plugin.PagePlugin {
|
|||||||
ontext(data) {
|
ontext(data) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case ParsingState.InScript:
|
case ParsingState.InScript:
|
||||||
const volume = JSON.parse(
|
{
|
||||||
data.match(/lnmtl.volumes = (.+])(?=;)/)![1] || '',
|
const volume = JSON.parse(
|
||||||
);
|
data.match(/lnmtl.volumes = (.+])(?=;)/)![1] || '',
|
||||||
novel.totalPages = volume.length;
|
);
|
||||||
|
novel.totalPages = volume.length;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ParsingState.InDescription:
|
case ParsingState.InDescription:
|
||||||
summaryParts.push(data.trim());
|
summaryParts.push(data.trim());
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { Filters, FilterTypes } from '@libs/filterInputs';
|
|||||||
import { Parser } from 'htmlparser2';
|
import { Parser } from 'htmlparser2';
|
||||||
|
|
||||||
//has to be here cus this scoping moment
|
//has to be here cus this scoping moment
|
||||||
|
// im just gonna disable eslint here instead of reading lol
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const parserData: {
|
const parserData: {
|
||||||
inTag?: string;
|
inTag?: string;
|
||||||
depthRatingWrapper?: number;
|
depthRatingWrapper?: number;
|
||||||
@@ -45,33 +47,29 @@ class MVLEMPYRPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
{ filters }: Plugin.PopularNovelsOptions<Filters>,
|
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const data = await this.getAllNovels();
|
const data = await this.getAllNovels();
|
||||||
const filtered = data.filter(novel => {
|
const filtered = data.filter(novel => {
|
||||||
// @ts-ignore
|
for (const genre of filters.genre.value.exclude || []) {
|
||||||
for (const genre of filters.genre.value.exclude) {
|
|
||||||
if (novel.genres.includes(genre)) {
|
if (novel.genres.includes(genre)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
for (const genre of filters.genre.value.include || []) {
|
||||||
for (const genre of filters.genre.value.include) {
|
|
||||||
if (!novel.genres.includes(genre)) {
|
if (!novel.genres.includes(genre)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
for (const tag of filters.tag.value.exclude || []) {
|
||||||
for (const tag of filters.tag.value.exclude) {
|
|
||||||
if (novel.tags.includes(tag)) {
|
if (novel.tags.includes(tag)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
for (const tag of filters.tag.value.include || []) {
|
||||||
for (const tag of filters.tag.value.include) {
|
|
||||||
if (!novel.tags.includes(tag)) {
|
if (!novel.tags.includes(tag)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { storage } from '@libs/storage';
|
|||||||
class NovelFire implements Plugin.PluginBase {
|
class NovelFire implements Plugin.PluginBase {
|
||||||
id = 'novelfire';
|
id = 'novelfire';
|
||||||
name = 'Novel Fire';
|
name = 'Novel Fire';
|
||||||
version = '1.4.1';
|
version = '1.4.2';
|
||||||
icon = 'src/en/novelfire/icon.png';
|
icon = 'src/en/novelfire/icon.png';
|
||||||
site = 'https://novelfire.net/';
|
site = 'https://novelfire.net/';
|
||||||
webStorageUtilized = true;
|
webStorageUtilized = true;
|
||||||
@@ -294,7 +294,7 @@ class NovelFire implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
novel.genres = $('.categories .property-item')
|
novel.genres = $('.categories .property-item')
|
||||||
.map((i, el) => $(el).text())
|
.map((_, el) => $(el).text())
|
||||||
.toArray()
|
.toArray()
|
||||||
.join(',');
|
.join(',');
|
||||||
|
|
||||||
@@ -356,7 +356,11 @@ class NovelFire implements Plugin.PluginBase {
|
|||||||
|
|
||||||
if (post_id && !isNaN(Number(post_id))) {
|
if (post_id && !isNaN(Number(post_id))) {
|
||||||
try {
|
try {
|
||||||
const chapters = await this.getAllChapters(novelPath, post_id, page);
|
const chapters = await this.getAllChapters(
|
||||||
|
novelPath,
|
||||||
|
String(post_id),
|
||||||
|
page,
|
||||||
|
);
|
||||||
return { chapters };
|
return { chapters };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fallback to scraping if AJAX fails
|
// Fallback to scraping if AJAX fails
|
||||||
@@ -373,7 +377,7 @@ class NovelFire implements Plugin.PluginBase {
|
|||||||
const loadedCheerio = load(body);
|
const loadedCheerio = load(body);
|
||||||
|
|
||||||
const chapters = loadedCheerio('.chapter-list li')
|
const chapters = loadedCheerio('.chapter-list li')
|
||||||
.map((index, ele) => {
|
.map((_, ele) => {
|
||||||
const chapterName =
|
const chapterName =
|
||||||
loadedCheerio(ele).find('a').attr('title') || 'No Title Found';
|
loadedCheerio(ele).find('a').attr('title') || 'No Title Found';
|
||||||
const chapterPath = loadedCheerio(ele).find('a').attr('href');
|
const chapterPath = loadedCheerio(ele).find('a').attr('href');
|
||||||
|
|||||||
+90
-31
@@ -3,13 +3,14 @@ import { Plugin } from '@/types/plugin';
|
|||||||
import { load as parseHTML } from 'cheerio';
|
import { load as parseHTML } from 'cheerio';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
import { defaultCover } from '@libs/defaultCover';
|
import { defaultCover } from '@libs/defaultCover';
|
||||||
|
import { Filters, FilterTypes } from '@/types/filters';
|
||||||
|
|
||||||
class NovelHi implements Plugin.PluginBase {
|
class NovelHi implements Plugin.PluginBase {
|
||||||
id = 'novelhi';
|
id = 'novelhi';
|
||||||
name = 'NovelHi';
|
name = 'NovelHi';
|
||||||
icon = 'src/en/novelhi/icon.png';
|
icon = 'src/en/novelhi/icon.png';
|
||||||
site = 'https://novelhi.com/';
|
site = 'https://novelhi.com/';
|
||||||
version = '1.0.0';
|
version = '1.1.0';
|
||||||
|
|
||||||
// flag indicates whether access to LocalStorage, SesesionStorage is required.
|
// flag indicates whether access to LocalStorage, SesesionStorage is required.
|
||||||
webStorageUtilized?: boolean;
|
webStorageUtilized?: boolean;
|
||||||
@@ -17,8 +18,25 @@ class NovelHi implements Plugin.PluginBase {
|
|||||||
// Cache for storing extended metadata from the list API | ie: copypasta from readfrom.ts
|
// Cache for storing extended metadata from the list API | ie: copypasta from readfrom.ts
|
||||||
loadedNovelCache: CachedNovel[] = [];
|
loadedNovelCache: CachedNovel[] = [];
|
||||||
|
|
||||||
parseNovels(novels: NovelData[]): CachedNovel[] {
|
private async getNovels(
|
||||||
const ret: CachedNovel[] = novels.map(item => ({
|
pageNo: number,
|
||||||
|
keyword = '',
|
||||||
|
filters?: Plugin.PopularNovelsOptions<typeof this.filters>['filters'],
|
||||||
|
): Promise<CachedNovel[]> {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
curr: pageNo.toString(),
|
||||||
|
limit: '10',
|
||||||
|
keyword,
|
||||||
|
...(filters?.genres.value && { 'bookGenres[]': filters.genres.value }),
|
||||||
|
...(filters?.order.value && { bookStatus: filters.order.value }),
|
||||||
|
...(filters?.time.value && { updatePeriod: filters.time.value }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = `${this.site}book/searchByPageInShelf?${params}`;
|
||||||
|
const response = await fetchApi(url);
|
||||||
|
const json: ApiResponse = await response.json();
|
||||||
|
|
||||||
|
const novels: CachedNovel[] = json.data.list.map(item => ({
|
||||||
name: item.bookName,
|
name: item.bookName,
|
||||||
path: `s/${item.simpleName}`,
|
path: `s/${item.simpleName}`,
|
||||||
cover: item.picUrl || defaultCover,
|
cover: item.picUrl || defaultCover,
|
||||||
@@ -28,30 +46,19 @@ class NovelHi implements Plugin.PluginBase {
|
|||||||
genres: item.genres.map(g => g.genreName).join(', '),
|
genres: item.genres.map(g => g.genreName).join(', '),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Manage cache size
|
this.loadedNovelCache.push(...novels);
|
||||||
this.loadedNovelCache.push(...ret);
|
|
||||||
if (this.loadedNovelCache.length > 100) {
|
if (this.loadedNovelCache.length > 100) {
|
||||||
this.loadedNovelCache = this.loadedNovelCache.slice(-100);
|
this.loadedNovelCache = this.loadedNovelCache.slice(-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
{ showLatestNovels }: Plugin.PopularNovelsOptions,
|
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<CachedNovel[]> {
|
||||||
const params = new URLSearchParams();
|
return this.getNovels(pageNo, '', filters);
|
||||||
|
|
||||||
params.append('curr', `${pageNo}`);
|
|
||||||
params.append('limit', '10');
|
|
||||||
params.append('keyword', '');
|
|
||||||
|
|
||||||
const jsonUrl = `${this.site}book/searchByPageInShelf?` + params.toString();
|
|
||||||
const response = await fetchApi(jsonUrl);
|
|
||||||
const json: ApiResponse = await response.json();
|
|
||||||
|
|
||||||
return this.parseNovels(json.data.list);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
@@ -130,19 +137,71 @@ class NovelHi implements Plugin.PluginBase {
|
|||||||
async searchNovels(
|
async searchNovels(
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<CachedNovel[]> {
|
||||||
const params = new URLSearchParams();
|
return this.getNovels(pageNo, searchTerm);
|
||||||
|
|
||||||
params.append('curr', `${pageNo}`);
|
|
||||||
params.append('limit', '10');
|
|
||||||
params.append('keyword', `${searchTerm}`);
|
|
||||||
|
|
||||||
const jsonUrl = `${this.site}book/searchByPageInShelf?` + params.toString();
|
|
||||||
const response = await fetchApi(jsonUrl);
|
|
||||||
const json: ApiResponse = await response.json();
|
|
||||||
|
|
||||||
return this.parseNovels(json.data.list);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
genres: {
|
||||||
|
label: 'Genres',
|
||||||
|
value: '',
|
||||||
|
options: [
|
||||||
|
{ label: 'All', value: '' },
|
||||||
|
{ label: 'Action', value: 'action' },
|
||||||
|
{ label: 'Adventure', value: 'adventure' },
|
||||||
|
{ label: 'Comedy', value: 'comedy' },
|
||||||
|
{ label: 'Light Novel', value: 'light-novel' },
|
||||||
|
{ label: 'Fanfiction', value: 'fanfiction' },
|
||||||
|
{ label: 'Fantasy', value: 'fantasy' },
|
||||||
|
{ label: 'Game', value: 'game' },
|
||||||
|
{ label: 'Gender Bender', value: 'gender-bender' },
|
||||||
|
{ label: 'Harem', value: 'harem' },
|
||||||
|
{ label: 'Historical', value: 'historical' },
|
||||||
|
{ label: 'Horror', value: 'horror' },
|
||||||
|
{ label: 'Martial Arts', value: 'martial-arts' },
|
||||||
|
{ label: 'Mature', value: 'mature' },
|
||||||
|
{ label: 'Mecha', value: 'mecha' },
|
||||||
|
{ label: 'Military', value: 'military' },
|
||||||
|
{ label: 'Mystery', value: 'mystery' },
|
||||||
|
{ label: 'Romance', value: 'romance' },
|
||||||
|
{ label: 'School Life', value: 'school-life' },
|
||||||
|
{ label: 'Sci-fi', value: 'sci-fi' },
|
||||||
|
{ label: 'Slice of Life', value: 'slice-of-life' },
|
||||||
|
{ label: 'Sports', value: 'sports' },
|
||||||
|
{ label: 'Supernatural', value: 'supernatural' },
|
||||||
|
{ label: 'Tragedy', value: 'tragedy' },
|
||||||
|
{ label: 'Urban Life', value: 'urban-life' },
|
||||||
|
{ label: 'Wuxia', value: 'wuxia' },
|
||||||
|
{ label: 'Xianxia', value: 'xianxia' },
|
||||||
|
{ label: 'Xuanhuan', value: 'xuanhuan' },
|
||||||
|
{ label: 'Yaoi', value: 'yaoi' },
|
||||||
|
{ label: 'Yuri', value: 'yuri' },
|
||||||
|
],
|
||||||
|
type: FilterTypes.Picker,
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
label: 'Status',
|
||||||
|
value: '',
|
||||||
|
options: [
|
||||||
|
{ label: 'All', value: '' },
|
||||||
|
{ label: 'Ongoing', value: '0' },
|
||||||
|
{ label: 'Completed', value: '1' },
|
||||||
|
],
|
||||||
|
type: FilterTypes.Picker,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
label: 'Update Period',
|
||||||
|
value: '',
|
||||||
|
options: [
|
||||||
|
{ label: 'All', value: '' },
|
||||||
|
{ label: '3 Days', value: '3' },
|
||||||
|
{ label: '7 Days', value: '7' },
|
||||||
|
{ label: '15 Days', value: '15' },
|
||||||
|
{ label: '30 Days', value: '30' },
|
||||||
|
],
|
||||||
|
type: FilterTypes.Picker,
|
||||||
|
},
|
||||||
|
} satisfies Filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new NovelHi();
|
export default new NovelHi();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { storage } from '@libs/storage';
|
|||||||
class Novelight implements Plugin.PagePlugin {
|
class Novelight implements Plugin.PagePlugin {
|
||||||
id = 'novelight';
|
id = 'novelight';
|
||||||
name = 'Novelight';
|
name = 'Novelight';
|
||||||
version = '1.1.4';
|
version = '1.1.5';
|
||||||
icon = 'src/en/novelight/icon.png';
|
icon = 'src/en/novelight/icon.png';
|
||||||
site = 'https://novelight.net/';
|
site = 'https://novelight.net/';
|
||||||
|
|
||||||
@@ -184,7 +184,9 @@ class Novelight implements Plugin.PagePlugin {
|
|||||||
parseHTML(ele)('.date').text().trim(),
|
parseHTML(ele)('.date').text().trim(),
|
||||||
'DD.MM.YYYY',
|
'DD.MM.YYYY',
|
||||||
).toISOString();
|
).toISOString();
|
||||||
} catch (error) {}
|
} catch (error) {
|
||||||
|
// linter happy
|
||||||
|
}
|
||||||
|
|
||||||
const chapterName = isLocked ? '🔒 ' + title : title;
|
const chapterName = isLocked ? '🔒 ' + title : title;
|
||||||
let chapterUrl = ele.attribs.href;
|
let chapterUrl = ele.attribs.href;
|
||||||
|
|||||||
@@ -342,23 +342,26 @@ class NovelUpdates implements Plugin.PluginBase {
|
|||||||
chapterContent = loadedCheerio('.content').html()!;
|
chapterContent = loadedCheerio('.content').html()!;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Last edited in 0.9.0 by Batorian - 19/03/2025
|
// Last edited in 0.9.0 by Batorian - 19/03/2025,
|
||||||
|
// remove any typing in 0.9.9 by K1ngfish3r - 04/05/2026, remove this comment if no issues
|
||||||
case 'genesistudio': {
|
case 'genesistudio': {
|
||||||
const url = `${chapterPath}/__data.json?x-sveltekit-invalidated=001`;
|
const url = `${chapterPath}/__data.json?x-sveltekit-invalidated=001`;
|
||||||
try {
|
try {
|
||||||
// Fetch the chapter's data in JSON format
|
// Fetch the chapter's data in JSON format
|
||||||
const json = await fetchApi(url).then(r => r.json());
|
const json = (await fetchApi(url).then(r => r.json())) as {
|
||||||
|
nodes: { type: string; data: Record<string, unknown> }[];
|
||||||
|
};
|
||||||
const nodes = json.nodes;
|
const nodes = json.nodes;
|
||||||
const data = nodes
|
const data = nodes
|
||||||
.filter((node: { type: string }) => node.type === 'data')
|
.filter(node => node.type === 'data')
|
||||||
.map((node: { data: any }) => node.data)[0];
|
.map(node => node.data)[0];
|
||||||
// Look for chapter container with required fields
|
// Look for chapter container with required fields
|
||||||
const contentKey = 'content';
|
const contentKey = 'content';
|
||||||
const notesKey = 'notes';
|
const notesKey = 'notes';
|
||||||
const footnotesKey = 'footnotes';
|
const footnotesKey = 'footnotes';
|
||||||
// Iterate over each property in data to find chapter containers
|
// Iterate over each property in data to find chapter containers
|
||||||
for (const key in data) {
|
for (const key in data) {
|
||||||
const mapping = data[key];
|
const mapping = data[key] as Record<string, number>;
|
||||||
// Check container for keys that match the required fields
|
// Check container for keys that match the required fields
|
||||||
if (
|
if (
|
||||||
mapping &&
|
mapping &&
|
||||||
@@ -368,9 +371,11 @@ class NovelUpdates implements Plugin.PluginBase {
|
|||||||
footnotesKey in mapping
|
footnotesKey in mapping
|
||||||
) {
|
) {
|
||||||
// Retrieve the chapter's content, notes, and footnotes using the mapping.
|
// Retrieve the chapter's content, notes, and footnotes using the mapping.
|
||||||
const content = data[mapping[contentKey]];
|
const content = data[String(mapping[contentKey])] as string;
|
||||||
const notes = data[mapping[notesKey]];
|
const notes = data[String(mapping[notesKey])] as string;
|
||||||
const footnotes = data[mapping[footnotesKey]];
|
const footnotes = data[String(mapping[footnotesKey])] as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
// Combine the parts with appropriate formatting
|
// Combine the parts with appropriate formatting
|
||||||
chapterText =
|
chapterText =
|
||||||
content +
|
content +
|
||||||
|
|||||||
+12
-12
@@ -6,7 +6,7 @@ import { Filters, FilterTypes } from '@libs/filterInputs';
|
|||||||
class PawRead implements Plugin.PluginBase {
|
class PawRead implements Plugin.PluginBase {
|
||||||
id = 'pawread';
|
id = 'pawread';
|
||||||
name = 'PawRead';
|
name = 'PawRead';
|
||||||
version = '2.1.0';
|
version = '2.1.1';
|
||||||
icon = 'src/en/pawread/icon.png';
|
icon = 'src/en/pawread/icon.png';
|
||||||
site = 'https://m.pawread.com/';
|
site = 'https://m.pawread.com/';
|
||||||
|
|
||||||
@@ -171,13 +171,15 @@ class PawRead implements Plugin.PluginBase {
|
|||||||
break;
|
break;
|
||||||
case ParsingState.ChapterTime:
|
case ParsingState.ChapterTime:
|
||||||
if (text?.includes('Advanced')) return;
|
if (text?.includes('Advanced')) return;
|
||||||
const releaseDate = text.split('.').map(x => Number(x));
|
{
|
||||||
if (releaseDate.length === 3) {
|
const releaseDate = text.split('.').map(x => Number(x));
|
||||||
tempChapter.releaseTime = new Date(
|
if (releaseDate.length === 3) {
|
||||||
releaseDate[0],
|
tempChapter.releaseTime = new Date(
|
||||||
releaseDate[1] - 1,
|
releaseDate[0],
|
||||||
releaseDate[2],
|
releaseDate[1] - 1,
|
||||||
).toISOString();
|
releaseDate[2],
|
||||||
|
).toISOString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -251,9 +253,7 @@ class PawRead implements Plugin.PluginBase {
|
|||||||
let state: ParsingState = ParsingState.Idle;
|
let state: ParsingState = ParsingState.Idle;
|
||||||
const chapterHtml: string[] = [];
|
const chapterHtml: string[] = [];
|
||||||
|
|
||||||
type EscapeChar = '&' | '<' | '>' | '"' | "'" | ' ';
|
const escapeMap: Record<string, string> = {
|
||||||
const escapeRegex = /[&<>"' ]/g;
|
|
||||||
const escapeMap: Record<EscapeChar, string> = {
|
|
||||||
'&': '&',
|
'&': '&',
|
||||||
'<': '<',
|
'<': '<',
|
||||||
'>': '>',
|
'>': '>',
|
||||||
@@ -262,7 +262,7 @@ class PawRead implements Plugin.PluginBase {
|
|||||||
' ': ' ',
|
' ': ' ',
|
||||||
};
|
};
|
||||||
const escapeHtml = (text: string): string =>
|
const escapeHtml = (text: string): string =>
|
||||||
text.replace(escapeRegex, char => escapeMap[char as EscapeChar]);
|
text.replace(/[&<>"'\xA0]/g, char => escapeMap[char]);
|
||||||
|
|
||||||
const parser = new Parser({
|
const parser = new Parser({
|
||||||
onopentag(name, attribs) {
|
onopentag(name, attribs) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Plugin } from '@/types/plugin';
|
|||||||
import { Filters } from '@libs/filterInputs';
|
import { Filters } from '@libs/filterInputs';
|
||||||
import { load as loadCheerio } from 'cheerio';
|
import { load as loadCheerio } from 'cheerio';
|
||||||
import { defaultCover } from '@libs/defaultCover';
|
import { defaultCover } from '@libs/defaultCover';
|
||||||
import { NovelItem } from '../../test_web/static/js';
|
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
|
|
||||||
type FuzzySearchOptions = {
|
type FuzzySearchOptions = {
|
||||||
@@ -164,7 +163,7 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
|||||||
name = 'Re:Library';
|
name = 'Re:Library';
|
||||||
icon = 'src/en/relibrary/icon.png';
|
icon = 'src/en/relibrary/icon.png';
|
||||||
site = 'https://re-library.com';
|
site = 'https://re-library.com';
|
||||||
version = '1.0.2';
|
version = '1.0.3';
|
||||||
imageRequestInit: Plugin.ImageRequestInit = {
|
imageRequestInit: Plugin.ImageRequestInit = {
|
||||||
headers: {
|
headers: {
|
||||||
Referer: 'https://re-library.com/',
|
Referer: 'https://re-library.com/',
|
||||||
@@ -183,7 +182,7 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
const loadedCheerio = loadCheerio(body);
|
const loadedCheerio = loadCheerio(body);
|
||||||
loadedCheerio('.entry-content > ol > li').each((_i, el) => {
|
loadedCheerio('.entry-content > ol > li').each((_i, el) => {
|
||||||
const novel: Partial<NovelItem> = {};
|
const novel: Partial<Plugin.NovelItem> = {};
|
||||||
novel.name = loadedCheerio(el).find('h3 > a').text();
|
novel.name = loadedCheerio(el).find('h3 > a').text();
|
||||||
novel.path = loadedCheerio(el)
|
novel.path = loadedCheerio(el)
|
||||||
.find('table > tbody > tr > td > a')
|
.find('table > tbody > tr > td > a')
|
||||||
@@ -197,9 +196,7 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
|||||||
.find('table > tbody > tr > td > a > img')
|
.find('table > tbody > tr > td > a > img')
|
||||||
.attr('src') ||
|
.attr('src') ||
|
||||||
defaultCover;
|
defaultCover;
|
||||||
if (novel.path.startsWith(this.site)) {
|
novel.path = new URL(novel.path, this.site).pathname;
|
||||||
novel.path = novel.path.slice(this.site.length);
|
|
||||||
}
|
|
||||||
novels.push(novel as Plugin.NovelItem);
|
novels.push(novel as Plugin.NovelItem);
|
||||||
});
|
});
|
||||||
return novels;
|
return novels;
|
||||||
@@ -224,9 +221,7 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
|||||||
.find('.entry-content > table > tbody > tr > td > a >img')
|
.find('.entry-content > table > tbody > tr > td > a >img')
|
||||||
.attr('src') ||
|
.attr('src') ||
|
||||||
defaultCover;
|
defaultCover;
|
||||||
if (novel.path.startsWith(this.site)) {
|
novel.path = new URL(novel.path, this.site).pathname;
|
||||||
novel.path = novel.path.slice(this.site.length);
|
|
||||||
}
|
|
||||||
novels.push(novel as Plugin.NovelItem);
|
novels.push(novel as Plugin.NovelItem);
|
||||||
});
|
});
|
||||||
return novels;
|
return novels;
|
||||||
@@ -257,7 +252,7 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
|||||||
// synopis: .entry-content > div.su-box > div.su-box-content
|
// synopis: .entry-content > div.su-box > div.su-box-content
|
||||||
// chapters: .entry-content > div.su-accordion <then> li.page_item[]
|
// chapters: .entry-content > div.su-accordion <then> li.page_item[]
|
||||||
|
|
||||||
const result = await fetchApi(`${this.site}/${novelPath}`);
|
const result = await fetchApi(`${this.site}${novelPath}`);
|
||||||
const body = await result.text();
|
const body = await result.text();
|
||||||
|
|
||||||
const loadedCheerio = loadCheerio(body);
|
const loadedCheerio = loadCheerio(body);
|
||||||
@@ -330,18 +325,11 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
|||||||
.find('li > a')
|
.find('li > a')
|
||||||
.each((_i2, chap_el) => {
|
.each((_i2, chap_el) => {
|
||||||
chapter_idx += 1;
|
chapter_idx += 1;
|
||||||
let chap_path = loadedCheerio(chap_el).attr('href')?.trim();
|
const chap_path = loadedCheerio(chap_el).attr('href')?.trim();
|
||||||
if (
|
if (loadedCheerio(chap_el).text() === undefined || !chap_path) return;
|
||||||
loadedCheerio(chap_el).text() === undefined ||
|
|
||||||
chap_path === undefined
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
if (chap_path.startsWith(this.site)) {
|
|
||||||
chap_path = chap_path.slice(this.site.length);
|
|
||||||
}
|
|
||||||
chapters.push({
|
chapters.push({
|
||||||
name: loadedCheerio(chap_el).text(),
|
name: loadedCheerio(chap_el).text(),
|
||||||
path: chap_path,
|
path: new URL(chap_path, this.site).pathname,
|
||||||
chapterNumber: chapter_idx,
|
chapterNumber: chapter_idx,
|
||||||
// we KNOW that we can't get the released time (at least without any additional fetches), so set it to null purposfully
|
// we KNOW that we can't get the released time (at least without any additional fetches), so set it to null purposfully
|
||||||
releaseTime: null,
|
releaseTime: null,
|
||||||
@@ -355,7 +343,7 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
// parse chapter text here
|
// parse chapter text here
|
||||||
const result = await fetchApi(`${this.site}/${chapterPath}`);
|
const result = await fetchApi(`${this.site}${chapterPath}`);
|
||||||
const body = await result.text();
|
const body = await result.text();
|
||||||
|
|
||||||
const loadedCheerio = loadCheerio(body);
|
const loadedCheerio = loadCheerio(body);
|
||||||
@@ -408,10 +396,11 @@ class ReLibraryPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
loadedCheerio('article article').each((_i, el) => {
|
loadedCheerio('article article').each((_i, el) => {
|
||||||
const e = loadedCheerio(el);
|
const e = loadedCheerio(el);
|
||||||
if (e.find('a').attr('href') && e.find('a').text()) {
|
const href = e.find('a').attr('href');
|
||||||
|
if (href && e.find('a').text()) {
|
||||||
novels.push({
|
novels.push({
|
||||||
name: e.find('h4').text(),
|
name: e.find('h4').text(),
|
||||||
path: e.find('a').attr('href')?.replace(this.site, '') || '',
|
path: new URL(href, this.site).pathname,
|
||||||
cover:
|
cover:
|
||||||
e.find('img').attr('data-cfsrc') ||
|
e.find('img').attr('data-cfsrc') ||
|
||||||
e.find('img').attr('src') ||
|
e.find('img').attr('src') ||
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { storage } from '@libs/storage';
|
|||||||
class RoyalRoad implements Plugin.PluginBase {
|
class RoyalRoad implements Plugin.PluginBase {
|
||||||
id = 'royalroad';
|
id = 'royalroad';
|
||||||
name = 'Royal Road';
|
name = 'Royal Road';
|
||||||
version = '2.3.0';
|
version = '2.3.1';
|
||||||
icon = 'src/en/royalroad/icon.png';
|
icon = 'src/en/royalroad/icon.png';
|
||||||
site = 'https://www.royalroad.com/';
|
site = 'https://www.royalroad.com/';
|
||||||
|
|
||||||
@@ -415,18 +415,20 @@ class RoyalRoad implements Plugin.PluginBase {
|
|||||||
depth--;
|
depth--;
|
||||||
return;
|
return;
|
||||||
case ParsingState.InNote:
|
case ParsingState.InNote:
|
||||||
const noteClass = `author-note-${isBeforeChapter ? 'before' : 'after'}`;
|
{
|
||||||
const notesHtml = notesHtmlParts.join('').trim();
|
const noteClass = `author-note-${isBeforeChapter ? 'before' : 'after'}`;
|
||||||
const fullNote = `<div class="${noteClass}">${notesHtml}</div>`;
|
const notesHtml = notesHtmlParts.join('').trim();
|
||||||
if (isBeforeChapter) {
|
const fullNote = `<div class="${noteClass}">${notesHtml}</div>`;
|
||||||
beforeNotesParts.push(fullNote);
|
if (isBeforeChapter) {
|
||||||
} else {
|
beforeNotesParts.push(fullNote);
|
||||||
afterNotesParts.push(fullNote);
|
} else {
|
||||||
|
afterNotesParts.push(fullNote);
|
||||||
|
}
|
||||||
|
notesHtmlParts.length = 0;
|
||||||
|
state = ParsingState.Idle;
|
||||||
|
stateDepth = 0;
|
||||||
|
depth--;
|
||||||
}
|
}
|
||||||
notesHtmlParts.length = 0;
|
|
||||||
state = ParsingState.Idle;
|
|
||||||
stateDepth = 0;
|
|
||||||
depth--;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
|
|||||||
+46
-50
@@ -4,26 +4,24 @@ import { defaultCover } from '@libs/defaultCover';
|
|||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
import { load as parseHTML } from 'cheerio';
|
import { load as parseHTML } from 'cheerio';
|
||||||
import dayjs from 'dayjs';
|
import dayjs, { ManipulateType } from 'dayjs';
|
||||||
|
|
||||||
class VyNovel implements Plugin.PluginBase {
|
class VyNovel implements Plugin.PluginBase {
|
||||||
id = 'vynovel';
|
id = 'vynovel';
|
||||||
name = 'VyNovel';
|
name = 'VyNovel';
|
||||||
site = 'https://vynovel.com';
|
site = 'https://vynovel.com';
|
||||||
version = '1.0.0';
|
version = '1.0.1';
|
||||||
icon = 'src/en/vynovel/icon.png';
|
icon = 'src/en/vynovel/icon.png';
|
||||||
|
|
||||||
async fetchNovels(
|
async fetchNovels(
|
||||||
page: number,
|
page: number,
|
||||||
{
|
showLatestNovels?: boolean,
|
||||||
filters,
|
filters?: Plugin.PopularNovelsOptions<typeof this.filters>['filters'],
|
||||||
showLatestNovels,
|
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
|
||||||
searchTerm?: string,
|
searchTerm?: string,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const data = new URLSearchParams({
|
const data = new URLSearchParams({
|
||||||
sort: showLatestNovels ? 'updated_at' : filters?.sort?.value || 'viewed',
|
sort: showLatestNovels ? 'updated_at' : filters?.sort?.value || 'viewed',
|
||||||
page,
|
page: page.toString(),
|
||||||
});
|
});
|
||||||
if (searchTerm) data.append('q', searchTerm);
|
if (searchTerm) data.append('q', searchTerm);
|
||||||
|
|
||||||
@@ -33,7 +31,7 @@ class VyNovel implements Plugin.PluginBase {
|
|||||||
const loadedCheerio = parseHTML(body);
|
const loadedCheerio = parseHTML(body);
|
||||||
|
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
loadedCheerio('div[class="comic-item"] > a').each((index, element) => {
|
loadedCheerio('div[class="comic-item"] > a').each((_, element) => {
|
||||||
const name = loadedCheerio(element)
|
const name = loadedCheerio(element)
|
||||||
.find('div[class="comic-title"]')
|
.find('div[class="comic-title"]')
|
||||||
.text()
|
.text()
|
||||||
@@ -52,17 +50,18 @@ class VyNovel implements Plugin.PluginBase {
|
|||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
popularNovels = this.fetchNovels;
|
async popularNovels(
|
||||||
|
|
||||||
async searchNovels(
|
|
||||||
searchTerm: string,
|
|
||||||
page: number,
|
page: number,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
{
|
||||||
const defaultOptions: any = {
|
showLatestNovels,
|
||||||
filters: undefined,
|
filters,
|
||||||
showLatestNovels: false,
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
};
|
) {
|
||||||
return this.fetchNovels(page, defaultOptions, searchTerm);
|
return this.fetchNovels(page, showLatestNovels, filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchNovels(searchTerm: string, page: number) {
|
||||||
|
return this.fetchNovels(page, false, undefined, searchTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
@@ -102,7 +101,7 @@ class VyNovel implements Plugin.PluginBase {
|
|||||||
chapters.push({
|
chapters.push({
|
||||||
name,
|
name,
|
||||||
path: novelPath + '/' + id,
|
path: novelPath + '/' + id,
|
||||||
releaseTime: this.parseDate(releaseDate?.trim()),
|
releaseTime: this.parseAgoDate(releaseDate),
|
||||||
chapterNumber: totalChapters - chapterIndex,
|
chapterNumber: totalChapters - chapterIndex,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -122,38 +121,35 @@ class VyNovel implements Plugin.PluginBase {
|
|||||||
return chapterText || '';
|
return chapterText || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
parseDate = (date = '') => {
|
private parseAgoDate(date: string | undefined) {
|
||||||
if (!date) return null;
|
//parseMadaraDate
|
||||||
if (date.includes('ago')) {
|
const parsed = dayjs(date);
|
||||||
const [value, type] = date.split(' ');
|
if (date && parsed.isValid()) {
|
||||||
if (!value || !type) return null;
|
return parsed.toISOString();
|
||||||
|
|
||||||
switch (type.toLowerCase()) {
|
|
||||||
case 'minutes': {
|
|
||||||
const minutes = parseInt(value, 10);
|
|
||||||
date = Date.now() - minutes * 60 * 1000;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'hour':
|
|
||||||
case 'hours': {
|
|
||||||
const hours = parseInt(value, 10);
|
|
||||||
date = Date.now() - hours * 60 * 60 * 1000;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'day':
|
|
||||||
case 'days': {
|
|
||||||
const days = parseInt(value, 10);
|
|
||||||
date = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
console.log(date);
|
|
||||||
date = undefined;
|
|
||||||
}
|
|
||||||
return dayjs(date).format('LLL');
|
|
||||||
}
|
}
|
||||||
return date;
|
|
||||||
};
|
const [amt, time, ago] = date?.toLowerCase().trim().split(/\s+/) || [];
|
||||||
|
const decade = time?.includes('decade'); // dayjs no support, but just in case
|
||||||
|
const amount = (amt === 'a' || amt === 'an' ? 1 : +amt) * (decade ? 10 : 1);
|
||||||
|
const unit = (decade ? 'year' : time) as ManipulateType;
|
||||||
|
|
||||||
|
const validUnits = [
|
||||||
|
'millisecond', // waow
|
||||||
|
'second',
|
||||||
|
'minute',
|
||||||
|
'hour',
|
||||||
|
'day',
|
||||||
|
'week',
|
||||||
|
'month',
|
||||||
|
'year',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (ago !== 'ago' || isNaN(amount) || !validUnits.includes(unit)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dayjs().subtract(amount, unit).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||||
this.site + (isNovel ? '/novel/' : '/read/') + path;
|
this.site + (isNovel ? '/novel/' : '/read/') + path;
|
||||||
|
|||||||
+26
-35
@@ -1,14 +1,14 @@
|
|||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { FilterTypes, Filters } from '@libs/filterInputs';
|
import { FilterTypes, Filters } from '@libs/filterInputs';
|
||||||
import { load as parseHTML } from 'cheerio';
|
import { CheerioAPI, load as parseHTML } from 'cheerio';
|
||||||
import { gcm } from '@libs/aes';
|
import { gcm } from '@libs/aes';
|
||||||
|
|
||||||
class WTRLAB implements Plugin.PluginBase {
|
class WTRLAB implements Plugin.PluginBase {
|
||||||
id = 'WTRLAB';
|
id = 'WTRLAB';
|
||||||
name = 'WTR-LAB';
|
name = 'WTR-LAB';
|
||||||
site = 'https://wtr-lab.com/';
|
site = 'https://wtr-lab.com/';
|
||||||
version = '1.1.0';
|
version = '1.1.2';
|
||||||
icon = 'src/en/wtrlab/icon.png';
|
icon = 'src/en/wtrlab/icon.png';
|
||||||
sourceLang = 'en/';
|
sourceLang = 'en/';
|
||||||
|
|
||||||
@@ -116,14 +116,14 @@ class WTRLAB implements Plugin.PluginBase {
|
|||||||
const seenIds = new Set();
|
const seenIds = new Set();
|
||||||
|
|
||||||
const novels: Plugin.NovelItem[] = json.pageProps.series
|
const novels: Plugin.NovelItem[] = json.pageProps.series
|
||||||
.filter((novel: any) => {
|
.filter((novel: Datum) => {
|
||||||
if (seenIds.has(novel.raw_id)) {
|
if (seenIds.has(novel.raw_id)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
seenIds.add(novel.raw_id);
|
seenIds.add(novel.raw_id);
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.map((novel: any) => ({
|
.map((novel: Datum) => ({
|
||||||
name: novel.data.title,
|
name: novel.data.title,
|
||||||
cover: novel.data.image,
|
cover: novel.data.image,
|
||||||
path: `${this.sourceLang}serie-${novel.raw_id}/${novel.slug}`,
|
path: `${this.sourceLang}serie-${novel.raw_id}/${novel.slug}`,
|
||||||
@@ -409,7 +409,7 @@ class WTRLAB implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getKey($: any): Promise<string> {
|
async getKey($: CheerioAPI): Promise<string> {
|
||||||
// Fetch the novel's data in JSON format
|
// Fetch the novel's data in JSON format
|
||||||
const searchKey = 'TextEncoder().encode("';
|
const searchKey = 'TextEncoder().encode("';
|
||||||
|
|
||||||
@@ -426,7 +426,7 @@ class WTRLAB implements Plugin.PluginBase {
|
|||||||
URLs.push(src);
|
URLs.push(src);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let src of URLs) {
|
for (const src of URLs) {
|
||||||
const script = await fetchApi(`${this.site}${src}`);
|
const script = await fetchApi(`${this.site}${src}`);
|
||||||
const raw = await script.text();
|
const raw = await script.text();
|
||||||
index = raw.indexOf(searchKey);
|
index = raw.indexOf(searchKey);
|
||||||
@@ -446,7 +446,7 @@ class WTRLAB implements Plugin.PluginBase {
|
|||||||
async translate(data: string[]): Promise<string[]> {
|
async translate(data: string[]): Promise<string[]> {
|
||||||
const contained = data.map((line, i) => `<a i=${i}>${line}</a>`);
|
const contained = data.map((line, i) => `<a i=${i}>${line}</a>`);
|
||||||
|
|
||||||
let translated: any = await fetchApi(
|
const response = await fetchApi(
|
||||||
'https://translate-pa.googleapis.com/v1/translateHtml',
|
'https://translate-pa.googleapis.com/v1/translateHtml',
|
||||||
{
|
{
|
||||||
'credentials': 'omit',
|
'credentials': 'omit',
|
||||||
@@ -457,11 +457,11 @@ class WTRLAB implements Plugin.PluginBase {
|
|||||||
'X-Goog-API-Key': 'AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520',
|
'X-Goog-API-Key': 'AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520',
|
||||||
},
|
},
|
||||||
'referrer': 'https://wtr-lab.com/',
|
'referrer': 'https://wtr-lab.com/',
|
||||||
'body': `[[${JSON.stringify(contained)},\"zh-CN\",\"en\"],\"te_lib\"]`,
|
'body': `[[${JSON.stringify(contained)},"zh-CN","en"],"te_lib"]`,
|
||||||
'method': 'POST',
|
'method': 'POST',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
translated = await translated.json();
|
const translated = await response.json();
|
||||||
const out = translated && translated[0] ? translated[0] : [];
|
const out = translated && translated[0] ? translated[0] : [];
|
||||||
return out as string[];
|
return out as string[];
|
||||||
}
|
}
|
||||||
@@ -536,15 +536,8 @@ class WTRLAB implements Plugin.PluginBase {
|
|||||||
throw new Error(errorMsg);
|
throw new Error(errorMsg);
|
||||||
}
|
}
|
||||||
let chapterContent = parsedJson.data.data.body;
|
let chapterContent = parsedJson.data.data.body;
|
||||||
let chapterGlossary = {} as JSON;
|
const chapterGlossary: ChapterContent['glossary_data'] | undefined =
|
||||||
if (
|
parsedJson?.data?.data?.glossary_data;
|
||||||
Object.prototype.hasOwnProperty.call(
|
|
||||||
parsedJson.data.data,
|
|
||||||
'glossary_data',
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
chapterGlossary = parsedJson.data.data.glossary_data;
|
|
||||||
}
|
|
||||||
|
|
||||||
let htmlString = '';
|
let htmlString = '';
|
||||||
|
|
||||||
@@ -571,19 +564,14 @@ class WTRLAB implements Plugin.PluginBase {
|
|||||||
htmlString += `<p style="color:darkred;">${eLog}</p>`;
|
htmlString += `<p style="color:darkred;">${eLog}</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let dictionary = [];
|
const dictionary = chapterGlossary?.terms?.map(t => t[0]) || [];
|
||||||
if (Object.prototype.hasOwnProperty.call(chapterGlossary, 'terms')) {
|
|
||||||
dictionary = Object.fromEntries(
|
|
||||||
chapterGlossary.terms.map((definition, index) => [
|
|
||||||
`※${index}⛬`,
|
|
||||||
definition[0],
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let text of chapterContent) {
|
for (let text of chapterContent) {
|
||||||
if (Object.keys(dictionary).length > 0) {
|
if (dictionary.length > 0) {
|
||||||
text = text.replaceAll(/※[0-9]+⛬/g, m => dictionary[m]);
|
text = text.replaceAll(
|
||||||
|
/(?:wtr-lab\s+)?※([0-9]+)[⛬〓]/g,
|
||||||
|
(m: string, index: string) => dictionary[parseInt(index)] || m,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
htmlString += `<p>${text}</p>`;
|
htmlString += `<p>${text}</p>`;
|
||||||
}
|
}
|
||||||
@@ -1716,18 +1704,21 @@ type ApiChapter = {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GlossaryTerm = {
|
// type GlossaryTerm = {
|
||||||
index: number;
|
// index: number;
|
||||||
english: string;
|
// english: string;
|
||||||
chinese: string;
|
// chinese: string;
|
||||||
symbol: string;
|
// symbol: string;
|
||||||
};
|
// };
|
||||||
type ChapterData = {
|
type ChapterData = {
|
||||||
data: ChapterContent;
|
data: ChapterContent;
|
||||||
};
|
};
|
||||||
type ChapterContent = {
|
type ChapterContent = {
|
||||||
title: string;
|
title: string;
|
||||||
body: string;
|
body: string;
|
||||||
|
glossary_data?: {
|
||||||
|
terms: string[][];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type SerieData = {
|
type SerieData = {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class NovelDeGlacePlugin implements Plugin.PluginBase {
|
|||||||
name = 'NovelDeGlace';
|
name = 'NovelDeGlace';
|
||||||
icon = 'src/fr/noveldeglace/icon.png';
|
icon = 'src/fr/noveldeglace/icon.png';
|
||||||
site = 'https://noveldeglace.com/';
|
site = 'https://noveldeglace.com/';
|
||||||
version = '1.0.4';
|
version = '1.0.5';
|
||||||
|
|
||||||
async getCheerio(url: string): Promise<CheerioAPI | undefined> {
|
async getCheerio(url: string): Promise<CheerioAPI | undefined> {
|
||||||
const r = await fetchApi(url, {
|
const r = await fetchApi(url, {
|
||||||
@@ -86,10 +86,7 @@ class NovelDeGlacePlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
const novel: Plugin.SourceNovel = { path: novelPath, name: 'Untitled' };
|
const novel: Plugin.SourceNovel = { path: novelPath, name: 'Untitled' };
|
||||||
|
|
||||||
novel.name =
|
novel.name = $('span.current').text().trim();
|
||||||
(
|
|
||||||
$('div.entry-content > div > strong')[0].nextSibling as string | null
|
|
||||||
)?.nodeValue?.trim() || 'Untitled';
|
|
||||||
|
|
||||||
novel.cover = $('.su-row > div > div > img').attr('src') || defaultCover;
|
novel.cover = $('.su-row > div > div > img').attr('src') || defaultCover;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Cheerio, CheerioAPI, load, Element } from 'cheerio';
|
import { Cheerio, CheerioAPI, load } from 'cheerio';
|
||||||
|
import { Element } from 'domhandler';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { defaultCover } from '@libs/defaultCover';
|
import { defaultCover } from '@libs/defaultCover';
|
||||||
@@ -9,12 +10,12 @@ class XiaowazPlugin implements Plugin.PluginBase {
|
|||||||
name = 'Xiaowaz';
|
name = 'Xiaowaz';
|
||||||
icon = 'src/fr/xiaowaz/icon.png';
|
icon = 'src/fr/xiaowaz/icon.png';
|
||||||
site = 'https://xiaowaz.fr';
|
site = 'https://xiaowaz.fr';
|
||||||
version = '1.0.1';
|
version = '1.0.2';
|
||||||
static novels: Plugin.NovelItem[] | undefined;
|
static novels: Plugin.NovelItem[] | undefined;
|
||||||
|
|
||||||
async getCheerio(url: string): Promise<CheerioAPI> {
|
async getCheerio(url: string): Promise<CheerioAPI> {
|
||||||
let retries = 5; // when fetching for images the sites sometimes terminates the connection
|
let retries = 5; // when fetching for images the sites sometimes terminates the connection
|
||||||
let returnError: any;
|
let returnError: unknown;
|
||||||
while (retries > 0) {
|
while (retries > 0) {
|
||||||
try {
|
try {
|
||||||
const r = await fetchApi(url);
|
const r = await fetchApi(url);
|
||||||
@@ -29,7 +30,9 @@ class XiaowazPlugin implements Plugin.PluginBase {
|
|||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(returnError ? returnError : 'Error fetching the page');
|
throw new Error(
|
||||||
|
returnError ? String(returnError) : 'Error fetching the page',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllNovels(): Promise<Plugin.NovelItem[]> {
|
async getAllNovels(): Promise<Plugin.NovelItem[]> {
|
||||||
|
|||||||
+233
-235
@@ -1,250 +1,249 @@
|
|||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import p_0 from '@plugins/arabic/ArNovel[madara]';
|
import p_0 from '@plugins/arabic/ArNovel[madara]';
|
||||||
import p_1 from '@plugins/arabic/Azora[madara]';
|
import p_1 from '@plugins/arabic/Azora[madara]';
|
||||||
import p_2 from '@plugins/arabic/dilartube';
|
import p_2 from '@plugins/arabic/FreeKolNovel[lightnovelwp]';
|
||||||
import p_3 from '@plugins/arabic/FreeKolNovel[lightnovelwp]';
|
import p_3 from '@plugins/arabic/HizoManga[madara]';
|
||||||
import p_4 from '@plugins/arabic/HizoManga[madara]';
|
import p_4 from '@plugins/arabic/KolNovel[lightnovelwp]';
|
||||||
import p_5 from '@plugins/arabic/KolNovel[lightnovelwp]';
|
import p_5 from '@plugins/arabic/Markazriwayat[madara]';
|
||||||
import p_6 from '@plugins/arabic/Markazriwayat[madara]';
|
import p_6 from '@plugins/arabic/Novel4Up[madara]';
|
||||||
import p_7 from '@plugins/arabic/Novel4Up[madara]';
|
import p_7 from '@plugins/arabic/NovelsParadise[lightnovelwp]';
|
||||||
import p_8 from '@plugins/arabic/NovelsParadise[lightnovelwp]';
|
import p_8 from '@plugins/arabic/Olaoecyou[madara]';
|
||||||
import p_9 from '@plugins/arabic/Olaoecyou[madara]';
|
import p_9 from '@plugins/arabic/Riwyat[madara]';
|
||||||
import p_10 from '@plugins/arabic/rewayatclub';
|
import p_10 from '@plugins/arabic/dilartube';
|
||||||
import p_11 from '@plugins/arabic/Riwyat[madara]';
|
import p_11 from '@plugins/arabic/rewayatclub';
|
||||||
import p_12 from '@plugins/arabic/sunovels';
|
import p_12 from '@plugins/arabic/sunovels';
|
||||||
import p_13 from '@plugins/chinese/69shu';
|
import p_13 from '@plugins/chinese/69shu';
|
||||||
import p_14 from '@plugins/chinese/ixdzs8';
|
import p_14 from '@plugins/chinese/Quanben';
|
||||||
import p_15 from '@plugins/chinese/linovel';
|
import p_15 from '@plugins/chinese/ixdzs8';
|
||||||
import p_16 from '@plugins/chinese/linovelib';
|
import p_16 from '@plugins/chinese/linovel';
|
||||||
import p_17 from '@plugins/chinese/linovelib_tw';
|
import p_17 from '@plugins/chinese/linovelib';
|
||||||
import p_18 from '@plugins/chinese/novel543';
|
import p_18 from '@plugins/chinese/linovelib_tw';
|
||||||
import p_19 from '@plugins/chinese/Quanben';
|
import p_19 from '@plugins/chinese/novel543';
|
||||||
import p_20 from '@plugins/english/AllNovelFull[readnovelfull]';
|
import p_20 from '@plugins/english/AllNovelFull[readnovelfull]';
|
||||||
import p_21 from '@plugins/english/AllNovel[readnovelfull]';
|
import p_21 from '@plugins/english/AllNovel[readnovelfull]';
|
||||||
import p_22 from '@plugins/english/ao3';
|
import p_22 from '@plugins/english/ArcaneTranslations[lightnovelwp]';
|
||||||
import p_23 from '@plugins/english/ArcaneTranslations[lightnovelwp]';
|
import p_23 from '@plugins/english/BelleReservoir[madara]';
|
||||||
import p_24 from '@plugins/english/BelleReservoir[madara]';
|
import p_24 from '@plugins/english/BoxNovel[readnovelfull]';
|
||||||
import p_25 from '@plugins/english/BoxNovel[readnovelfull]';
|
import p_25 from '@plugins/english/CPUnovel[lightnovelwp]';
|
||||||
import p_26 from '@plugins/english/chrysanthemumgarden';
|
import p_26 from '@plugins/english/CitrusAurora[madara]';
|
||||||
import p_27 from '@plugins/english/CitrusAurora[madara]';
|
import p_27 from '@plugins/english/CoralBoutique[madara]';
|
||||||
import p_28 from '@plugins/english/CoralBoutique[madara]';
|
import p_28 from '@plugins/english/DaoNovel[madara]';
|
||||||
import p_29 from '@plugins/english/CPUnovel[lightnovelwp]';
|
import p_29 from '@plugins/english/DaoTranslate[lightnovelwp]';
|
||||||
import p_30 from '@plugins/english/crimsonscrolls';
|
import p_30 from '@plugins/english/DaoistQuest[fictioneer]';
|
||||||
import p_31 from '@plugins/english/DaoistQuest[fictioneer]';
|
import p_31 from '@plugins/english/DearestRosalie[fictioneer]';
|
||||||
import p_32 from '@plugins/english/DaoNovel[madara]';
|
import p_32 from '@plugins/english/DragonTea[madara]';
|
||||||
import p_33 from '@plugins/english/DaoTranslate[lightnovelwp]';
|
import p_33 from '@plugins/english/Dragonholic[madara]';
|
||||||
import p_34 from '@plugins/english/DearestRosalie[fictioneer]';
|
import p_34 from '@plugins/english/DuskBlossoms[madara]';
|
||||||
import p_35 from '@plugins/english/divinedaolibrary';
|
import p_35 from '@plugins/english/ElloTL[lightnovelwp]';
|
||||||
import p_36 from '@plugins/english/Dragonholic[madara]';
|
import p_36 from '@plugins/english/Eternalune[madara]';
|
||||||
import p_37 from '@plugins/english/DragonTea[madara]';
|
import p_37 from '@plugins/english/EtudeTranslations[madara]';
|
||||||
import p_38 from '@plugins/english/dreambigtl';
|
import p_38 from '@plugins/english/FanNovel[readwn]';
|
||||||
import p_39 from '@plugins/english/DuskBlossoms[madara]';
|
import p_39 from '@plugins/english/FansMTL[readwn]';
|
||||||
import p_40 from '@plugins/english/ElloTL[lightnovelwp]';
|
import p_40 from '@plugins/english/FansTranslations[madara]';
|
||||||
import p_41 from '@plugins/english/Eternalune[madara]';
|
import p_41 from '@plugins/english/FirstKissNovel[madara]';
|
||||||
import p_42 from '@plugins/english/EtudeTranslations[madara]';
|
import p_42 from '@plugins/english/Foxaholic[madara]';
|
||||||
import p_43 from '@plugins/english/FanNovel[readwn]';
|
import p_43 from '@plugins/english/FreeWebNovel[readnovelfull]';
|
||||||
import p_44 from '@plugins/english/FansMTL[readwn]';
|
import p_44 from '@plugins/english/GalaxyTranslations[madara]';
|
||||||
import p_45 from '@plugins/english/FansTranslations[madara]';
|
import p_45 from '@plugins/english/Guavaread[madara]';
|
||||||
import p_46 from '@plugins/english/faqwikius';
|
import p_46 from '@plugins/english/HiraethTranslation[madara]';
|
||||||
import p_47 from '@plugins/english/fenrirrealm';
|
import p_47 from '@plugins/english/HotNovelPub[hotnovelpub]';
|
||||||
import p_48 from '@plugins/english/fictionzone';
|
import p_48 from '@plugins/english/Ippotranslations[lightnovelwp]';
|
||||||
import p_49 from '@plugins/english/FirstKissNovel[madara]';
|
import p_49 from '@plugins/english/KDTNovels[lightnovelwp]';
|
||||||
import p_50 from '@plugins/english/Foxaholic[madara]';
|
import p_50 from '@plugins/english/KeopiTranslations[lightnovelwp]';
|
||||||
import p_51 from '@plugins/english/foxteller';
|
import p_51 from '@plugins/english/KnoxT[lightnovelwp]';
|
||||||
import p_52 from '@plugins/english/FreeWebNovel[readnovelfull]';
|
import p_52 from '@plugins/english/LazyGirlTranslations[lightnovelwp]';
|
||||||
import p_53 from '@plugins/english/GalaxyTranslations[madara]';
|
import p_53 from '@plugins/english/LibRead[readnovelfull]';
|
||||||
import p_54 from '@plugins/english/genesis';
|
import p_54 from '@plugins/english/LightNovelCave[lightnovelworld]';
|
||||||
import p_55 from '@plugins/english/Guavaread[madara]';
|
import p_55 from '@plugins/english/LightNovelHeaven[madara]';
|
||||||
import p_56 from '@plugins/english/HiraethTranslation[madara]';
|
import p_56 from '@plugins/english/LightNovelPlus[readnovelfull]';
|
||||||
import p_57 from '@plugins/english/HotNovelPub[hotnovelpub]';
|
import p_57 from '@plugins/english/LightNovelPubVip[lightnovelworld]';
|
||||||
import p_58 from '@plugins/english/indraTranslations';
|
import p_58 from '@plugins/english/LightNovelUpdates[madara]';
|
||||||
import p_59 from '@plugins/english/inkitt';
|
import p_59 from '@plugins/english/LilyontheValley[fictioneer]';
|
||||||
import p_60 from '@plugins/english/inoveltranslation';
|
import p_60 from '@plugins/english/Ltnovel[readwn]';
|
||||||
import p_61 from '@plugins/english/Ippotranslations[lightnovelwp]';
|
import p_61 from '@plugins/english/LulloBox[madara]';
|
||||||
import p_62 from '@plugins/english/KDTNovels[lightnovelwp]';
|
import p_62 from '@plugins/english/LunarLetters[madara]';
|
||||||
import p_63 from '@plugins/english/KeopiTranslations[lightnovelwp]';
|
import p_63 from '@plugins/english/MTLNovel[madara]';
|
||||||
import p_64 from '@plugins/english/KnoxT[lightnovelwp]';
|
import p_64 from '@plugins/english/MTLNovel[mtlnovel]';
|
||||||
import p_65 from '@plugins/english/LazyGirlTranslations[lightnovelwp]';
|
import p_65 from '@plugins/english/Meownovel[madara]';
|
||||||
import p_66 from '@plugins/english/leafstudio';
|
import p_66 from '@plugins/english/MoonlightNovels[lightnovelwp]';
|
||||||
import p_67 from '@plugins/english/LibRead[readnovelfull]';
|
import p_67 from '@plugins/english/MostNovel[madara]';
|
||||||
import p_68 from '@plugins/english/LightNovelCave[lightnovelworld]';
|
import p_68 from '@plugins/english/MysticalSeries[madara]';
|
||||||
import p_69 from '@plugins/english/LightNovelHeaven[madara]';
|
import p_69 from '@plugins/english/NeoSekaiTranslations[madara]';
|
||||||
import p_70 from '@plugins/english/LightNovelPlus[readnovelfull]';
|
import p_70 from '@plugins/english/NitroManga[madara]';
|
||||||
import p_71 from '@plugins/english/LightNovelPubVip[lightnovelworld]';
|
import p_71 from '@plugins/english/NobleMTL[lightnovelwp]';
|
||||||
import p_72 from '@plugins/english/lightnoveltranslation';
|
import p_72 from '@plugins/english/NoiceTranslations[madara]';
|
||||||
import p_73 from '@plugins/english/LightNovelUpdates[madara]';
|
import p_73 from '@plugins/english/NovelBin[readnovelfull]';
|
||||||
import p_74 from '@plugins/english/LilyontheValley[fictioneer]';
|
import p_74 from '@plugins/english/NovelCool[novelcool]';
|
||||||
import p_75 from '@plugins/english/lnmtl';
|
import p_75 from '@plugins/english/NovelFull[readnovelfull]';
|
||||||
import p_76 from '@plugins/english/Ltnovel[readwn]';
|
import p_76 from '@plugins/english/NovelLib[fictioneer]';
|
||||||
import p_77 from '@plugins/english/LulloBox[madara]';
|
import p_77 from '@plugins/english/NovelMultiverse[madara]';
|
||||||
import p_78 from '@plugins/english/LunarLetters[madara]';
|
import p_78 from '@plugins/english/NovelNinja[madara]';
|
||||||
import p_79 from '@plugins/english/Meownovel[madara]';
|
import p_79 from '@plugins/english/NovelOnline';
|
||||||
import p_80 from '@plugins/english/MoonlightNovels[lightnovelwp]';
|
import p_80 from '@plugins/english/NovelTranslate[madara]';
|
||||||
import p_81 from '@plugins/english/MostNovel[madara]';
|
import p_81 from '@plugins/english/NovelsKnight[lightnovelwp]';
|
||||||
import p_82 from '@plugins/english/MTLNovel[madara]';
|
import p_82 from '@plugins/english/PandaMachineTranslations[lightnovelwp]';
|
||||||
import p_83 from '@plugins/english/MTLNovel[mtlnovel]';
|
import p_83 from '@plugins/english/PastelTales[madara]';
|
||||||
import p_84 from '@plugins/english/mtlreader';
|
import p_84 from '@plugins/english/PenguinSquad[fictioneer]';
|
||||||
import p_85 from '@plugins/english/mvlempyr';
|
import p_85 from '@plugins/english/Prizma[fictioneer]';
|
||||||
import p_86 from '@plugins/english/MysticalSeries[madara]';
|
import p_86 from '@plugins/english/Ranobes[ranobes]';
|
||||||
import p_87 from '@plugins/english/NeoSekaiTranslations[madara]';
|
import p_87 from '@plugins/english/Ranovel[madara]';
|
||||||
import p_88 from '@plugins/english/NitroManga[madara]';
|
import p_88 from '@plugins/english/ReadFanfic[madara]';
|
||||||
import p_89 from '@plugins/english/NobleMTL[lightnovelwp]';
|
import p_89 from '@plugins/english/ReadNovelFull[readnovelfull]';
|
||||||
import p_90 from '@plugins/english/NoiceTranslations[madara]';
|
import p_90 from '@plugins/english/RequiemTranslations[lightnovelwp]';
|
||||||
import p_91 from '@plugins/english/NovelBin[readnovelfull]';
|
import p_91 from '@plugins/english/SalmonLatte[madara]';
|
||||||
import p_92 from '@plugins/english/novelbuddy';
|
import p_92 from '@plugins/english/SleepyTranslations[madara]';
|
||||||
import p_93 from '@plugins/english/NovelCool[novelcool]';
|
import p_93 from '@plugins/english/SonicMTL[madara]';
|
||||||
import p_94 from '@plugins/english/novelfire';
|
import p_94 from '@plugins/english/SrankManga[madara]';
|
||||||
import p_95 from '@plugins/english/NovelFull[readnovelfull]';
|
import p_95 from '@plugins/english/StorySeedling';
|
||||||
import p_96 from '@plugins/english/novelhall';
|
import p_96 from '@plugins/english/SweetEscape[madara]';
|
||||||
import p_97 from '@plugins/english/novelhi';
|
import p_97 from '@plugins/english/SystemTranslation[lightnovelwp]';
|
||||||
import p_98 from '@plugins/english/novelight';
|
import p_98 from '@plugins/english/TranslatinOtaku[madara]';
|
||||||
import p_99 from '@plugins/english/NovelLib[fictioneer]';
|
import p_99 from '@plugins/english/TranslationWeaver[lightnovelwp]';
|
||||||
import p_100 from '@plugins/english/NovelMultiverse[madara]';
|
import p_100 from '@plugins/english/UniversalNovel[lightnovelwp]';
|
||||||
import p_101 from '@plugins/english/NovelNinja[madara]';
|
import p_101 from '@plugins/english/VandyTranslate[lightnovelwp]';
|
||||||
import p_102 from '@plugins/english/NovelOnline';
|
import p_102 from '@plugins/english/VioletLily[madara]';
|
||||||
import p_103 from '@plugins/english/novelrest';
|
import p_103 from '@plugins/english/WebNovelLover[madara]';
|
||||||
import p_104 from '@plugins/english/NovelsKnight[lightnovelwp]';
|
import p_104 from '@plugins/english/WebNovelPub[lightnovelworld]';
|
||||||
import p_105 from '@plugins/english/NovelTranslate[madara]';
|
import p_105 from '@plugins/english/WebNovelTranslation[madara]';
|
||||||
import p_106 from '@plugins/english/novelupdates';
|
import p_106 from '@plugins/english/WhiteMoonlightNovels[lightnovelwp]';
|
||||||
import p_107 from '@plugins/english/PandaMachineTranslations[lightnovelwp]';
|
import p_107 from '@plugins/english/WooksTeahouse[madara]';
|
||||||
import p_108 from '@plugins/english/PastelTales[madara]';
|
import p_108 from '@plugins/english/WordExcerpt[madara]';
|
||||||
import p_109 from '@plugins/english/pawread';
|
import p_109 from '@plugins/english/WuxiaSpace[readwn]';
|
||||||
import p_110 from '@plugins/english/PenguinSquad[fictioneer]';
|
import p_110 from '@plugins/english/WuxiaV[readwn]';
|
||||||
import p_111 from '@plugins/english/Prizma[fictioneer]';
|
import p_111 from '@plugins/english/WuxiaWorldSite[madara]';
|
||||||
import p_112 from '@plugins/english/rainofsnow';
|
import p_112 from '@plugins/english/Wuxiabox[readwn]';
|
||||||
import p_113 from '@plugins/english/Ranobes[ranobes]';
|
import p_113 from '@plugins/english/Wuxiafox[readwn]';
|
||||||
import p_114 from '@plugins/english/Ranovel[madara]';
|
import p_114 from '@plugins/english/ZetroTranslation[madara]';
|
||||||
import p_115 from '@plugins/english/ReadFanfic[madara]';
|
import p_115 from '@plugins/english/ao3';
|
||||||
import p_116 from '@plugins/english/readfrom';
|
import p_116 from '@plugins/english/chrysanthemumgarden';
|
||||||
import p_117 from '@plugins/english/ReadNovelFull[readnovelfull]';
|
import p_117 from '@plugins/english/crimsonscrolls';
|
||||||
import p_118 from '@plugins/english/relibrary';
|
import p_118 from '@plugins/english/divinedaolibrary';
|
||||||
import p_119 from '@plugins/english/RequiemTranslations[lightnovelwp]';
|
import p_119 from '@plugins/english/dreambigtl';
|
||||||
import p_120 from '@plugins/english/royalroad';
|
import p_120 from '@plugins/english/faqwikius';
|
||||||
import p_121 from '@plugins/english/SalmonLatte[madara]';
|
import p_121 from '@plugins/english/fenrirrealm';
|
||||||
import p_122 from '@plugins/english/scribblehub';
|
import p_122 from '@plugins/english/fictionzone';
|
||||||
import p_123 from '@plugins/english/SleepyTranslations[madara]';
|
import p_123 from '@plugins/english/foxteller';
|
||||||
import p_124 from '@plugins/english/SonicMTL[madara]';
|
import p_124 from '@plugins/english/genesis';
|
||||||
import p_125 from '@plugins/english/SrankManga[madara]';
|
import p_125 from '@plugins/english/indraTranslations';
|
||||||
import p_126 from '@plugins/english/StorySeedling';
|
import p_126 from '@plugins/english/inkitt';
|
||||||
import p_127 from '@plugins/english/SweetEscape[madara]';
|
import p_127 from '@plugins/english/inoveltranslation';
|
||||||
import p_128 from '@plugins/english/SystemTranslation[lightnovelwp]';
|
import p_128 from '@plugins/english/leafstudio';
|
||||||
import p_129 from '@plugins/english/TranslatinOtaku[madara]';
|
import p_129 from '@plugins/english/lightnoveltranslation';
|
||||||
import p_130 from '@plugins/english/TranslationWeaver[lightnovelwp]';
|
import p_130 from '@plugins/english/lnmtl';
|
||||||
import p_131 from '@plugins/english/UniversalNovel[lightnovelwp]';
|
import p_131 from '@plugins/english/mvlempyr';
|
||||||
import p_132 from '@plugins/english/VandyTranslate[lightnovelwp]';
|
import p_132 from '@plugins/english/novelbuddy';
|
||||||
import p_133 from '@plugins/english/VioletLily[madara]';
|
import p_133 from '@plugins/english/novelfire';
|
||||||
import p_134 from '@plugins/english/vynovel';
|
import p_134 from '@plugins/english/novelhall';
|
||||||
import p_135 from '@plugins/english/wct';
|
import p_135 from '@plugins/english/novelhi';
|
||||||
import p_136 from '@plugins/english/webnovel';
|
import p_136 from '@plugins/english/novelight';
|
||||||
import p_137 from '@plugins/english/WebNovelLover[madara]';
|
import p_137 from '@plugins/english/novelrest';
|
||||||
import p_138 from '@plugins/english/WebNovelPub[lightnovelworld]';
|
import p_138 from '@plugins/english/novelupdates';
|
||||||
import p_139 from '@plugins/english/WebNovelTranslation[madara]';
|
import p_139 from '@plugins/english/pawread';
|
||||||
import p_140 from '@plugins/english/WhiteMoonlightNovels[lightnovelwp]';
|
import p_140 from '@plugins/english/rainofsnow';
|
||||||
import p_141 from '@plugins/english/WooksTeahouse[madara]';
|
import p_141 from '@plugins/english/readfrom';
|
||||||
import p_142 from '@plugins/english/WordExcerpt[madara]';
|
import p_142 from '@plugins/english/relibrary';
|
||||||
import p_143 from '@plugins/english/wtrlab';
|
import p_143 from '@plugins/english/royalroad';
|
||||||
import p_144 from '@plugins/english/Wuxiabox[readwn]';
|
import p_144 from '@plugins/english/scribblehub';
|
||||||
import p_145 from '@plugins/english/Wuxiafox[readwn]';
|
import p_145 from '@plugins/english/vynovel';
|
||||||
import p_146 from '@plugins/english/WuxiaSpace[readwn]';
|
import p_146 from '@plugins/english/wct';
|
||||||
import p_147 from '@plugins/english/WuxiaV[readwn]';
|
import p_147 from '@plugins/english/webnovel';
|
||||||
import p_148 from '@plugins/english/wuxiaworld';
|
import p_148 from '@plugins/english/wtrlab';
|
||||||
import p_149 from '@plugins/english/WuxiaWorldSite[madara]';
|
import p_149 from '@plugins/english/wuxiaworld';
|
||||||
import p_150 from '@plugins/english/ZetroTranslation[madara]';
|
import p_150 from '@plugins/french/LighNovelFR[lightnovelwp]';
|
||||||
import p_151 from '@plugins/french/chireads';
|
import p_151 from '@plugins/french/MTLNovel(FR)[mtlnovel]';
|
||||||
import p_152 from '@plugins/french/harkeneliwood';
|
import p_152 from '@plugins/french/MassNovel[madara]';
|
||||||
import p_153 from '@plugins/french/kisswood';
|
import p_153 from '@plugins/french/WorldNovel[madara]';
|
||||||
import p_154 from '@plugins/french/LighNovelFR[lightnovelwp]';
|
import p_154 from '@plugins/french/chireads';
|
||||||
import p_155 from '@plugins/french/MassNovel[madara]';
|
import p_155 from '@plugins/french/harkeneliwood';
|
||||||
import p_156 from '@plugins/french/MTLNovel(FR)[mtlnovel]';
|
import p_156 from '@plugins/french/kisswood';
|
||||||
import p_157 from '@plugins/french/noveldeglace';
|
import p_157 from '@plugins/french/noveldeglace';
|
||||||
import p_158 from '@plugins/french/novhell';
|
import p_158 from '@plugins/french/novhell';
|
||||||
import p_159 from '@plugins/french/warriorlegendtrad';
|
import p_159 from '@plugins/french/warriorlegendtrad';
|
||||||
import p_160 from '@plugins/french/WorldNovel[madara]';
|
import p_160 from '@plugins/french/wuxialnscantrad';
|
||||||
import p_161 from '@plugins/french/wuxialnscantrad';
|
import p_161 from '@plugins/french/xiaowaz';
|
||||||
import p_162 from '@plugins/french/xiaowaz';
|
import p_162 from '@plugins/indonesian/BacaLightNovel[lightnovelwp]';
|
||||||
import p_163 from '@plugins/indonesian/BacaLightNovel[lightnovelwp]';
|
import p_163 from '@plugins/indonesian/MTLNovel(ID)[mtlnovel]';
|
||||||
import p_164 from '@plugins/indonesian/indowebnovel';
|
import p_164 from '@plugins/indonesian/MeioNovel[madara]';
|
||||||
import p_165 from '@plugins/indonesian/MeioNovel[madara]';
|
import p_165 from '@plugins/indonesian/NovelBookID[madara]';
|
||||||
import p_166 from '@plugins/indonesian/MTLNovel(ID)[mtlnovel]';
|
import p_166 from '@plugins/indonesian/SekteNovel[lightnovelwp]';
|
||||||
import p_167 from '@plugins/indonesian/NovelBookID[madara]';
|
import p_167 from '@plugins/indonesian/Vanovel[madara]';
|
||||||
import p_168 from '@plugins/indonesian/sakuranovel';
|
import p_168 from '@plugins/indonesian/WBNovel[madara]';
|
||||||
import p_169 from '@plugins/indonesian/SekteNovel[lightnovelwp]';
|
import p_169 from '@plugins/indonesian/indowebnovel';
|
||||||
import p_170 from '@plugins/indonesian/Vanovel[madara]';
|
import p_170 from '@plugins/indonesian/sakuranovel';
|
||||||
import p_171 from '@plugins/indonesian/WBNovel[madara]';
|
import p_171 from '@plugins/japanese/Syosetu';
|
||||||
import p_172 from '@plugins/japanese/kakuyomu';
|
import p_172 from '@plugins/japanese/kakuyomu';
|
||||||
import p_173 from '@plugins/japanese/Syosetu';
|
import p_173 from '@plugins/korean/Agitoon';
|
||||||
import p_174 from '@plugins/korean/Agitoon';
|
import p_174 from '@plugins/korean/FortuneEternal[madara]';
|
||||||
import p_175 from '@plugins/korean/FortuneEternal[madara]';
|
import p_175 from '@plugins/multi/komga';
|
||||||
import p_176 from '@plugins/multi/komga';
|
import p_176 from '@plugins/polish/novelki';
|
||||||
import p_177 from '@plugins/polish/novelki';
|
import p_177 from '@plugins/portuguese/BetterNovels[lightnovelwp]';
|
||||||
import p_178 from '@plugins/portuguese/BetterNovels[lightnovelwp]';
|
import p_178 from '@plugins/portuguese/CentralNovel[lightnovelwp]';
|
||||||
import p_179 from '@plugins/portuguese/blogdoamonnovels';
|
import p_179 from '@plugins/portuguese/Kiniga[madara]';
|
||||||
import p_180 from '@plugins/portuguese/CentralNovel[lightnovelwp]';
|
import p_180 from '@plugins/portuguese/LaNovels[hotnovelpub]';
|
||||||
import p_181 from '@plugins/portuguese/illusia';
|
import p_181 from '@plugins/portuguese/LightNovelBrasil[lightnovelwp]';
|
||||||
import p_182 from '@plugins/portuguese/Kiniga[madara]';
|
import p_182 from '@plugins/portuguese/MTLNovel(PT)[mtlnovel]';
|
||||||
import p_183 from '@plugins/portuguese/LaNovels[hotnovelpub]';
|
import p_183 from '@plugins/portuguese/blogdoamonnovels';
|
||||||
import p_184 from '@plugins/portuguese/LightNovelBrasil[lightnovelwp]';
|
import p_184 from '@plugins/portuguese/illusia';
|
||||||
import p_185 from '@plugins/portuguese/MTLNovel(PT)[mtlnovel]';
|
import p_185 from '@plugins/portuguese/novelmania';
|
||||||
import p_186 from '@plugins/portuguese/novelmania';
|
import p_186 from '@plugins/portuguese/tsundoku';
|
||||||
import p_187 from '@plugins/portuguese/tsundoku';
|
import p_187 from '@plugins/russian/Bllate[rulate]';
|
||||||
import p_188 from '@plugins/russian/authortoday';
|
import p_188 from '@plugins/russian/Bookhamster[ifreedom]';
|
||||||
import p_189 from '@plugins/russian/Bllate[rulate]';
|
import p_189 from '@plugins/russian/Erolate[rulate]';
|
||||||
import p_190 from '@plugins/russian/Bookhamster[ifreedom]';
|
import p_190 from '@plugins/russian/EzNovels[hotnovelpub]';
|
||||||
import p_191 from '@plugins/russian/bookriver';
|
import p_191 from '@plugins/russian/MTLNovel(RU)[mtlnovel]';
|
||||||
import p_192 from '@plugins/russian/Erolate[rulate]';
|
import p_192 from '@plugins/russian/NovelCool(RU)[novelcool]';
|
||||||
import p_193 from '@plugins/russian/EzNovels[hotnovelpub]';
|
import p_193 from '@plugins/russian/Ranobes(RU)[ranobes]';
|
||||||
import p_194 from '@plugins/russian/ficbook';
|
import p_194 from '@plugins/russian/Rulate[rulate]';
|
||||||
import p_195 from '@plugins/russian/jaomix';
|
import p_195 from '@plugins/russian/authortoday';
|
||||||
import p_196 from '@plugins/russian/MTLNovel(RU)[mtlnovel]';
|
import p_196 from '@plugins/russian/bookriver';
|
||||||
import p_197 from '@plugins/russian/neobook';
|
import p_197 from '@plugins/russian/ficbook';
|
||||||
import p_198 from '@plugins/russian/NovelCool(RU)[novelcool]';
|
import p_198 from '@plugins/russian/jaomix';
|
||||||
import p_199 from '@plugins/russian/novelTL';
|
import p_199 from '@plugins/russian/neobook';
|
||||||
import p_200 from '@plugins/russian/ranobehub';
|
import p_200 from '@plugins/russian/novelTL';
|
||||||
import p_201 from '@plugins/russian/ranobelib';
|
import p_201 from '@plugins/russian/ranobehub';
|
||||||
import p_202 from '@plugins/russian/ranoberf';
|
import p_202 from '@plugins/russian/ranobelib';
|
||||||
import p_203 from '@plugins/russian/Ranobes(RU)[ranobes]';
|
import p_203 from '@plugins/russian/ranoberf';
|
||||||
import p_204 from '@plugins/russian/renovels';
|
import p_204 from '@plugins/russian/renovels';
|
||||||
import p_205 from '@plugins/russian/Rulate[rulate]';
|
import p_205 from '@plugins/russian/topliba';
|
||||||
import p_206 from '@plugins/russian/topliba';
|
import p_206 from '@plugins/russian/zelluloza';
|
||||||
import p_207 from '@plugins/russian/zelluloza';
|
import p_207 from '@plugins/russian/СвободныйМирРанобэ[ifreedom]';
|
||||||
import p_208 from '@plugins/russian/СвободныйМирРанобэ[ifreedom]';
|
import p_208 from '@plugins/spanish/AllNovelRead[lightnovelwp]';
|
||||||
import p_209 from '@plugins/spanish/AllNovelRead[lightnovelwp]';
|
import p_209 from '@plugins/spanish/AnimesHoy12[madara]';
|
||||||
import p_210 from '@plugins/spanish/AnimesHoy12[madara]';
|
import p_210 from '@plugins/spanish/LightNovelDaily[hotnovelpub]';
|
||||||
import p_211 from '@plugins/spanish/hasutl';
|
import p_211 from '@plugins/spanish/MTLNovel(ES)[mtlnovel]';
|
||||||
import p_212 from '@plugins/spanish/LightNovelDaily[hotnovelpub]';
|
import p_212 from '@plugins/spanish/NOVA';
|
||||||
import p_213 from '@plugins/spanish/MTLNovel(ES)[mtlnovel]';
|
import p_213 from '@plugins/spanish/PanchoTranslations[madara]';
|
||||||
import p_214 from '@plugins/spanish/NOVA';
|
import p_214 from '@plugins/spanish/TC&Sega[lightnovelwp]';
|
||||||
import p_215 from '@plugins/spanish/novelasligera';
|
import p_215 from '@plugins/spanish/TraduccionesAmistosas[madara]';
|
||||||
import p_216 from '@plugins/spanish/novelawuxia';
|
import p_216 from '@plugins/spanish/hasutl';
|
||||||
import p_217 from '@plugins/spanish/novelyra';
|
import p_217 from '@plugins/spanish/novelasligera';
|
||||||
import p_218 from '@plugins/spanish/oasistranslations';
|
import p_218 from '@plugins/spanish/novelawuxia';
|
||||||
import p_219 from '@plugins/spanish/PanchoTranslations[madara]';
|
import p_219 from '@plugins/spanish/novelyra';
|
||||||
import p_220 from '@plugins/spanish/skynovels';
|
import p_220 from '@plugins/spanish/oasistranslations';
|
||||||
import p_221 from '@plugins/spanish/TC&Sega[lightnovelwp]';
|
import p_221 from '@plugins/spanish/skynovels';
|
||||||
import p_222 from '@plugins/spanish/TraduccionesAmistosas[madara]';
|
import p_222 from '@plugins/spanish/tunovelaligera';
|
||||||
import p_223 from '@plugins/spanish/tunovelaligera';
|
import p_223 from '@plugins/spanish/yukitls';
|
||||||
import p_224 from '@plugins/spanish/yukitls';
|
import p_224 from '@plugins/thai/NovelLucky[madara]';
|
||||||
import p_225 from '@plugins/thai/NovelLucky[madara]';
|
import p_225 from '@plugins/thai/NovelPDF[madara]';
|
||||||
import p_226 from '@plugins/thai/NovelPDF[madara]';
|
import p_226 from '@plugins/turkish/ArazNovel[madara]';
|
||||||
import p_227 from '@plugins/turkish/ArazNovel[madara]';
|
import p_227 from '@plugins/turkish/EKTAPLAR[madara]';
|
||||||
import p_228 from '@plugins/turkish/EKTAPLAR[madara]';
|
import p_228 from '@plugins/turkish/KodeksLibrary[lightnovelwp]';
|
||||||
import p_229 from '@plugins/turkish/epiknovel';
|
import p_229 from '@plugins/turkish/MangaTR';
|
||||||
import p_230 from '@plugins/turkish/kakikata[madara]';
|
import p_230 from '@plugins/turkish/NABSCANS[madara]';
|
||||||
import p_231 from '@plugins/turkish/KodeksLibrary[lightnovelwp]';
|
import p_231 from '@plugins/turkish/Namevt[lightnovelwp]';
|
||||||
import p_232 from '@plugins/turkish/MangaTR';
|
import p_232 from '@plugins/turkish/NovelTR[lightnovelwp]';
|
||||||
import p_233 from '@plugins/turkish/NABSCANS[madara]';
|
import p_233 from '@plugins/turkish/Noveloku[madara]';
|
||||||
import p_234 from '@plugins/turkish/Namevt[lightnovelwp]';
|
import p_234 from '@plugins/turkish/RagnarScans[madara]';
|
||||||
import p_235 from '@plugins/turkish/Noveloku[madara]';
|
import p_235 from '@plugins/turkish/ThNovels[hotnovelpub]';
|
||||||
import p_236 from '@plugins/turkish/NovelTR[lightnovelwp]';
|
import p_236 from '@plugins/turkish/TurkceLightNovels[madara]';
|
||||||
import p_237 from '@plugins/turkish/RagnarScans[madara]';
|
import p_237 from '@plugins/turkish/WebNovelOku[madara]';
|
||||||
import p_238 from '@plugins/turkish/ThNovels[hotnovelpub]';
|
import p_238 from '@plugins/turkish/epiknovel';
|
||||||
import p_239 from '@plugins/turkish/TurkceLightNovels[madara]';
|
import p_239 from '@plugins/turkish/kakikata[madara]';
|
||||||
import p_240 from '@plugins/turkish/WebNovelOku[madara]';
|
import p_240 from '@plugins/ukrainian/bakainua';
|
||||||
import p_241 from '@plugins/ukrainian/bakainua';
|
import p_241 from '@plugins/ukrainian/smakolykytl';
|
||||||
import p_242 from '@plugins/ukrainian/smakolykytl';
|
import p_242 from '@plugins/vietnamese/LNHako';
|
||||||
import p_243 from '@plugins/vietnamese/lightnovelvn';
|
import p_243 from '@plugins/vietnamese/lightnovelvn';
|
||||||
import p_244 from '@plugins/vietnamese/LNHako';
|
import p_244 from '@plugins/vietnamese/nettruyen';
|
||||||
import p_245 from '@plugins/vietnamese/nettruyen';
|
|
||||||
|
|
||||||
const PLUGINS: Plugin.PluginBase[] = [
|
const PLUGINS: Plugin.PluginBase[] = [
|
||||||
p_0,
|
p_0,
|
||||||
@@ -492,6 +491,5 @@ const PLUGINS: Plugin.PluginBase[] = [
|
|||||||
p_242,
|
p_242,
|
||||||
p_243,
|
p_243,
|
||||||
p_244,
|
p_244,
|
||||||
p_245,
|
|
||||||
];
|
];
|
||||||
export default PLUGINS;
|
export default PLUGINS;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CheerioAPI, load as parseHTML } from 'cheerio';
|
|||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
// import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||||
|
|
||||||
class IndoWebNovel implements Plugin.PluginBase {
|
class IndoWebNovel implements Plugin.PluginBase {
|
||||||
id = 'IDWN.id';
|
id = 'IDWN.id';
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ class NovelRingan implements Plugin.PluginBase {
|
|||||||
link += `&order=${filters.sort.value}`;
|
link += `&order=${filters.sort.value}`;
|
||||||
|
|
||||||
if (filters.type.value.length)
|
if (filters.type.value.length)
|
||||||
link += filters.type.value.map(i => `&type[]=${i}`).join('');
|
link += filters.type.value.map((i: string) => `&type[]=${i}`).join('');
|
||||||
if (filters.genre.value.length)
|
if (filters.genre.value.length)
|
||||||
link += filters.genre.value.map(i => `&genre[]=${i}`).join('');
|
link += filters.genre.value.map((i: string) => `&genre[]=${i}`).join('');
|
||||||
|
|
||||||
const result = await fetchApi(link);
|
const result = await fetchApi(link);
|
||||||
const body = await result.text();
|
const body = await result.text();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { load as loadCheerio } from 'cheerio';
|
import { CheerioAPI, load as loadCheerio } from 'cheerio';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { defaultCover } from '@libs/defaultCover';
|
import { defaultCover } from '@libs/defaultCover';
|
||||||
@@ -14,7 +14,7 @@ class Syosetu implements Plugin.PluginBase {
|
|||||||
icon = 'src/jp/syosetu/icon.png';
|
icon = 'src/jp/syosetu/icon.png';
|
||||||
site = 'https://yomou.syosetu.com/';
|
site = 'https://yomou.syosetu.com/';
|
||||||
novelPrefix = 'https://ncode.syosetu.com';
|
novelPrefix = 'https://ncode.syosetu.com';
|
||||||
version = '1.1.2';
|
version = '1.1.4';
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent':
|
'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',
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
@@ -45,9 +45,7 @@ class Syosetu implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
const html = await (await fetchApi(url)).text();
|
const html = await (await fetchApi(url)).text();
|
||||||
|
|
||||||
const loadedCheerio = loadCheerio(html, {
|
const loadedCheerio = loadCheerio(html);
|
||||||
decodeEntities: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (parseInt(loadedCheerio('.is-current').html() || '1') !== pagenumber)
|
if (parseInt(loadedCheerio('.is-current').html() || '1') !== pagenumber)
|
||||||
return [];
|
return [];
|
||||||
@@ -71,7 +69,7 @@ class Syosetu implements Plugin.PluginBase {
|
|||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
private async parseChaptersFromPage(
|
private async parseChaptersFromPage(
|
||||||
loadedCheerio: cheerio.CheerioAPI,
|
loadedCheerio: CheerioAPI,
|
||||||
): Promise<Plugin.ChapterItem[]> {
|
): Promise<Plugin.ChapterItem[]> {
|
||||||
const chapters: Plugin.ChapterItem[] = [];
|
const chapters: Plugin.ChapterItem[] = [];
|
||||||
|
|
||||||
@@ -103,7 +101,7 @@ class Syosetu implements Plugin.PluginBase {
|
|||||||
headers: this.headers,
|
headers: this.headers,
|
||||||
});
|
});
|
||||||
const body = await result.text();
|
const body = await result.text();
|
||||||
const loadedCheerio = loadCheerio(body, { decodeEntities: false });
|
const loadedCheerio = loadCheerio(body);
|
||||||
|
|
||||||
// Parse status
|
// Parse status
|
||||||
let status = 'Unknown';
|
let status = 'Unknown';
|
||||||
@@ -182,7 +180,7 @@ class Syosetu implements Plugin.PluginBase {
|
|||||||
|
|
||||||
// Process each page's chapters
|
// Process each page's chapters
|
||||||
pageResults.forEach(pageBody => {
|
pageResults.forEach(pageBody => {
|
||||||
const pageCheerio = loadCheerio(pageBody, { decodeEntities: false });
|
const pageCheerio = loadCheerio(pageBody);
|
||||||
pageCheerio('.p-eplist__sublist').each((_, element) => {
|
pageCheerio('.p-eplist__sublist').each((_, element) => {
|
||||||
const chapterLink = pageCheerio(element).find('a');
|
const chapterLink = pageCheerio(element).find('a');
|
||||||
const chapterUrl = chapterLink.attr('href');
|
const chapterUrl = chapterLink.attr('href');
|
||||||
@@ -214,9 +212,7 @@ class Syosetu implements Plugin.PluginBase {
|
|||||||
});
|
});
|
||||||
const body = await result.text();
|
const body = await result.text();
|
||||||
|
|
||||||
const cheerioQuery = loadCheerio(body, {
|
const cheerioQuery = loadCheerio(body);
|
||||||
decodeEntities: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the chapter title
|
// Get the chapter title
|
||||||
const chapterTitle = cheerioQuery('.p-novel__title').html() || '';
|
const chapterTitle = cheerioQuery('.p-novel__title').html() || '';
|
||||||
@@ -243,7 +239,7 @@ class Syosetu implements Plugin.PluginBase {
|
|||||||
const result = await fetchApi(url, { headers: this.headers });
|
const result = await fetchApi(url, { headers: this.headers });
|
||||||
const body = await result.text();
|
const body = await result.text();
|
||||||
// Cheerio it!
|
// Cheerio it!
|
||||||
const cheerioQuery = loadCheerio(body, { decodeEntities: false });
|
const cheerioQuery = loadCheerio(body);
|
||||||
|
|
||||||
const pageNovels: Plugin.NovelItem[] = [];
|
const pageNovels: Plugin.NovelItem[] = [];
|
||||||
// find class=searchkekka_box
|
// find class=searchkekka_box
|
||||||
|
|||||||
@@ -31,15 +31,15 @@ class Agitoon implements Plugin.PluginBase {
|
|||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
mode: 'get_data_novel_list_p',
|
mode: 'get_data_novel_list_p',
|
||||||
novel_menu: showLatestNovels ? '1' : '3',
|
novel_menu: showLatestNovels ? '1' : '3',
|
||||||
np_day: new Date().getDay(),
|
np_day: new Date().getDay().toString(),
|
||||||
np_rank: '1',
|
np_rank: '1',
|
||||||
np_distributor: '0',
|
np_distributor: '0',
|
||||||
np_genre: '00',
|
np_genre: '00',
|
||||||
np_order: '1',
|
np_order: '1',
|
||||||
np_genre_ex_1: '00',
|
np_genre_ex_1: '00',
|
||||||
np_genre_ex_2: '00',
|
np_genre_ex_2: '00',
|
||||||
list_limit: 20 * (pageNo - 1),
|
list_limit: (20 * (pageNo - 1)).toString(),
|
||||||
is_query_first: pageNo == 1,
|
is_query_first: (pageNo == 1).toString(),
|
||||||
}).toString(),
|
}).toString(),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -66,7 +66,7 @@ class Agitoon implements Plugin.PluginBase {
|
|||||||
const result = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
|
const result = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
|
||||||
res.text(),
|
res.text(),
|
||||||
);
|
);
|
||||||
const loadedCheerio = parseHTML(result, { decodeEntities: false });
|
const loadedCheerio = parseHTML(result);
|
||||||
|
|
||||||
const novel: Plugin.SourceNovel = {
|
const novel: Plugin.SourceNovel = {
|
||||||
path: novelPath,
|
path: novelPath,
|
||||||
@@ -147,7 +147,7 @@ class Agitoon implements Plugin.PluginBase {
|
|||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
mode: 'get_data_novel_list_p_sch',
|
mode: 'get_data_novel_list_p_sch',
|
||||||
search_novel: searchTerm,
|
search_novel: searchTerm,
|
||||||
list_limit: 0,
|
list_limit: '0',
|
||||||
}).toString(),
|
}).toString(),
|
||||||
});
|
});
|
||||||
const resJson = (await rawResults.json()) as response;
|
const resJson = (await rawResults.json()) as response;
|
||||||
|
|||||||
+50
-14
@@ -5,13 +5,49 @@ import { Plugin } from '@/types/plugin';
|
|||||||
import { load as parseHTML } from 'cheerio';
|
import { load as parseHTML } from 'cheerio';
|
||||||
import { storage } from '@libs/storage';
|
import { storage } from '@libs/storage';
|
||||||
|
|
||||||
|
type Author = {
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TocItem = {
|
||||||
|
title?: string;
|
||||||
|
href?: string;
|
||||||
|
children?: TocItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type SeriesResponse = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
metadata: {
|
||||||
|
status: string;
|
||||||
|
genres: string[];
|
||||||
|
};
|
||||||
|
booksMetadata: {
|
||||||
|
authors: Author[];
|
||||||
|
summary: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type BookResponse = {
|
||||||
|
id: string;
|
||||||
|
metadata: {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type BookManifest = {
|
||||||
|
toc: TocItem[];
|
||||||
|
readingOrder: { href: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
class KomgaPlugin implements Plugin.PluginBase {
|
class KomgaPlugin implements Plugin.PluginBase {
|
||||||
id = 'komga';
|
id = 'komga';
|
||||||
name = 'Komga';
|
name = 'Komga';
|
||||||
icon = 'src/multi/komga/icon.png';
|
icon = 'src/multi/komga/icon.png';
|
||||||
version = '1.0.1';
|
version = '1.0.2';
|
||||||
|
|
||||||
site = storage.get('url');
|
site = storage.get('url') as string;
|
||||||
email = storage.get('email');
|
email = storage.get('email');
|
||||||
password = storage.get('password');
|
password = storage.get('password');
|
||||||
|
|
||||||
@@ -51,8 +87,8 @@ class KomgaPlugin implements Plugin.PluginBase {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
flattenArray(arr: any) {
|
flattenArray(arr: TocItem[]): TocItem[] {
|
||||||
return arr.reduce((acc: any, obj: any) => {
|
return arr.reduce((acc: TocItem[], obj: TocItem) => {
|
||||||
const { children, ...rest } = obj;
|
const { children, ...rest } = obj;
|
||||||
acc.push(rest);
|
acc.push(rest);
|
||||||
|
|
||||||
@@ -69,7 +105,7 @@ class KomgaPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
const response = await this.makeRequest(url);
|
const response = await this.makeRequest(url);
|
||||||
|
|
||||||
const series = JSON.parse(response).content;
|
const series: SeriesResponse[] = JSON.parse(response).content;
|
||||||
|
|
||||||
for (const s of series) {
|
for (const s of series) {
|
||||||
novels.push({
|
novels.push({
|
||||||
@@ -112,13 +148,13 @@ class KomgaPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
const response = await this.makeRequest(url);
|
const response = await this.makeRequest(url);
|
||||||
|
|
||||||
const series = JSON.parse(response);
|
const series: SeriesResponse = JSON.parse(response);
|
||||||
|
|
||||||
novel.name = series.name;
|
novel.name = series.name;
|
||||||
novel.author = series.booksMetadata.authors
|
novel.author = series.booksMetadata.authors
|
||||||
.filter((author: any) => author.role === 'writer')
|
.filter((author: Author) => author.role === 'writer')
|
||||||
.reduce(
|
.reduce(
|
||||||
(accumulated: string, current: any) =>
|
(accumulated: string, current: Author) =>
|
||||||
accumulated + (accumulated !== '' ? ', ' : '') + current.name,
|
accumulated + (accumulated !== '' ? ', ' : '') + current.name,
|
||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
@@ -150,21 +186,21 @@ class KomgaPlugin implements Plugin.PluginBase {
|
|||||||
this.site + `api/v1/series/${series.id}/books?unpaged=true`,
|
this.site + `api/v1/series/${series.id}/books?unpaged=true`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const booksData = JSON.parse(booksResponse).content;
|
const booksData: BookResponse[] = JSON.parse(booksResponse).content;
|
||||||
|
|
||||||
for (const book of booksData) {
|
for (const book of booksData) {
|
||||||
const bookManifestResponse = await this.makeRequest(
|
const bookManifestResponse = await this.makeRequest(
|
||||||
this.site + `opds/v2/books/${book.id}/manifest`,
|
this.site + `opds/v2/books/${book.id}/manifest`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const bookManifest = JSON.parse(bookManifestResponse);
|
const bookManifest: BookManifest = JSON.parse(bookManifestResponse);
|
||||||
|
|
||||||
const toc = this.flattenArray(bookManifest.toc);
|
const toc = this.flattenArray(bookManifest.toc);
|
||||||
|
|
||||||
let i = 1;
|
let i = 1;
|
||||||
for (const page of bookManifest.readingOrder) {
|
for (const page of bookManifest.readingOrder) {
|
||||||
const tocItem = toc.find(
|
const tocItem = toc.find(
|
||||||
(v: any) => v.href?.split('#')[0] === page.href,
|
(v: TocItem) => v.href?.split('#')[0] === page.href,
|
||||||
);
|
);
|
||||||
const title = tocItem ? tocItem.title : null;
|
const title = tocItem ? tocItem.title : null;
|
||||||
chapters.push({
|
chapters.push({
|
||||||
@@ -198,9 +234,9 @@ class KomgaPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
if (href) {
|
if (href) {
|
||||||
const img = $('<img />').attr({
|
const img = $('<img />').attr({
|
||||||
src: href.startsWith('http') ? href : `${baseUrl}${href}`,
|
src: new URL(href, baseUrl).href,
|
||||||
width: width || undefined,
|
width: width || '',
|
||||||
height: height || undefined,
|
height: height || '',
|
||||||
});
|
});
|
||||||
$(image).closest('svg').replaceWith(img);
|
$(image).closest('svg').replaceWith(img);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
if (scriptContent) {
|
if (scriptContent) {
|
||||||
const gibMatch = scriptContent.match(/var gib = (\[.*?\])/);
|
const gibMatch = scriptContent.match(/var gib = (\[.*?\])/);
|
||||||
if (gibMatch) {
|
if (gibMatch) {
|
||||||
const gibArray = eval(gibMatch[1]) as string[];
|
const gibArray = eval(gibMatch[1]);
|
||||||
gibArray.forEach(cssClass => {
|
gibArray.forEach(cssClass => {
|
||||||
loadedCheerio(`.${cssClass}`).remove();
|
loadedCheerio(`.${cssClass}`).remove();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CheerioAPI, load as loadCheerio, load } from 'cheerio';
|
import { CheerioAPI, load as loadCheerio } from 'cheerio';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
@@ -17,7 +17,7 @@ export type FictioneerMetadata = {
|
|||||||
options: FictioneerOptions;
|
options: FictioneerOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
class FictioneerPlugin implements Plugin.PluginBase {
|
export class FictioneerPlugin implements Plugin.PluginBase {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -32,16 +32,38 @@ class FictioneerPlugin implements Plugin.PluginBase {
|
|||||||
this.icon = `multisrc/fictioneer/${metadata.id.toLowerCase()}/icon.png`;
|
this.icon = `multisrc/fictioneer/${metadata.id.toLowerCase()}/icon.png`;
|
||||||
this.site = metadata.sourceSite;
|
this.site = metadata.sourceSite;
|
||||||
const versionIncrements = metadata.options?.versionIncrements || 0;
|
const versionIncrements = metadata.options?.versionIncrements || 0;
|
||||||
this.version = `1.0.${0 + versionIncrements}`;
|
this.version = `1.1.${0 + versionIncrements}`;
|
||||||
this.options = metadata.options;
|
this.options = metadata.options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseNovels(
|
||||||
|
loadedCheerio: CheerioAPI,
|
||||||
|
selector: string,
|
||||||
|
): Plugin.NovelItem[] {
|
||||||
|
return loadedCheerio(selector)
|
||||||
|
.map((i, el) => {
|
||||||
|
const element = loadedCheerio(el);
|
||||||
|
const novelName = element.find('h3 > a').text();
|
||||||
|
const novelCover = element.find('a.cell-img:has(img)').attr('href');
|
||||||
|
const novelUrl = element.find('h3 > a').attr('href');
|
||||||
|
|
||||||
|
if (!novelUrl) return;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: novelName,
|
||||||
|
cover: novelCover,
|
||||||
|
path: new URL(novelUrl, this.site).pathname.substring(1),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
{
|
// {
|
||||||
showLatestNovels,
|
// showLatestNovels,
|
||||||
filters,
|
// filters,
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
// }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const req = await fetchApi(
|
const req = await fetchApi(
|
||||||
this.site +
|
this.site +
|
||||||
@@ -53,23 +75,10 @@ class FictioneerPlugin implements Plugin.PluginBase {
|
|||||||
const body = await req.text();
|
const body = await req.text();
|
||||||
const loadedCheerio = loadCheerio(body);
|
const loadedCheerio = loadCheerio(body);
|
||||||
|
|
||||||
return loadedCheerio(
|
return this.parseNovels(
|
||||||
|
loadedCheerio,
|
||||||
'#featured-list > li > div > div, #list-of-stories > li > div > div',
|
'#featured-list > li > div > div, #list-of-stories > li > div > div',
|
||||||
)
|
);
|
||||||
.map((i, el) => {
|
|
||||||
const novelName = loadedCheerio(el).find('h3 > a').text();
|
|
||||||
const novelCover = loadedCheerio(el)
|
|
||||||
.find('a.cell-img:has(img)')
|
|
||||||
.attr('href');
|
|
||||||
const novelUrl = loadedCheerio(el).find('h3 > a').attr('href');
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: novelName,
|
|
||||||
cover: novelCover,
|
|
||||||
path: novelUrl!.replace(this.site + '/', '').replace(/\/$/, ''),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.toArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
@@ -94,6 +103,8 @@ class FictioneerPlugin implements Plugin.PluginBase {
|
|||||||
.map((i, el) => loadedCheerio(el).text())
|
.map((i, el) => loadedCheerio(el).text())
|
||||||
.toArray()
|
.toArray()
|
||||||
.join(',');
|
.join(',');
|
||||||
|
|
||||||
|
loadedCheerio('section.story__summary .related-stories-block').remove();
|
||||||
novel.summary = loadedCheerio('section.story__summary').text();
|
novel.summary = loadedCheerio('section.story__summary').text();
|
||||||
|
|
||||||
novel.chapters = loadedCheerio('li.chapter-group__list-item._publish')
|
novel.chapters = loadedCheerio('li.chapter-group__list-item._publish')
|
||||||
@@ -108,15 +119,12 @@ class FictioneerPlugin implements Plugin.PluginBase {
|
|||||||
)
|
)
|
||||||
.map((i, el) => {
|
.map((i, el) => {
|
||||||
const chapterName = loadedCheerio(el).find('a').text();
|
const chapterName = loadedCheerio(el).find('a').text();
|
||||||
const chapterUrl = loadedCheerio(el)
|
const chapterUrl = loadedCheerio(el).find('a').attr('href');
|
||||||
.find('a')
|
|
||||||
.attr('href')
|
|
||||||
?.replace(this.site + '/', '')
|
|
||||||
.replace(/\/$/, '');
|
|
||||||
|
|
||||||
|
if (!chapterUrl) return;
|
||||||
return {
|
return {
|
||||||
name: chapterName,
|
name: chapterName,
|
||||||
path: chapterUrl!,
|
path: new URL(chapterUrl, this.site).pathname.substring(1),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.toArray();
|
.toArray();
|
||||||
@@ -152,23 +160,12 @@ class FictioneerPlugin implements Plugin.PluginBase {
|
|||||||
const body = await req.text();
|
const body = await req.text();
|
||||||
const loadedCheerio = loadCheerio(body);
|
const loadedCheerio = loadCheerio(body);
|
||||||
|
|
||||||
return loadedCheerio('#search-result-list > li > div > div')
|
return this.parseNovels(
|
||||||
.map((i, el) => {
|
loadedCheerio,
|
||||||
const novelName = loadedCheerio(el).find('h3 > a').text();
|
'#search-result-list > li > div > div',
|
||||||
const novelCover = loadedCheerio(el)
|
);
|
||||||
.find('a.cell-img:has(img)')
|
|
||||||
.attr('href');
|
|
||||||
const novelUrl = loadedCheerio(el).find('h3 > a').attr('href');
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: novelName,
|
|
||||||
cover: novelCover,
|
|
||||||
path: novelUrl!.replace(this.site + '/', '').replace(/\/$/, ''),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.toArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
// resolveUrl = (path: string, isNovel?: boolean) =>
|
||||||
this.site + '/' + path + '/';
|
// this.site + '/' + path + '/';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import * as cheerio from 'cheerio';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import list from './sources.json' with { type: 'json' };
|
import list from './sources.json' with { type: 'json' };
|
||||||
import { HotNovelPubMetadata } from './template';
|
import { HotNovelPubMetadata } from './template';
|
||||||
|
import { Filters } from '@libs/filterInputs';
|
||||||
|
|
||||||
async function getFilters(sources: HotNovelPubMetadata) {
|
async function getFilters(sources: HotNovelPubMetadata) {
|
||||||
const filters: any = {
|
const filters: Filters = {
|
||||||
sort: {
|
sort: {
|
||||||
type: 'Picker',
|
type: 'Picker',
|
||||||
label: 'Order',
|
label: 'Order',
|
||||||
@@ -70,7 +71,7 @@ async function start() {
|
|||||||
const result = [];
|
const result = [];
|
||||||
for (const sources of list) {
|
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);
|
const NewFilters = await getFilters(sources as HotNovelPubMetadata);
|
||||||
sources.filters = NewFilters;
|
sources.filters = NewFilters;
|
||||||
result.push(sources);
|
result.push(sources);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,10 +48,7 @@ const generator = function generator(metadata) {
|
|||||||
|
|
||||||
const pluginScript = `
|
const pluginScript = `
|
||||||
${HotNovelPubTemplate}
|
${HotNovelPubTemplate}
|
||||||
const plugin = new HotNovelPubPlugin(${JSON.stringify(metadata).replace(
|
const plugin = new HotNovelPubPlugin(${JSON.stringify(metadata)});
|
||||||
/"type":"([^"]+)"/g,
|
|
||||||
'"type":FilterTypes.$1',
|
|
||||||
)});
|
|
||||||
export default plugin;
|
export default plugin;
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
import { Filters } from '@libs/filterInputs';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ type HotNovelPubOptions = {
|
|||||||
lang?: string;
|
lang?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
class HotNovelPubPlugin implements Plugin.PluginBase {
|
export class HotNovelPubPlugin implements Plugin.PluginBase {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -38,7 +38,10 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
{ filters, showLatestNovels }: Plugin.PopularNovelsOptions,
|
{
|
||||||
|
filters,
|
||||||
|
showLatestNovels,
|
||||||
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
let url = this.apiSite + '/books/';
|
let url = this.apiSite + '/books/';
|
||||||
url += showLatestNovels ? 'new' : filters?.sort?.value || 'hot';
|
url += showLatestNovels ? 'new' : filters?.sort?.value || 'hot';
|
||||||
@@ -52,7 +55,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
|
|||||||
headers: {
|
headers: {
|
||||||
lang: this.lang,
|
lang: this.lang,
|
||||||
},
|
},
|
||||||
}).then(res => res.json());
|
}).then((res: Response) => res.json());
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
if (result.status && result.data.books.data?.length) {
|
if (result.status && result.data.books.data?.length) {
|
||||||
@@ -75,7 +78,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
|
|||||||
lang: this.lang,
|
lang: this.lang,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
).then(res => res.json());
|
).then((res: Response) => res.json());
|
||||||
|
|
||||||
const novel: Plugin.SourceNovel = {
|
const novel: Plugin.SourceNovel = {
|
||||||
name: json.data.book.name,
|
name: json.data.book.name,
|
||||||
@@ -110,8 +113,8 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
const body = await fetchApi(this.resolveUrl(chapterPath)).then(res =>
|
const body = await fetchApi(this.resolveUrl(chapterPath)).then(
|
||||||
res.text(),
|
(res: Response) => res.text(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let chapterText =
|
let chapterText =
|
||||||
@@ -144,7 +147,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase {
|
|||||||
},
|
},
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ key_search: searchTerm }),
|
body: JSON.stringify({ key_search: searchTerm }),
|
||||||
}).then(res => res.json());
|
}).then((res: Response) => res.json());
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
if (result.status && result.data.books?.length) {
|
if (result.status && result.data.books?.length) {
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ const generator = function generator(metadata) {
|
|||||||
|
|
||||||
const pluginScript = `
|
const pluginScript = `
|
||||||
${IfreedomTemplate}
|
${IfreedomTemplate}
|
||||||
const plugin = new IfreedomPlugin(${JSON.stringify(metadata).replace(
|
const plugin = new IfreedomPlugin(${JSON.stringify(metadata)});
|
||||||
/"type":"([^"]+)"/g,
|
|
||||||
'"type":FilterTypes.$1',
|
|
||||||
)});
|
|
||||||
export default plugin;
|
export default plugin;
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
import { Filters, FilterToValues } from '@libs/filterInputs';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
import { Parser } from 'htmlparser2';
|
import { Parser } from 'htmlparser2';
|
||||||
@@ -12,7 +12,7 @@ export type IfreedomMetadata = {
|
|||||||
filters?: Filters;
|
filters?: Filters;
|
||||||
};
|
};
|
||||||
|
|
||||||
class IfreedomPlugin implements Plugin.PluginBase {
|
export class IfreedomPlugin implements Plugin.PluginBase {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -25,14 +25,14 @@ class IfreedomPlugin implements Plugin.PluginBase {
|
|||||||
this.name = metadata.sourceName;
|
this.name = metadata.sourceName;
|
||||||
this.icon = `multisrc/ifreedom/${metadata.id.toLowerCase()}/icon.png`;
|
this.icon = `multisrc/ifreedom/${metadata.id.toLowerCase()}/icon.png`;
|
||||||
this.site = metadata.sourceSite;
|
this.site = metadata.sourceSite;
|
||||||
this.version = '1.1.0';
|
this.version = '1.1.1';
|
||||||
this.filters = metadata.filters;
|
this.filters = metadata.filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseNovels(url: string) {
|
parseNovels(url: string) {
|
||||||
return fetchApi(url)
|
return fetchApi(url)
|
||||||
.then(res => res.text())
|
.then((res: Response) => res.text())
|
||||||
.then(html => {
|
.then((html: string) => {
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
let tempNovel = {} as Plugin.NovelItem;
|
let tempNovel = {} as Plugin.NovelItem;
|
||||||
let isInsideNovelCard = false;
|
let isInsideNovelCard = false;
|
||||||
@@ -83,7 +83,8 @@ class IfreedomPlugin implements Plugin.PluginBase {
|
|||||||
): Promise<Plugin.NovelItem[]> {
|
): 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 }]) => {
|
Object.entries(filters || {}).forEach(([type, filter]) => {
|
||||||
|
const { value } = filter as FilterToValues<Filters>[string];
|
||||||
if (Array.isArray(value) && value.length) {
|
if (Array.isArray(value) && value.length) {
|
||||||
url += `&${type}[]=${value.join(`&${type}[]=`)}`;
|
url += `&${type}[]=${value.join(`&${type}[]=`)}`;
|
||||||
}
|
}
|
||||||
@@ -94,7 +95,9 @@ class IfreedomPlugin implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
const html = await fetchApi(this.site + novelPath).then(res => res.text());
|
const html = await fetchApi(this.site + novelPath).then((res: Response) =>
|
||||||
|
res.text(),
|
||||||
|
);
|
||||||
const novel: Plugin.SourceNovel = {
|
const novel: Plugin.SourceNovel = {
|
||||||
path: novelPath,
|
path: novelPath,
|
||||||
name: '',
|
name: '',
|
||||||
@@ -288,7 +291,7 @@ class IfreedomPlugin implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
const body = await fetchApi(this.site + chapterPath).then(res =>
|
const body = await fetchApi(this.site + chapterPath).then((res: Response) =>
|
||||||
res.text(),
|
res.text(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -328,7 +331,7 @@ class IfreedomPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async searchNovels(
|
async searchNovels(
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
page: number = 1,
|
page = 1,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const url = `${this.site}/vse-knigi/?searchname=${encodeURIComponent(searchTerm)}&bpage=${page}`;
|
const url = `${this.site}/vse-knigi/?searchname=${encodeURIComponent(searchTerm)}&bpage=${page}`;
|
||||||
return this.parseNovels(url);
|
return this.parseNovels(url);
|
||||||
@@ -357,7 +360,7 @@ function parseStatus(statusString: string): string {
|
|||||||
return NovelStatus.Unknown;
|
return NovelStatus.Unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDate(dateString: string = ''): string | null {
|
function parseDate(dateString = ''): string | null {
|
||||||
const months: Record<string, number> = {
|
const months: Record<string, number> = {
|
||||||
января: 1,
|
января: 1,
|
||||||
февраля: 2,
|
февраля: 2,
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ export type LightNovelWorldMetadata = {
|
|||||||
sourceSite: string;
|
sourceSite: string;
|
||||||
sourceName: string;
|
sourceName: string;
|
||||||
options?: LightNovelWorldOptions;
|
options?: LightNovelWorldOptions;
|
||||||
filters?: any;
|
filters?: Filters;
|
||||||
};
|
};
|
||||||
|
|
||||||
class LightNovelWorld implements Plugin.PagePlugin {
|
export class LightNovelWorld implements Plugin.PagePlugin {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
site: string;
|
site: string;
|
||||||
@@ -52,7 +52,7 @@ class LightNovelWorld implements Plugin.PagePlugin {
|
|||||||
link += `${filters.status.value}/`;
|
link += `${filters.status.value}/`;
|
||||||
link += page;
|
link += page;
|
||||||
|
|
||||||
const body = await fetchApi(link).then(r => r.text());
|
const body = await fetchApi(link).then((r: Response) => r.text());
|
||||||
|
|
||||||
const loadedCheerio = parseHTML(body);
|
const loadedCheerio = parseHTML(body);
|
||||||
|
|
||||||
@@ -84,7 +84,9 @@ class LightNovelWorld implements Plugin.PagePlugin {
|
|||||||
async parseNovel(
|
async parseNovel(
|
||||||
novelPath: string,
|
novelPath: string,
|
||||||
): Promise<Plugin.SourceNovel & { totalPages: number }> {
|
): Promise<Plugin.SourceNovel & { totalPages: number }> {
|
||||||
const body = await fetchApi(this.site + novelPath).then(r => r.text());
|
const body = await fetchApi(this.site + novelPath).then((r: Response) =>
|
||||||
|
r.text(),
|
||||||
|
);
|
||||||
|
|
||||||
const loadedCheerio = parseHTML(body);
|
const loadedCheerio = parseHTML(body);
|
||||||
const totalChapters = parseInt(
|
const totalChapters = parseInt(
|
||||||
@@ -113,7 +115,7 @@ class LightNovelWorld implements Plugin.PagePlugin {
|
|||||||
|
|
||||||
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
|
||||||
const url = this.site + novelPath + '/chapters/page-' + page;
|
const url = this.site + novelPath + '/chapters/page-' + page;
|
||||||
const body = await fetchApi(url).then(res => res.text());
|
const body = await fetchApi(url).then((res: Response) => res.text());
|
||||||
const loadedCheerio = parseHTML(body);
|
const loadedCheerio = parseHTML(body);
|
||||||
const chapter: Plugin.ChapterItem[] = [];
|
const chapter: Plugin.ChapterItem[] = [];
|
||||||
loadedCheerio('.chapter-list li').each(function () {
|
loadedCheerio('.chapter-list li').each(function () {
|
||||||
@@ -144,7 +146,9 @@ class LightNovelWorld implements Plugin.PagePlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
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: Response) =>
|
||||||
|
r.text(),
|
||||||
|
);
|
||||||
|
|
||||||
const loadedCheerio = parseHTML(body);
|
const loadedCheerio = parseHTML(body);
|
||||||
|
|
||||||
@@ -156,7 +160,7 @@ class LightNovelWorld implements Plugin.PagePlugin {
|
|||||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||||
const url = `${this.site}lnsearchlive`;
|
const url = `${this.site}lnsearchlive`;
|
||||||
const link = `${this.site}search`;
|
const link = `${this.site}search`;
|
||||||
const response = await fetchApi(link).then(r => r.text());
|
const response = await fetchApi(link).then((r: Response) => r.text());
|
||||||
const token = parseHTML(response);
|
const token = parseHTML(response);
|
||||||
const verifytoken = token('#novelSearchForm > input').attr('value');
|
const verifytoken = token('#novelSearchForm > input').attr('value');
|
||||||
|
|
||||||
@@ -167,7 +171,7 @@ class LightNovelWorld implements Plugin.PagePlugin {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { LNRequestVerifyToken: verifytoken! },
|
headers: { LNRequestVerifyToken: verifytoken! },
|
||||||
body: formData,
|
body: formData,
|
||||||
}).then(r => r.json());
|
}).then((r: Response) => r.json());
|
||||||
|
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ export type LightNovelWPMetadata = {
|
|||||||
sourceSite: string;
|
sourceSite: string;
|
||||||
sourceName: string;
|
sourceName: string;
|
||||||
options?: LightNovelWPOptions;
|
options?: LightNovelWPOptions;
|
||||||
filters?: any;
|
filters?: Filters;
|
||||||
};
|
};
|
||||||
|
|
||||||
class LightNovelWPPlugin implements Plugin.PluginBase {
|
export class LightNovelWPPlugin implements Plugin.PluginBase {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -34,7 +34,7 @@ class LightNovelWPPlugin implements Plugin.PluginBase {
|
|||||||
filters?: Filters;
|
filters?: Filters;
|
||||||
|
|
||||||
hideLocked = storage.get('hideLocked');
|
hideLocked = storage.get('hideLocked');
|
||||||
pluginSettings?: Record<string, any>;
|
pluginSettings?: Filters;
|
||||||
|
|
||||||
constructor(metadata: LightNovelWPMetadata) {
|
constructor(metadata: LightNovelWPMetadata) {
|
||||||
this.id = metadata.id;
|
this.id = metadata.id;
|
||||||
@@ -42,7 +42,7 @@ class LightNovelWPPlugin implements Plugin.PluginBase {
|
|||||||
this.icon = `multisrc/lightnovelwp/${metadata.id.toLowerCase()}/icon.png`;
|
this.icon = `multisrc/lightnovelwp/${metadata.id.toLowerCase()}/icon.png`;
|
||||||
this.site = metadata.sourceSite;
|
this.site = metadata.sourceSite;
|
||||||
const versionIncrements = metadata.options?.versionIncrements || 0;
|
const versionIncrements = metadata.options?.versionIncrements || 0;
|
||||||
this.version = `1.1.${9 + versionIncrements}`;
|
this.version = `1.1.${10 + versionIncrements}`;
|
||||||
this.options = metadata.options ?? ({} as LightNovelWPOptions);
|
this.options = metadata.options ?? ({} as LightNovelWPOptions);
|
||||||
this.filters = metadata.filters satisfies Filters;
|
this.filters = metadata.filters satisfies Filters;
|
||||||
|
|
||||||
@@ -99,12 +99,12 @@ class LightNovelWPPlugin implements Plugin.PluginBase {
|
|||||||
const articles = html.match(/<article([^]*?)<\/article>/g) || [];
|
const articles = html.match(/<article([^]*?)<\/article>/g) || [];
|
||||||
articles.forEach(article => {
|
articles.forEach(article => {
|
||||||
const [, novelUrl, novelName] =
|
const [, novelUrl, novelName] =
|
||||||
article.match(/<a href="([^\"]*)".*? title="([^\"]*)"/) || [];
|
article.match(/<a href="([^"]*)".*? title="([^"]*)"/) || [];
|
||||||
|
|
||||||
if (novelName && novelUrl) {
|
if (novelName && novelUrl) {
|
||||||
const novelCover =
|
const novelCover =
|
||||||
article.match(
|
article.match(
|
||||||
/<img [^>]*?src="([^\"]*)"[^>]*?(?: data-src="([^\"]*)")?[^>]*>/,
|
/<img [^>]*?src="([^"]*)"[^>]*?(?: data-src="([^"]*)")?[^>]*>/,
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
let novelPath;
|
let novelPath;
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"sourceName": "DaoNovel",
|
"sourceName": "DaoNovel",
|
||||||
"options": {
|
"options": {
|
||||||
"useNewChapterEndpoint": true,
|
"useNewChapterEndpoint": true,
|
||||||
"versionIncrements": 1
|
"versionIncrements": 2,
|
||||||
|
"down": true,
|
||||||
|
"downSince": 1777153113000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -150,7 +152,10 @@
|
|||||||
"sourceSite": "https://novel4up.com/",
|
"sourceSite": "https://novel4up.com/",
|
||||||
"sourceName": "Novel4Up",
|
"sourceName": "Novel4Up",
|
||||||
"options": {
|
"options": {
|
||||||
"lang": "Arabic"
|
"lang": "Arabic",
|
||||||
|
"versionIncrements": 1,
|
||||||
|
"down": true,
|
||||||
|
"downSince": 1777153113000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -420,7 +425,10 @@
|
|||||||
"sourceName": "Belle Reservoir",
|
"sourceName": "Belle Reservoir",
|
||||||
"options": {
|
"options": {
|
||||||
"useNewChapterEndpoint": true,
|
"useNewChapterEndpoint": true,
|
||||||
"lang": "English"
|
"versionIncrements": 1,
|
||||||
|
"lang": "English",
|
||||||
|
"down": true,
|
||||||
|
"downSince": 1777153113000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -439,7 +447,10 @@
|
|||||||
"sourceName": "Coral Boutique",
|
"sourceName": "Coral Boutique",
|
||||||
"options": {
|
"options": {
|
||||||
"useNewChapterEndpoint": true,
|
"useNewChapterEndpoint": true,
|
||||||
"lang": "English"
|
"lang": "English",
|
||||||
|
"versionIncrements": 1,
|
||||||
|
"down": true,
|
||||||
|
"downSince": 1777153113000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Filters } from '@libs/filterInputs';
|
import { Filters } from '@libs/filterInputs';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { Cheerio, AnyNode, CheerioAPI, load as parseHTML } from 'cheerio';
|
import { Cheerio, CheerioAPI, load as parseHTML } from 'cheerio';
|
||||||
|
import { AnyNode } from 'domhandler';
|
||||||
import { defaultCover } from '@libs/defaultCover';
|
import { defaultCover } from '@libs/defaultCover';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -24,10 +25,10 @@ export type MadaraMetadata = {
|
|||||||
sourceSite: string;
|
sourceSite: string;
|
||||||
sourceName: string;
|
sourceName: string;
|
||||||
options?: MadaraOptions;
|
options?: MadaraOptions;
|
||||||
filters?: any;
|
filters?: Filters;
|
||||||
};
|
};
|
||||||
|
|
||||||
class MadaraPlugin implements Plugin.PluginBase {
|
export class MadaraPlugin implements Plugin.PluginBase {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -37,7 +38,7 @@ class MadaraPlugin implements Plugin.PluginBase {
|
|||||||
filters?: Filters | undefined;
|
filters?: Filters | undefined;
|
||||||
|
|
||||||
hideLocked = storage.get('hideLocked');
|
hideLocked = storage.get('hideLocked');
|
||||||
pluginSettings?: Record<string, any>;
|
pluginSettings?: Filters;
|
||||||
|
|
||||||
constructor(metadata: MadaraMetadata) {
|
constructor(metadata: MadaraMetadata) {
|
||||||
this.id = metadata.id;
|
this.id = metadata.id;
|
||||||
@@ -45,7 +46,7 @@ class MadaraPlugin implements Plugin.PluginBase {
|
|||||||
this.icon = `multisrc/madara/${metadata.id.toLowerCase()}/icon.png`;
|
this.icon = `multisrc/madara/${metadata.id.toLowerCase()}/icon.png`;
|
||||||
this.site = metadata.sourceSite;
|
this.site = metadata.sourceSite;
|
||||||
const versionIncrements = metadata.options?.versionIncrements || 0;
|
const versionIncrements = metadata.options?.versionIncrements || 0;
|
||||||
this.version = `1.0.${8 + versionIncrements}`;
|
this.version = `1.0.${9 + versionIncrements}`;
|
||||||
this.options = metadata.options;
|
this.options = metadata.options;
|
||||||
this.filters = metadata.filters;
|
this.filters = metadata.filters;
|
||||||
|
|
||||||
@@ -302,7 +303,7 @@ class MadaraPlugin implements Plugin.PluginBase {
|
|||||||
html = await fetchApi(this.site + novelPath + 'ajax/chapters/', {
|
html = await fetchApi(this.site + novelPath + 'ajax/chapters/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
referrer: this.site + novelPath,
|
referrer: this.site + novelPath,
|
||||||
}).then(res => res.text());
|
}).then((res: Response) => res.text());
|
||||||
} else {
|
} else {
|
||||||
const novelId =
|
const novelId =
|
||||||
loadedCheerio('.rating-post-id').attr('value') ||
|
loadedCheerio('.rating-post-id').attr('value') ||
|
||||||
@@ -316,7 +317,7 @@ class MadaraPlugin implements Plugin.PluginBase {
|
|||||||
html = await fetchApi(this.site + 'wp-admin/admin-ajax.php', {
|
html = await fetchApi(this.site + 'wp-admin/admin-ajax.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
}).then(res => res.text());
|
}).then((res: Response) => res.text());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (html !== '0') {
|
if (html !== '0') {
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ export const generateAll = function () {
|
|||||||
readFileSync(`${__dirname}/filters/mtlnovel.json`, 'utf-8'),
|
readFileSync(`${__dirname}/filters/mtlnovel.json`, 'utf-8'),
|
||||||
);
|
);
|
||||||
source.filters = filters;
|
source.filters = filters;
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
// for the linter
|
||||||
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`[mtlnovel] Generating: ${source.id}`.padEnd(35),
|
`[mtlnovel] Generating: ${source.id}`.padEnd(35),
|
||||||
source.filters ? '🔎with filters🔍' : '🚫 no filters 🚫',
|
source.filters ? '🔎with filters🔍' : '🚫 no filters 🚫',
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ export type MTLNovelMetadata = {
|
|||||||
sourceSite: string;
|
sourceSite: string;
|
||||||
sourceName: string;
|
sourceName: string;
|
||||||
options?: MTLNovelOptions;
|
options?: MTLNovelOptions;
|
||||||
filters?: any;
|
filters?: Filters;
|
||||||
};
|
};
|
||||||
|
|
||||||
class MTLNovelPlugin implements Plugin.PluginBase {
|
export class MTLNovelPlugin implements Plugin.PluginBase {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export type NovelCoolMetadata = {
|
|||||||
options: NovelCoolOptions;
|
options: NovelCoolOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
class NovelCoolPlugin implements Plugin.PluginBase {
|
export class NovelCoolPlugin implements Plugin.PluginBase {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -78,7 +78,7 @@ class NovelCoolPlugin implements Plugin.PluginBase {
|
|||||||
page_size: '20',
|
page_size: '20',
|
||||||
}).toString(),
|
}).toString(),
|
||||||
},
|
},
|
||||||
).then(res => res.json());
|
).then((res: Response) => res.json());
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
list.forEach(novel =>
|
list.forEach(novel =>
|
||||||
@@ -110,7 +110,7 @@ class NovelCoolPlugin implements Plugin.PluginBase {
|
|||||||
lang: this.options.langCode,
|
lang: this.options.langCode,
|
||||||
}).toString(),
|
}).toString(),
|
||||||
},
|
},
|
||||||
).then(res => res.json());
|
).then((res: Response) => res.json());
|
||||||
|
|
||||||
const novel: Plugin.SourceNovel = {
|
const novel: Plugin.SourceNovel = {
|
||||||
path: novelPath,
|
path: novelPath,
|
||||||
@@ -142,7 +142,7 @@ class NovelCoolPlugin implements Plugin.PluginBase {
|
|||||||
lang: this.options.langCode,
|
lang: this.options.langCode,
|
||||||
}).toString(),
|
}).toString(),
|
||||||
},
|
},
|
||||||
).then(res => res.json());
|
).then((res: Response) => res.json());
|
||||||
|
|
||||||
list.forEach(chapter => {
|
list.forEach(chapter => {
|
||||||
if (!chapter.is_locked) {
|
if (!chapter.is_locked) {
|
||||||
@@ -179,7 +179,7 @@ class NovelCoolPlugin implements Plugin.PluginBase {
|
|||||||
lang: this.options.langCode,
|
lang: this.options.langCode,
|
||||||
}).toString(),
|
}).toString(),
|
||||||
},
|
},
|
||||||
).then(res => res.json());
|
).then((res: Response) => res.json());
|
||||||
|
|
||||||
return info.content;
|
return info.content;
|
||||||
}
|
}
|
||||||
@@ -207,7 +207,7 @@ class NovelCoolPlugin implements Plugin.PluginBase {
|
|||||||
page_size: '20',
|
page_size: '20',
|
||||||
}).toString(),
|
}).toString(),
|
||||||
},
|
},
|
||||||
).then(res => res.json());
|
).then((res: Response) => res.json());
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
list.forEach(novel =>
|
list.forEach(novel =>
|
||||||
@@ -266,7 +266,7 @@ type Novel = {
|
|||||||
last_url?: string;
|
last_url?: string;
|
||||||
category_str: string;
|
category_str: string;
|
||||||
category_list: string[];
|
category_list: string[];
|
||||||
star_list: any[];
|
// star_list: any[];
|
||||||
int_mark: string;
|
int_mark: string;
|
||||||
time: string;
|
time: string;
|
||||||
show_ads: string;
|
show_ads: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Parser } from 'htmlparser2';
|
import { Parser } from 'htmlparser2';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi, FetchInit } from '@libs/fetch';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ export type RanobesMetadata = {
|
|||||||
options?: RanobesOptions;
|
options?: RanobesOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
class RanobesPlugin implements Plugin.PluginBase {
|
export class RanobesPlugin implements Plugin.PluginBase {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -32,7 +32,7 @@ class RanobesPlugin implements Plugin.PluginBase {
|
|||||||
this.options = metadata.options as RanobesOptions;
|
this.options = metadata.options as RanobesOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
async safeFecth(url: string, init: any = {}): Promise<string> {
|
async safeFecth(url: string, init?: FetchInit): Promise<string> {
|
||||||
const r = await fetchApi(url, init);
|
const r = await fetchApi(url, init);
|
||||||
if (!r.ok)
|
if (!r.ok)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Parser } from 'htmlparser2';
|
import { Parser } from 'htmlparser2';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi, FetchInit } from '@libs/fetch';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
import { Filters } from '@libs/filterInputs';
|
import { Filters } from '@libs/filterInputs';
|
||||||
@@ -35,7 +35,7 @@ export type ReadNovelFullMetadata = {
|
|||||||
filters?: Filters;
|
filters?: Filters;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ReadNovelFullPlugin implements Plugin.PluginBase {
|
export class ReadNovelFullPlugin implements Plugin.PluginBase {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -50,7 +50,7 @@ class ReadNovelFullPlugin implements Plugin.PluginBase {
|
|||||||
this.icon = `multisrc/readnovelfull/${metadata.id.toLowerCase()}/icon.png`;
|
this.icon = `multisrc/readnovelfull/${metadata.id.toLowerCase()}/icon.png`;
|
||||||
this.site = metadata.sourceSite;
|
this.site = metadata.sourceSite;
|
||||||
const versionIncrements = metadata.options?.versionIncrements || 0;
|
const versionIncrements = metadata.options?.versionIncrements || 0;
|
||||||
this.version = `2.2.${0 + versionIncrements}`;
|
this.version = `2.2.${1 + versionIncrements}`;
|
||||||
this.options = metadata.options;
|
this.options = metadata.options;
|
||||||
this.filters = metadata.filters;
|
this.filters = metadata.filters;
|
||||||
}
|
}
|
||||||
@@ -145,7 +145,10 @@ class ReadNovelFullPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
{ filters, showLatestNovels }: Plugin.PopularNovelsOptions,
|
{
|
||||||
|
filters,
|
||||||
|
showLatestNovels,
|
||||||
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const {
|
const {
|
||||||
pageParam = 'page',
|
pageParam = 'page',
|
||||||
@@ -582,9 +585,10 @@ class ReadNovelFullPlugin implements Plugin.PluginBase {
|
|||||||
'"': '"',
|
'"': '"',
|
||||||
"'": ''',
|
"'": ''',
|
||||||
' ': ' ',
|
' ': ' ',
|
||||||
|
'\u200C': '', // this is probably a breaking change, report if paragraphs look weird
|
||||||
};
|
};
|
||||||
const escapeHtml = (text: string): string =>
|
const escapeHtml = (text: string) =>
|
||||||
text.replace(/[&<>"' ]/g, char => escapeMap[char]);
|
text.replace(/[&<>"'\xA0\u200C]/g, char => escapeMap[char]);
|
||||||
|
|
||||||
const parser = new Parser({
|
const parser = new Parser({
|
||||||
onopentag(name, attribs) {
|
onopentag(name, attribs) {
|
||||||
@@ -602,7 +606,7 @@ class ReadNovelFullPlugin implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ParsingState.Chapter:
|
case ParsingState.Chapter:
|
||||||
if (name === 'sub') {
|
if (name === 'sub' || name === 'iframe') {
|
||||||
pushState(ParsingState.Hidden);
|
pushState(ParsingState.Hidden);
|
||||||
} else if (name === 'div') {
|
} else if (name === 'div') {
|
||||||
depth++;
|
depth++;
|
||||||
@@ -655,7 +659,8 @@ class ReadNovelFullPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
ontext(text) {
|
ontext(text) {
|
||||||
if (currentState() === ParsingState.Chapter) {
|
if (currentState() === ParsingState.Chapter) {
|
||||||
chapterHtml.push(escapeHtml(text));
|
const data = escapeHtml(text);
|
||||||
|
chapterHtml.push(data.trim().replace(/\s\s+/, ' '));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -663,7 +668,7 @@ class ReadNovelFullPlugin implements Plugin.PluginBase {
|
|||||||
const state = currentState();
|
const state = currentState();
|
||||||
|
|
||||||
if (state === ParsingState.Hidden) {
|
if (state === ParsingState.Hidden) {
|
||||||
if (name === 'sub') {
|
if (name === 'sub' || name === 'iframe') {
|
||||||
popState();
|
popState();
|
||||||
} else if (name === 'div') {
|
} else if (name === 'div') {
|
||||||
depthHide--;
|
depthHide--;
|
||||||
@@ -728,7 +733,7 @@ class ReadNovelFullPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
const url = `${this.site}${searchPage}${!postSearch ? `?${params.toString()}` : ''}`;
|
const url = `${this.site}${searchPage}${!postSearch ? `?${params.toString()}` : ''}`;
|
||||||
|
|
||||||
const fetchOptions: RequestInit | undefined = postSearch
|
const fetchOptions: FetchInit | undefined = postSearch
|
||||||
? {
|
? {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: params.toString(),
|
body: params.toString(),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ require('module-alias/register');
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { FilterTypes, FilterOption } from '@libs/filterInputs';
|
import { Filters, FilterTypes, FilterOption } from '@libs/filterInputs';
|
||||||
const type: string[] = ['genres', 'status', 'sort'];
|
const type: string[] = ['genres', 'status', 'sort'];
|
||||||
|
|
||||||
async function getFilters(name: string, url: string) {
|
async function getFilters(name: string, url: string) {
|
||||||
@@ -10,7 +10,7 @@ async function getFilters(name: string, url: string) {
|
|||||||
res.text(),
|
res.text(),
|
||||||
);
|
);
|
||||||
const $: cheerio.CheerioAPI = cheerio.load(html);
|
const $: cheerio.CheerioAPI = cheerio.load(html);
|
||||||
const filters: any = {
|
const filters: Filters = {
|
||||||
'sort': {
|
'sort': {
|
||||||
type: FilterTypes.Picker,
|
type: FilterTypes.Picker,
|
||||||
label: 'Sort By',
|
label: 'Sort By',
|
||||||
@@ -76,9 +76,9 @@ async function getFilters(name: string, url: string) {
|
|||||||
.map((index, element) => loadedCheerio(element).attr('href'))
|
.map((index, element) => loadedCheerio(element).attr('href'))
|
||||||
.get();
|
.get();
|
||||||
// ===================== tags ======================
|
// ===================== tags ======================
|
||||||
for (let i = 0; i < allPage.length; i++) {
|
for (const page of allPage) {
|
||||||
console.log('fetch', url + allPage[i]);
|
console.log('fetch', url + page);
|
||||||
const resTags = await fetch(url + allPage[i]).then(res => res.text());
|
const resTags = await fetch(url + page).then(res => res.text());
|
||||||
const $: cheerio.CheerioAPI = cheerio.load(resTags);
|
const $: cheerio.CheerioAPI = cheerio.load(resTags);
|
||||||
$('.tag-items > li > a').each((index, element) =>
|
$('.tag-items > li > a').each((index, element) =>
|
||||||
filters['tags'].options.push({
|
filters['tags'].options.push({
|
||||||
@@ -97,10 +97,10 @@ async function getFilters(name: string, url: string) {
|
|||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
console.log(
|
console.log(
|
||||||
'fetch',
|
'fetch',
|
||||||
url + allPage[i].replace('-0.html', `-${pageNo + 1}.html`),
|
url + page.replace('-0.html', `-${pageNo + 1}.html`),
|
||||||
);
|
);
|
||||||
const resTags = await fetch(
|
const resTags = await fetch(
|
||||||
url + allPage[i].replace('-0.html', `-${pageNo + 1}.html`),
|
url + page.replace('-0.html', `-${pageNo + 1}.html`),
|
||||||
).then(res => res.text());
|
).then(res => res.text());
|
||||||
const $: cheerio.CheerioAPI = cheerio.load(resTags);
|
const $: cheerio.CheerioAPI = cheerio.load(resTags);
|
||||||
|
|
||||||
@@ -141,9 +141,9 @@ async function askGetFilter() {
|
|||||||
async (url: string) => {
|
async (url: string) => {
|
||||||
try {
|
try {
|
||||||
await getFilters(name, url);
|
await getFilters(name, url);
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error('Error while getting filters from', url);
|
console.error('Error while getting filters from', url);
|
||||||
console.log(e.message || e);
|
console.log(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
readline.close();
|
readline.close();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ export type ReadwnMetadata = {
|
|||||||
id: string;
|
id: string;
|
||||||
sourceSite: string;
|
sourceSite: string;
|
||||||
sourceName: string;
|
sourceName: string;
|
||||||
filters?: any;
|
filters?: Filters;
|
||||||
options?: ReadwnOptions;
|
options?: ReadwnOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ReadwnPlugin implements Plugin.PluginBase {
|
export class ReadwnPlugin implements Plugin.PluginBase {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -31,13 +31,16 @@ class ReadwnPlugin implements Plugin.PluginBase {
|
|||||||
this.icon = `multisrc/readwn/${metadata.id.toLowerCase()}/icon.png`;
|
this.icon = `multisrc/readwn/${metadata.id.toLowerCase()}/icon.png`;
|
||||||
this.site = metadata.sourceSite;
|
this.site = metadata.sourceSite;
|
||||||
const versionIncrements = metadata.options?.versionIncrements || 0;
|
const versionIncrements = metadata.options?.versionIncrements || 0;
|
||||||
this.version = `1.0.${2 + versionIncrements}`;
|
this.version = `1.0.${3 + versionIncrements}`;
|
||||||
this.filters = metadata.filters;
|
this.filters = metadata.filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
{ filters, showLatestNovels }: Plugin.PopularNovelsOptions,
|
{
|
||||||
|
filters,
|
||||||
|
showLatestNovels,
|
||||||
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
let url = this.site + '/list/';
|
let url = this.site + '/list/';
|
||||||
url += (filters?.genres?.value || 'all') + '/';
|
url += (filters?.genres?.value || 'all') + '/';
|
||||||
@@ -50,7 +53,7 @@ class ReadwnPlugin implements Plugin.PluginBase {
|
|||||||
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: Response) => res.text());
|
||||||
const loadedCheerio = parseHTML(body);
|
const loadedCheerio = parseHTML(body);
|
||||||
|
|
||||||
const novels: Plugin.NovelItem[] = loadedCheerio('li.novel-item')
|
const novels: Plugin.NovelItem[] = loadedCheerio('li.novel-item')
|
||||||
@@ -68,7 +71,9 @@ class ReadwnPlugin implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
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: Response) =>
|
||||||
|
res.text(),
|
||||||
|
);
|
||||||
const loadedCheerio = parseHTML(body);
|
const loadedCheerio = parseHTML(body);
|
||||||
|
|
||||||
const novel: Plugin.SourceNovel = {
|
const novel: Plugin.SourceNovel = {
|
||||||
@@ -186,7 +191,7 @@ class ReadwnPlugin implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
const body = await fetchApi(this.site + chapterPath).then(res =>
|
const body = await fetchApi(this.site + chapterPath).then((res: Response) =>
|
||||||
res.text(),
|
res.text(),
|
||||||
);
|
);
|
||||||
const loadedCheerio = parseHTML(body);
|
const loadedCheerio = parseHTML(body);
|
||||||
@@ -205,11 +210,11 @@ class ReadwnPlugin implements Plugin.PluginBase {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
show: 'title',
|
show: 'title',
|
||||||
tempid: 1,
|
tempid: '1',
|
||||||
tbname: 'news',
|
tbname: 'news',
|
||||||
keyboard: searchTerm,
|
keyboard: searchTerm,
|
||||||
}).toString(),
|
}).toString(),
|
||||||
}).then(res => res.text());
|
}).then((res: Response) => res.text());
|
||||||
const loadedCheerio = parseHTML(result);
|
const loadedCheerio = parseHTML(result);
|
||||||
|
|
||||||
const novels: Plugin.NovelItem[] = loadedCheerio('li.novel-item')
|
const novels: Plugin.NovelItem[] = loadedCheerio('li.novel-item')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Filters } from '@libs/filterInputs';
|
import { FilterToValues, Filters } from '@libs/filterInputs';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -18,7 +18,7 @@ const headers = {
|
|||||||
'accept-encoding': 'gzip',
|
'accept-encoding': 'gzip',
|
||||||
};
|
};
|
||||||
|
|
||||||
class RulatePlugin implements Plugin.PluginBase {
|
export class RulatePlugin implements Plugin.PluginBase {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -32,14 +32,14 @@ class RulatePlugin implements Plugin.PluginBase {
|
|||||||
this.name = metadata.sourceName + ' (API)';
|
this.name = metadata.sourceName + ' (API)';
|
||||||
this.icon = `multisrc/rulate/${metadata.id.toLowerCase()}/icon.png`;
|
this.icon = `multisrc/rulate/${metadata.id.toLowerCase()}/icon.png`;
|
||||||
this.site = metadata.sourceSite;
|
this.site = metadata.sourceSite;
|
||||||
this.version = '1.0.' + (0 + metadata.versionIncrements);
|
this.version = '1.0.' + (1 + metadata.versionIncrements);
|
||||||
this.filters = metadata.filters;
|
this.filters = metadata.filters;
|
||||||
this.key = metadata.key;
|
this.key = metadata.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseNovels(url: string) {
|
parseNovels(url: string) {
|
||||||
return fetchApi(url, { headers })
|
return fetchApi(url, { headers })
|
||||||
.then(res => res.json() as Promise<SearchResponse>)
|
.then((res: Response) => res.json() as Promise<SearchResponse>)
|
||||||
.then((data: SearchResponse) => {
|
.then((data: SearchResponse) => {
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
@@ -59,12 +59,16 @@ class RulatePlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
page: number,
|
page: number,
|
||||||
{ filters, showLatestNovels }: Plugin.PopularNovelsOptions,
|
{
|
||||||
|
filters,
|
||||||
|
showLatestNovels,
|
||||||
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
let url = this.site + '/api3/searchBooks?limit=40&page=' + page;
|
let url = this.site + '/api3/searchBooks?limit=40&page=' + page;
|
||||||
url += '&sort=' + (showLatestNovels ? '4' : filters?.sort?.value || '6');
|
url += '&sort=' + (showLatestNovels ? '4' : filters?.sort?.value || '6');
|
||||||
|
|
||||||
Object.entries(filters || {}).forEach(([type, { value }]) => {
|
Object.entries(filters || {}).forEach(([type, filter]) => {
|
||||||
|
const { value } = filter as FilterToValues<Filters>[string];
|
||||||
if (value instanceof Array && value.length) {
|
if (value instanceof Array && value.length) {
|
||||||
url += '&' + value.map(val => type + '[]=' + val).join('&');
|
url += '&' + value.map(val => type + '[]=' + val).join('&');
|
||||||
}
|
}
|
||||||
@@ -76,7 +80,7 @@ class RulatePlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async searchNovels(
|
async searchNovels(
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
page: number = 1,
|
page = 1,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const url = `${this.site}/api3/searchBooks?t=${encodeURIComponent(
|
const url = `${this.site}/api3/searchBooks?t=${encodeURIComponent(
|
||||||
searchTerm,
|
searchTerm,
|
||||||
@@ -88,14 +92,16 @@ class RulatePlugin implements Plugin.PluginBase {
|
|||||||
const book = await fetchApi(
|
const book = await fetchApi(
|
||||||
this.site + '/api3/book?book_id=' + novelPath + '&key=' + this.key,
|
this.site + '/api3/book?book_id=' + novelPath + '&key=' + this.key,
|
||||||
{ headers },
|
{ headers },
|
||||||
).then(res => res.json() as Promise<BookResponse>);
|
).then((res: Response) => res.json() as Promise<BookResponse>);
|
||||||
|
|
||||||
const novel: Plugin.SourceNovel = {
|
const novel: Plugin.SourceNovel = {
|
||||||
name: book.response.t_title || book.response.s_title,
|
name: book.response.t_title || book.response.s_title,
|
||||||
path: novelPath,
|
path: novelPath,
|
||||||
cover: book.response.img,
|
cover: book.response.img,
|
||||||
genres: [book.response.genres, book.response.tags]
|
genres: [book.response.genres, book.response.tags]
|
||||||
.flatMap(c => c?.map?.((g: any) => g.title || g.name))
|
.flatMap(c =>
|
||||||
|
c?.map?.((g: { title?: string; name?: string }) => g.title || g.name),
|
||||||
|
)
|
||||||
.join(','),
|
.join(','),
|
||||||
summary: book.response.description,
|
summary: book.response.description,
|
||||||
author: book.response.author,
|
author: book.response.author,
|
||||||
@@ -118,12 +124,12 @@ class RulatePlugin implements Plugin.PluginBase {
|
|||||||
'&key=' +
|
'&key=' +
|
||||||
this.key,
|
this.key,
|
||||||
{ headers },
|
{ headers },
|
||||||
).then(res => res.json() as Promise<ChaptersResponse>);
|
).then((res: Response) => res.json() as Promise<ChaptersResponse>);
|
||||||
|
|
||||||
const chapters: Plugin.ChapterItem[] = [];
|
const chapters: Plugin.ChapterItem[] = [];
|
||||||
|
|
||||||
if (chaptersData.response && Array.isArray(chaptersData.response)) {
|
if (chaptersData.response && Array.isArray(chaptersData.response)) {
|
||||||
chaptersData.response.forEach(chapter => {
|
chaptersData.response.forEach((chapter: ChapterResponse) => {
|
||||||
if (chapter.can_read && chapter.subscription === 0) {
|
if (chapter.can_read && chapter.subscription === 0) {
|
||||||
chapters.push({
|
chapters.push({
|
||||||
name: chapter.title + (chapter.illustrated ? ' 🖼️' : ''),
|
name: chapter.title + (chapter.illustrated ? ' 🖼️' : ''),
|
||||||
@@ -150,7 +156,7 @@ class RulatePlugin implements Plugin.PluginBase {
|
|||||||
'&key=' +
|
'&key=' +
|
||||||
this.key,
|
this.key,
|
||||||
{ headers },
|
{ headers },
|
||||||
).then(res => res.json() as Promise<ChapterTextResponse>);
|
).then((res: Response) => res.json() as Promise<ChapterTextResponse>);
|
||||||
|
|
||||||
return body.response.text;
|
return body.response.text;
|
||||||
}
|
}
|
||||||
@@ -158,7 +164,7 @@ class RulatePlugin implements Plugin.PluginBase {
|
|||||||
this.site + '/book/' + path + (isNovel ? '/' : '/ready_new');
|
this.site + '/book/' + path + (isNovel ? '/' : '/ready_new');
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SearchResponse {
|
type SearchResponse = {
|
||||||
status: string;
|
status: string;
|
||||||
response: {
|
response: {
|
||||||
t_title?: string;
|
t_title?: string;
|
||||||
@@ -166,9 +172,9 @@ interface SearchResponse {
|
|||||||
id: number;
|
id: number;
|
||||||
img: string;
|
img: string;
|
||||||
}[];
|
}[];
|
||||||
}
|
};
|
||||||
|
|
||||||
interface BookResponse {
|
type BookResponse = {
|
||||||
response: {
|
response: {
|
||||||
t_title?: string;
|
t_title?: string;
|
||||||
s_title: string;
|
s_title: string;
|
||||||
@@ -181,22 +187,24 @@ interface BookResponse {
|
|||||||
status: string;
|
status: string;
|
||||||
rate?: { sum: number; count: number };
|
rate?: { sum: number; count: number };
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
interface ChaptersResponse {
|
type ChapterResponse = {
|
||||||
response: {
|
title: string;
|
||||||
title: string;
|
id: number;
|
||||||
id: number;
|
ord: number;
|
||||||
ord: number;
|
cdate: number;
|
||||||
cdate: number;
|
subscription: number;
|
||||||
subscription: number;
|
can_read: boolean;
|
||||||
can_read: boolean;
|
illustrated?: boolean;
|
||||||
illustrated?: boolean;
|
};
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChapterTextResponse {
|
type ChaptersResponse = {
|
||||||
|
response: ChapterResponse[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChapterTextResponse = {
|
||||||
response: {
|
response: {
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -4,10 +4,18 @@ import { Plugin } from '@/types/plugin';
|
|||||||
import { defaultCover } from '@libs/defaultCover';
|
import { defaultCover } from '@libs/defaultCover';
|
||||||
import { Filters } from '@libs/filterInputs';
|
import { Filters } from '@libs/filterInputs';
|
||||||
|
|
||||||
|
type BloggerEntry = {
|
||||||
|
title: { $t: string };
|
||||||
|
link: { rel: string; href: string }[];
|
||||||
|
media$thumbnail?: { url: string };
|
||||||
|
updated?: { $t: string };
|
||||||
|
content?: { $t: string };
|
||||||
|
};
|
||||||
|
|
||||||
class BlogDoAnonNovelsPlugin implements Plugin.PluginBase {
|
class BlogDoAnonNovelsPlugin implements Plugin.PluginBase {
|
||||||
id = 'blogdoamonnovels';
|
id = 'blogdoamonnovels';
|
||||||
name = 'Blog do Amon Novels';
|
name = 'Blog do Amon Novels';
|
||||||
version = '1.0.0';
|
version = '1.0.1';
|
||||||
icon = 'src/pt-br/blogdoamonnovels/icon.png';
|
icon = 'src/pt-br/blogdoamonnovels/icon.png';
|
||||||
site = 'https://www.blogdoamonnovels.com';
|
site = 'https://www.blogdoamonnovels.com';
|
||||||
|
|
||||||
@@ -20,15 +28,15 @@ class BlogDoAnonNovelsPlugin implements Plugin.PluginBase {
|
|||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
result.feed.entry.forEach((n: any) => {
|
result.feed.entry.forEach((n: BloggerEntry) => {
|
||||||
const novelName: string = n.title.$t;
|
const novelName: string = n.title.$t;
|
||||||
|
|
||||||
const novelUrl = n.link.find(
|
const novelUrl = n.link.find(
|
||||||
(t: { rel: string }) => 'alternate' == t.rel,
|
(t: { rel: string }) => 'alternate' == t.rel,
|
||||||
).href;
|
)?.href;
|
||||||
if (!novelUrl) return;
|
if (!novelUrl) return;
|
||||||
|
|
||||||
const coverUrl = n.media$thumbnail.url.replace('/s72-c/', '/w340/');
|
const coverUrl = n.media$thumbnail?.url.replace('/s72-c/', '/w340/');
|
||||||
|
|
||||||
const novel = {
|
const novel = {
|
||||||
name: novelName,
|
name: novelName,
|
||||||
@@ -164,7 +172,7 @@ class BlogDoAnonNovelsPlugin implements Plugin.PluginBase {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
result.feed.entry.forEach((n: any) => {
|
result.feed.entry.forEach((n: BloggerEntry) => {
|
||||||
let chapterName: string = n.title.$t;
|
let chapterName: string = n.title.$t;
|
||||||
|
|
||||||
// Skip self
|
// Skip self
|
||||||
@@ -175,8 +183,8 @@ class BlogDoAnonNovelsPlugin implements Plugin.PluginBase {
|
|||||||
// const chapterNumber: number = parseFloat(chapterName.split(' ', 2)[1]);
|
// const chapterNumber: number = parseFloat(chapterName.split(' ', 2)[1]);
|
||||||
const chapterUrl = n.link.find(
|
const chapterUrl = n.link.find(
|
||||||
(t: { rel: string }) => 'alternate' == t.rel,
|
(t: { rel: string }) => 'alternate' == t.rel,
|
||||||
).href;
|
)?.href;
|
||||||
const releaseTime = new Date(n.updated.$t);
|
const releaseTime = n.updated ? new Date(n.updated.$t) : null;
|
||||||
if (!chapterUrl) return;
|
if (!chapterUrl) return;
|
||||||
|
|
||||||
if (n.content && n.content.$t) {
|
if (n.content && n.content.$t) {
|
||||||
@@ -191,7 +199,7 @@ class BlogDoAnonNovelsPlugin implements Plugin.PluginBase {
|
|||||||
chapters.push({
|
chapters.push({
|
||||||
name: chapterName,
|
name: chapterName,
|
||||||
path: chapterUrl.replace(this.site, ''),
|
path: chapterUrl.replace(this.site, ''),
|
||||||
releaseTime: releaseTime.toISOString(),
|
releaseTime: releaseTime?.toISOString(),
|
||||||
// chapterNumber: chapterNumber,
|
// chapterNumber: chapterNumber,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class Illusia implements Plugin.PluginBase {
|
|||||||
name = 'Illusia';
|
name = 'Illusia';
|
||||||
icon = 'src/pt-br/illusia/icon.png';
|
icon = 'src/pt-br/illusia/icon.png';
|
||||||
site = 'https://illusia.com.br';
|
site = 'https://illusia.com.br';
|
||||||
version = '1.0.1';
|
version = '1.0.2';
|
||||||
filters: Filters | undefined = undefined;
|
filters: Filters | undefined = undefined;
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
@@ -323,7 +323,7 @@ class Illusia implements Plugin.PluginBase {
|
|||||||
return uniqueNovels;
|
return uniqueNovels;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveUrl = (path: string, isNovel?: boolean) => `${this.site}/${path}/`;
|
// resolveUrl = (path: string, isNovel?: boolean) => `${this.site}/${path}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Illusia();
|
export default new Illusia();
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { FilterTypes, Filters } from '@libs/filterInputs';
|
|||||||
import { defaultCover } from '@libs/defaultCover';
|
import { defaultCover } from '@libs/defaultCover';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
|
|
||||||
const headers: any = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-Inertia': true,
|
'X-Inertia': 'true',
|
||||||
'X-Inertia-Version': '6666cd76f96956469e7be39d750cc7d9',
|
'X-Inertia-Version': '6666cd76f96956469e7be39d750cc7d9',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,10 +30,12 @@ class freedlit implements Plugin.PluginBase {
|
|||||||
url += '&page=' + page;
|
url += '&page=' + page;
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
const { books }: { books: Books } = await fetchApi(url).then(res => {
|
const { books }: { books: Books } = await fetchApi(url).then(
|
||||||
this.getToken(res.headers);
|
(res: Response) => {
|
||||||
return res.json();
|
this.getToken(res.headers);
|
||||||
});
|
return res.json();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
books.data.forEach(novel =>
|
books.data.forEach(novel =>
|
||||||
novels.push({
|
novels.push({
|
||||||
@@ -57,7 +59,7 @@ class freedlit implements Plugin.PluginBase {
|
|||||||
headers,
|
headers,
|
||||||
Referer: this.resolveUrl(novelPath, true),
|
Referer: this.resolveUrl(novelPath, true),
|
||||||
},
|
},
|
||||||
).then(res => {
|
).then((res: Response) => {
|
||||||
this.getToken(res.headers);
|
this.getToken(res.headers);
|
||||||
return res.json();
|
return res.json();
|
||||||
});
|
});
|
||||||
@@ -79,7 +81,7 @@ class freedlit implements Plugin.PluginBase {
|
|||||||
Referer: this.resolveUrl(novelPath),
|
Referer: this.resolveUrl(novelPath),
|
||||||
body: JSON.stringify({ book_id: novelPath }),
|
body: JSON.stringify({ book_id: novelPath }),
|
||||||
},
|
},
|
||||||
).then(res => {
|
).then((res: Response) => {
|
||||||
this.getToken(res.headers);
|
this.getToken(res.headers);
|
||||||
return res.json();
|
return res.json();
|
||||||
});
|
});
|
||||||
@@ -103,7 +105,9 @@ class freedlit implements Plugin.PluginBase {
|
|||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
const [book_id, chapter_id] = chapterPath.split('/');
|
const [book_id, chapter_id] = chapterPath.split('/');
|
||||||
if (!headers['X-XSRF-TOKEN']) {
|
if (!headers['X-XSRF-TOKEN']) {
|
||||||
await fetchApi(this.site).then(res => this.getToken(res.headers));
|
await fetchApi(this.site).then((res: Response) =>
|
||||||
|
this.getToken(res.headers),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { success }: { success: chapterContent } = await fetchApi(
|
const { success }: { success: chapterContent } = await fetchApi(
|
||||||
@@ -114,7 +118,7 @@ class freedlit implements Plugin.PluginBase {
|
|||||||
Referer: this.resolveUrl(chapterPath),
|
Referer: this.resolveUrl(chapterPath),
|
||||||
body: JSON.stringify({ book_id, chapter_id }),
|
body: JSON.stringify({ book_id, chapter_id }),
|
||||||
},
|
},
|
||||||
).then(res => {
|
).then((res: Response) => {
|
||||||
this.getToken(res.headers);
|
this.getToken(res.headers);
|
||||||
return res.json();
|
return res.json();
|
||||||
});
|
});
|
||||||
@@ -126,7 +130,7 @@ class freedlit implements Plugin.PluginBase {
|
|||||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||||
const { success }: { success: book[] } = await fetchApi(
|
const { success }: { success: book[] } = await fetchApi(
|
||||||
this.site + '/api/search?query=' + searchTerm,
|
this.site + '/api/search?query=' + searchTerm,
|
||||||
).then(res => {
|
).then((res: Response) => {
|
||||||
this.getToken(res.headers);
|
this.getToken(res.headers);
|
||||||
return res.json();
|
return res.json();
|
||||||
});
|
});
|
||||||
@@ -152,9 +156,9 @@ class freedlit implements Plugin.PluginBase {
|
|||||||
resolveUrl = (path: string, isNovel?: boolean) =>
|
resolveUrl = (path: string, isNovel?: boolean) =>
|
||||||
this.site + (isNovel ? '/book/' : '/reader/') + path;
|
this.site + (isNovel ? '/book/' : '/reader/') + path;
|
||||||
|
|
||||||
getToken = (header: any) => {
|
getToken = (header: Headers) => {
|
||||||
const cookies = header.map['set-cookie'] || '';
|
const cookies = header.get('set-cookie') || '';
|
||||||
for (const cookie of cookies.split('; ') as string[]) {
|
for (const cookie of cookies.split('; ')) {
|
||||||
const [key, val] = cookie.split('=');
|
const [key, val] = cookie.split('=');
|
||||||
if (key === 'XSRF-TOKEN') {
|
if (key === 'XSRF-TOKEN') {
|
||||||
headers['X-XSRF-TOKEN'] = decodeURIComponent(val);
|
headers['X-XSRF-TOKEN'] = decodeURIComponent(val);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class AuthorToday implements Plugin.PluginBase {
|
|||||||
name = 'Автор Тудей';
|
name = 'Автор Тудей';
|
||||||
icon = 'src/ru/authortoday/icon.png';
|
icon = 'src/ru/authortoday/icon.png';
|
||||||
site = 'https://author.today';
|
site = 'https://author.today';
|
||||||
version = '1.2.1';
|
version = '1.2.2';
|
||||||
|
|
||||||
private userAgent =
|
private userAgent =
|
||||||
'Mozilla/5.0 (Android 15; Mobile; rv:138.0) Gecko/138.0 Firefox/138.0';
|
'Mozilla/5.0 (Android 15; Mobile; rv:138.0) Gecko/138.0 Firefox/138.0';
|
||||||
@@ -236,7 +236,8 @@ class AuthorToday implements Plugin.PluginBase {
|
|||||||
headers: { 'User-Agent': this.userAgent },
|
headers: { 'User-Agent': this.userAgent },
|
||||||
}).then(res => res.text());
|
}).then(res => res.text());
|
||||||
|
|
||||||
let [workID, chapterID] = chapterPath.split('/');
|
const [workID, initialChapterID] = chapterPath.split('/');
|
||||||
|
let chapterID = initialChapterID;
|
||||||
const userRaw = html.match(/userId:(.*?),/)?.[1]?.trim();
|
const userRaw = html.match(/userId:(.*?),/)?.[1]?.trim();
|
||||||
const userId = userRaw === 'null' ? '' : userRaw;
|
const userId = userRaw === 'null' ? '' : userRaw;
|
||||||
|
|
||||||
|
|||||||
+24
-25
@@ -17,15 +17,13 @@ class Neobook implements Plugin.PluginBase {
|
|||||||
name = 'Neobook';
|
name = 'Neobook';
|
||||||
site = 'https://neobook.org';
|
site = 'https://neobook.org';
|
||||||
apiSite = 'https://api.neobook.org/';
|
apiSite = 'https://api.neobook.org/';
|
||||||
version = '1.0.2';
|
version = '1.0.3';
|
||||||
icon = 'src/ru/neobook/icon.png';
|
icon = 'src/ru/neobook/icon.png';
|
||||||
|
|
||||||
async fetchNovels(
|
async fetchNovels(
|
||||||
page: number,
|
page: number,
|
||||||
{
|
showLatestNovels?: boolean,
|
||||||
filters,
|
filters?: Plugin.PopularNovelsOptions<typeof this.filters>['filters'],
|
||||||
showLatestNovels,
|
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
|
||||||
searchTerm?: string,
|
searchTerm?: string,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -70,17 +68,18 @@ class Neobook implements Plugin.PluginBase {
|
|||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
popularNovels = this.fetchNovels;
|
async popularNovels(
|
||||||
|
|
||||||
async searchNovels(
|
|
||||||
searchTerm: string,
|
|
||||||
page: number,
|
page: number,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
{
|
||||||
const defaultOptions: any = {
|
showLatestNovels,
|
||||||
filters: undefined,
|
filters,
|
||||||
showLatestNovels: false,
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
};
|
) {
|
||||||
return this.fetchNovels(page, defaultOptions, searchTerm);
|
return this.fetchNovels(page, showLatestNovels, filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchNovels(searchTerm: string, page: number) {
|
||||||
|
return this.fetchNovels(page, false, undefined, searchTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
@@ -215,8 +214,8 @@ class Neobook implements Plugin.PluginBase {
|
|||||||
export default new Neobook();
|
export default new Neobook();
|
||||||
|
|
||||||
type BundleBooks = {
|
type BundleBooks = {
|
||||||
categories: any[];
|
// categories: any[];
|
||||||
topSection: any[];
|
// topSection: any[];
|
||||||
feed: Novels[];
|
feed: Novels[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -286,7 +285,7 @@ type Novels = {
|
|||||||
readProgress?: ReadProgress;
|
readProgress?: ReadProgress;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
chapters?: Chapter[];
|
chapters?: Chapter[];
|
||||||
lastComments?: any[];
|
// lastComments?: any[];
|
||||||
bottomSection?: BottomSection;
|
bottomSection?: BottomSection;
|
||||||
carouselItems?: Novels[];
|
carouselItems?: Novels[];
|
||||||
book?: Novels;
|
book?: Novels;
|
||||||
@@ -339,7 +338,7 @@ type Data = {
|
|||||||
dateEdit: string;
|
dateEdit: string;
|
||||||
datePublish: string;
|
datePublish: string;
|
||||||
attachment: Attachment;
|
attachment: Attachment;
|
||||||
lastComments: any[];
|
// lastComments: any[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type NovelsCounters = {
|
type NovelsCounters = {
|
||||||
@@ -371,12 +370,12 @@ type Login = {
|
|||||||
grm: string;
|
grm: string;
|
||||||
counters: LoginCounters;
|
counters: LoginCounters;
|
||||||
user: LoginUser;
|
user: LoginUser;
|
||||||
pro: any[];
|
// pro: any[];
|
||||||
earn: any[];
|
// earn: any[];
|
||||||
boost: any[];
|
// boost: any[];
|
||||||
deposit: any[];
|
// deposit: any[];
|
||||||
affiliate: any[];
|
// affiliate: any[];
|
||||||
preferences: any[];
|
// preferences: any[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type LoginCounters = {
|
type LoginCounters = {
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ class novelOvh implements Plugin.PluginBase {
|
|||||||
: filters?.sort?.value || 'averageRating') +
|
: filters?.sort?.value || 'averageRating') +
|
||||||
',desc';
|
',desc';
|
||||||
|
|
||||||
const books: BooksEntity[] = await fetchApi(url).then(res => res.json());
|
const books: BooksEntity[] = await fetchApi(url).then((res: Response) =>
|
||||||
|
res.json(),
|
||||||
|
);
|
||||||
|
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
books.forEach(novel =>
|
books.forEach(novel =>
|
||||||
@@ -44,7 +46,7 @@ class novelOvh implements Plugin.PluginBase {
|
|||||||
'/content/' +
|
'/content/' +
|
||||||
novelPath +
|
novelPath +
|
||||||
'?_data=routes/reader/book/$slug/index',
|
'?_data=routes/reader/book/$slug/index',
|
||||||
).then(res => res.json());
|
).then((res: Response) => res.json());
|
||||||
|
|
||||||
const novel: Plugin.SourceNovel = {
|
const novel: Plugin.SourceNovel = {
|
||||||
path: novelPath,
|
path: novelPath,
|
||||||
@@ -99,7 +101,7 @@ class novelOvh implements Plugin.PluginBase {
|
|||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
const book: responseChapter = await fetchApi(
|
const book: responseChapter = await fetchApi(
|
||||||
this.apiSite + 'chapters/' + chapterPath.split('/')[1],
|
this.apiSite + 'chapters/' + chapterPath.split('/')[1],
|
||||||
).then(res => res.json());
|
).then((res: Response) => res.json());
|
||||||
|
|
||||||
const image = Object.fromEntries(
|
const image = Object.fromEntries(
|
||||||
book?.pages?.map(({ id, image }) => [id, image]) || [],
|
book?.pages?.map(({ id, image }) => [id, image]) || [],
|
||||||
@@ -111,7 +113,9 @@ class novelOvh implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||||
const url = this.apiSite + 'books?type=NOVEL&search=' + searchTerm;
|
const url = this.apiSite + 'books?type=NOVEL&search=' + searchTerm;
|
||||||
const books: BooksEntity[] = await fetchApi(url).then(res => res.json());
|
const books: BooksEntity[] = await fetchApi(url).then((res: Response) =>
|
||||||
|
res.json(),
|
||||||
|
);
|
||||||
|
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
books.forEach(novel =>
|
books.forEach(novel =>
|
||||||
|
|||||||
+12
-13
@@ -16,12 +16,12 @@ class TL implements Plugin.PluginBase {
|
|||||||
id = 'TL';
|
id = 'TL';
|
||||||
name = 'NovelTL';
|
name = 'NovelTL';
|
||||||
site = 'https://novel.tl';
|
site = 'https://novel.tl';
|
||||||
version = '1.0.1';
|
version = '1.0.2';
|
||||||
icon = 'src/ru/noveltl/icon.png';
|
icon = 'src/ru/noveltl/icon.png';
|
||||||
|
|
||||||
async fetchNovels(
|
async fetchNovels(
|
||||||
page: number,
|
page: number,
|
||||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
filters?: Plugin.PopularNovelsOptions<typeof this.filters>['filters'],
|
||||||
searchTerm?: string,
|
searchTerm?: string,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const { data }: response = await fetchApi(
|
const { data }: response = await fetchApi(
|
||||||
@@ -40,10 +40,10 @@ class TL implements Plugin.PluginBase {
|
|||||||
variables: {
|
variables: {
|
||||||
filter: {
|
filter: {
|
||||||
query: searchTerm || undefined,
|
query: searchTerm || undefined,
|
||||||
tags: filters.tags?.value?.length
|
tags: filters?.tags?.value?.length
|
||||||
? filters.tags.value
|
? filters.tags.value
|
||||||
: undefined,
|
: undefined,
|
||||||
genres: filters.genres?.value?.length
|
genres: filters?.genres?.value?.length
|
||||||
? filters.genres.value
|
? filters.genres.value
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
@@ -69,16 +69,15 @@ class TL implements Plugin.PluginBase {
|
|||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
popularNovels = this.fetchNovels;
|
async popularNovels(
|
||||||
|
|
||||||
async searchNovels(
|
|
||||||
searchTerm: string,
|
|
||||||
page: number,
|
page: number,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
const defaultOptions: any = {
|
) {
|
||||||
filters: {},
|
return this.fetchNovels(page, filters);
|
||||||
};
|
}
|
||||||
return this.fetchNovels(page, defaultOptions, searchTerm);
|
|
||||||
|
async searchNovels(searchTerm: string, page: number) {
|
||||||
|
return this.fetchNovels(page, undefined, searchTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class RLIB implements Plugin.PluginBase {
|
|||||||
name = 'RanobeLib';
|
name = 'RanobeLib';
|
||||||
site = 'https://ranobelib.me';
|
site = 'https://ranobelib.me';
|
||||||
apiSite = 'https://api.cdnlibs.org/api/manga/';
|
apiSite = 'https://api.cdnlibs.org/api/manga/';
|
||||||
version = '2.2.3';
|
version = '2.2.4';
|
||||||
icon = 'src/ru/ranobelib/icon.png';
|
icon = 'src/ru/ranobelib/icon.png';
|
||||||
webStorageUtilized = true;
|
webStorageUtilized = true;
|
||||||
imageRequestInit = {
|
imageRequestInit = {
|
||||||
@@ -223,13 +223,11 @@ class RLIB implements Plugin.PluginBase {
|
|||||||
volume,
|
volume,
|
||||||
{ headers: this.getHeaders() },
|
{ headers: this.getHeaders() },
|
||||||
).then(res => res.json());
|
).then(res => res.json());
|
||||||
|
const content = result?.data?.content;
|
||||||
chapterText =
|
chapterText =
|
||||||
result?.data?.content?.type == 'doc'
|
typeof content === 'object' && content?.type === 'doc'
|
||||||
? jsonToHtml(
|
? jsonToHtml(content.content, result.data.attachments || [])
|
||||||
result.data.content.content,
|
: (content as string) || '';
|
||||||
result.data.attachments || [],
|
|
||||||
)
|
|
||||||
: result?.data?.content;
|
|
||||||
}
|
}
|
||||||
return chapterText;
|
return chapterText;
|
||||||
}
|
}
|
||||||
@@ -277,7 +275,9 @@ class RLIB implements Plugin.PluginBase {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getUser = () => {
|
getUser = () => {
|
||||||
const user = storage.get('user');
|
const user = storage.get('user') as
|
||||||
|
| { token: string; id: number }
|
||||||
|
| undefined;
|
||||||
if (user) {
|
if (user) {
|
||||||
return { token: { Authorization: 'Bearer ' + user.token }, ui: user.id };
|
return { token: { Authorization: 'Bearer ' + user.token }, ui: user.id };
|
||||||
}
|
}
|
||||||
@@ -719,7 +719,7 @@ type DataClass = {
|
|||||||
created_at?: string;
|
created_at?: string;
|
||||||
moderated?: AgeRestriction;
|
moderated?: AgeRestriction;
|
||||||
likes_count?: number;
|
likes_count?: number;
|
||||||
content?: any;
|
content?: string | { type: string; content: HTML[] };
|
||||||
attachments?: Attachment[];
|
attachments?: Attachment[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+18
-19
@@ -11,7 +11,7 @@ class ReN implements Plugin.PluginBase {
|
|||||||
name = 'Renovels';
|
name = 'Renovels';
|
||||||
icon = 'src/ru/renovels/icon.png';
|
icon = 'src/ru/renovels/icon.png';
|
||||||
site = 'https://renovels.org';
|
site = 'https://renovels.org';
|
||||||
version = '1.0.3';
|
version = '1.0.4';
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
@@ -20,26 +20,25 @@ class ReN implements Plugin.PluginBase {
|
|||||||
filters,
|
filters,
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
let url = this.site + '/api/v2/search/catalog/?count=30&ordering=-';
|
const params = new URLSearchParams({
|
||||||
url += showLatestNovels ? 'chapter_date' : filters?.sort?.value || 'rating';
|
count: '30',
|
||||||
|
ordering: '-' + (showLatestNovels ? 'chapter_date' : filters.sort.value),
|
||||||
Object.entries(filters || {}).forEach(([type, { value }]: any) => {
|
|
||||||
if (value instanceof Array && value.length) {
|
|
||||||
url += '&' + type + '=' + value.join('&' + type + '=');
|
|
||||||
}
|
|
||||||
if (value?.include instanceof Array && value.include.length) {
|
|
||||||
url += '&' + type + '=' + value.include.join('&' + type + '=');
|
|
||||||
}
|
|
||||||
if (value?.exclude instanceof Array && value.exclude.length) {
|
|
||||||
url +=
|
|
||||||
'&exclude_' +
|
|
||||||
type +
|
|
||||||
'=' +
|
|
||||||
value.exclude.join('&exclude_' + type + '=');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
url += '&page=' + pageNo;
|
for (const [type, filter] of Object.entries(filters)) {
|
||||||
|
const value = filter.value;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach(v => params.append(type, v));
|
||||||
|
} else if (typeof value === 'object' && value !== null) {
|
||||||
|
value.include?.forEach((v: string) => params.append(type, v));
|
||||||
|
value.exclude?.forEach((v: string) =>
|
||||||
|
params.append(`exclude_${type}`, v),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.append('page', pageNo.toString());
|
||||||
|
const url = `${this.site}/api/v2/search/catalog/?${params.toString()}`;
|
||||||
|
|
||||||
const { results }: { results: responseNovels[] } = await fetchApi(url).then(
|
const { results }: { results: responseNovels[] } = await fetchApi(url).then(
|
||||||
res => res.json(),
|
res => res.json(),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class RV implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async fetchNovels(
|
async fetchNovels(
|
||||||
page: number,
|
page: number,
|
||||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
filters?: Plugin.PopularNovelsOptions<typeof this.filters>['filters'],
|
||||||
searchTerm?: string,
|
searchTerm?: string,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
let url = this.site + 'api/books?page=' + page;
|
let url = this.site + 'api/books?page=' + page;
|
||||||
@@ -23,8 +23,8 @@ class RV implements Plugin.PluginBase {
|
|||||||
|
|
||||||
if (searchTerm) url += '&search=' + searchTerm;
|
if (searchTerm) url += '&search=' + searchTerm;
|
||||||
|
|
||||||
const { data }: { data: book[] } = await fetchApi(url).then(res =>
|
const { data }: { data: book[] } = await fetchApi(url).then(
|
||||||
res.json(),
|
(res: Response) => res.json(),
|
||||||
);
|
);
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
@@ -39,18 +39,21 @@ class RV implements Plugin.PluginBase {
|
|||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
popularNovels = this.fetchNovels;
|
async popularNovels(
|
||||||
|
|
||||||
async searchNovels(
|
|
||||||
searchTerm: string,
|
|
||||||
page: number,
|
page: number,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
const defaultOptions: any = { filters: {} };
|
) {
|
||||||
return this.fetchNovels(page, defaultOptions, searchTerm);
|
return this.fetchNovels(page, filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchNovels(searchTerm: string, page: number) {
|
||||||
|
return this.fetchNovels(page, undefined, searchTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
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: Response) =>
|
||||||
|
res.text(),
|
||||||
|
);
|
||||||
const loadedCheerio = parseHTML(body);
|
const loadedCheerio = parseHTML(body);
|
||||||
|
|
||||||
const novel: Plugin.SourceNovel = {
|
const novel: Plugin.SourceNovel = {
|
||||||
@@ -73,7 +76,7 @@ class RV implements Plugin.PluginBase {
|
|||||||
|
|
||||||
const chaptersJSON: { data: chapters[] } = await fetchApi(
|
const chaptersJSON: { data: chapters[] } = await fetchApi(
|
||||||
this.site + 'api/books/' + bookId + '/chapters/all',
|
this.site + 'api/books/' + bookId + '/chapters/all',
|
||||||
).then(res => res.json());
|
).then((res: Response) => res.json());
|
||||||
|
|
||||||
if (chaptersJSON.data.length) {
|
if (chaptersJSON.data.length) {
|
||||||
const chapters: Plugin.ChapterItem[] = [];
|
const chapters: Plugin.ChapterItem[] = [];
|
||||||
@@ -98,7 +101,7 @@ class RV implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
const body = await fetchApi(this.site + chapterPath).then(res =>
|
const body = await fetchApi(this.site + chapterPath).then((res: Response) =>
|
||||||
res.text(),
|
res.text(),
|
||||||
);
|
);
|
||||||
const encrypted = body.match(
|
const encrypted = body.match(
|
||||||
|
|||||||
+19
-18
@@ -7,21 +7,19 @@ class TopLiba implements Plugin.PluginBase {
|
|||||||
id = 'TopLiba';
|
id = 'TopLiba';
|
||||||
name = 'ТопЛиба';
|
name = 'ТопЛиба';
|
||||||
site = 'https://topliba.com';
|
site = 'https://topliba.com';
|
||||||
version = '1.0.0';
|
version = '1.0.1';
|
||||||
icon = 'src/ru/topliba/icon.png';
|
icon = 'src/ru/topliba/icon.png';
|
||||||
_token = '';
|
_token = '';
|
||||||
|
|
||||||
async fetchNovels(
|
async fetchNovels(
|
||||||
page: number,
|
page: number,
|
||||||
{
|
showLatestNovels?: boolean,
|
||||||
showLatestNovels,
|
filters?: Plugin.PopularNovelsOptions<typeof this.filters>['filters'],
|
||||||
filters,
|
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
|
||||||
searchTerm?: string,
|
searchTerm?: string,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const data = new URLSearchParams({
|
const data = new URLSearchParams({
|
||||||
order_field: showLatestNovels ? 'date' : filters?.sort?.value || 'rating',
|
order_field: showLatestNovels ? 'date' : filters?.sort?.value || 'rating',
|
||||||
p: page,
|
p: page.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (searchTerm) data.append('q', searchTerm);
|
if (searchTerm) data.append('q', searchTerm);
|
||||||
@@ -30,7 +28,9 @@ class TopLiba implements Plugin.PluginBase {
|
|||||||
);
|
);
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
this._token = body.match(/<meta name="_token" content="(.*?)"/)?.[1];
|
this._token = body.match(
|
||||||
|
/<meta name="_token" content="(.*?)"/,
|
||||||
|
)?.[1] as string;
|
||||||
|
|
||||||
const elements = body.match(/<img class="cover" data-original=".*>/g) || [];
|
const elements = body.match(/<img class="cover" data-original=".*>/g) || [];
|
||||||
elements.forEach(element => {
|
elements.forEach(element => {
|
||||||
@@ -48,17 +48,18 @@ class TopLiba implements Plugin.PluginBase {
|
|||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
popularNovels = this.fetchNovels;
|
async popularNovels(
|
||||||
|
|
||||||
async searchNovels(
|
|
||||||
searchTerm: string,
|
|
||||||
page: number,
|
page: number,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
{
|
||||||
const defaultOptions: any = {
|
showLatestNovels,
|
||||||
showLatestNovels: false,
|
filters,
|
||||||
filters: {},
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
};
|
) {
|
||||||
return this.fetchNovels(page, defaultOptions, searchTerm);
|
return this.fetchNovels(page, showLatestNovels, filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchNovels(searchTerm: string, page: number) {
|
||||||
|
return this.fetchNovels(page, false, undefined, searchTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
@@ -124,7 +125,7 @@ class TopLiba implements Plugin.PluginBase {
|
|||||||
);
|
);
|
||||||
this._token = chaptersHTML.match(
|
this._token = chaptersHTML.match(
|
||||||
/<meta name="_token" content="(.*?)"/,
|
/<meta name="_token" content="(.*?)"/,
|
||||||
)?.[1];
|
)?.[1] as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chapterText = await fetchApi(this.resolveUrl(bookID) + '/chapter', {
|
const chapterText = await fetchApi(this.resolveUrl(bookID) + '/chapter', {
|
||||||
|
|||||||
+252
-223
@@ -1,261 +1,290 @@
|
|||||||
import { fetchApi } from "@libs/fetch";
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Plugin } from "@typings/plugin";
|
import { Plugin } from '@/types/plugin';
|
||||||
import { NovelStatus } from "@libs/novelStatus";
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
import * as cheerio from "cheerio";
|
import { Element } from 'domhandler';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
|
||||||
class NovaPlugin implements Plugin.PluginBase {
|
class NovaPlugin implements Plugin.PluginBase {
|
||||||
id = 'nova';
|
id = 'nova';
|
||||||
name = 'NOVA';
|
name = 'NOVA';
|
||||||
icon = 'src/es/nova/icon.png';
|
icon = 'src/es/nova/icon.png';
|
||||||
site = 'https://novelasligeras.net';
|
site = 'https://novelasligeras.net';
|
||||||
version = '1.1.0';
|
version = '1.1.1';
|
||||||
|
|
||||||
// Regex para parsear títulos de capítulos
|
// Regex para parsear títulos de capítulos
|
||||||
private readonly CHAPTER_REGEX = /(Parte \d+) . (.+?): (.+)/;
|
private readonly CHAPTER_REGEX = /(Parte \d+) . (.+?): (.+)/;
|
||||||
|
|
||||||
// Helper para bypass de imágenes de Cloudflare
|
// Helper para bypass de imágenes de Cloudflare
|
||||||
private async bypassCloudflareImages(
|
private async bypassCloudflareImages(
|
||||||
$: cheerio.CheerioAPI,
|
$: cheerio.CheerioAPI,
|
||||||
$content: cheerio.Cheerio<cheerio.Element>
|
$content: cheerio.Cheerio<Element>,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
$content.find('img').each((i, img) => {
|
$content.find('img').each((i, img) => {
|
||||||
const $img = $(img);
|
const $img = $(img);
|
||||||
let src = $img.attr('src') || $img.attr('data-src') || $img.attr('data-cfsrc');
|
const src =
|
||||||
|
$img.attr('src') || $img.attr('data-src') || $img.attr('data-cfsrc');
|
||||||
|
|
||||||
if (src) {
|
if (src) {
|
||||||
// Si la imagen tiene atributos de Cloudflare, usar la URL directa
|
// Si la imagen tiene atributos de Cloudflare, usar la URL directa
|
||||||
$img.attr('src', src);
|
$img.attr('src', src);
|
||||||
$img.removeAttr('data-src');
|
$img.removeAttr('data-src');
|
||||||
$img.removeAttr('data-cfsrc');
|
$img.removeAttr('data-cfsrc');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return $content.html() || '';
|
return $content.html() || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper para convertir HTML a texto limpio (si es necesario)
|
||||||
|
private htmlToText(html: string | null | undefined): string {
|
||||||
|
if (!html) return '';
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
$('script, style').remove();
|
||||||
|
return $.text().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Método para obtener novelas populares
|
||||||
|
async popularNovels(
|
||||||
|
pageNo: number,
|
||||||
|
// options: Plugin.PopularNovelsOptions,
|
||||||
|
): Promise<Plugin.NovelItem[]> {
|
||||||
|
// Para la primera página, usar la búsqueda AJAX
|
||||||
|
if (pageNo === 1) {
|
||||||
|
return this.searchNovels('', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper para convertir HTML a texto limpio (si es necesario)
|
// Para páginas siguientes, usar la paginación normal
|
||||||
private htmlToText(html: string | null | undefined): string {
|
const url = `${this.site}/index.php/page/${pageNo}/?post_type=product&orderby=popularity`;
|
||||||
if (!html) return '';
|
const body = await fetchApi(url).then(res => res.text());
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(body);
|
||||||
$('script, style').remove();
|
|
||||||
return $.text().trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Método para obtener novelas populares
|
const novels: Plugin.NovelItem[] = [];
|
||||||
async popularNovels(
|
|
||||||
pageNo: number,
|
$('.dt-css-grid div.wf-cell').each((i, element) => {
|
||||||
options: Plugin.PopularNovelsOptions
|
const $el = $(element);
|
||||||
): Promise<Plugin.NovelItem[]> {
|
const $img = $el.find('img');
|
||||||
// Para la primera página, usar la búsqueda AJAX
|
const $link = $el.find('h4.entry-title a');
|
||||||
if (pageNo === 1) {
|
|
||||||
return this.searchNovels('', 1);
|
const path = $link.attr('href')?.replace(this.site, '') || '';
|
||||||
|
const name = $link.text().trim();
|
||||||
|
const cover =
|
||||||
|
$img.attr('data-src') ||
|
||||||
|
$img.attr('data-cfsrc') ||
|
||||||
|
$img.attr('src') ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
if (name && path) {
|
||||||
|
novels.push({ name, path, cover });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return novels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Método para buscar novelas
|
||||||
|
async searchNovels(
|
||||||
|
searchTerm: string,
|
||||||
|
pageNo: number,
|
||||||
|
): Promise<Plugin.NovelItem[]> {
|
||||||
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
|
if (pageNo > 1) {
|
||||||
|
// Búsqueda paginada normal
|
||||||
|
const encodedTerm = encodeURIComponent(searchTerm);
|
||||||
|
const url = `${this.site}/index.php/page/${pageNo}/?s=${encodedTerm}&post_type=product&title=1&excerpt=1&content=0&categories=1&attributes=1&tags=1&sku=0&orderby=popularity&ixwps=1`;
|
||||||
|
|
||||||
|
const body = await fetchApi(url).then(res => res.text());
|
||||||
|
const $ = cheerio.load(body);
|
||||||
|
|
||||||
|
$('.dt-css-grid div.wf-cell').each((i, element) => {
|
||||||
|
const $el = $(element);
|
||||||
|
const $img = $el.find('img');
|
||||||
|
const $link = $el.find('h4.entry-title a');
|
||||||
|
|
||||||
|
const path = $link.attr('href')?.replace(this.site, '') || '';
|
||||||
|
const name = $link.text().trim();
|
||||||
|
const cover =
|
||||||
|
$img.attr('data-src') ||
|
||||||
|
$img.attr('data-cfsrc') ||
|
||||||
|
$img.attr('src') ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
if (name && path) {
|
||||||
|
novels.push({ name, path, cover });
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Primera página: usar búsqueda AJAX
|
||||||
|
const url = `${this.site}/wp-admin/admin-ajax.php?tags=1&sku=&limit=30&category_results=&order=DESC&category_limit=5&order_by=title&product_thumbnails=1&title=1&excerpt=1&content=&categories=1&attributes=1`;
|
||||||
|
|
||||||
// Para páginas siguientes, usar la paginación normal
|
const formData = new FormData();
|
||||||
const url = `${this.site}/index.php/page/${pageNo}/?post_type=product&orderby=popularity`;
|
formData.append('action', 'product_search');
|
||||||
const body = await fetchApi(url).then(res => res.text());
|
formData.append('product-search', '1');
|
||||||
const $ = cheerio.load(body);
|
formData.append('product-query', searchTerm);
|
||||||
|
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const response = await fetchApi(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
$('.dt-css-grid div.wf-cell').each((i, element) => {
|
const data = await response.json();
|
||||||
const $el = $(element);
|
|
||||||
const $img = $el.find('img');
|
|
||||||
const $link = $el.find('h4.entry-title a');
|
|
||||||
|
|
||||||
const path = $link.attr('href')?.replace(this.site, '') || '';
|
if (Array.isArray(data)) {
|
||||||
const name = $link.text().trim();
|
data.forEach(novel => {
|
||||||
const cover = $img.attr('data-src') || $img.attr('data-cfsrc') || $img.attr('src') || '';
|
const path = novel.url?.replace(this.site, '') || '';
|
||||||
|
const name = novel.title || '';
|
||||||
|
const cover = novel.thumbnail || '';
|
||||||
|
|
||||||
if (name && path) {
|
if (name && path) {
|
||||||
novels.push({ name, path, cover });
|
novels.push({ name, path, cover });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
return novels;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Método para buscar novelas
|
return novels;
|
||||||
async searchNovels(
|
}
|
||||||
searchTerm: string,
|
|
||||||
pageNo: number
|
|
||||||
): Promise<Plugin.NovelItem[]> {
|
|
||||||
const novels: Plugin.NovelItem[] = [];
|
|
||||||
|
|
||||||
if (pageNo > 1) {
|
// Método para obtener detalles de una novela
|
||||||
// Búsqueda paginada normal
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
const encodedTerm = encodeURIComponent(searchTerm);
|
const url = `${this.site}${novelPath}`;
|
||||||
const url = `${this.site}/index.php/page/${pageNo}/?s=${encodedTerm}&post_type=product&title=1&excerpt=1&content=0&categories=1&attributes=1&tags=1&sku=0&orderby=popularity&ixwps=1`;
|
const body = await fetchApi(url).then(res => res.text());
|
||||||
|
const $ = cheerio.load(body);
|
||||||
|
|
||||||
const body = await fetchApi(url).then(res => res.text());
|
// Extraer información básica
|
||||||
const $ = cheerio.load(body);
|
const name = $('h1').first().text().trim();
|
||||||
|
const $coverImg = $('.woocommerce-product-gallery').find('img').first();
|
||||||
|
const cover =
|
||||||
|
$coverImg.attr('src') ||
|
||||||
|
$coverImg.attr('data-cfsrc') ||
|
||||||
|
$coverImg.attr('data-src') ||
|
||||||
|
'';
|
||||||
|
|
||||||
$('.dt-css-grid div.wf-cell').each((i, element) => {
|
// Extraer autor, artista
|
||||||
const $el = $(element);
|
const author =
|
||||||
const $img = $el.find('img');
|
$('.woocommerce-product-attributes-item--attribute_pa_escritor td')
|
||||||
const $link = $el.find('h4.entry-title a');
|
.text()
|
||||||
|
.trim() || 'Desconocido';
|
||||||
|
const artist =
|
||||||
|
$('.woocommerce-product-attributes-item--attribute_pa_ilustrador td')
|
||||||
|
.text()
|
||||||
|
.trim() || '';
|
||||||
|
|
||||||
const path = $link.attr('href')?.replace(this.site, '') || '';
|
// Extraer resumen
|
||||||
const name = $link.text().trim();
|
const summaryHtml = $(
|
||||||
const cover = $img.attr('data-src') || $img.attr('data-cfsrc') || $img.attr('src') || '';
|
'.woocommerce-product-details__short-description',
|
||||||
|
).html();
|
||||||
|
const summary = this.htmlToText(summaryHtml);
|
||||||
|
|
||||||
if (name && path) {
|
// Determinar estado
|
||||||
novels.push({ name, path, cover });
|
const statusText = $(
|
||||||
}
|
'.woocommerce-product-attributes-item--attribute_pa_estado td',
|
||||||
});
|
)
|
||||||
|
.text()
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
let status = '';
|
||||||
|
if (statusText.includes('en curso') || statusText.includes('ongoing')) {
|
||||||
|
status = NovelStatus.Ongoing;
|
||||||
|
} else if (
|
||||||
|
statusText.includes('completado') ||
|
||||||
|
statusText.includes('completed')
|
||||||
|
) {
|
||||||
|
status = NovelStatus.Completed;
|
||||||
|
} else {
|
||||||
|
status = NovelStatus.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extraer capítulos
|
||||||
|
const chapters: Plugin.ChapterItem[] = [];
|
||||||
|
let chapterIndex = 0;
|
||||||
|
|
||||||
|
$('.vc_row div.vc_column-inner > div.wpb_wrapper').each((i, element) => {
|
||||||
|
const $el = $(element);
|
||||||
|
const volume = $el.find('.dt-fancy-title').first().text().trim();
|
||||||
|
|
||||||
|
if (!volume.startsWith('Volumen')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$el.find('.wpb_tab a').each((j, chapterEl) => {
|
||||||
|
const $chapter = $(chapterEl);
|
||||||
|
const chapterPartName = $chapter.text().trim();
|
||||||
|
const chapterPath = $chapter.attr('href')?.replace(this.site, '') || '';
|
||||||
|
|
||||||
|
if (!chapterPath) return;
|
||||||
|
|
||||||
|
const match = this.CHAPTER_REGEX.exec(chapterPartName);
|
||||||
|
let chapterName: string;
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const [, part, chapter, name] = match;
|
||||||
|
chapterName = `${volume} - ${chapter} - ${part}: ${name}`;
|
||||||
} else {
|
} else {
|
||||||
// Primera página: usar búsqueda AJAX
|
chapterName = `${volume} - ${chapterPartName}`;
|
||||||
const url = `${this.site}/wp-admin/admin-ajax.php?tags=1&sku=&limit=30&category_results=&order=DESC&category_limit=5&order_by=title&product_thumbnails=1&title=1&excerpt=1&content=&categories=1&attributes=1`;
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('action', 'product_search');
|
|
||||||
formData.append('product-search', '1');
|
|
||||||
formData.append('product-query', searchTerm);
|
|
||||||
|
|
||||||
const response = await fetchApi(url, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
data.forEach(novel => {
|
|
||||||
const path = novel.url?.replace(this.site, '') || '';
|
|
||||||
const name = novel.title || '';
|
|
||||||
const cover = novel.thumbnail || '';
|
|
||||||
|
|
||||||
if (name && path) {
|
|
||||||
novels.push({ name, path, cover });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return novels;
|
chapters.push({
|
||||||
}
|
name: chapterName,
|
||||||
|
path: chapterPath,
|
||||||
// Método para obtener detalles de una novela
|
releaseTime: '',
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
chapterNumber: chapterIndex + 1,
|
||||||
const url = `${this.site}${novelPath}`;
|
|
||||||
const body = await fetchApi(url).then(res => res.text());
|
|
||||||
const $ = cheerio.load(body);
|
|
||||||
|
|
||||||
// Extraer información básica
|
|
||||||
const name = $('h1').first().text().trim();
|
|
||||||
const $coverImg = $('.woocommerce-product-gallery').find('img').first();
|
|
||||||
const cover = $coverImg.attr('src') || $coverImg.attr('data-cfsrc') || $coverImg.attr('data-src') || '';
|
|
||||||
|
|
||||||
// Extraer autor, artista
|
|
||||||
const author = $('.woocommerce-product-attributes-item--attribute_pa_escritor td')
|
|
||||||
.text().trim() || 'Desconocido';
|
|
||||||
const artist = $('.woocommerce-product-attributes-item--attribute_pa_ilustrador td')
|
|
||||||
.text().trim() || '';
|
|
||||||
|
|
||||||
// Extraer resumen
|
|
||||||
const summaryHtml = $('.woocommerce-product-details__short-description').html();
|
|
||||||
const summary = this.htmlToText(summaryHtml);
|
|
||||||
|
|
||||||
// Determinar estado
|
|
||||||
const statusText = $('.woocommerce-product-attributes-item--attribute_pa_estado td')
|
|
||||||
.text().trim().toLowerCase();
|
|
||||||
let status = NovelStatus.Unknown;
|
|
||||||
if (statusText.includes('en curso') || statusText.includes('ongoing')) {
|
|
||||||
status = NovelStatus.Ongoing;
|
|
||||||
} else if (statusText.includes('completado') || statusText.includes('completed')) {
|
|
||||||
status = NovelStatus.Completed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraer capítulos
|
|
||||||
const chapters: Plugin.ChapterItem[] = [];
|
|
||||||
let chapterIndex = 0;
|
|
||||||
|
|
||||||
$('.vc_row div.vc_column-inner > div.wpb_wrapper').each((i, element) => {
|
|
||||||
const $el = $(element);
|
|
||||||
const volume = $el.find('.dt-fancy-title').first().text().trim();
|
|
||||||
|
|
||||||
if (!volume.startsWith('Volumen')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$el.find('.wpb_tab a').each((j, chapterEl) => {
|
|
||||||
const $chapter = $(chapterEl);
|
|
||||||
const chapterPartName = $chapter.text().trim();
|
|
||||||
const chapterPath = $chapter.attr('href')?.replace(this.site, '') || '';
|
|
||||||
|
|
||||||
if (!chapterPath) return;
|
|
||||||
|
|
||||||
const match = this.CHAPTER_REGEX.exec(chapterPartName);
|
|
||||||
let chapterName: string;
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const [, part, chapter, name] = match;
|
|
||||||
chapterName = `${volume} - ${chapter} - ${part}: ${name}`;
|
|
||||||
} else {
|
|
||||||
chapterName = `${volume} - ${chapterPartName}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
chapters.push({
|
|
||||||
name: chapterName,
|
|
||||||
path: chapterPath,
|
|
||||||
releaseTime: '',
|
|
||||||
chapterNumber: chapterIndex + 1
|
|
||||||
});
|
|
||||||
|
|
||||||
chapterIndex++;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const novel: Plugin.SourceNovel = {
|
chapterIndex++;
|
||||||
path: novelPath,
|
});
|
||||||
name,
|
});
|
||||||
cover,
|
|
||||||
summary,
|
|
||||||
author,
|
|
||||||
artist,
|
|
||||||
status,
|
|
||||||
chapters
|
|
||||||
};
|
|
||||||
|
|
||||||
return novel;
|
const novel: Plugin.SourceNovel = {
|
||||||
|
path: novelPath,
|
||||||
|
name,
|
||||||
|
cover,
|
||||||
|
summary,
|
||||||
|
author,
|
||||||
|
artist,
|
||||||
|
status,
|
||||||
|
chapters,
|
||||||
|
};
|
||||||
|
|
||||||
|
return novel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Método para obtener contenido del capítulo
|
||||||
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
|
const url = `${this.site}${chapterPath}`;
|
||||||
|
const body = await fetchApi(url).then(res => res.text());
|
||||||
|
const $ = cheerio.load(body);
|
||||||
|
|
||||||
|
// Determinar el selector correcto basado en el contenido
|
||||||
|
let $chapterText: cheerio.Cheerio<Element>;
|
||||||
|
|
||||||
|
if (body.includes('Nadie entra sin permiso en la Gran Tumba de Nazarick')) {
|
||||||
|
$chapterText = $('#content');
|
||||||
|
} else {
|
||||||
|
$chapterText = $('.wpb_text_column.wpb_content_element > .wpb_wrapper');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Método para obtener contenido del capítulo
|
// Remover anuncios y elementos no deseados
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
$chapterText.find('center').remove();
|
||||||
const url = `${this.site}${chapterPath}`;
|
|
||||||
const body = await fetchApi(url).then(res => res.text());
|
|
||||||
const $ = cheerio.load(body);
|
|
||||||
|
|
||||||
// Determinar el selector correcto basado en el contenido
|
// Convertir elementos con text-align center a tags <center>
|
||||||
let $chapterText: cheerio.Cheerio<cheerio.Element>;
|
$chapterText.find('*').each((i, el) => {
|
||||||
|
const $el = $(el);
|
||||||
|
const style = $el.attr('style') || '';
|
||||||
|
if (/text-align:.?center/.test(style)) {
|
||||||
|
$el.replaceWith(`<center>${$el.html()}</center>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (body.includes('Nadie entra sin permiso en la Gran Tumba de Nazarick')) {
|
// Aplicar bypass de imágenes de Cloudflare
|
||||||
$chapterText = $('#content');
|
const chapterContent = await this.bypassCloudflareImages($, $chapterText);
|
||||||
} else {
|
|
||||||
$chapterText = $('.wpb_text_column.wpb_content_element > .wpb_wrapper');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remover anuncios y elementos no deseados
|
// Limpiar scripts, estilos y otros elementos innecesarios
|
||||||
$chapterText.find('center').remove();
|
const $clean = cheerio.load(chapterContent);
|
||||||
|
$clean('script, style, iframe, .ads, .advertisement').remove();
|
||||||
|
|
||||||
// Convertir elementos con text-align center a tags <center>
|
return $clean.html() || chapterContent;
|
||||||
$chapterText.find('*').each((i, el) => {
|
}
|
||||||
const $el = $(el);
|
|
||||||
const style = $el.attr('style') || '';
|
|
||||||
if (/text-align:.?center/.test(style)) {
|
|
||||||
$el.replaceWith(`<center>${$el.html()}</center>`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Aplicar bypass de imágenes de Cloudflare
|
|
||||||
let chapterContent = await this.bypassCloudflareImages($, $chapterText);
|
|
||||||
|
|
||||||
// Limpiar scripts, estilos y otros elementos innecesarios
|
|
||||||
const $clean = cheerio.load(chapterContent);
|
|
||||||
$clean('script, style, iframe, .ads, .advertisement').remove();
|
|
||||||
|
|
||||||
return $clean.html() || chapterContent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new NovaPlugin();
|
export default new NovaPlugin();
|
||||||
|
|||||||
+14
-12
@@ -48,7 +48,7 @@ function parseSpanishTextToISO(text: string) {
|
|||||||
// --- 3. EVALUAR FECHAS RELATIVAS ("hace...") ---
|
// --- 3. EVALUAR FECHAS RELATIVAS ("hace...") ---
|
||||||
if (textLower.startsWith('hace')) {
|
if (textLower.startsWith('hace')) {
|
||||||
// Normalizar "un" / "una" a "1" para facilitar el cálculo
|
// Normalizar "un" / "una" a "1" para facilitar el cálculo
|
||||||
let normalized = textLower
|
const normalized = textLower
|
||||||
.replace(/\b(un|una)\b/g, '1')
|
.replace(/\b(un|una)\b/g, '1')
|
||||||
.replace('un momento', '0 segundos');
|
.replace('un momento', '0 segundos');
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ class Novelyra implements Plugin.PluginBase {
|
|||||||
name = 'Novelyra';
|
name = 'Novelyra';
|
||||||
icon = 'src/es/novelyra/icon.png';
|
icon = 'src/es/novelyra/icon.png';
|
||||||
site = 'https://novelyra.com/';
|
site = 'https://novelyra.com/';
|
||||||
version = '1.0.0';
|
version = '1.0.1';
|
||||||
filters: Filters = {
|
filters: Filters = {
|
||||||
genres: {
|
genres: {
|
||||||
type: FilterTypes.Picker,
|
type: FilterTypes.Picker,
|
||||||
@@ -279,16 +279,18 @@ class Novelyra implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'tag':
|
case 'tag':
|
||||||
const originalTag = element.tagName;
|
{
|
||||||
if (tagsPermisive.includes(originalTag)) {
|
const originalTag = element.tagName;
|
||||||
paragraph.push(loadedCheerio.html(element));
|
if (tagsPermisive.includes(originalTag)) {
|
||||||
} else {
|
paragraph.push(loadedCheerio.html(element));
|
||||||
if (paragraph.length > 0) {
|
} else {
|
||||||
chapterHtml.push(`<p>${paragraph.join(' ').trim()}</p>`);
|
if (paragraph.length > 0) {
|
||||||
paragraph = [];
|
chapterHtml.push(`<p>${paragraph.join(' ').trim()}</p>`);
|
||||||
if (originalTag === 'br') break;
|
paragraph = [];
|
||||||
|
if (originalTag === 'br') break;
|
||||||
|
}
|
||||||
|
chapterHtml.push(loadedCheerio.html(element));
|
||||||
}
|
}
|
||||||
chapterHtml.push(loadedCheerio.html(element));
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -301,7 +303,7 @@ class Novelyra implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
async searchNovels(
|
async searchNovels(
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
pageNo: number,
|
// pageNo: number,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
searchTerm = searchTerm.toLowerCase();
|
searchTerm = searchTerm.toLowerCase();
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class TuNovelaLigera implements Plugin.PagePlugin {
|
|||||||
name = 'TuNovelaLigera';
|
name = 'TuNovelaLigera';
|
||||||
icon = 'src/es/tunovelaligera/icon.png';
|
icon = 'src/es/tunovelaligera/icon.png';
|
||||||
site = 'https://tunovelaligera.com';
|
site = 'https://tunovelaligera.com';
|
||||||
version = '1.2.0';
|
version = '1.2.1';
|
||||||
|
|
||||||
async sleep(ms: number) {
|
async sleep(ms: number) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
@@ -18,7 +18,7 @@ class TuNovelaLigera implements Plugin.PagePlugin {
|
|||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
{
|
{
|
||||||
showLatestNovels,
|
// showLatestNovels,
|
||||||
filters,
|
filters,
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
@@ -228,16 +228,18 @@ class TuNovelaLigera implements Plugin.PagePlugin {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'tag':
|
case 'tag':
|
||||||
const originalTag = element.tagName;
|
{
|
||||||
if (tagsPermisive.includes(originalTag)) {
|
const originalTag = element.tagName;
|
||||||
paragraph.push(loadedCheerio.html(element));
|
if (tagsPermisive.includes(originalTag)) {
|
||||||
} else {
|
paragraph.push(loadedCheerio.html(element));
|
||||||
if (paragraph.length > 0) {
|
} else {
|
||||||
chapterHtml.push(`<p>${paragraph.join(' ').trim()}</p>`);
|
if (paragraph.length > 0) {
|
||||||
paragraph = [];
|
chapterHtml.push(`<p>${paragraph.join(' ').trim()}</p>`);
|
||||||
if (originalTag === 'br') break;
|
paragraph = [];
|
||||||
|
if (originalTag === 'br') break;
|
||||||
|
}
|
||||||
|
chapterHtml.push(loadedCheerio.html(element));
|
||||||
}
|
}
|
||||||
chapterHtml.push(loadedCheerio.html(element));
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class MangaTR implements Plugin.PluginBase {
|
|||||||
name = 'MangaTR';
|
name = 'MangaTR';
|
||||||
icon = 'src/tr/mangatr/icon.png';
|
icon = 'src/tr/mangatr/icon.png';
|
||||||
site = 'https://manga-tr.com/';
|
site = 'https://manga-tr.com/';
|
||||||
version = '1.0.0';
|
version = '1.0.1';
|
||||||
|
|
||||||
opts = {
|
opts = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -258,9 +258,9 @@ class MangaTR implements Plugin.PluginBase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveUrl(path: string, isNovel?: boolean): string {
|
// resolveUrl(path: string, isNovel?: boolean): string {
|
||||||
return this.site + path;
|
// return this.site + path;
|
||||||
}
|
// }
|
||||||
|
|
||||||
filters = {
|
filters = {
|
||||||
sort: {
|
sort: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CheerioAPI, load as parseHTML } from 'cheerio';
|
import { load as parseHTML } from 'cheerio';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
@@ -9,7 +9,7 @@ class BakaInUa implements Plugin.PluginBase {
|
|||||||
name = 'BakaInUA';
|
name = 'BakaInUA';
|
||||||
icon = 'src/uk/bakainua/icon.png';
|
icon = 'src/uk/bakainua/icon.png';
|
||||||
site = 'https://baka.in.ua';
|
site = 'https://baka.in.ua';
|
||||||
version = '3.1.6';
|
version = '3.1.7';
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
@@ -43,24 +43,26 @@ class BakaInUa implements Plugin.PluginBase {
|
|||||||
if (id) fictionIds.push(id);
|
if (id) fictionIds.push(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
const requests = fictionIds.map(async id => {
|
const requests = fictionIds.map(
|
||||||
try {
|
async (id): Promise<Plugin.NovelItem | null> => {
|
||||||
const res = await fetchApi(`${this.site}/fictions/${id}/details`, {
|
try {
|
||||||
headers: { 'user-agent': 'Mozilla/5.0' },
|
const res = await fetchApi(`${this.site}/fictions/${id}/details`, {
|
||||||
});
|
headers: { 'user-agent': 'Mozilla/5.0' },
|
||||||
const detailHtml = await res.text();
|
});
|
||||||
const $d = parseHTML(detailHtml);
|
const detailHtml = await res.text();
|
||||||
const link = $d('a').first();
|
const $d = parseHTML(detailHtml);
|
||||||
|
const link = $d('a').first();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: $d('h3').text().trim(),
|
name: $d('h3').text().trim(),
|
||||||
path: link.attr('href')?.replace(this.site + '/', '') || '',
|
path: link.attr('href')?.replace(this.site + '/', '') || '',
|
||||||
cover: this.site + link.find('img').attr('src'),
|
cover: this.site + link.find('img').attr('src'),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const novels = await Promise.all(requests);
|
const novels = await Promise.all(requests);
|
||||||
return novels.filter((n): n is Plugin.NovelItem => n !== null);
|
return novels.filter((n): n is Plugin.NovelItem => n !== null);
|
||||||
@@ -169,7 +171,9 @@ class BakaInUa implements Plugin.PluginBase {
|
|||||||
const $ = parseHTML(body);
|
const $ = parseHTML(body);
|
||||||
|
|
||||||
// Baka.in.ua використовує ActionText (Trix), текст зазвичай у .trix-content або .prose
|
// Baka.in.ua використовує ActionText (Trix), текст зазвичай у .trix-content або .prose
|
||||||
let content = $('.trix-content, .prose, article, #chapter-content').first();
|
const content = $(
|
||||||
|
'.trix-content, .prose, article, #chapter-content',
|
||||||
|
).first();
|
||||||
|
|
||||||
// Якщо основний селектор порожній, шукаємо прихований текст у data-атрібутах (особливість Hotwire/Turbo)
|
// Якщо основний селектор порожній, шукаємо прихований текст у data-атрібутах (особливість Hotwire/Turbo)
|
||||||
if (!content.text().trim()) {
|
if (!content.text().trim()) {
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ const settings: ServerSetting = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const proxySettingMiddleware: Connect.NextHandleFunction = (req, res) => {
|
const proxySettingMiddleware: Connect.NextHandleFunction = (req, res) => {
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.write(JSON.stringify(settings));
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let str = '';
|
let str = '';
|
||||||
req.on('data', chunk => {
|
req.on('data', chunk => {
|
||||||
str += chunk;
|
str += chunk;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -1 +1 @@
|
|||||||
var test = 2;
|
// var test = 2;
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ const results = {
|
|||||||
|
|
||||||
let totalSites = 0;
|
let totalSites = 0;
|
||||||
let checkedSites = 0;
|
let checkedSites = 0;
|
||||||
let activeRequests = 0;
|
|
||||||
|
|
||||||
function checkSite(url, pluginInfo) {
|
function checkSite(url, pluginInfo) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
@@ -115,7 +114,6 @@ async function processSites(sites) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
activeRequests++;
|
|
||||||
const { url, pluginInfo } = queue.shift();
|
const { url, pluginInfo } = queue.shift();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -149,7 +147,6 @@ async function processSites(sites) {
|
|||||||
`\r✗ ${checkedSites}/${totalSites} - ${pluginInfo.name || url} (Error)`,
|
`\r✗ ${checkedSites}/${totalSites} - ${pluginInfo.name || url} (Error)`,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
activeRequests--;
|
|
||||||
await processNext();
|
await processNext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* global Buffer */
|
// /* global Buffer */
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import sizeOf from 'image-size';
|
import sizeOf from 'image-size';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState, ReactNode } from 'react';
|
import React from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
@@ -158,8 +158,6 @@ export function FiltersSheet({
|
|||||||
filters,
|
filters,
|
||||||
refetch,
|
refetch,
|
||||||
}: FiltersSheetProps) {
|
}: FiltersSheetProps) {
|
||||||
const [filterElements, setFilterElements] = useState<ReactNode>(null);
|
|
||||||
|
|
||||||
const setFilterWithKey = (key: string, newValue: AnyFilterValue) =>
|
const setFilterWithKey = (key: string, newValue: AnyFilterValue) =>
|
||||||
setValues(fValues =>
|
setValues(fValues =>
|
||||||
!fValues
|
!fValues
|
||||||
@@ -186,15 +184,13 @@ export function FiltersSheet({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFilterElements(renderFilters(filters, values, setFilterWithKey));
|
|
||||||
}, [values, filters]);
|
|
||||||
|
|
||||||
const handleApply = () => {
|
const handleApply = () => {
|
||||||
refetch();
|
refetch();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filterElements = renderFilters(filters, values, setFilterWithKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<SheetContent side="right" className="w-full sm:max-w-md overflow-y-auto">
|
<SheetContent side="right" className="w-full sm:max-w-md overflow-y-auto">
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store';
|
||||||
import { usePluginCustomAssets } from '@/hooks/usePluginCustomAssets';
|
import { usePluginCustomAssets } from '@/hooks/usePluginCustomAssets';
|
||||||
|
|
||||||
export default function ParseChapterSection() {
|
const ParseChapterSection = React.memo(function ParseChapterSection() {
|
||||||
const plugin = useAppStore(state => state.plugin);
|
const plugin = useAppStore(state => state.plugin);
|
||||||
const parseChapterPath = useAppStore(state => state.parseChapterPath);
|
const parseChapterPath = useAppStore(state => state.parseChapterPath);
|
||||||
const shouldAutoSubmitChapter = useAppStore(
|
const shouldAutoSubmitChapter = useAppStore(
|
||||||
@@ -30,6 +30,15 @@ export default function ParseChapterSection() {
|
|||||||
const [fetchError, setFetchError] = useState('');
|
const [fetchError, setFetchError] = useState('');
|
||||||
const [showRawHtml, setShowRawHtml] = useState(false);
|
const [showRawHtml, setShowRawHtml] = useState(false);
|
||||||
const lastProcessedPath = useRef<string>();
|
const lastProcessedPath = useRef<string>();
|
||||||
|
const [prevPluginId, setPrevPluginId] = useState<string | undefined>();
|
||||||
|
|
||||||
|
if (plugin?.id !== prevPluginId) {
|
||||||
|
setPrevPluginId(plugin?.id);
|
||||||
|
setChapterPath('');
|
||||||
|
setChapterText('');
|
||||||
|
setFetchError('');
|
||||||
|
setShowRawHtml(false);
|
||||||
|
}
|
||||||
|
|
||||||
const { customCSSLoaded, customJSLoaded, customCSSError, customJSError } =
|
const { customCSSLoaded, customJSLoaded, customCSSError, customJSError } =
|
||||||
usePluginCustomAssets(plugin, chapterText);
|
usePluginCustomAssets(plugin, chapterText);
|
||||||
@@ -56,7 +65,7 @@ export default function ParseChapterSection() {
|
|||||||
await fetchChapterByPath(chapterPath);
|
await fetchChapterByPath(chapterPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === 'Enter' && chapterPath.trim()) {
|
if (e.key === 'Enter' && chapterPath.trim()) {
|
||||||
fetchChapter();
|
fetchChapter();
|
||||||
}
|
}
|
||||||
@@ -128,7 +137,7 @@ export default function ParseChapterSection() {
|
|||||||
placeholder="Enter chapter path..."
|
placeholder="Enter chapter path..."
|
||||||
value={chapterPath}
|
value={chapterPath}
|
||||||
onChange={e => setChapterPath(e.target.value)}
|
onChange={e => setChapterPath(e.target.value)}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyDown={handleKeyDown}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
disabled={!plugin}
|
disabled={!plugin}
|
||||||
/>
|
/>
|
||||||
@@ -327,4 +336,6 @@ export default function ParseChapterSection() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default ParseChapterSection;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ type ParseNovelSectionProps = {
|
|||||||
onNavigateToParseChapter?: () => void;
|
onNavigateToParseChapter?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ParseNovelSection({
|
const ParseNovelSection = React.memo(function ParseNovelSection({
|
||||||
onNavigateToParseChapter,
|
onNavigateToParseChapter,
|
||||||
}: ParseNovelSectionProps) {
|
}: ParseNovelSectionProps) {
|
||||||
const plugin = useAppStore(state => state.plugin);
|
const plugin = useAppStore(state => state.plugin);
|
||||||
@@ -46,6 +46,16 @@ export default function ParseNovelSection({
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [fetchError, setFetchError] = useState('');
|
const [fetchError, setFetchError] = useState('');
|
||||||
const lastProcessedPath = useRef<string>();
|
const lastProcessedPath = useRef<string>();
|
||||||
|
const [prevPluginId, setPrevPluginId] = useState<string | undefined>();
|
||||||
|
|
||||||
|
if (plugin?.id !== prevPluginId) {
|
||||||
|
setPrevPluginId(plugin?.id);
|
||||||
|
setNovelPath('');
|
||||||
|
setSourceNovel(undefined);
|
||||||
|
setChapters([]);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setFetchError('');
|
||||||
|
}
|
||||||
|
|
||||||
const { exportEpub, isExporting } = useEpubExport({
|
const { exportEpub, isExporting } = useEpubExport({
|
||||||
plugin: plugin || null,
|
plugin: plugin || null,
|
||||||
@@ -100,7 +110,7 @@ export default function ParseNovelSection({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === 'Enter' && novelPath.trim()) {
|
if (e.key === 'Enter' && novelPath.trim()) {
|
||||||
fetchNovel();
|
fetchNovel();
|
||||||
}
|
}
|
||||||
@@ -168,7 +178,7 @@ export default function ParseNovelSection({
|
|||||||
placeholder="Enter novel path..."
|
placeholder="Enter novel path..."
|
||||||
value={novelPath}
|
value={novelPath}
|
||||||
onChange={e => setNovelPath(e.target.value)}
|
onChange={e => setNovelPath(e.target.value)}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyDown={handleKeyDown}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
disabled={!plugin}
|
disabled={!plugin}
|
||||||
/>
|
/>
|
||||||
@@ -551,4 +561,6 @@ export default function ParseNovelSection({
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default ParseNovelSection;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Filter, BookOpen } from 'lucide-react';
|
import { Filter, BookOpen } from 'lucide-react';
|
||||||
|
|
||||||
import { FiltersSheet } from '@/components/filters/filters-sheet';
|
import { FiltersSheet } from '@/components/filters/filters-sheet';
|
||||||
@@ -16,7 +16,7 @@ type PopularNovelsSectionProps = {
|
|||||||
onNavigateToParseNovel?: () => void;
|
onNavigateToParseNovel?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PopularNovelsSection({
|
const PopularNovelsSection = React.memo(function PopularNovelsSection({
|
||||||
onNavigateToParseNovel,
|
onNavigateToParseNovel,
|
||||||
}: PopularNovelsSectionProps) {
|
}: PopularNovelsSectionProps) {
|
||||||
const plugin = useAppStore(state => state.plugin);
|
const plugin = useAppStore(state => state.plugin);
|
||||||
@@ -30,14 +30,38 @@ export default function PopularNovelsSection({
|
|||||||
const [filterValues, setFilterValues] = useState<
|
const [filterValues, setFilterValues] = useState<
|
||||||
FilterToValues<Filters> | undefined
|
FilterToValues<Filters> | undefined
|
||||||
>();
|
>();
|
||||||
|
const [prevPluginId, setPrevPluginId] = useState<string | undefined>();
|
||||||
|
|
||||||
const fetchNovelsByIndex = async (index: number) => {
|
if (plugin?.id !== prevPluginId) {
|
||||||
|
setPrevPluginId(plugin?.id);
|
||||||
|
setCurrentIndex(0);
|
||||||
|
setMaxIndex(0);
|
||||||
|
setNovels([]);
|
||||||
|
|
||||||
|
if (plugin?.filters) {
|
||||||
|
const filters: FilterToValues<typeof plugin.filters> = {};
|
||||||
|
for (const fKey in plugin.filters) {
|
||||||
|
filters[fKey as keyof typeof filters] = {
|
||||||
|
type: plugin.filters[fKey].type,
|
||||||
|
value: plugin.filters[fKey].value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
setFilterValues(filters);
|
||||||
|
} else {
|
||||||
|
setFilterValues(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchNovelsByIndex = async (
|
||||||
|
index: number,
|
||||||
|
latestOverride?: boolean,
|
||||||
|
) => {
|
||||||
if (plugin && index) {
|
if (plugin && index) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const fetchedNovels = await plugin.popularNovels(index, {
|
const fetchedNovels = await plugin.popularNovels(index, {
|
||||||
filters: filterValues || {},
|
filters: filterValues || {},
|
||||||
showLatestNovels: isLatest,
|
showLatestNovels: latestOverride ?? isLatest,
|
||||||
});
|
});
|
||||||
if (fetchedNovels.length !== 0) {
|
if (fetchedNovels.length !== 0) {
|
||||||
setCurrentIndex(index);
|
setCurrentIndex(index);
|
||||||
@@ -54,31 +78,13 @@ export default function PopularNovelsSection({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleIsLatestChange = (latest: boolean) => {
|
||||||
if (plugin) {
|
if (isLatest === latest) return;
|
||||||
setCurrentIndex(1);
|
setIsLatest(latest);
|
||||||
setMaxIndex(1);
|
|
||||||
fetchNovelsByIndex(1);
|
|
||||||
}
|
|
||||||
}, [isLatest]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Reset when changing plugins
|
|
||||||
setCurrentIndex(0);
|
setCurrentIndex(0);
|
||||||
setMaxIndex(0);
|
setMaxIndex(0);
|
||||||
setNovels([]);
|
setNovels([]);
|
||||||
|
};
|
||||||
if (plugin?.filters) {
|
|
||||||
const filters: FilterToValues<typeof plugin.filters> = {};
|
|
||||||
for (const fKey in plugin.filters) {
|
|
||||||
filters[fKey as keyof typeof filters] = {
|
|
||||||
type: plugin.filters[fKey].type,
|
|
||||||
value: plugin.filters[fKey].value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
setFilterValues(filters);
|
|
||||||
}
|
|
||||||
}, [plugin]);
|
|
||||||
|
|
||||||
const handleParseNovel = (path: string) => {
|
const handleParseNovel = (path: string) => {
|
||||||
setParseNovelPath(path, true);
|
setParseNovelPath(path, true);
|
||||||
@@ -136,7 +142,7 @@ export default function PopularNovelsSection({
|
|||||||
isLatest === (option === 'Latest') ? 'default' : 'outline'
|
isLatest === (option === 'Latest') ? 'default' : 'outline'
|
||||||
}
|
}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => setIsLatest(option === 'Latest')}
|
onClick={() => handleIsLatestChange(option === 'Latest')}
|
||||||
>
|
>
|
||||||
{option}
|
{option}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -149,7 +155,7 @@ export default function PopularNovelsSection({
|
|||||||
min="1"
|
min="1"
|
||||||
max={maxIndex}
|
max={maxIndex}
|
||||||
value={currentIndex}
|
value={currentIndex}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
onChange={e => {
|
||||||
const page = parseInt(e.target.value);
|
const page = parseInt(e.target.value);
|
||||||
if (page > 0 && page <= maxIndex) {
|
if (page > 0 && page <= maxIndex) {
|
||||||
fetchNovelsByIndex(page);
|
fetchNovelsByIndex(page);
|
||||||
@@ -216,4 +222,6 @@ export default function PopularNovelsSection({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default PopularNovelsSection;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Search as SearchIcon } from 'lucide-react';
|
import { Search as SearchIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { NovelCard } from '@/components/novel-card';
|
import { NovelCard } from '@/components/novel-card';
|
||||||
@@ -14,7 +14,7 @@ type SearchNovelsSectionProps = {
|
|||||||
onNavigateToParseNovel?: () => void;
|
onNavigateToParseNovel?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SearchNovelsSection({
|
const SearchNovelsSection = React.memo(function SearchNovelsSection({
|
||||||
onNavigateToParseNovel,
|
onNavigateToParseNovel,
|
||||||
}: SearchNovelsSectionProps) {
|
}: SearchNovelsSectionProps) {
|
||||||
const plugin = useAppStore(state => state.plugin);
|
const plugin = useAppStore(state => state.plugin);
|
||||||
@@ -24,6 +24,15 @@ export default function SearchNovelsSection({
|
|||||||
const [novels, setNovels] = useState<Plugin.NovelItem[]>([]);
|
const [novels, setNovels] = useState<Plugin.NovelItem[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [fetchError, setFetchError] = useState('');
|
const [fetchError, setFetchError] = useState('');
|
||||||
|
const [prevPluginId, setPrevPluginId] = useState<string | undefined>();
|
||||||
|
|
||||||
|
if (plugin?.id !== prevPluginId) {
|
||||||
|
setPrevPluginId(plugin?.id);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setNovels([]);
|
||||||
|
setSearchTerm('');
|
||||||
|
setFetchError('');
|
||||||
|
}
|
||||||
|
|
||||||
const fetchNovels = async (page: number) => {
|
const fetchNovels = async (page: number) => {
|
||||||
if (plugin && searchTerm.trim()) {
|
if (plugin && searchTerm.trim()) {
|
||||||
@@ -44,14 +53,7 @@ export default function SearchNovelsSection({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
setCurrentPage(1);
|
|
||||||
setNovels([]);
|
|
||||||
setSearchTerm('');
|
|
||||||
setFetchError('');
|
|
||||||
}, [plugin]);
|
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === 'Enter' && searchTerm.trim()) {
|
if (e.key === 'Enter' && searchTerm.trim()) {
|
||||||
fetchNovels(1);
|
fetchNovels(1);
|
||||||
}
|
}
|
||||||
@@ -86,7 +88,7 @@ export default function SearchNovelsSection({
|
|||||||
placeholder="Enter search term..."
|
placeholder="Enter search term..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyDown={handleKeyDown}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
disabled={!plugin}
|
disabled={!plugin}
|
||||||
/>
|
/>
|
||||||
@@ -159,4 +161,6 @@ export default function SearchNovelsSection({
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default SearchNovelsSection;
|
||||||
|
|||||||
+138
-124
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { CheckedState } from '@radix-ui/react-checkbox';
|
import { CheckedState } from '@radix-ui/react-checkbox';
|
||||||
import { Check } from 'lucide-react';
|
import { Check } from 'lucide-react';
|
||||||
|
|
||||||
@@ -16,59 +16,75 @@ import {
|
|||||||
import useDebounce from '@/hooks/useDebounce';
|
import useDebounce from '@/hooks/useDebounce';
|
||||||
import { FetchMode } from '@/types/types';
|
import { FetchMode } from '@/types/types';
|
||||||
|
|
||||||
export default function SettingsSection() {
|
const FETCH_MODES = {
|
||||||
const [cookies, setCookies] = useState('');
|
[FetchMode.PROXY]: 'Proxy',
|
||||||
const debouncedCookies = useDebounce(cookies, 500);
|
[FetchMode.NODE_FETCH]: 'Node Fetch',
|
||||||
const [fetchMode, setFetchMode] = useState<FetchMode>(FetchMode.PROXY);
|
[FetchMode.CURL]: 'Curl',
|
||||||
const [useUserAgent, setUseUserAgent] = useState<CheckedState>(true);
|
};
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [alertVisible, setAlertVisible] = useState(false);
|
const SettingsSection = React.memo(function SettingsSection() {
|
||||||
|
const [settings, setSettings] = useState({
|
||||||
|
cookies: '',
|
||||||
|
fetchMode: FetchMode.PROXY,
|
||||||
|
useUserAgent: true as CheckedState,
|
||||||
|
});
|
||||||
|
const [status, setStatus] = useState<'idle' | 'loading' | 'saved'>('idle');
|
||||||
|
const init = useRef(false);
|
||||||
|
const lastSaved = useRef<typeof settings | null>(null);
|
||||||
|
const debouncedCookies = useDebounce(settings.cookies, 500);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (alertVisible) {
|
fetch('settings')
|
||||||
const id = setTimeout(() => setAlertVisible(false), 2000);
|
.then(res => res.json())
|
||||||
return () => clearTimeout(id);
|
.then(data => {
|
||||||
}
|
const loaded = {
|
||||||
}, [alertVisible]);
|
cookies: data.cookies || '',
|
||||||
|
fetchMode: data.fetchMode ?? FetchMode.PROXY,
|
||||||
|
useUserAgent: data.useUserAgent ?? true,
|
||||||
|
};
|
||||||
|
setSettings(loaded);
|
||||||
|
lastSaved.current = loaded;
|
||||||
|
init.current = true;
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
if (!init.current || debouncedCookies !== settings.cookies) return;
|
||||||
|
const current = { ...settings, cookies: debouncedCookies };
|
||||||
|
|
||||||
|
if (JSON.stringify(lastSaved.current) === JSON.stringify(current)) return;
|
||||||
|
|
||||||
|
setStatus('loading');
|
||||||
fetch('settings', {
|
fetch('settings', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json',
|
body: JSON.stringify(current),
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
cookies: debouncedCookies,
|
|
||||||
fetchMode: fetchMode,
|
|
||||||
useUserAgent: useUserAgent === true,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.then(() => setAlertVisible(true))
|
.then(() => {
|
||||||
.catch(error => console.error('Failed to save settings:', error))
|
lastSaved.current = current;
|
||||||
.finally(() => setLoading(false));
|
setStatus('saved');
|
||||||
}, [debouncedCookies, fetchMode, useUserAgent]);
|
setTimeout(() => setStatus('idle'), 2000);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}, [
|
||||||
|
debouncedCookies,
|
||||||
|
settings.fetchMode,
|
||||||
|
settings.useUserAgent,
|
||||||
|
settings.cookies,
|
||||||
|
]);
|
||||||
|
|
||||||
const getFetchModeLabel = (mode: FetchMode) => {
|
const update = <K extends keyof typeof settings>(
|
||||||
switch (mode) {
|
k: K,
|
||||||
case FetchMode.PROXY:
|
v: (typeof settings)[K],
|
||||||
return 'Proxy';
|
) => setSettings(settings => ({ ...settings, [k]: v }));
|
||||||
case FetchMode.NODE_FETCH:
|
|
||||||
return 'Node Fetch';
|
|
||||||
case FetchMode.CURL:
|
|
||||||
return 'Curl';
|
|
||||||
default:
|
|
||||||
return 'Proxy';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card className="p-6 relative">
|
<Card className="p-6 relative">
|
||||||
{alertVisible && (
|
{status === 'saved' && (
|
||||||
<div className="absolute top-4 right-4 z-10 bg-green-500/90 text-white px-4 py-2 rounded-md flex items-center gap-2 shadow-lg animate-in fade-in slide-in-from-top-2">
|
<div className="absolute top-4 right-4 z-10 bg-green-500/90 text-white px-4 py-2 rounded-md flex items-center gap-2 shadow-lg animate-in fade-in slide-in-from-top-2">
|
||||||
<Check className="w-4 h-4" />
|
<Check className="w-4 h-4" /> Settings updated
|
||||||
Settings updated
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -79,86 +95,61 @@ export default function SettingsSection() {
|
|||||||
Settings are automatically saved
|
Settings are automatically saved
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{loading && (
|
{status === 'loading' && (
|
||||||
<div className="text-sm text-muted-foreground">Saving...</div>
|
<div className="text-sm text-muted-foreground">Saving...</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Request Configuration Section */}
|
<Section title="Request Configuration">
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<Label className="font-semibold text-foreground">
|
||||||
<div className="h-px flex-1 bg-border"></div>
|
Browser User Agent
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
</Label>
|
||||||
Request Configuration
|
<div className="flex items-center gap-3">
|
||||||
</h3>
|
|
||||||
<div className="h-px flex-1 bg-border"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* User Agent */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="font-semibold text-foreground">
|
|
||||||
Browser User Agent
|
|
||||||
</Label>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Input
|
|
||||||
value={navigator.userAgent}
|
|
||||||
disabled
|
|
||||||
className="font-mono text-xs flex-1 opacity-60"
|
|
||||||
title={navigator.userAgent}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-2 whitespace-nowrap">
|
|
||||||
<Checkbox
|
|
||||||
id="use-ua"
|
|
||||||
checked={useUserAgent}
|
|
||||||
onCheckedChange={setUseUserAgent}
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
htmlFor="use-ua"
|
|
||||||
className="text-sm text-foreground cursor-pointer"
|
|
||||||
>
|
|
||||||
Use
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Enable to send your browser's user agent with requests
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cookies */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label
|
|
||||||
htmlFor="cookies"
|
|
||||||
className="font-semibold text-foreground"
|
|
||||||
>
|
|
||||||
Cookies
|
|
||||||
</Label>
|
|
||||||
<Input
|
<Input
|
||||||
id="cookies"
|
value={navigator.userAgent}
|
||||||
value={cookies}
|
disabled
|
||||||
onChange={e => setCookies(e.target.value.trim())}
|
className="font-mono text-xs flex-1 opacity-60"
|
||||||
placeholder="Enter cookies (optional)..."
|
title={navigator.userAgent}
|
||||||
className="font-mono text-xs"
|
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<div className="flex items-center gap-2 whitespace-nowrap">
|
||||||
Additional cookies to send with requests (optional)
|
<Checkbox
|
||||||
</p>
|
id="use-ua"
|
||||||
|
checked={settings.useUserAgent}
|
||||||
|
onCheckedChange={v => update('useUserAgent', v)}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor="use-ua"
|
||||||
|
className="text-sm text-foreground cursor-pointer"
|
||||||
|
>
|
||||||
|
Use
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Fetch Settings Section */}
|
<div className="space-y-2">
|
||||||
<div>
|
<Label
|
||||||
<div className="flex items-center gap-2 mb-4">
|
htmlFor="cookies"
|
||||||
<div className="h-px flex-1 bg-border"></div>
|
className="font-semibold text-foreground"
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
>
|
||||||
Fetch Settings
|
Cookies
|
||||||
</h3>
|
</Label>
|
||||||
<div className="h-px flex-1 bg-border"></div>
|
<Input
|
||||||
|
id="cookies"
|
||||||
|
value={settings.cookies}
|
||||||
|
onChange={e => update('cookies', e.target.value.trim())}
|
||||||
|
placeholder="Enter cookies (optional)..."
|
||||||
|
className="font-mono text-xs"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Additional cookies to send with requests (optional)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Fetch Settings">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label
|
<Label
|
||||||
htmlFor="fetch-mode"
|
htmlFor="fetch-mode"
|
||||||
@@ -167,33 +158,56 @@ export default function SettingsSection() {
|
|||||||
Fetch Mode
|
Fetch Mode
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={fetchMode.toString()}
|
value={settings.fetchMode.toString()}
|
||||||
onValueChange={value =>
|
onValueChange={v => update('fetchMode', parseInt(v))}
|
||||||
setFetchMode(parseInt(value) as FetchMode)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger id="fetch-mode">
|
<SelectTrigger id="fetch-mode">
|
||||||
<SelectValue>{getFetchModeLabel(fetchMode)}</SelectValue>
|
<SelectValue>
|
||||||
|
{
|
||||||
|
FETCH_MODES[
|
||||||
|
settings.fetchMode as keyof typeof FETCH_MODES
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value={FetchMode.PROXY.toString()}>
|
{Object.entries(FETCH_MODES).map(([v, l]) => (
|
||||||
Proxy
|
<SelectItem key={v} value={v}>
|
||||||
</SelectItem>
|
{l}
|
||||||
<SelectItem value={FetchMode.NODE_FETCH.toString()}>
|
</SelectItem>
|
||||||
Node Fetch
|
))}
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value={FetchMode.CURL.toString()}>
|
|
||||||
Curl
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Select the method used to fetch data from sources
|
Select the method used to fetch data from sources
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function Section({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<div className="h-px flex-1 bg-border" />
|
||||||
|
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<div className="h-px flex-1 bg-border" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-6">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default SettingsSection;
|
||||||
|
|||||||
+19
-16
@@ -25,22 +25,25 @@ const badgeVariants = cva(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function Badge({
|
type BadgeProps = React.ComponentPropsWithoutRef<'span'> &
|
||||||
className,
|
VariantProps<typeof badgeVariants> & {
|
||||||
variant,
|
asChild?: boolean;
|
||||||
asChild = false,
|
};
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'span'> &
|
|
||||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
||||||
const Comp = asChild ? Slot : 'span';
|
|
||||||
|
|
||||||
return (
|
const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
||||||
<Comp
|
({ className, variant, asChild = false, ...props }, ref) => {
|
||||||
data-slot="badge"
|
const Comp = asChild ? Slot : 'span';
|
||||||
className={cn(badgeVariants({ variant }), className)}
|
|
||||||
{...props}
|
return (
|
||||||
/>
|
<Comp
|
||||||
);
|
ref={ref}
|
||||||
}
|
data-slot="badge"
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Badge.displayName = 'Badge';
|
||||||
|
|
||||||
export { Badge, badgeVariants };
|
export { Badge, badgeVariants };
|
||||||
|
|||||||
@@ -36,25 +36,25 @@ const buttonVariants = cva(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function Button({
|
type ButtonProps = React.ComponentPropsWithoutRef<'button'> &
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
size,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'button'> &
|
|
||||||
VariantProps<typeof buttonVariants> & {
|
VariantProps<typeof buttonVariants> & {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}) {
|
};
|
||||||
const Comp = asChild ? Slot : 'button';
|
|
||||||
|
|
||||||
return (
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
<Comp
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
data-slot="button"
|
const Comp = asChild ? Slot : 'button';
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
{...props}
|
return (
|
||||||
/>
|
<Comp
|
||||||
);
|
data-slot="button"
|
||||||
}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Button.displayName = 'Button';
|
||||||
|
|
||||||
export { Button, buttonVariants };
|
export { Button, buttonVariants };
|
||||||
|
|||||||
+78
-59
@@ -2,9 +2,10 @@ import * as React from 'react';
|
|||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
const Card = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
|
||||||
return (
|
({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
|
ref={ref}
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||||
@@ -12,74 +13,92 @@ function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
|
Card.displayName = 'Card';
|
||||||
|
|
||||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
const CardHeader = React.forwardRef<
|
||||||
return (
|
HTMLDivElement,
|
||||||
<div
|
React.ComponentProps<'div'>
|
||||||
data-slot="card-header"
|
>(({ className, ...props }, ref) => (
|
||||||
className={cn(
|
<div
|
||||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
ref={ref}
|
||||||
className,
|
data-slot="card-header"
|
||||||
)}
|
className={cn(
|
||||||
{...props}
|
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||||
/>
|
className,
|
||||||
);
|
)}
|
||||||
}
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardHeader.displayName = 'CardHeader';
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
const CardTitle = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
|
||||||
return (
|
({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
|
ref={ref}
|
||||||
data-slot="card-title"
|
data-slot="card-title"
|
||||||
className={cn('leading-none font-semibold', className)}
|
className={cn('leading-none font-semibold', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
|
CardTitle.displayName = 'CardTitle';
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
const CardDescription = React.forwardRef<
|
||||||
return (
|
HTMLDivElement,
|
||||||
<div
|
React.ComponentProps<'div'>
|
||||||
data-slot="card-description"
|
>(({ className, ...props }, ref) => (
|
||||||
className={cn('text-muted-foreground text-sm', className)}
|
<div
|
||||||
{...props}
|
ref={ref}
|
||||||
/>
|
data-slot="card-description"
|
||||||
);
|
className={cn('text-muted-foreground text-sm', className)}
|
||||||
}
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardDescription.displayName = 'CardDescription';
|
||||||
|
|
||||||
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
const CardAction = React.forwardRef<
|
||||||
return (
|
HTMLDivElement,
|
||||||
<div
|
React.ComponentProps<'div'>
|
||||||
data-slot="card-action"
|
>(({ className, ...props }, ref) => (
|
||||||
className={cn(
|
<div
|
||||||
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
ref={ref}
|
||||||
className,
|
data-slot="card-action"
|
||||||
)}
|
className={cn(
|
||||||
{...props}
|
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||||
/>
|
className,
|
||||||
);
|
)}
|
||||||
}
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardAction.displayName = 'CardAction';
|
||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
const CardContent = React.forwardRef<
|
||||||
return (
|
HTMLDivElement,
|
||||||
<div
|
React.ComponentProps<'div'>
|
||||||
data-slot="card-content"
|
>(({ className, ...props }, ref) => (
|
||||||
className={cn('px-6', className)}
|
<div
|
||||||
{...props}
|
ref={ref}
|
||||||
/>
|
data-slot="card-content"
|
||||||
);
|
className={cn('px-6', className)}
|
||||||
}
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardContent.displayName = 'CardContent';
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
const CardFooter = React.forwardRef<
|
||||||
return (
|
HTMLDivElement,
|
||||||
<div
|
React.ComponentProps<'div'>
|
||||||
data-slot="card-footer"
|
>(({ className, ...props }, ref) => (
|
||||||
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
<div
|
||||||
{...props}
|
ref={ref}
|
||||||
/>
|
data-slot="card-footer"
|
||||||
);
|
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||||
}
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardFooter.displayName = 'CardFooter';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Card,
|
Card,
|
||||||
|
|||||||
@@ -6,27 +6,27 @@ import { CheckIcon } from 'lucide-react';
|
|||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
function Checkbox({
|
const Checkbox = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
...props
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
>(({ className, ...props }, ref) => (
|
||||||
return (
|
<CheckboxPrimitive.Root
|
||||||
<CheckboxPrimitive.Root
|
ref={ref}
|
||||||
data-slot="checkbox"
|
data-slot="checkbox"
|
||||||
className={cn(
|
className={cn(
|
||||||
'peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
'peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
data-slot="checkbox-indicator"
|
||||||
|
className="grid place-content-center text-current transition-none"
|
||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator
|
<CheckIcon className="size-3.5" />
|
||||||
data-slot="checkbox-indicator"
|
</CheckboxPrimitive.Indicator>
|
||||||
className="grid place-content-center text-current transition-none"
|
</CheckboxPrimitive.Root>
|
||||||
>
|
));
|
||||||
<CheckIcon className="size-3.5" />
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||||
</CheckboxPrimitive.Indicator>
|
|
||||||
</CheckboxPrimitive.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Checkbox };
|
export { Checkbox };
|
||||||
|
|||||||
+19
-15
@@ -2,20 +2,24 @@ import * as React from 'react';
|
|||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||||
return (
|
({ className, type, ...props }, ref) => {
|
||||||
<input
|
return (
|
||||||
type={type}
|
<input
|
||||||
data-slot="input"
|
type={type}
|
||||||
className={cn(
|
data-slot="input"
|
||||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
className={cn(
|
||||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||||
className,
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||||
)}
|
className,
|
||||||
{...props}
|
)}
|
||||||
/>
|
ref={ref}
|
||||||
);
|
{...props}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Input.displayName = 'Input';
|
||||||
|
|
||||||
export { Input };
|
export { Input };
|
||||||
|
|||||||
+15
-15
@@ -3,20 +3,20 @@ import * as LabelPrimitive from '@radix-ui/react-label';
|
|||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
function Label({
|
const Label = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
...props
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
>(({ className, ...props }, ref) => (
|
||||||
return (
|
<LabelPrimitive.Root
|
||||||
<LabelPrimitive.Root
|
ref={ref}
|
||||||
data-slot="label"
|
data-slot="label"
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
));
|
||||||
}
|
Label.displayName = LabelPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Label };
|
export { Label };
|
||||||
|
|||||||
+119
-131
@@ -4,60 +4,47 @@ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
|
|||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
function Select({
|
const Select = SelectPrimitive.Root;
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
|
||||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectGroup({
|
const SelectGroup = SelectPrimitive.Group;
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
|
||||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectValue({
|
const SelectValue = SelectPrimitive.Value;
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
|
||||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectTrigger({
|
const SelectTrigger = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
size = 'default',
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
||||||
children,
|
size?: 'sm' | 'default';
|
||||||
...props
|
}
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
>(({ className, size = 'default', children, ...props }, ref) => (
|
||||||
size?: 'sm' | 'default';
|
<SelectPrimitive.Trigger
|
||||||
}) {
|
ref={ref}
|
||||||
return (
|
data-slot="select-trigger"
|
||||||
<SelectPrimitive.Trigger
|
data-size={size}
|
||||||
data-slot="select-trigger"
|
className={cn(
|
||||||
data-size={size}
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className={cn(
|
className,
|
||||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
)}
|
||||||
className,
|
{...props}
|
||||||
)}
|
>
|
||||||
{...props}
|
{children}
|
||||||
>
|
<SelectPrimitive.Icon asChild>
|
||||||
{children}
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
<SelectPrimitive.Icon asChild>
|
</SelectPrimitive.Icon>
|
||||||
<ChevronDownIcon className="size-4 opacity-50" />
|
</SelectPrimitive.Trigger>
|
||||||
</SelectPrimitive.Icon>
|
));
|
||||||
</SelectPrimitive.Trigger>
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectContent({
|
const SelectContent = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
children,
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
position = 'popper',
|
>(
|
||||||
align = 'center',
|
(
|
||||||
...props
|
{ className, children, position = 'popper', align = 'center', ...props },
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
ref,
|
||||||
return (
|
) => (
|
||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
data-slot="select-content"
|
data-slot="select-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
||||||
@@ -82,94 +69,95 @@ function SelectContent({
|
|||||||
<SelectScrollDownButton />
|
<SelectScrollDownButton />
|
||||||
</SelectPrimitive.Content>
|
</SelectPrimitive.Content>
|
||||||
</SelectPrimitive.Portal>
|
</SelectPrimitive.Portal>
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||||
|
|
||||||
function SelectLabel({
|
const SelectLabel = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
...props
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
>(({ className, ...props }, ref) => (
|
||||||
return (
|
<SelectPrimitive.Label
|
||||||
<SelectPrimitive.Label
|
ref={ref}
|
||||||
data-slot="select-label"
|
data-slot="select-label"
|
||||||
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
|
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
));
|
||||||
}
|
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||||
|
|
||||||
function SelectItem({
|
const SelectItem = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
children,
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
...props
|
>(({ className, children, ...props }, ref) => (
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
<SelectPrimitive.Item
|
||||||
return (
|
ref={ref}
|
||||||
<SelectPrimitive.Item
|
data-slot="select-item"
|
||||||
data-slot="select-item"
|
className={cn(
|
||||||
className={cn(
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
<SelectPrimitive.ItemIndicator>
|
||||||
<SelectPrimitive.ItemIndicator>
|
<CheckIcon className="size-4" />
|
||||||
<CheckIcon className="size-4" />
|
</SelectPrimitive.ItemIndicator>
|
||||||
</SelectPrimitive.ItemIndicator>
|
</span>
|
||||||
</span>
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
</SelectPrimitive.Item>
|
||||||
</SelectPrimitive.Item>
|
));
|
||||||
);
|
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||||
}
|
|
||||||
|
|
||||||
function SelectSeparator({
|
const SelectSeparator = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
...props
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
>(({ className, ...props }, ref) => (
|
||||||
return (
|
<SelectPrimitive.Separator
|
||||||
<SelectPrimitive.Separator
|
ref={ref}
|
||||||
data-slot="select-separator"
|
data-slot="select-separator"
|
||||||
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
|
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
));
|
||||||
}
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||||
|
|
||||||
function SelectScrollUpButton({
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
...props
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
>(({ className, ...props }, ref) => (
|
||||||
return (
|
<SelectPrimitive.ScrollUpButton
|
||||||
<SelectPrimitive.ScrollUpButton
|
ref={ref}
|
||||||
data-slot="select-scroll-up-button"
|
data-slot="select-scroll-up-button"
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex cursor-default items-center justify-center py-1',
|
'flex cursor-default items-center justify-center py-1',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronUpIcon className="size-4" />
|
<ChevronUpIcon className="size-4" />
|
||||||
</SelectPrimitive.ScrollUpButton>
|
</SelectPrimitive.ScrollUpButton>
|
||||||
);
|
));
|
||||||
}
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||||
|
|
||||||
function SelectScrollDownButton({
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
...props
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
>(({ className, ...props }, ref) => (
|
||||||
return (
|
<SelectPrimitive.ScrollDownButton
|
||||||
<SelectPrimitive.ScrollDownButton
|
ref={ref}
|
||||||
data-slot="select-scroll-down-button"
|
data-slot="select-scroll-down-button"
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex cursor-default items-center justify-center py-1',
|
'flex cursor-default items-center justify-center py-1',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronDownIcon className="size-4" />
|
<ChevronDownIcon className="size-4" />
|
||||||
</SelectPrimitive.ScrollDownButton>
|
</SelectPrimitive.ScrollDownButton>
|
||||||
);
|
));
|
||||||
}
|
SelectScrollDownButton.displayName =
|
||||||
|
SelectPrimitive.ScrollDownButton.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Select,
|
Select,
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
function Skeleton({
|
const Skeleton = React.forwardRef<
|
||||||
className,
|
HTMLDivElement,
|
||||||
...props
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={ref}
|
||||||
className={cn('animate-pulse rounded-md bg-muted/50', className)}
|
className={cn('animate-pulse rounded-md bg-muted/50', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
Skeleton.displayName = 'Skeleton';
|
||||||
|
|
||||||
export { Skeleton };
|
export { Skeleton };
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user