diff --git a/eslint.config.js b/eslint.config.js index a74240122..30c4da5df 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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'], rules: { 'no-unused-vars': 'off', + '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/no-namespace': 'off', '@typescript-eslint/consistent-type-definitions': ['error', 'type'], @@ -191,7 +192,14 @@ export default tseslint.config( globals: { ...globals.serviceworker, ...globals.browser, + ...globals.node, }, }, }, + { + files: ['**/fictioneer/custom/*/*.js'], + rules: { + 'no-undef': 'off', + }, + }, ); diff --git a/package-lock.json b/package-lock.json index 9066d41d4..7fc3c3f2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5556,9 +5556,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -5566,6 +5566,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -7605,9 +7606,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "funding": [ { "type": "opencollective", @@ -7624,7 +7625,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7690,11 +7691,12 @@ "dev": true }, "node_modules/protobufjs": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", - "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", + "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", "dev": true, "hasInstallScript": true, + "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -12966,9 +12968,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true }, "for-each": { @@ -14298,11 +14300,11 @@ "dev": true }, "postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "requires": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } @@ -14350,9 +14352,9 @@ } }, "protobufjs": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", - "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", + "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", "dev": true, "requires": { "@protobufjs/aspromise": "^1.1.2", diff --git a/plugins/arabic/dilartube.ts b/plugins/arabic/dilartube.ts index b190fba32..f24158982 100644 --- a/plugins/arabic/dilartube.ts +++ b/plugins/arabic/dilartube.ts @@ -1,13 +1,13 @@ -import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { load as parseHTML } from 'cheerio'; import { fetchApi } from '@libs/fetch'; import { Plugin } from '@/types/plugin'; -import { Filters, FilterTypes } from '@libs/filterInputs'; +import { Filters } from '@libs/filterInputs'; import { defaultCover } from '@libs/defaultCover'; class dilartube implements Plugin.PluginBase { id = 'dilartube'; name = 'dilar tube'; - version = '1.0.1'; + version = '1.0.2'; icon = 'src/ar/dilartube/icon.png'; site = 'https://golden.rest/'; @@ -53,7 +53,7 @@ class dilartube implements Plugin.PluginBase { async popularNovels( page: number, - { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + { showLatestNovels }: Plugin.PopularNovelsOptions, ): Promise { let link = `${this.site}api/releases?page=${page}`; if (showLatestNovels) { @@ -82,14 +82,14 @@ class dilartube implements Plugin.PluginBase { const chapterItems: Plugin.ChapterItem[] = []; const fullUrl = this.site + 'api/' + novelUrl; 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 mangaData = manga.mangaData; const chapterData = chapters.releases; const novel: Plugin.SourceNovel = { path: novelUrl, - name: mangaData.arabic_title || 'Untitled', + name: mangaData.arabic_title ?? mangaData.title ?? 'Untitled', author: (mangaData.authors.length > 0 ? mangaData.authors[0].name : '') || 'Unknown', @@ -97,7 +97,7 @@ class dilartube implements Plugin.PluginBase { cover: `${this.site}uploads/manga/cover/${mangaData.id}/${mangaData.cover}`, chapters: [], }; - const translationStatusId: string = mangaData.translation_status; + const translationStatusId = mangaData.translation_status.toString(); const translationText = { '1': 'مستمره', @@ -105,13 +105,15 @@ class dilartube implements Plugin.PluginBase { '2': 'متوقفة', '3': 'غير مترجمه', }[translationStatusId] || 'غير معروف'; - const statusWords = new Set(['مكتمل', 'جديد', 'مستمر']); - const mainGenres = mangaData.categories - .map((category: { name: any }) => category.name) + // const statusWords = new Set(['مكتمل', 'جديد', 'مستمر']); + const mainGenres = Array.from( + new Set(mangaData.categories.map(g => g.name)), + ) + .filter(Boolean) .join(','); novel.genres = `${translationText},${mainGenres}`; - const statusId: string = mangaData.story_status; + const statusId = mangaData.story_status.toString(); const statusText = { '2': 'Ongoing', @@ -138,12 +140,18 @@ class dilartube implements Plugin.PluginBase { const parsedData = JSON.parse(jsonData as string); 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) => `

${line}

`) + .join(''); } async searchNovels( searchTerm: string, - page: number, + // page: number, ): Promise { const formData = new FormData(); formData.append('query', searchTerm); @@ -156,6 +164,7 @@ class dilartube implements Plugin.PluginBase { return this.parseNovels(data); } + filters: Filters | undefined = undefined; // filters = { // types: { // value: [], @@ -245,8 +254,8 @@ type Manga = { publisher_name: string | null; discord_url: string | null; mobile_exclusive: boolean; - authors: any[]; - artists: any[]; + // authors: any[]; + // artists: any[]; categories: Category[]; type: Type; }; @@ -327,7 +336,7 @@ type MangaData = { uniq_visitors_count: number; publisher_id: number | null; publisher_name: string | null; - arabic_title: string; + arabic_title: string | null; english: string; synonyms: string; japanese: string; @@ -360,11 +369,12 @@ type MangaData = { }; type MangaResponse = { - membersMentioning: any[]; - memberRates: any | null; - mangaLogs: Record; + // membersMentioning: any[]; + // memberRates: any | null; + // mangaLogs: Record; mangaData: MangaData; }; + type ChapterRelease = { id: number; manga_id: number; @@ -385,7 +395,7 @@ type ChapterRelease = { has_rev_link: boolean; }; type searchManga = { - filter: any; + // filter: any; id: number; title: string; summary: string; @@ -417,8 +427,8 @@ type searchManga = { publisher_name: string | null; discord_url: string | null; mobile_exclusive: boolean; - authors: any[]; - artists: any[]; + // authors: any[]; + // artists: any[]; categories: Category[]; type: Type; }; diff --git a/plugins/arabic/rewayatclub.ts b/plugins/arabic/rewayatclub.ts index 3f4983b5e..025b43683 100644 --- a/plugins/arabic/rewayatclub.ts +++ b/plugins/arabic/rewayatclub.ts @@ -1,4 +1,4 @@ -import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { load as parseHTML } from 'cheerio'; import { fetchApi } from '@libs/fetch'; import { Plugin } from '@/types/plugin'; import { Filters, FilterTypes } from '@libs/filterInputs'; @@ -7,7 +7,7 @@ import { defaultCover } from '@libs/defaultCover'; class RewayatClub implements Plugin.PagePlugin { id = 'rewayatclub'; name = 'Rewayat Club'; - version = '1.0.2'; + version = '1.0.3'; icon = 'src/ar/rewayatclub/icon.png'; site = 'https://rewayat.club/'; @@ -34,7 +34,10 @@ class RewayatClub implements Plugin.PagePlugin { async popularNovels( page: number, - { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, ): Promise { let link = `https://api.rewayat.club/api/novels/`; let body: NovelData = { @@ -253,7 +256,7 @@ type ChapterEntry = { hits: number; id: number; }; - read: any[]; + // read: any[]; }; type ChapterData = { diff --git a/plugins/arabic/sunovels.ts b/plugins/arabic/sunovels.ts index abdbc3eef..d7e34063d 100644 --- a/plugins/arabic/sunovels.ts +++ b/plugins/arabic/sunovels.ts @@ -7,7 +7,7 @@ import { defaultCover } from '@libs/defaultCover'; class Sunovels implements Plugin.PagePlugin { id = 'sunovels'; name = 'Sunovels'; - version = '1.0.0'; + version = '1.0.1'; icon = 'src/ar/sunovels/icon.png'; site = 'https://sunovels.com/'; @@ -52,7 +52,7 @@ class Sunovels implements Plugin.PagePlugin { async popularNovels( page: number, - { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + { filters }: Plugin.PopularNovelsOptions, ): Promise { const pageCorrected = page - 1; let link = `${this.site}library?`; @@ -137,7 +137,7 @@ class Sunovels implements Plugin.PagePlugin { name: item.chapterName, releaseTime: new Date(item.releaseTime).toISOString(), path: item.chapterUrl, - chapterNumber: item.chapterNumber, + chapterNumber: Number(item.chapterNumber), }); }); return chapter; @@ -169,8 +169,7 @@ class Sunovels implements Plugin.PagePlugin { const dateAttr = loadedCheerio(el) .find('time.chapter-update') .attr('datetime'); - const date = new Date(dateAttr); - const releaseTime = date.toISOString(); + const releaseTime = dateAttr ? new Date(dateAttr).toISOString() : ''; const chapternumber = loadedCheerio(el) .find('strong.chapter-title') .text() diff --git a/plugins/chinese/Quanben.ts b/plugins/chinese/Quanben.ts index 4933eae42..5434d91c7 100644 --- a/plugins/chinese/Quanben.ts +++ b/plugins/chinese/Quanben.ts @@ -17,7 +17,7 @@ const parseUrl = (url?: string): URL | undefined => { const getStandardNovelPath = (url?: string): string | undefined => { const parsedUrl = parseUrl(url); if (!parsedUrl) return undefined; - const match = parsedUrl.pathname.match(/^(\/amp)?(\/n\/[^\/]+\/)/); + const match = parsedUrl.pathname.match(/^(\/amp)?(\/n\/[^/]+\/)/); return match?.[2]; }; @@ -47,7 +47,7 @@ class QuanbenPlugin implements Plugin.PluginBase { id = 'quanben'; name = 'Quanben'; site = 'https://www.quanben.io/'; - version = '1.0.1'; + version = '1.0.2'; icon = 'src/cn/quanben/icon.png'; defaultCover = defaultCover; @@ -180,7 +180,7 @@ class QuanbenPlugin implements Plugin.PluginBase { const $ = parseHTML(await res.text()); const chapters: Plugin.ChapterItem[] = []; - const novelName = novelPath.match(/\/n\/([^\/]+)\//)?.[1]; + const novelName = novelPath.match(/\/n\/([^/]+)\//)?.[1]; if (!novelName) return []; $('ul.list3 li a').each((_i, el) => { @@ -224,7 +224,7 @@ class QuanbenPlugin implements Plugin.PluginBase { // Helper function to extract and clean chapter content from HTML body private extractChapterContent(body: string): string { const $ = parseHTML(body); - let $content = $('#contentbody, #content, .content').first(); + const $content = $('#contentbody, #content, .content').first(); if (!$content.length) return 'Error: Chapter content not found.'; $content diff --git a/plugins/chinese/ixdzs8.ts b/plugins/chinese/ixdzs8.ts index 1bd28d5d4..b9539f6ef 100644 --- a/plugins/chinese/ixdzs8.ts +++ b/plugins/chinese/ixdzs8.ts @@ -29,7 +29,7 @@ class ixdzs8Plugin implements Plugin.PluginBase { id = 'ixdzs8'; name = '爱下电子书'; site = 'https://ixdzs8.com/'; - version = '2.2.8'; + version = '2.2.9'; icon = 'src/cn/ixdzs8/favicon.png'; imageRequestInit = { @@ -49,14 +49,18 @@ class ixdzs8Plugin implements Plugin.PluginBase { $('ul.u-list > li.burl').each((_i, el) => { const $el = $(el); - let novelPath: string | undefined; - let novelName: string | undefined; - let novelCover: string | undefined; const $link = $el.find('.l-info h3 a'); - novelPath = $link.attr('href')?.trim(); - novelName = ($link.attr('title') || $link.text() || '').trim(); - novelCover = $el.find('.l-img img').attr('src')?.trim(); + const novelPath: string | undefined = $link.attr('href')?.trim(); + const novelName: string | undefined = ( + $link.attr('title') || + $link.text() || + '' + ).trim(); + const novelCover: string | undefined = $el + .find('.l-img img') + .attr('src') + ?.trim(); if (novelPath && novelName) { novels.push({ @@ -159,14 +163,14 @@ class ixdzs8Plugin implements Plugin.PluginBase { 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)) { throw new Error('Invalid response format from chapter list'); } // Build chapters array - const chapters: Plugin.ChapterItem[] = json.data.map((ch: any) => { + const chapters: Plugin.ChapterItem[] = json.data.map(ch => { return { name: ch.title, 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( searchTerm: string, - pageNo: number, + // pageNo: number, ): Promise { const searchUrl = `${this.site}bsearch?q=${encodeURIComponent(searchTerm)}`; let body = ''; @@ -298,3 +302,12 @@ class ixdzs8Plugin implements Plugin.PluginBase { } export default new ixdzs8Plugin(); + +type ChapterJSON = { + rs: number; + data?: { + ctype: string; + ordernum: string; + title: string; + }[]; +}; diff --git a/plugins/chinese/linovel.ts b/plugins/chinese/linovel.ts index 0eaff6035..b6ed952bc 100644 --- a/plugins/chinese/linovel.ts +++ b/plugins/chinese/linovel.ts @@ -9,7 +9,7 @@ class LinovelPlugin implements Plugin.PluginBase { name = 'linovel'; icon = 'src/cn/linovel/icon.png'; site = 'https://www.linovel.net'; - version = '1.0.0'; + version = '1.0.1'; filters: Filters | undefined = undefined; imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; @@ -27,13 +27,12 @@ class LinovelPlugin implements Plugin.PluginBase { return parseHTML(body); } - async popularNovels( - pageNo: number, - { - showLatestNovels, - filters, - }: Plugin.PopularNovelsOptions, - ): Promise { + async popularNovels() // pageNo: number, + // { + // showLatestNovels, + // filters, + // }: Plugin.PopularNovelsOptions, + : Promise { const novels: Plugin.NovelItem[] = []; const loadedCheerio = await this.fetchHTML(this.site); @@ -139,7 +138,7 @@ class LinovelPlugin implements Plugin.PluginBase { } async searchNovels( searchTerm: string, - pageNo: number, + // pageNo: number, ): Promise { const loadedCheerio = await this.fetchHTML( this.site + '/search/?kw=' + searchTerm, diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index 097f32d4c..898d79a63 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -9,7 +9,7 @@ class Linovelib implements Plugin.PluginBase { name = 'Linovelib'; icon = 'src/cn/linovelib/icon.png'; site = 'https://www.bilinovel.com'; - version = '1.2.0'; + version = '1.2.1'; imageRequestInit?: Plugin.ImageRequestInit | undefined = { method: 'GET', headers: { @@ -29,7 +29,7 @@ class Linovelib implements Plugin.PluginBase { type: 'Text', }, }; - serverUrl = storage.get('host') || 'http://localhost:5301'; + serverUrl: string = storage.get('host') || 'http://localhost:5301'; async popularNovels( pageNo: number, @@ -82,7 +82,7 @@ class Linovelib implements Plugin.PluginBase { async parseChapter(chapterPath: string): Promise { // move major logic to LDS const lastFetchChapterTime = - storage.get('lastFetchChapterTime_' + chapterPath) || 0; + Number(storage.get('lastFetchChapterTime_' + chapterPath)) || 0; if (Date.now() - lastFetchChapterTime < 10000) { return storage.get('chapterContent_' + chapterPath) || ''; } @@ -97,10 +97,11 @@ class Linovelib implements Plugin.PluginBase { async searchNovels( searchTerm: string, - pageNo: number, + // pageNo: number, ): Promise { // 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) { return []; } diff --git a/plugins/chinese/linovelib_tw.ts b/plugins/chinese/linovelib_tw.ts index 78ee95825..ffa8d834a 100644 --- a/plugins/chinese/linovelib_tw.ts +++ b/plugins/chinese/linovelib_tw.ts @@ -9,7 +9,7 @@ class Linovelib_tw implements Plugin.PluginBase { name = 'Linovelib(繁體)'; icon = 'src/cn/linovelib/icon.png'; site = 'https://tw.linovelib.com/'; - version = '1.0.0'; + version = '1.0.1'; async popularNovels( pageNo: number, @@ -368,7 +368,7 @@ class Linovelib_tw implements Plugin.PluginBase { pageCheerio('div.cgo, center').remove(); // 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 const imgSrc = pageCheerio(el).attr('data-src') || pageCheerio(el).attr('src'); @@ -382,7 +382,7 @@ class Linovelib_tw implements Plugin.PluginBase { }); // Recover the original character - pageText = pageCheerio('#acontentl').html() || ''; + pageText = pageCheerio('[id*=acontent]').html() || ''; pageText = pageText.replace(/./g, char => skillgg[char] || char); return Promise.resolve(); diff --git a/plugins/english/NovelOnline.ts b/plugins/english/NovelOnline.ts index ad4f7dae7..4c9e0cb6c 100644 --- a/plugins/english/NovelOnline.ts +++ b/plugins/english/NovelOnline.ts @@ -8,7 +8,7 @@ class NovelsOnline implements Plugin.PluginBase { name = 'novelsOnline'; site = 'https://novelsonline.org'; icon = 'src/en/novelsonline/icon.png'; - version = '1.0.1'; + version = '1.0.2'; async safeFetch( url: string, @@ -131,12 +131,14 @@ class NovelsOnline implements Plugin.PluginBase { .join(', '); break; case 'Artist(s)': - const artist = detail - .find('li') - .map((_, el) => $(el).text()) - .get() - .join(', '); - if (artist && artist != 'N/A') novel.artist = artist; + { + const artist = detail + .find('li') + .map((_, el) => $(el).text()) + .get() + .join(', '); + if (artist && artist != 'N/A') novel.artist = artist; + } break; case 'Status': novel.status = detail.text().trim(); diff --git a/plugins/english/StorySeedling.ts b/plugins/english/StorySeedling.ts index 0d1ecb643..9c0171d2f 100644 --- a/plugins/english/StorySeedling.ts +++ b/plugins/english/StorySeedling.ts @@ -1,14 +1,36 @@ import { CheerioAPI, load } from 'cheerio'; import { Plugin } from '@/types/plugin'; -import { fetchApi, fetchText } from '@libs/fetch'; +import { fetchApi } from '@libs/fetch'; 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 { id = 'storyseedling'; name = 'StorySeedling'; icon = 'src/en/storyseedling/icon.png'; site = 'https://storyseedling.com/'; - version = '1.0.4'; + version = '1.0.6'; nonce: string | undefined; async getCheerio(url: string, search: boolean): Promise { @@ -22,8 +44,40 @@ class StorySeedlingPlugin implements Plugin.PluginBase { return $; } - async popularNovels(pageNo: number): Promise { - const novels: Plugin.NovelItem[] = []; + private parseAgoDate(date: string | undefined) { + //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 { const body = await fetchApi(this.site + 'browse').then(r => r.text()); const loadedCheerio = load(body); @@ -33,31 +87,32 @@ class StorySeedlingPlugin implements Plugin.PluginBase { .replace("')", '') as string; const data = new FormData(); - data.append('search', ''); + 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', { + const response = (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, ''), - }), - ); + }).then(res => res.json())) as NovelJSON; + + const novels: Plugin.NovelItem[] = response.data.posts.map(element => ({ + name: element.title, + cover: element.thumbnail, + path: element.permalink.replace(this.site, ''), + })); return novels; } + async popularNovels(pageNo: number): Promise { + return this.getNovels(pageNo); + } + async parseNovel(novelPath: string): Promise { const $ = await this.getCheerio(this.site + novelPath, false); - const baseUrl = this.site; const novel: Partial = { path: novelPath, @@ -67,7 +122,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase { const coverUrl = $('img[x-ref="art"].w-full.rounded.shadow-md').attr('src'); if (coverUrl) { - novel.cover = new URL(coverUrl, baseUrl).href; + novel.cover = new URL(coverUrl, this.site).href; } const genres: string[] = []; @@ -122,7 +177,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase { const chaptersUrl = `${this.site}${chapterListing}`; const refererUrl = `${this.site}${novel.path}`; - let results = await fetchApi(chaptersUrl, { + const results: ChapterJSON = await fetchApi(chaptersUrl, { method: 'POST', referrer: refererUrl, referrerPolicy: 'origin', @@ -136,20 +191,18 @@ class StorySeedlingPlugin implements Plugin.PluginBase { ); if (results.data) { - results = results.data; - results.forEach(function (chap: any) { + results.data.forEach(chap => { if (chap.url == null) { return; } const name = chap.title; const url = chap.url as string; - const releaseTime = chap.date; const chapterNumber = chap.slug; chapters.push({ name: name, - path: url.replace(baseUrl, ''), - releaseTime, + path: url.replace(this.site, ''), + releaseTime: this.parseAgoDate(chap.date), chapterNumber: parseInt(chapterNumber), }); }); @@ -166,7 +219,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase { const $ = await this.getCheerio(this.site + chapterPath, false); this.nonce = $('div.mb-4:has(h1.text-xl) > div') .attr('x-data') - ?.match(/loadChapter\('.+?', '(.+?)'\)/)[1]; + ?.match(/loadChapter\('.+?', '(.+?)'\)/)![1]; } async parseChapter(chapterPath: string): Promise { @@ -200,7 +253,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase { ); } } - let html = text + const html = text .replace(/cls[a-f0-9]+/g, '') .split('') .map(char => { @@ -212,7 +265,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase { : char; }) .join(''); - let $ = load(html); + const $ = load(html); $('span').text((_, txt) => txt.toLowerCase().includes('storyseedling') || @@ -228,37 +281,7 @@ class StorySeedlingPlugin implements Plugin.PluginBase { searchTerm: string, pageNo: number, ): Promise { - const novels: Plugin.NovelItem[] = []; - - 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; + return this.getNovels(pageNo, searchTerm); } } diff --git a/plugins/english/ao3.ts b/plugins/english/ao3.ts index 63266d0b6..8979df382 100644 --- a/plugins/english/ao3.ts +++ b/plugins/english/ao3.ts @@ -7,7 +7,7 @@ import { defaultCover } from '@libs/defaultCover'; class ArchiveOfOurOwn implements Plugin.PluginBase { id = 'archiveofourown'; name = 'Archive Of Our Own'; - version = '1.0.3'; + version = '1.0.4'; icon = 'src/en/ao3/icon.png'; site = 'https://archiveofourown.org/'; @@ -42,57 +42,73 @@ class ArchiveOfOurOwn implements Plugin.PluginBase { async popularNovels( page: number, - { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, ): Promise { - // Base URL and common parameters - let link = `${this.site}works/search?page=${page}&work_search%5Blanguage_id%5D=en`; + const params = new URLSearchParams({ + commit: 'Search', + page: page.toString(), + 'work_search[language_id]': filters.language.value, + }); - // Apply sorting based on showLatestNovels if (showLatestNovels) { - link += `&work_search%5Bsort_column%5D=revised_at&work_search%5Bsort_direction%5D=${filters.sortdir.value}`; - } else if (filters) { - link += `&work_search%5Bsort_column%5D=${filters.sort.value}&work_search%5Bsort_direction%5D=${filters.sortdir.value}`; + params.set('work_search[sort_column]', 'revised_at'); + } else { + params.set('work_search[sort_column]', filters.sort.value); } + params.set('work_search[sort_direction]', filters.sortdir.value); - // Apply additional filters - if (filters) { - // if (filters.genre.value !== '') link += `&work_search%5Bfandom_names%5D=${filters.genre.value}`; - if (filters.completion.value !== '') - link += `&work_search%5Bcomplete%5D=${filters.completion.value}`; - if (filters.crossover.value !== '') - link += `&work_search%5Bcrossover%5D=${filters.crossover.value}`; - if (filters.categories.value.length > 0) { - filters.categories.value.forEach((category: string) => { - link += `&work_search%5Bcategory_ids%5D%5B%5D=${category}`; - }); - } - if (filters.warningsFilter.value.length > 0) { - filters.warningsFilter.value.forEach((warning: string) => { - link += `&work_search%5Barchive_warning_ids%5D%5B%5D=${warning}`; - }); - } - if (filters.singlechap.value !== '') - link += `&work_search%5Bsingle_chapter%5D=${filters.singlechap.value}`; - if (filters.author.value !== '') - link += `&work_search%5Bcreators%5D=${filters.author.value}`; - if ( - filters.dateFilter.value !== '' && - filters.dateIncrements.value !== '' - ) { - link += `&work_search%5Brevised_at%5D=${filters.dateFilter.value}+${filters.dateIncrements.value}`; - } - if (filters.words.value !== '') - link += `&work_search%5Bword_count%5D=${filters.words.value}`; - if (filters.hits.value !== '') - link += `&work_search%5Bhits%5D=${filters.hits.value}`; - if (filters.bookmarks.value !== '') - link += `&work_search%5Bbookmarks_count%5D=${filters.bookmarks.value}`; - if (filters.comments.value !== '') - link += `&work_search%5Bcomments_count%5D=${filters.comments.value}`; - if (filters.kudos.value !== '') - link += `&work_search%5Bkudos_count%5D=${filters.kudos.value}`; + // we could send in the entire thing without checking for blanks + if (filters.completion.value !== '') { + params.set('work_search[complete]', filters.completion.value); + } + if (filters.crossover.value !== '') { + params.set('work_search[crossover]', filters.crossover.value); + } + if (filters.categories.value.length > 0) { + 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) => { + params.append('work_search[archive_warning_ids][]', warning); + }); + } + if (filters.singlechap.value) { + params.set('work_search[single_chapter]', '1'); + } + if (filters.author.value !== '') { + params.set('work_search[creators]', filters.author.value); + } + if ( + filters.dateFilter.value !== '' && + filters.dateIncrements.value !== '' + ) { + params.set( + 'work_search[revised_at]', + `${filters.dateFilter.value} ${filters.dateIncrements.value}`, + ); + } + if (filters.words.value !== '') { + params.set('work_search[word_count]', filters.words.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 loadedCheerio = parseHTML(body); 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}`; const chapterItems: Plugin.ChapterItem[] = []; const longReleaseDate: string[] = []; - let match: RegExpExecArray | null; + // let match: RegExpExecArray | null; chapterlistload('ol.index').each((i, ele) => { chapterlistload(ele) .find('li') .each((i, el) => { - const chapterNameMatch = chapterlistload(el).find('a').text().trim(); + // const chapterNameMatch = chapterlistload(el).find('a').text().trim(); const releaseTimeText = chapterlistload(el) .find('span.datetime') .text() @@ -282,7 +298,13 @@ class ArchiveOfOurOwn implements Plugin.PluginBase { searchTerm: string, page: number, ): Promise { - 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 body = await result.text(); @@ -521,10 +543,9 @@ class ArchiveOfOurOwn implements Plugin.PluginBase { type: FilterTypes.CheckboxGroup, }, singlechap: { - value: '', + value: false, label: 'Single Chapter Stories', - options: [{ label: 'Single Chapter', value: '1' }], - type: FilterTypes.Picker, + type: FilterTypes.Switch, }, author: { value: '', diff --git a/plugins/english/chrysanthemumgarden.ts b/plugins/english/chrysanthemumgarden.ts index 1f8412e56..2f6ec9fb4 100644 --- a/plugins/english/chrysanthemumgarden.ts +++ b/plugins/english/chrysanthemumgarden.ts @@ -9,7 +9,7 @@ class Chrysanthemumgarden implements Plugin.PluginBase { name = 'Chrysanthemum Garden'; icon = 'src/en/chrysanthemumgarden/icon.png'; site = 'https://chrysanthemumgarden.com'; - version = '1.0.1'; + version = '1.0.3'; filters: Filters | undefined = undefined; imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; @@ -18,10 +18,10 @@ class Chrysanthemumgarden implements Plugin.PluginBase { async popularNovels( pageNo: number, - { - showLatestNovels, - filters, - }: Plugin.PopularNovelsOptions, + // { + // showLatestNovels, + // filters, + // }: Plugin.PopularNovelsOptions, ): Promise { const req = await fetchApi( 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) => this.site + (isNovel ? '/book/' : '/chapter/') + path; diff --git a/plugins/english/crimsonscrolls.ts b/plugins/english/crimsonscrolls.ts index fef8bbe70..41bb9b810 100644 --- a/plugins/english/crimsonscrolls.ts +++ b/plugins/english/crimsonscrolls.ts @@ -1,26 +1,42 @@ import { CheerioAPI, load as parseHTML } from 'cheerio'; import { fetchApi } from '@libs/fetch'; -import { FilterTypes, Filters } from '@libs/filterInputs'; import { Plugin } from '@/types/plugin'; import { storage } from '@libs/storage'; -import dayjs from 'dayjs'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; enum APIAction { novels = 'load_novels', search = 'live_novel_search', } -interface APIParams { +type APIParams = { action: APIAction; params: Record; -} +}; + +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 { id = 'crimsonscrolls'; name = 'Crimson Scrolls'; icon = 'src/en/crimsonscrolls/icon.png'; site = 'https://crimsonscrolls.net'; - version = '1.0.0'; + version = '1.0.1'; hideLocked = storage.get('hideLocked'); pluginSettings = { @@ -35,7 +51,7 @@ class CrimsonScrollsPlugin implements Plugin.PluginBase { const formData = new FormData(); formData.append('action', query.action); 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`, { method: 'POST', @@ -45,17 +61,28 @@ class CrimsonScrollsPlugin implements Plugin.PluginBase { return parseHTML(result.html); } - async fetchChapters(id: number, page?: number | undefined) { - const chapter: any[] = []; - const url = `${this.site}/wp-json/cs/v1/novels/${id}/chapters?per_page=75&order=asc`; //page=${page} - const data = await fetchApi(`${url}&page=${page || 1}`).then(r => r.json()); - const locked = data.items.some(e => e.locked); - - if (data.page < data.total_pages && !(locked && this.hideLocked)) - return data.items.concat( - await this.fetchChapters(id, parseInt(data.page) + 1), - ); - else return data.items; + async fetchChapters( + id: number, + page?: number | undefined, + ): Promise { + 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(), + ); + + const items = data.items || []; + const locked = items.some(e => e.locked); + + if ( + 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) { @@ -83,7 +110,9 @@ class CrimsonScrollsPlugin implements Plugin.PluginBase { .filter(e => e.length > 0) .join(' '), cover: novelCover, - path: novelUrl.replace(this.site, '').split('/').at(2), + path: novelUrl + ? new URL(novelUrl, this.site).pathname.substring(1) + : defaultCover, }; novels.push(novel); }, @@ -94,59 +123,61 @@ class CrimsonScrollsPlugin implements Plugin.PluginBase { async popularNovels(page: number): Promise { const loadedCheerio = await this.queryAPI({ action: APIAction.novels, - params: { page: page as string }, + params: { page: page.toString() }, }); return this.parseNovels(loadedCheerio); } async parseNovel(novelPath: string): Promise { - const result = await fetchApi(`${this.site}/novel/${novelPath}`).then(r => + const result = await fetchApi(`${this.site}/${novelPath}`).then(r => r.text(), ); - let loadedCheerio = parseHTML(result); - let novelInfo = loadedCheerio('#single-novel-content-wrapper'); + const loadedCheerio = parseHTML(result); + const novelInfo = loadedCheerio('#single-novel-content-wrapper'); const novel: Plugin.SourceNovel = { path: novelPath, - name: novelInfo.find('h1.chapter-title').text().trim() || 'Untitled', - cover: novelInfo.find('.single-novel-cover > img').data('src'), + name: novelInfo.find('h1').text().trim() ?? 'Untitled', + cover: + novelInfo.find('img:first').data('src')?.toString() ?? defaultCover, summary: novelInfo.find('#synopsis-full').text().trim(), - author: novelInfo - .find('.single-novel-meta strong') - .filter( - (i, el) => - loadedCheerio(el).text().toLowerCase().search('author') >= 0, - )[0] - .next.data.trim(), + author: novelInfo.find('strong:first').next().text().trim(), chapters: [], }; novel.genres = novelInfo - .find('.single-novel-meta strong') - .filter( - (i, el) => loadedCheerio(el).text().toLowerCase().search('genre') >= 0, - )[0] - .next.data.split(',') - .map(e => e.trim()) + .find('.cs-genre-chip') + .map((_, el) => loadedCheerio(el).text().trim()) + .toArray() .join(','); - novel.status = 'Unknown'; + const rawStatus = novelInfo.find('.cs-nsb-badge').text().trim(); + const map: Record = { + ongoing: NovelStatus.Ongoing, + hiatus: NovelStatus.OnHiatus, + dropped: NovelStatus.Cancelled, + cancelled: NovelStatus.Cancelled, + completed: NovelStatus.Completed, + }; + novel.status = map[rawStatus.toLowerCase()] ?? NovelStatus.Unknown; + const id = loadedCheerio('#chapter-list').data('novel'); - const chapters = await this.fetchChapters(id); - - novel.chapters = []; - for (const idx in chapters) { - if (!(chapters[idx].locked && this.hideLocked)) { - novel.chapters.push({ - name: chapters[idx].locked - ? `🔒 ${chapters[idx].title}` - : chapters[idx].title, - path: chapters[idx].url.replace(this.site, '').split('/').at(2), - chapterNumber: parseInt(idx) + 1, + const chapters = await this.fetchChapters(Number(id)); + + const novelChapters: Plugin.ChapterItem[] = []; + chapters.forEach((chapter, index) => { + if (!(chapter.locked && this.hideLocked)) { + novelChapters.push({ + name: chapter.locked ? `🔒 ${chapter.title}` : chapter.title, + path: chapter.url + ? new URL(chapter.url, this.site).pathname.split('/')[2] + : '', + chapterNumber: index + 1, }); } - } + }); + novel.chapters = novelChapters; return novel; } @@ -176,8 +207,9 @@ class CrimsonScrollsPlugin implements Plugin.PluginBase { return this.parseNovels(loadedCheerio); } - resolveUrl = (path: string, isNovel?: boolean) => - this.site + '/novel/' + path; + // not sure purpose of this, commented out + // resolveUrl = (path: string, isNovel?: boolean) => + // this.site + '/novel/' + path; } export default new CrimsonScrollsPlugin(); diff --git a/plugins/english/divinedaolibrary.ts b/plugins/english/divinedaolibrary.ts index 1c6b251a9..9c3e5447e 100644 --- a/plugins/english/divinedaolibrary.ts +++ b/plugins/english/divinedaolibrary.ts @@ -4,7 +4,7 @@ import { Filters, FilterTypes, FilterValueWithType } from '@libs/filterInputs'; import { Plugin } from '@/types/plugin'; 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 { id = 'DDL.com'; @@ -291,10 +291,7 @@ class DDLPlugin implements Plugin.PluginBase { name.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()), ) .map(([, , path]) => path); - return await this.asyncMap( - await foundNovels, - this.grabCachedNovel.bind(this), - ); + return await this.asyncMap(foundNovels, this.grabCachedNovel.bind(this)); } } diff --git a/plugins/english/earlynovel.broken.ts b/plugins/english/earlynovel.broken.ts index 46000671e..0017c838c 100644 --- a/plugins/english/earlynovel.broken.ts +++ b/plugins/english/earlynovel.broken.ts @@ -56,7 +56,7 @@ class EarlyNovelPlugin implements Plugin.PagePlugin { 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); return this.parseNovels(loadedCheerio); @@ -110,7 +110,7 @@ class EarlyNovelPlugin implements Plugin.PagePlugin { async parsePage(novelPath: string, page: string): Promise { 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 chapters = this.parseChapters(loadedCheerio); return { chapters }; diff --git a/plugins/english/fenrirrealm.ts b/plugins/english/fenrirrealm.ts index 461625de4..62a85de76 100644 --- a/plugins/english/fenrirrealm.ts +++ b/plugins/english/fenrirrealm.ts @@ -1,6 +1,5 @@ import { fetchApi } from '@libs/fetch'; import { Plugin } from '@/types/plugin'; -import { Node } from 'domhandler'; import { load as loadCheerio } from 'cheerio'; import { Filters, FilterTypes } from '@libs/filterInputs'; import { storage } from '@libs/storage'; @@ -13,6 +12,10 @@ type APINovel = { description: string; status: string; genres: { name: string }[]; + user?: { + username?: string; + name?: string; + }; }; type APIChapter = { @@ -35,12 +38,44 @@ type ChapterInfo = { 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 { id = 'fenrir'; name = 'Fenrir Realm'; icon = 'src/en/fenrirrealm/icon.png'; site = 'https://fenrirealm.com'; - version = '1.0.13'; + version = '1.1.0'; imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; hideLocked = storage.get('hideLocked'); @@ -62,14 +97,16 @@ class FenrirRealmPlugin implements Plugin.PluginBase { filters, }: Plugin.PopularNovelsOptions, ): Promise { - // let sort = "updated"; - let sort = filters.sort.value; - if (showLatestNovels) sort = 'latest'; - const genresFilter = filters.genres.value - .map(g => '&genres%5B%5D=' + g) - .join(''); + const params = new URLSearchParams({ + page: pageNo.toString(), + per_page: '20', + status: filters.status.value, + order: showLatestNovels ? 'latest' : filters.sort.value, + }); + filters.genres.value.forEach(g => params.append('genres[]', g)); + 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 => r.json().catch(() => { throw new Error( @@ -90,7 +127,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase { if (!apiRes.ok) { const slugMatch = novelPath.match(/^\d+-(.+)$/); - let searchSlug = slugMatch ? slugMatch[1] : novelPath; + const searchSlug = slugMatch ? slugMatch[1] : novelPath; apiRes = await fetchApi( `${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}`, ).then(r => r.json()); const summaryCheerio = loadCheerio(seriesData.description || ''); @@ -131,7 +168,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase { summary: summaryCheerio('p').length > 0 ? summaryCheerio('p') - .map((i, el) => loadCheerio(el).text()) + .map((_, el) => loadCheerio(el).text()) .get() .join('\n\n') : summaryCheerio.text() || '', @@ -139,7 +176,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase { cover: seriesData.cover ? this.site + '/' + seriesData.cover : defaultCover, - genres: (seriesData.genres || []).map((g: any) => g.name).join(','), + genres: (seriesData.genres || []).map(g => g.name).join(','), status: seriesData.status || 'Unknown', }; @@ -184,14 +221,14 @@ class FenrirRealmPlugin implements Plugin.PluginBase { const content = json.content; if (content) { - const parsedContent = JSON.parse(content); + const parsedContent: Chapter = JSON.parse(content); if (parsedContent.type === 'doc') { return parsedContent.content - .map((node: any) => { + .map(node => { if (node.type === 'paragraph') { const innerHtml = node.content - ?.map((c: any) => { + ?.map(c => { if (c.type === 'text') { let text = c.text; if (c.marks) { @@ -216,8 +253,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase { } if (node.type === 'heading') { const level = node.attrs?.level || 1; - const innerHtml = - node.content?.map((c: any) => c.text).join('') || ''; + const innerHtml = node.content?.map(c => c.text).join('') || ''; return `${innerHtml}`; } return ''; @@ -235,7 +271,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase { const loadedCheerio = loadCheerio(body); let chapterText = loadedCheerio('div.content-area p') - .map((i, el) => `

${loadCheerio(el).html()}

`) + .map((_, el) => `

${loadCheerio(el).html()}

`) .get() .join('\n'); @@ -247,24 +283,24 @@ class FenrirRealmPlugin implements Plugin.PluginBase { try { const jsonUrl = `${this.site}/series/${chapterPath.split('~~')[0]}/__data.json?x-sveltekit-invalidated=001`; const jsonRes = await fetchApi(jsonUrl); - const json = await jsonRes.json(); + const json: Nodes = await jsonRes.json(); 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) { const contentStr = data.find( - (d: any) => typeof d === 'string' && d.includes('{"type":"doc"'), + d => typeof d === 'string' && d.includes('{"type":"doc"'), ); if (contentStr) { - const contentJson = JSON.parse(contentStr); + const contentJson: Chapter = JSON.parse(contentStr); if (contentJson.type === 'doc') { chapterText = contentJson.content - .map((node: any) => { + .map(node => { if (node.type === 'paragraph') { const innerHtml = node.content - ?.map((c: any) => { + ?.map(c => { if (c.type === 'text') { let text = c.text; if (c.marks) { @@ -330,8 +366,8 @@ class FenrirRealmPlugin implements Plugin.PluginBase { }; } - resolveUrl = (path: string, isNovel?: boolean) => - this.site + '/series/' + path.split('~~')[0]; + // resolveUrl = (path: string, isNovel?: boolean) => + // this.site + '/series/' + path.split('~~')[0]; filters = { status: { diff --git a/plugins/english/fictionzone.ts b/plugins/english/fictionzone.ts index 71ed1cd4b..27bbba37c 100644 --- a/plugins/english/fictionzone.ts +++ b/plugins/english/fictionzone.ts @@ -15,7 +15,7 @@ class FictionZonePlugin implements Plugin.PluginBase { pageNo: number, { showLatestNovels, - filters, + // filters, }: Plugin.PopularNovelsOptions, ): Promise { 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(url: string): Promise> { return await fetchApi(this.site + '/api/__api_party/fictionzone', { method: 'POST', headers: { @@ -41,20 +42,25 @@ class FictionZonePlugin implements Plugin.PluginBase { }).then(r => r.json()); } - async getPage(url: string) { - const data = await this.getData(url); + async getPage(url: string): Promise { + const data = await this.getData<{ novels: NovelItem[] }>(url); - return data.data.novels.map((n: any) => ({ + return data.data.novels.map(n => ({ name: n.title, cover: `https://cdn.fictionzone.net/insecure/rs:fill:165:250/${n.image}.webp`, path: `novel/${n.slug}`, })); } - async getChapterPage(id: string, novelPath: string) { - const data = await this.getData('/platform/chapter-lists?novel_id=' + id); + async getChapterPage( + id: string, + novelPath: string, + ): Promise { + 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, number: n.chapter_number, date: n.published_date @@ -66,7 +72,7 @@ class FictionZonePlugin implements Plugin.PluginBase { async parseNovel(novelPath: string): Promise { const novelSlug = novelPath.replace('novel/', ''); - const data = await this.getData( + const data = await this.getData( `/platform/novel-details?slug=${novelSlug}`, ); @@ -75,8 +81,8 @@ class FictionZonePlugin implements Plugin.PluginBase { name: data.data.title, cover: `https://cdn.fictionzone.net/insecure/rs:fill:165:250/${data.data.image}.webp`, genres: [ - ...data.data.genres.map((g: any) => g.name), - ...data.data.tags.map((g: any) => g.name), + ...data.data.genres.map(g => g.name), + ...data.data.tags.map(g => g.name), ].join(','), status: data.data.status == 1 @@ -85,7 +91,7 @@ class FictionZonePlugin implements Plugin.PluginBase { ? NovelStatus.Completed : NovelStatus.Unknown, author: - data.data.contributors.filter((c: any) => c.role == 'author')[0] + data.data.contributors.filter(c => c.role == 'author')[0] ?.display_name || '', summary: data.data.synopsis, chapters: await this.getChapterPage(data.data.id, novelPath), @@ -93,8 +99,10 @@ class FictionZonePlugin implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const data = await this.getData(chapterPath.split('|')[1]); - return '

' + data.data.content.replaceAll('\n', '

') + '

'; + const data = await this.getData<{ content: string }>( + chapterPath.split('|')[1], + ); + return '

' + data.data.content.split('\n').join('

') + '

'; } async searchNovels( @@ -106,8 +114,36 @@ class FictionZonePlugin implements Plugin.PluginBase { ); } - resolveUrl = (path: string, isNovel?: boolean) => - this.site + '/' + path.split('|')[0]; + // resolveUrl = (path: string, isNovel?: boolean) => + // this.site + '/' + path.split('|')[0]; } export default new FictionZonePlugin(); + +type Response = { + 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; +}; diff --git a/plugins/english/foxteller.ts b/plugins/english/foxteller.ts index 81fa12025..c257c2be2 100644 --- a/plugins/english/foxteller.ts +++ b/plugins/english/foxteller.ts @@ -2,17 +2,17 @@ import { Plugin } from '@/types/plugin'; import { Parser } from 'htmlparser2'; import { FilterTypes, Filters } from '@libs/filterInputs'; import { defaultCover } from '@libs/defaultCover'; -import { fetchApi } from '@libs/fetch'; +import { fetchApi, FetchInit } from '@libs/fetch'; import { NovelStatus } from '@libs/novelStatus'; class Foxteller implements Plugin.PluginBase { id = 'foxteller'; name = 'Foxteller'; site = 'https://www.foxteller.com'; - version = '1.0.2'; + version = '1.0.3'; icon = 'src/en/foxteller/icon.png'; - async safeFecth(url: string, init: any = {}): Promise { + async safeFecth(url: string, init?: FetchInit): Promise { const r = await fetchApi(url, init); if (!r.ok) throw new Error( @@ -221,8 +221,8 @@ class Foxteller implements Plugin.PluginBase { Accept: 'application/json, text/plain, */*', 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', 'Content-Type': 'application/json;charset=utf-8', + Referer: this.resolveUrl(chapterPath), }, - Referer: this.resolveUrl(chapterPath), body: JSON.stringify({ 'x1': novelID, 'x2': chapterID }), }).then(res => res.json()); @@ -259,8 +259,8 @@ class Foxteller implements Plugin.PluginBase { Accept: 'application/json, text/plain, */*', 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', 'Content-Type': 'application/json;charset=utf-8', + Referer: this.site, }, - Referer: this.site, body: JSON.stringify({ query: searchTerm }), }); const novels: Plugin.NovelItem[] = []; diff --git a/plugins/english/genesis.ts b/plugins/english/genesis.ts index a44d03b01..e6644b3b1 100644 --- a/plugins/english/genesis.ts +++ b/plugins/english/genesis.ts @@ -1,9 +1,10 @@ -import { CheerioAPI, load } from 'cheerio'; +import { load } from 'cheerio'; import { fetchApi } from '@libs/fetch'; import { Filters, FilterTypes } from '@libs/filterInputs'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; import { storage } from '@libs/storage'; +import { defaultCover } from '@libs/defaultCover'; class Genesis implements Plugin.PluginBase { id = 'genesistudio'; @@ -12,7 +13,7 @@ class Genesis implements Plugin.PluginBase { customCSS = 'src/en/genesis/customCSS.css'; site = 'https://genesistudio.com'; api = 'https://api.genesistudio.com'; - version = '2.0.0'; + version = '2.0.1'; hideLocked = storage.get('hideLocked'); pluginSettings = { @@ -23,14 +24,27 @@ class Genesis implements Plugin.PluginBase { }, }; - imageRequestInit?: Plugin.ImageRequestInit | undefined = { + imageRequestInit?: Plugin.ImageRequestInit = { headers: { 'referrer': this.site, }, }; - async parseNovelJSON(json: any[]): Promise { - return json.map((novel: any) => ({ + async parseNovelJSON(): Promise { + // 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, path: `/novels/${novel.abbreviation}`.trim(), 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 { // There is only one page of results, and no known page function, so do not try if (pageNo !== 1) return []; - // Only 14 results, no use in sorting or status - // 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()); - return this.parseNovelJSON(json); + return this.parseNovelJSON(); + } + + async getCoverUrl(coverId: string): Promise { + // 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 { @@ -53,13 +75,20 @@ class Genesis implements Plugin.PluginBase { // Fetch the novel's data in JSON format 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 = { ongoing: NovelStatus.Ongoing, hiatus: NovelStatus.OnHiatus, @@ -68,21 +97,8 @@ class Genesis implements Plugin.PluginBase { completed: NovelStatus.Completed, unknown: NovelStatus.Unknown, }; - novel.status = map[json.serialization.toLowerCase()] ?? NovelStatus.Unknown; - if (json.cover) { - 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], - ); - } - } + novel.status = + map[json.serialization?.toLowerCase() || ''] ?? NovelStatus.Unknown; // Parse the chapters if available and assign them to the novel object novel.chapters = await this.extractChapters(json.id); @@ -91,12 +107,12 @@ class Genesis implements Plugin.PluginBase { } // Helper function to extract and format chapters - async extractChapters(id: string): Plugin.ChapterItem[] { + async extractChapters(id: string): Promise { const url = `${this.site}/api/novels-chapter/${id}`; // Fetch the chapter data in JSON format const raw = await fetchApi(url); - const json = await raw.json(); + const json: ChapterJSON = await raw.json(); // Format each chapter and add only valid ones const chapters = json.data.chapters @@ -116,7 +132,7 @@ class Genesis implements Plugin.PluginBase { chapterNumber: Number(chapterNum), }; }) - .filter(chapter => chapter !== null) as Plugin.ChapterItem[]; + .filter(chapter => chapter !== null); return chapters; } @@ -131,21 +147,17 @@ class Genesis implements Plugin.PluginBase { let external_api; let apikey; - let URLs = []; + const URLs: string[] = []; let code; - // Find URL with API Key - const srcs = $('head') - .find('script') - .map(function () { - const src = $(this).attr('src'); - if (src in URLs) { - return null; - } + $('head script[src]').each((_, el) => { + const src = $(el).attr('src')!; + if (!URLs.includes(src)) { URLs.push(src); - }) - .toArray(); - for (let src of URLs) { + } + }); + + for (const src of URLs) { const script = await fetchApi(`${this.site}${src}`); const raw = await script.text(); 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', headers: { // Cookie: 'csrftoken=' + csrftoken, @@ -197,12 +214,21 @@ class Genesis implements Plugin.PluginBase { pageNo: number, ): Promise { if (pageNo !== 1) return []; - // TODO: Figure out how to search - const url = `${this.site}/api/novels/search?title=${encodeURIComponent(searchTerm)}`; - const json = await fetchApi(url).then(r => r.json()); - return this.parseNovelJSON(json); + // Since only 26 novels, fetch all the novels + // then filter out the novels which match the criteria + const novels = await this.parseNovelJSON(); + 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 = { sort: { label: 'Sort Results By', @@ -213,32 +239,43 @@ class Genesis implements Plugin.PluginBase { ], type: FilterTypes.Picker, }, - storyStatus: { - label: 'Status', - value: 'All', - options: [ - { label: 'All', value: 'All' }, - { label: 'Ongoing', value: 'Ongoing' }, - { label: 'Completed', value: 'Completed' }, - ], - type: FilterTypes.Picker, - }, + // storyStatus: { + // label: 'Status', + // value: 'All', + // options: [ + // { label: 'All', value: 'All' }, + // { label: 'Ongoing', value: 'Ongoing' }, + // { label: 'Completed', value: 'Completed' }, + // ], + // type: FilterTypes.Picker, + // }, genres: { label: 'Genres', value: [], options: [ - { label: 'Action', value: 'Action' }, - { label: 'Comedy', value: 'Comedy' }, - { label: 'Drama', value: 'Drama' }, - { label: 'Fantasy', value: 'Fantasy' }, - { label: 'Harem', value: 'Harem' }, - { label: 'Martial Arts', value: 'Martial Arts' }, - { label: 'Modern', value: 'Modern' }, - { label: 'Mystery', value: 'Mystery' }, - { label: 'Psychological', value: 'Psychological' }, - { label: 'Romance', value: 'Romance' }, - { label: 'Slice of life', value: 'Slice of Life' }, - { label: 'Tragedy', value: 'Tragedy' }, + { 'label': 'Academy', 'value': '21' }, + { 'label': 'Action', 'value': '1' }, + { 'label': 'Adventure', 'value': '15' }, + { 'label': 'Calm Protagonist', 'value': '22' }, + { 'label': 'Comedy', 'value': '2' }, + { 'label': 'Cultivation', 'value': '25' }, + { 'label': 'Drama', 'value': '3' }, + { 'label': 'Fantasy', 'value': '5' }, + { 'label': 'Harem', 'value': '11' }, + { 'label': 'Idol', 'value': '20' }, + { 'label': 'Martial Arts', 'value': '6' }, + { '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, }, @@ -246,3 +283,30 @@ class Genesis implements Plugin.PluginBase { } 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; + }[]; + }; +}; diff --git a/plugins/english/indraTranslations.ts b/plugins/english/indraTranslations.ts index bc309e489..e8aed1002 100644 --- a/plugins/english/indraTranslations.ts +++ b/plugins/english/indraTranslations.ts @@ -8,8 +8,8 @@ class IndraTranslations implements Plugin.PluginBase { id = 'indratranslations'; name = 'Indra Translations'; site = 'https://indratranslations.com'; - version = '1.2.0'; - // icon = 'src/en/indratranslations/icon.png'; + version = '1.2.1'; + icon = 'src/en/indratranslations/icon.png'; // 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. @@ -101,12 +101,12 @@ class IndraTranslations implements Plugin.PluginBase { $(el).find('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 = this.clean($(el).find('.post-title a').text()) || this.clean($(el).find('.tab-summary .post-title a').text()) || - this.clean((a as any).attr?.('title')) || - this.clean((a as any).text?.()); + this.clean(a.attr?.('title')) || + this.clean(a.text?.()); const img = $(el).find('img').attr('data-src') || diff --git a/plugins/english/inkitt.ts b/plugins/english/inkitt.ts index e3019d500..585a3df5a 100644 --- a/plugins/english/inkitt.ts +++ b/plugins/english/inkitt.ts @@ -18,7 +18,7 @@ class InkittPlugin implements Plugin.PluginBase { async popularNovels( pageNo: number, { - showLatestNovels, + // showLatestNovels, filters, }: Plugin.PopularNovelsOptions, ): Promise { @@ -40,7 +40,7 @@ class InkittPlugin implements Plugin.PluginBase { throw new Error('Failed to load novels, try opening in webview.'); } - return data.stories.map((novel: any) => { + return data.stories.map((novel: InkittNovel) => { return { name: novel.title, 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; } @@ -69,7 +69,7 @@ class InkittPlugin implements Plugin.PluginBase { novel.author = loadedCheerio('dl > dd > a.author-link').text(); novel.genres = loadedCheerio('dd.genres > a') - .map((i, el) => loadedCheerio(el).text()) + .map((_, el) => loadedCheerio(el).text()) .toArray() .join(', '); const status = loadedCheerio('div.dlc > dl:has(dt:contains("Status")) > dd') @@ -81,12 +81,12 @@ class InkittPlugin implements Plugin.PluginBase { const apiReq = await fetchApi( 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.summary = loadedCheerio('p.story-summary').text(); - novel.chapters = apiData.chapters.map((c: any) => { + novel.chapters = apiData.chapters.map((c: InkittChapter) => { return { name: c.name, 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.'); } - return data.stories.map((novel: any) => { + return data.stories.map((novel: InkittNovel) => { return { name: novel.title, path: this.getPath(novel), @@ -131,8 +131,8 @@ class InkittPlugin implements Plugin.PluginBase { }); } - resolveUrl = (path: string, isNovel?: boolean) => - this.site + '/stories/' + path; + // resolveUrl = (path: string, isNovel?: boolean) => + // this.site + '/stories/' + path; filters = { genres: { @@ -160,3 +160,31 @@ class InkittPlugin implements Plugin.PluginBase { } 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[]; +}; diff --git a/plugins/english/inoveltranslation.ts b/plugins/english/inoveltranslation.ts index 7188062eb..28578b482 100644 --- a/plugins/english/inoveltranslation.ts +++ b/plugins/english/inoveltranslation.ts @@ -11,7 +11,7 @@ class INovelTranslation implements Plugin.PluginBase { name = 'iNovelTranslation'; icon = 'src/en/inoveltranslation/icon.png'; site = 'https://inoveltranslation.com'; - version = '1.0.1'; + version = '1.0.2'; filters: Filters | undefined = undefined; pluginSettings = { @@ -22,7 +22,6 @@ class INovelTranslation implements Plugin.PluginBase { }, }; - // Optimized stealth headers to mirror a real browser environment private readonly HEADERS = { 'Accept': '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', }; - async popularNovels( - pageNo: number, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - options: Plugin.PopularNovelsOptions, - ): Promise { + async popularNovels(pageNo: number): Promise { const url = `${this.site}/api/novels?limit=50&page=${pageNo}`; - const result = await fetchApi(url, { headers: this.HEADERS }).then(r => - r.json(), - ); + const result: ApiResponse = await fetchApi(url, { + headers: this.HEADERS, + }).then(r => r.json()); const novels: Plugin.NovelItem[] = []; if (result.docs) { - result.docs.forEach((doc: any) => { + result.docs.forEach(doc => { novels.push({ name: doc.title, path: `/novels/${doc.id}`, @@ -61,27 +56,24 @@ class INovelTranslation implements Plugin.PluginBase { async parseNovel(novelPath: string): Promise { const id = novelPath.split('/').pop(); const novelUrl = `${this.site}/api/novels/${id}?depth=1`; - const novelData = await fetchApi(novelUrl, { headers: this.HEADERS }).then( - r => r.json(), - ); + const novelData: NovelData = await fetchApi(novelUrl, { + headers: this.HEADERS, + }).then(r => r.json()); const chaptersUrl = `${this.site}/api/chapters?where[novel][equals]=${id}&limit=999&depth=0`; - const chaptersData = await fetchApi(chaptersUrl, { + const chaptersData: ApiResponse = await fetchApi(chaptersUrl, { headers: this.HEADERS, }).then(r => r.json()); - // Extract status const status = novelData.publication === 'completed' ? NovelStatus.Completed : NovelStatus.Ongoing; - // Extract genres (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 = ''; if (novelData.sypnosis && novelData.sypnosis.root) { summary = this.lexicalToText(novelData.sypnosis.root); @@ -103,7 +95,7 @@ class INovelTranslation implements Plugin.PluginBase { const hideLocked = storage.get('hideLocked'); if (chaptersData.docs) { - chaptersData.docs.forEach((doc: any) => { + chaptersData.docs.forEach(doc => { const isLocked = doc.tier !== null; if (isLocked && hideLocked) { return; @@ -121,7 +113,6 @@ class INovelTranslation implements Plugin.PluginBase { }); } - // Ensure chapters are sorted numerically novel.chapters = chapters.sort( (a, b) => (a.chapterNumber || 0) - (b.chapterNumber || 0), ); @@ -129,7 +120,6 @@ class INovelTranslation implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - // Artificial delay to prevent aggressive rate limiting await new Promise(res => setTimeout(res, 1500)); const rscHeader = { ...this.HEADERS, rsc: '1' }; @@ -139,8 +129,8 @@ class INovelTranslation implements Plugin.PluginBase { response = await fetchApi(this.site + chapterPath, { headers: rscHeader, }); - } catch (e: any) { - throw new Error(`Network error: ${e.message}`); + } catch (e) { + throw new Error(`Network error: ${(e as Error).message}`); } if (response.status !== 200) { @@ -155,7 +145,6 @@ class INovelTranslation implements Plugin.PluginBase { throw new Error('Server returned empty data.'); } - // 1. Proactive Cloudflare Detection if ( rscText.includes('cf-browser-verification') || rscText.includes('cf-challenge') || @@ -192,7 +181,12 @@ class INovelTranslation implements Plugin.PluginBase { let startIndex = rscText.lastIndexOf('{', sigIndex); // 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) { const keyIndex = rscText.lastIndexOf(key, sigIndex); if (keyIndex !== -1 && keyIndex > startIndex - 50) { @@ -236,8 +230,8 @@ class INovelTranslation implements Plugin.PluginBase { if (jsonStr) { try { - // Attempt to parse directly (if it's unescaped RSC) - let safeJson = jsonStr.replace(/[\x00-\x1F\x7F-\x9F]/g, ''); + // eslint-disable-next-line no-control-regex + const safeJson = jsonStr.replace(/[\x00-\x1F\x7F-\x9F]/g, ''); let parsedData; try { parsedData = JSON.parse(safeJson); @@ -246,18 +240,22 @@ class INovelTranslation implements Plugin.PluginBase { const cleanJson = jsonStr .replace(/\\"/g, '"') .replace(/\\\\/g, '\\') + // eslint-disable-next-line no-control-regex .replace(/[\x00-\x1F\x7F-\x9F]/g, ''); parsedData = JSON.parse(cleanJson); } - let lexicalRoot = parsedData.root || parsedData.content?.root || parsedData; + const lexicalRoot = + parsedData.root || parsedData.content?.root || parsedData; if (lexicalRoot && lexicalRoot.children) { return this.lexicalToHtml(lexicalRoot); } - } catch (e: any) { + } catch (e: unknown) { // Fallback to regex text extraction if JSON parsing fails let fallbackHtml = ''; - const textMatches = jsonStr.match(/\\?"text\\?"\s*:\s*\\?"(.*?)\\?"/g); + const textMatches = jsonStr.match( + /\\?"text\\?"\s*:\s*\\?"(.*?)\\?"/g, + ); if (textMatches && textMatches.length > 0) { textMatches.forEach(m => { let text = m.match(/: ?"?(.*?)"?$/)?.[1] || ''; @@ -296,11 +294,7 @@ class INovelTranslation implements Plugin.PluginBase { ); } - /** - * Recursively converts Lexical JSON nodes to HTML strings. - * Suitable for Chapter Content. - */ - private lexicalToHtml(node: any): string { + private lexicalToHtml(node: LexicalNode): string { let html = ''; if (node.children) { for (const child of node.children) { @@ -308,8 +302,8 @@ class INovelTranslation implements Plugin.PluginBase { html += `

${this.lexicalToHtml(child)}

`; } else if (child.type === 'text') { let text = child.text || ''; - if (child.format & 1) text = `${text}`; - if (child.format & 2) text = `${text}`; + if (child.format && child.format & 1) text = `${text}`; + if (child.format && child.format & 2) text = `${text}`; html += text; } else if (child.type === 'list') { const tag = child.listType === 'number' ? 'ol' : 'ul'; @@ -327,11 +321,7 @@ class INovelTranslation implements Plugin.PluginBase { return html; } - /** - * Recursively converts Lexical JSON nodes to Plain Text with newlines. - * Suitable for Novel Synopsis in the app. - */ - private lexicalToText(node: any): string { + private lexicalToText(node: LexicalNode): string { let textOut = ''; if (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( searchTerm, )}&limit=50&page=${pageNo}`; - const result = await fetchApi(url, { headers: this.HEADERS }).then(r => - r.json(), - ); + const result: ApiResponse = await fetchApi(url, { + headers: this.HEADERS, + }).then(r => r.json()); const novels: Plugin.NovelItem[] = []; if (result.docs) { - result.docs.forEach((doc: any) => { + result.docs.forEach(doc => { novels.push({ name: doc.title, path: `/novels/${doc.id}`, @@ -374,8 +364,43 @@ class INovelTranslation implements Plugin.PluginBase { return novels; } - - resolveUrl = (path: string, isNovel?: boolean) => this.site + path; } 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 = { + docs: T[]; +}; diff --git a/plugins/english/leafstudio.ts b/plugins/english/leafstudio.ts index 976cbdfa3..ddfaf45d0 100644 --- a/plugins/english/leafstudio.ts +++ b/plugins/english/leafstudio.ts @@ -28,7 +28,7 @@ class LeafStudio implements Plugin.PluginBase { async popularNovels( page: number, - { filters }: Plugin.PopularNovelsOptions, + // { filters }: Plugin.PopularNovelsOptions, ): Promise { let link = this.site + 'novels'; if (page > 1) { diff --git a/plugins/english/lightnoveltranslation.ts b/plugins/english/lightnoveltranslation.ts index cb5ed8db0..347a6d009 100644 --- a/plugins/english/lightnoveltranslation.ts +++ b/plugins/english/lightnoveltranslation.ts @@ -2,13 +2,13 @@ import { fetchApi } from '@libs/fetch'; import { Plugin } from '@/types/plugin'; import { Filters } from '@libs/filterInputs'; import { load as loadCheerio } from 'cheerio'; -import { defaultCover } from '@libs/defaultCover'; +// import { defaultCover } from '@libs/defaultCover'; import { NovelStatus } from '@libs/novelStatus'; // import { isUrlAbsolute } from '@libs/isAbsoluteUrl'; // import { storage, localStorage, sessionStorage } from '@libs/storage'; // import { encode, decode } from 'urlencode'; // import dayjs from 'dayjs'; -import { Parser } from 'htmlparser2'; +// import { Parser } from 'htmlparser2'; class LNTPlugin implements Plugin.PluginBase { id = 'lightnoveltranslations'; @@ -26,7 +26,7 @@ class LNTPlugin implements Plugin.PluginBase { pageNo: number, { showLatestNovels, - filters, + // filters, }: Plugin.PopularNovelsOptions, ): Promise { let link = this.site + 'read/'; @@ -193,11 +193,11 @@ class LNTPlugin implements Plugin.PluginBase { novels.push(tempNovel); }); - type SearchEntry = { - title: string; - thumbnail: string; - permalink: string; - }; + // type SearchEntry = { + // title: string; + // thumbnail: string; + // permalink: string; + // }; return novels; } diff --git a/plugins/english/lnmtl.ts b/plugins/english/lnmtl.ts index 779894736..f8dae9be8 100644 --- a/plugins/english/lnmtl.ts +++ b/plugins/english/lnmtl.ts @@ -8,7 +8,7 @@ class LnMTLPlugin implements Plugin.PagePlugin { name = 'LnMTL'; icon = 'src/en/lnmtl/icon.png'; site = 'https://lnmtl.com/'; - version = '2.1.0'; + version = '2.1.1'; async sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); @@ -126,10 +126,12 @@ class LnMTLPlugin implements Plugin.PagePlugin { ontext(data) { switch (state) { case ParsingState.InScript: - const volume = JSON.parse( - data.match(/lnmtl.volumes = (.+])(?=;)/)![1] || '', - ); - novel.totalPages = volume.length; + { + const volume = JSON.parse( + data.match(/lnmtl.volumes = (.+])(?=;)/)![1] || '', + ); + novel.totalPages = volume.length; + } break; case ParsingState.InDescription: summaryParts.push(data.trim()); diff --git a/plugins/english/mtlreader.ts b/plugins/english/mtlreader.broken.ts similarity index 100% rename from plugins/english/mtlreader.ts rename to plugins/english/mtlreader.broken.ts diff --git a/plugins/english/mvlempyr.ts b/plugins/english/mvlempyr.ts index 4b09acec8..dc7723b0a 100644 --- a/plugins/english/mvlempyr.ts +++ b/plugins/english/mvlempyr.ts @@ -5,6 +5,8 @@ import { Filters, FilterTypes } from '@libs/filterInputs'; import { Parser } from 'htmlparser2'; //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: { inTag?: string; depthRatingWrapper?: number; @@ -45,33 +47,29 @@ class MVLEMPYRPlugin implements Plugin.PluginBase { async popularNovels( pageNo: number, - { filters }: Plugin.PopularNovelsOptions, + { filters }: Plugin.PopularNovelsOptions, ): Promise { const data = await this.getAllNovels(); 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)) { 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)) { 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)) { 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)) { return false; } diff --git a/plugins/english/novelfire.ts b/plugins/english/novelfire.ts index 7d3111927..97caa7b8d 100644 --- a/plugins/english/novelfire.ts +++ b/plugins/english/novelfire.ts @@ -9,7 +9,7 @@ import { storage } from '@libs/storage'; class NovelFire implements Plugin.PluginBase { id = 'novelfire'; name = 'Novel Fire'; - version = '1.4.1'; + version = '1.4.2'; icon = 'src/en/novelfire/icon.png'; site = 'https://novelfire.net/'; webStorageUtilized = true; @@ -294,7 +294,7 @@ class NovelFire implements Plugin.PluginBase { } novel.genres = $('.categories .property-item') - .map((i, el) => $(el).text()) + .map((_, el) => $(el).text()) .toArray() .join(','); @@ -356,7 +356,11 @@ class NovelFire implements Plugin.PluginBase { if (post_id && !isNaN(Number(post_id))) { try { - const chapters = await this.getAllChapters(novelPath, post_id, page); + const chapters = await this.getAllChapters( + novelPath, + String(post_id), + page, + ); return { chapters }; } catch (e) { // Fallback to scraping if AJAX fails @@ -373,7 +377,7 @@ class NovelFire implements Plugin.PluginBase { const loadedCheerio = load(body); const chapters = loadedCheerio('.chapter-list li') - .map((index, ele) => { + .map((_, ele) => { const chapterName = loadedCheerio(ele).find('a').attr('title') || 'No Title Found'; const chapterPath = loadedCheerio(ele).find('a').attr('href'); diff --git a/plugins/english/novelhi.ts b/plugins/english/novelhi.ts index 9bf9b7aff..2e766283f 100644 --- a/plugins/english/novelhi.ts +++ b/plugins/english/novelhi.ts @@ -3,13 +3,14 @@ import { Plugin } from '@/types/plugin'; import { load as parseHTML } from 'cheerio'; import { NovelStatus } from '@libs/novelStatus'; import { defaultCover } from '@libs/defaultCover'; +import { Filters, FilterTypes } from '@/types/filters'; class NovelHi implements Plugin.PluginBase { id = 'novelhi'; name = 'NovelHi'; icon = 'src/en/novelhi/icon.png'; site = 'https://novelhi.com/'; - version = '1.0.0'; + version = '1.1.0'; // flag indicates whether access to LocalStorage, SesesionStorage is required. 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 loadedNovelCache: CachedNovel[] = []; - parseNovels(novels: NovelData[]): CachedNovel[] { - const ret: CachedNovel[] = novels.map(item => ({ + private async getNovels( + pageNo: number, + keyword = '', + filters?: Plugin.PopularNovelsOptions['filters'], + ): Promise { + 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, path: `s/${item.simpleName}`, cover: item.picUrl || defaultCover, @@ -28,30 +46,19 @@ class NovelHi implements Plugin.PluginBase { genres: item.genres.map(g => g.genreName).join(', '), })); - // Manage cache size - this.loadedNovelCache.push(...ret); + this.loadedNovelCache.push(...novels); if (this.loadedNovelCache.length > 100) { this.loadedNovelCache = this.loadedNovelCache.slice(-100); } - return ret; + return novels; } async popularNovels( pageNo: number, - { showLatestNovels }: Plugin.PopularNovelsOptions, - ): Promise { - const params = new URLSearchParams(); - - 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); + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + return this.getNovels(pageNo, '', filters); } async parseNovel(novelPath: string): Promise { @@ -130,19 +137,71 @@ class NovelHi implements Plugin.PluginBase { async searchNovels( searchTerm: string, pageNo: number, - ): Promise { - const params = new URLSearchParams(); - - 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); + ): Promise { + return this.getNovels(pageNo, searchTerm); } + + 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(); diff --git a/plugins/english/novelight.ts b/plugins/english/novelight.ts index 27e2dc226..92e0211db 100644 --- a/plugins/english/novelight.ts +++ b/plugins/english/novelight.ts @@ -10,7 +10,7 @@ import { storage } from '@libs/storage'; class Novelight implements Plugin.PagePlugin { id = 'novelight'; name = 'Novelight'; - version = '1.1.4'; + version = '1.1.5'; icon = 'src/en/novelight/icon.png'; site = 'https://novelight.net/'; @@ -184,7 +184,9 @@ class Novelight implements Plugin.PagePlugin { parseHTML(ele)('.date').text().trim(), 'DD.MM.YYYY', ).toISOString(); - } catch (error) {} + } catch (error) { + // linter happy + } const chapterName = isLocked ? '🔒 ' + title : title; let chapterUrl = ele.attribs.href; diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index a824f5f4a..5b675d0e3 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin'; class NovelUpdates implements Plugin.PluginBase { id = 'novelupdates'; name = 'Novel Updates'; - version = '0.9.8'; + version = '0.9.9'; icon = 'src/en/novelupdates/icon.png'; customCSS = 'src/en/novelupdates/customCSS.css'; site = 'https://www.novelupdates.com/'; @@ -342,23 +342,26 @@ class NovelUpdates implements Plugin.PluginBase { chapterContent = loadedCheerio('.content').html()!; 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': { const url = `${chapterPath}/__data.json?x-sveltekit-invalidated=001`; try { // 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 }[]; + }; const nodes = json.nodes; const data = nodes - .filter((node: { type: string }) => node.type === 'data') - .map((node: { data: any }) => node.data)[0]; + .filter(node => node.type === 'data') + .map(node => node.data)[0]; // Look for chapter container with required fields const contentKey = 'content'; const notesKey = 'notes'; const footnotesKey = 'footnotes'; // Iterate over each property in data to find chapter containers for (const key in data) { - const mapping = data[key]; + const mapping = data[key] as Record; // Check container for keys that match the required fields if ( mapping && @@ -368,9 +371,11 @@ class NovelUpdates implements Plugin.PluginBase { footnotesKey in mapping ) { // Retrieve the chapter's content, notes, and footnotes using the mapping. - const content = data[mapping[contentKey]]; - const notes = data[mapping[notesKey]]; - const footnotes = data[mapping[footnotesKey]]; + const content = data[String(mapping[contentKey])] as string; + const notes = data[String(mapping[notesKey])] as string; + const footnotes = data[String(mapping[footnotesKey])] as + | string + | undefined; // Combine the parts with appropriate formatting chapterText = content + diff --git a/plugins/english/pawread.ts b/plugins/english/pawread.ts index cde78be68..09054ac6d 100644 --- a/plugins/english/pawread.ts +++ b/plugins/english/pawread.ts @@ -6,7 +6,7 @@ import { Filters, FilterTypes } from '@libs/filterInputs'; class PawRead implements Plugin.PluginBase { id = 'pawread'; name = 'PawRead'; - version = '2.1.0'; + version = '2.1.1'; icon = 'src/en/pawread/icon.png'; site = 'https://m.pawread.com/'; @@ -171,13 +171,15 @@ class PawRead implements Plugin.PluginBase { break; case ParsingState.ChapterTime: if (text?.includes('Advanced')) return; - const releaseDate = text.split('.').map(x => Number(x)); - if (releaseDate.length === 3) { - tempChapter.releaseTime = new Date( - releaseDate[0], - releaseDate[1] - 1, - releaseDate[2], - ).toISOString(); + { + const releaseDate = text.split('.').map(x => Number(x)); + if (releaseDate.length === 3) { + tempChapter.releaseTime = new Date( + releaseDate[0], + releaseDate[1] - 1, + releaseDate[2], + ).toISOString(); + } } break; } @@ -251,9 +253,7 @@ class PawRead implements Plugin.PluginBase { let state: ParsingState = ParsingState.Idle; const chapterHtml: string[] = []; - type EscapeChar = '&' | '<' | '>' | '"' | "'" | ' '; - const escapeRegex = /[&<>"' ]/g; - const escapeMap: Record = { + const escapeMap: Record = { '&': '&', '<': '<', '>': '>', @@ -262,7 +262,7 @@ class PawRead implements Plugin.PluginBase { ' ': ' ', }; const escapeHtml = (text: string): string => - text.replace(escapeRegex, char => escapeMap[char as EscapeChar]); + text.replace(/[&<>"'\xA0]/g, char => escapeMap[char]); const parser = new Parser({ onopentag(name, attribs) { diff --git a/plugins/english/relibrary.ts b/plugins/english/relibrary.ts index 0f0685cc1..8cbd1844b 100644 --- a/plugins/english/relibrary.ts +++ b/plugins/english/relibrary.ts @@ -3,7 +3,6 @@ import { Plugin } from '@/types/plugin'; import { Filters } from '@libs/filterInputs'; import { load as loadCheerio } from 'cheerio'; import { defaultCover } from '@libs/defaultCover'; -import { NovelItem } from '../../test_web/static/js'; import { NovelStatus } from '@libs/novelStatus'; type FuzzySearchOptions = { @@ -164,7 +163,7 @@ class ReLibraryPlugin implements Plugin.PluginBase { name = 'Re:Library'; icon = 'src/en/relibrary/icon.png'; site = 'https://re-library.com'; - version = '1.0.2'; + version = '1.0.3'; imageRequestInit: Plugin.ImageRequestInit = { headers: { Referer: 'https://re-library.com/', @@ -183,7 +182,7 @@ class ReLibraryPlugin implements Plugin.PluginBase { const loadedCheerio = loadCheerio(body); loadedCheerio('.entry-content > ol > li').each((_i, el) => { - const novel: Partial = {}; + const novel: Partial = {}; novel.name = loadedCheerio(el).find('h3 > a').text(); novel.path = loadedCheerio(el) .find('table > tbody > tr > td > a') @@ -197,9 +196,7 @@ class ReLibraryPlugin implements Plugin.PluginBase { .find('table > tbody > tr > td > a > img') .attr('src') || defaultCover; - if (novel.path.startsWith(this.site)) { - novel.path = novel.path.slice(this.site.length); - } + novel.path = new URL(novel.path, this.site).pathname; novels.push(novel as Plugin.NovelItem); }); return novels; @@ -224,9 +221,7 @@ class ReLibraryPlugin implements Plugin.PluginBase { .find('.entry-content > table > tbody > tr > td > a >img') .attr('src') || defaultCover; - if (novel.path.startsWith(this.site)) { - novel.path = novel.path.slice(this.site.length); - } + novel.path = new URL(novel.path, this.site).pathname; novels.push(novel as Plugin.NovelItem); }); return novels; @@ -257,7 +252,7 @@ class ReLibraryPlugin implements Plugin.PluginBase { // synopis: .entry-content > div.su-box > div.su-box-content // chapters: .entry-content > div.su-accordion li.page_item[] - const result = await fetchApi(`${this.site}/${novelPath}`); + const result = await fetchApi(`${this.site}${novelPath}`); const body = await result.text(); const loadedCheerio = loadCheerio(body); @@ -330,18 +325,11 @@ class ReLibraryPlugin implements Plugin.PluginBase { .find('li > a') .each((_i2, chap_el) => { chapter_idx += 1; - let chap_path = loadedCheerio(chap_el).attr('href')?.trim(); - if ( - loadedCheerio(chap_el).text() === undefined || - chap_path === undefined - ) - return; - if (chap_path.startsWith(this.site)) { - chap_path = chap_path.slice(this.site.length); - } + const chap_path = loadedCheerio(chap_el).attr('href')?.trim(); + if (loadedCheerio(chap_el).text() === undefined || !chap_path) return; chapters.push({ name: loadedCheerio(chap_el).text(), - path: chap_path, + path: new URL(chap_path, this.site).pathname, 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 releaseTime: null, @@ -355,7 +343,7 @@ class ReLibraryPlugin implements Plugin.PluginBase { async parseChapter(chapterPath: string): Promise { // parse chapter text here - const result = await fetchApi(`${this.site}/${chapterPath}`); + const result = await fetchApi(`${this.site}${chapterPath}`); const body = await result.text(); const loadedCheerio = loadCheerio(body); @@ -408,10 +396,11 @@ class ReLibraryPlugin implements Plugin.PluginBase { loadedCheerio('article article').each((_i, 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({ name: e.find('h4').text(), - path: e.find('a').attr('href')?.replace(this.site, '') || '', + path: new URL(href, this.site).pathname, cover: e.find('img').attr('data-cfsrc') || e.find('img').attr('src') || diff --git a/plugins/english/royalroad.ts b/plugins/english/royalroad.ts index 1c54b1fd2..6fcaeda6c 100644 --- a/plugins/english/royalroad.ts +++ b/plugins/english/royalroad.ts @@ -9,7 +9,7 @@ import { storage } from '@libs/storage'; class RoyalRoad implements Plugin.PluginBase { id = 'royalroad'; name = 'Royal Road'; - version = '2.3.0'; + version = '2.3.1'; icon = 'src/en/royalroad/icon.png'; site = 'https://www.royalroad.com/'; @@ -415,18 +415,20 @@ class RoyalRoad implements Plugin.PluginBase { depth--; return; case ParsingState.InNote: - const noteClass = `author-note-${isBeforeChapter ? 'before' : 'after'}`; - const notesHtml = notesHtmlParts.join('').trim(); - const fullNote = `
${notesHtml}
`; - if (isBeforeChapter) { - beforeNotesParts.push(fullNote); - } else { - afterNotesParts.push(fullNote); + { + const noteClass = `author-note-${isBeforeChapter ? 'before' : 'after'}`; + const notesHtml = notesHtmlParts.join('').trim(); + const fullNote = `
${notesHtml}
`; + if (isBeforeChapter) { + beforeNotesParts.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; } } else if ( diff --git a/plugins/english/vynovel.ts b/plugins/english/vynovel.ts index be88ca4e8..9f0d2564f 100644 --- a/plugins/english/vynovel.ts +++ b/plugins/english/vynovel.ts @@ -4,26 +4,24 @@ import { defaultCover } from '@libs/defaultCover'; import { fetchApi } from '@libs/fetch'; import { NovelStatus } from '@libs/novelStatus'; import { load as parseHTML } from 'cheerio'; -import dayjs from 'dayjs'; +import dayjs, { ManipulateType } from 'dayjs'; class VyNovel implements Plugin.PluginBase { id = 'vynovel'; name = 'VyNovel'; site = 'https://vynovel.com'; - version = '1.0.0'; + version = '1.0.1'; icon = 'src/en/vynovel/icon.png'; async fetchNovels( page: number, - { - filters, - showLatestNovels, - }: Plugin.PopularNovelsOptions, + showLatestNovels?: boolean, + filters?: Plugin.PopularNovelsOptions['filters'], searchTerm?: string, ): Promise { const data = new URLSearchParams({ sort: showLatestNovels ? 'updated_at' : filters?.sort?.value || 'viewed', - page, + page: page.toString(), }); if (searchTerm) data.append('q', searchTerm); @@ -33,7 +31,7 @@ class VyNovel implements Plugin.PluginBase { const loadedCheerio = parseHTML(body); 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) .find('div[class="comic-title"]') .text() @@ -52,17 +50,18 @@ class VyNovel implements Plugin.PluginBase { return novels; } - popularNovels = this.fetchNovels; - - async searchNovels( - searchTerm: string, + async popularNovels( page: number, - ): Promise { - const defaultOptions: any = { - filters: undefined, - showLatestNovels: false, - }; - return this.fetchNovels(page, defaultOptions, searchTerm); + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ) { + return this.fetchNovels(page, showLatestNovels, filters); + } + + async searchNovels(searchTerm: string, page: number) { + return this.fetchNovels(page, false, undefined, searchTerm); } async parseNovel(novelPath: string): Promise { @@ -102,7 +101,7 @@ class VyNovel implements Plugin.PluginBase { chapters.push({ name, path: novelPath + '/' + id, - releaseTime: this.parseDate(releaseDate?.trim()), + releaseTime: this.parseAgoDate(releaseDate), chapterNumber: totalChapters - chapterIndex, }); }, @@ -122,38 +121,35 @@ class VyNovel implements Plugin.PluginBase { return chapterText || ''; } - parseDate = (date = '') => { - if (!date) return null; - if (date.includes('ago')) { - const [value, type] = date.split(' '); - if (!value || !type) return null; - - 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'); + private parseAgoDate(date: string | undefined) { + //parseMadaraDate + const parsed = dayjs(date); + if (date && parsed.isValid()) { + return parsed.toISOString(); } - 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) => this.site + (isNovel ? '/novel/' : '/read/') + path; diff --git a/plugins/english/wtrlab.ts b/plugins/english/wtrlab.ts index b9799ad71..5ef02c328 100644 --- a/plugins/english/wtrlab.ts +++ b/plugins/english/wtrlab.ts @@ -1,14 +1,14 @@ import { Plugin } from '@/types/plugin'; import { fetchApi } from '@libs/fetch'; import { FilterTypes, Filters } from '@libs/filterInputs'; -import { load as parseHTML } from 'cheerio'; +import { CheerioAPI, load as parseHTML } from 'cheerio'; import { gcm } from '@libs/aes'; class WTRLAB implements Plugin.PluginBase { id = 'WTRLAB'; name = 'WTR-LAB'; site = 'https://wtr-lab.com/'; - version = '1.1.0'; + version = '1.1.2'; icon = 'src/en/wtrlab/icon.png'; sourceLang = 'en/'; @@ -116,14 +116,14 @@ class WTRLAB implements Plugin.PluginBase { const seenIds = new Set(); const novels: Plugin.NovelItem[] = json.pageProps.series - .filter((novel: any) => { + .filter((novel: Datum) => { if (seenIds.has(novel.raw_id)) { return false; } seenIds.add(novel.raw_id); return true; }) - .map((novel: any) => ({ + .map((novel: Datum) => ({ name: novel.data.title, cover: novel.data.image, path: `${this.sourceLang}serie-${novel.raw_id}/${novel.slug}`, @@ -409,7 +409,7 @@ class WTRLAB implements Plugin.PluginBase { } } - async getKey($: any): Promise { + async getKey($: CheerioAPI): Promise { // Fetch the novel's data in JSON format const searchKey = 'TextEncoder().encode("'; @@ -426,7 +426,7 @@ class WTRLAB implements Plugin.PluginBase { URLs.push(src); } - for (let src of URLs) { + for (const src of URLs) { const script = await fetchApi(`${this.site}${src}`); const raw = await script.text(); index = raw.indexOf(searchKey); @@ -446,7 +446,7 @@ class WTRLAB implements Plugin.PluginBase { async translate(data: string[]): Promise { const contained = data.map((line, i) => `${line}`); - let translated: any = await fetchApi( + const response = await fetchApi( 'https://translate-pa.googleapis.com/v1/translateHtml', { 'credentials': 'omit', @@ -457,11 +457,11 @@ class WTRLAB implements Plugin.PluginBase { 'X-Goog-API-Key': 'AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520', }, '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', }, ); - translated = await translated.json(); + const translated = await response.json(); const out = translated && translated[0] ? translated[0] : []; return out as string[]; } @@ -536,15 +536,8 @@ class WTRLAB implements Plugin.PluginBase { throw new Error(errorMsg); } let chapterContent = parsedJson.data.data.body; - let chapterGlossary = {} as JSON; - if ( - Object.prototype.hasOwnProperty.call( - parsedJson.data.data, - 'glossary_data', - ) - ) { - chapterGlossary = parsedJson.data.data.glossary_data; - } + const chapterGlossary: ChapterContent['glossary_data'] | undefined = + parsedJson?.data?.data?.glossary_data; let htmlString = ''; @@ -571,19 +564,14 @@ class WTRLAB implements Plugin.PluginBase { htmlString += `

${eLog}

`; } - let dictionary = []; - if (Object.prototype.hasOwnProperty.call(chapterGlossary, 'terms')) { - dictionary = Object.fromEntries( - chapterGlossary.terms.map((definition, index) => [ - `※${index}⛬`, - definition[0], - ]), - ); - } + const dictionary = chapterGlossary?.terms?.map(t => t[0]) || []; for (let text of chapterContent) { - if (Object.keys(dictionary).length > 0) { - text = text.replaceAll(/※[0-9]+⛬/g, m => dictionary[m]); + if (dictionary.length > 0) { + text = text.replaceAll( + /(?:wtr-lab\s+)?※([0-9]+)[⛬〓]/g, + (m: string, index: string) => dictionary[parseInt(index)] || m, + ); } htmlString += `

${text}

`; } @@ -1716,18 +1704,21 @@ type ApiChapter = { updated_at: string; }; -type GlossaryTerm = { - index: number; - english: string; - chinese: string; - symbol: string; -}; +// type GlossaryTerm = { +// index: number; +// english: string; +// chinese: string; +// symbol: string; +// }; type ChapterData = { data: ChapterContent; }; type ChapterContent = { title: string; body: string; + glossary_data?: { + terms: string[][]; + }; }; type SerieData = { diff --git a/plugins/french/noveldeglace.ts b/plugins/french/noveldeglace.ts index 05806e942..10410df1c 100644 --- a/plugins/french/noveldeglace.ts +++ b/plugins/french/noveldeglace.ts @@ -10,7 +10,7 @@ class NovelDeGlacePlugin implements Plugin.PluginBase { name = 'NovelDeGlace'; icon = 'src/fr/noveldeglace/icon.png'; site = 'https://noveldeglace.com/'; - version = '1.0.4'; + version = '1.0.5'; async getCheerio(url: string): Promise { const r = await fetchApi(url, { @@ -86,10 +86,7 @@ class NovelDeGlacePlugin implements Plugin.PluginBase { const novel: Plugin.SourceNovel = { path: novelPath, name: 'Untitled' }; - novel.name = - ( - $('div.entry-content > div > strong')[0].nextSibling as string | null - )?.nodeValue?.trim() || 'Untitled'; + novel.name = $('span.current').text().trim(); novel.cover = $('.su-row > div > div > img').attr('src') || defaultCover; diff --git a/plugins/french/xiaowaz.ts b/plugins/french/xiaowaz.ts index 9c3a0fb88..5d3a09e50 100644 --- a/plugins/french/xiaowaz.ts +++ b/plugins/french/xiaowaz.ts @@ -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 { Plugin } from '@/types/plugin'; import { defaultCover } from '@libs/defaultCover'; @@ -9,12 +10,12 @@ class XiaowazPlugin implements Plugin.PluginBase { name = 'Xiaowaz'; icon = 'src/fr/xiaowaz/icon.png'; site = 'https://xiaowaz.fr'; - version = '1.0.1'; + version = '1.0.2'; static novels: Plugin.NovelItem[] | undefined; async getCheerio(url: string): Promise { let retries = 5; // when fetching for images the sites sometimes terminates the connection - let returnError: any; + let returnError: unknown; while (retries > 0) { try { const r = await fetchApi(url); @@ -29,7 +30,9 @@ class XiaowazPlugin implements Plugin.PluginBase { 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 { diff --git a/plugins/index.ts b/plugins/index.ts index a85a18918..2307f3026 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -1,250 +1,249 @@ import { Plugin } from '@/types/plugin'; import p_0 from '@plugins/arabic/ArNovel[madara]'; import p_1 from '@plugins/arabic/Azora[madara]'; -import p_2 from '@plugins/arabic/dilartube'; -import p_3 from '@plugins/arabic/FreeKolNovel[lightnovelwp]'; -import p_4 from '@plugins/arabic/HizoManga[madara]'; -import p_5 from '@plugins/arabic/KolNovel[lightnovelwp]'; -import p_6 from '@plugins/arabic/Markazriwayat[madara]'; -import p_7 from '@plugins/arabic/Novel4Up[madara]'; -import p_8 from '@plugins/arabic/NovelsParadise[lightnovelwp]'; -import p_9 from '@plugins/arabic/Olaoecyou[madara]'; -import p_10 from '@plugins/arabic/rewayatclub'; -import p_11 from '@plugins/arabic/Riwyat[madara]'; +import p_2 from '@plugins/arabic/FreeKolNovel[lightnovelwp]'; +import p_3 from '@plugins/arabic/HizoManga[madara]'; +import p_4 from '@plugins/arabic/KolNovel[lightnovelwp]'; +import p_5 from '@plugins/arabic/Markazriwayat[madara]'; +import p_6 from '@plugins/arabic/Novel4Up[madara]'; +import p_7 from '@plugins/arabic/NovelsParadise[lightnovelwp]'; +import p_8 from '@plugins/arabic/Olaoecyou[madara]'; +import p_9 from '@plugins/arabic/Riwyat[madara]'; +import p_10 from '@plugins/arabic/dilartube'; +import p_11 from '@plugins/arabic/rewayatclub'; import p_12 from '@plugins/arabic/sunovels'; import p_13 from '@plugins/chinese/69shu'; -import p_14 from '@plugins/chinese/ixdzs8'; -import p_15 from '@plugins/chinese/linovel'; -import p_16 from '@plugins/chinese/linovelib'; -import p_17 from '@plugins/chinese/linovelib_tw'; -import p_18 from '@plugins/chinese/novel543'; -import p_19 from '@plugins/chinese/Quanben'; +import p_14 from '@plugins/chinese/Quanben'; +import p_15 from '@plugins/chinese/ixdzs8'; +import p_16 from '@plugins/chinese/linovel'; +import p_17 from '@plugins/chinese/linovelib'; +import p_18 from '@plugins/chinese/linovelib_tw'; +import p_19 from '@plugins/chinese/novel543'; import p_20 from '@plugins/english/AllNovelFull[readnovelfull]'; import p_21 from '@plugins/english/AllNovel[readnovelfull]'; -import p_22 from '@plugins/english/ao3'; -import p_23 from '@plugins/english/ArcaneTranslations[lightnovelwp]'; -import p_24 from '@plugins/english/BelleReservoir[madara]'; -import p_25 from '@plugins/english/BoxNovel[readnovelfull]'; -import p_26 from '@plugins/english/chrysanthemumgarden'; -import p_27 from '@plugins/english/CitrusAurora[madara]'; -import p_28 from '@plugins/english/CoralBoutique[madara]'; -import p_29 from '@plugins/english/CPUnovel[lightnovelwp]'; -import p_30 from '@plugins/english/crimsonscrolls'; -import p_31 from '@plugins/english/DaoistQuest[fictioneer]'; -import p_32 from '@plugins/english/DaoNovel[madara]'; -import p_33 from '@plugins/english/DaoTranslate[lightnovelwp]'; -import p_34 from '@plugins/english/DearestRosalie[fictioneer]'; -import p_35 from '@plugins/english/divinedaolibrary'; -import p_36 from '@plugins/english/Dragonholic[madara]'; -import p_37 from '@plugins/english/DragonTea[madara]'; -import p_38 from '@plugins/english/dreambigtl'; -import p_39 from '@plugins/english/DuskBlossoms[madara]'; -import p_40 from '@plugins/english/ElloTL[lightnovelwp]'; -import p_41 from '@plugins/english/Eternalune[madara]'; -import p_42 from '@plugins/english/EtudeTranslations[madara]'; -import p_43 from '@plugins/english/FanNovel[readwn]'; -import p_44 from '@plugins/english/FansMTL[readwn]'; -import p_45 from '@plugins/english/FansTranslations[madara]'; -import p_46 from '@plugins/english/faqwikius'; -import p_47 from '@plugins/english/fenrirrealm'; -import p_48 from '@plugins/english/fictionzone'; -import p_49 from '@plugins/english/FirstKissNovel[madara]'; -import p_50 from '@plugins/english/Foxaholic[madara]'; -import p_51 from '@plugins/english/foxteller'; -import p_52 from '@plugins/english/FreeWebNovel[readnovelfull]'; -import p_53 from '@plugins/english/GalaxyTranslations[madara]'; -import p_54 from '@plugins/english/genesis'; -import p_55 from '@plugins/english/Guavaread[madara]'; -import p_56 from '@plugins/english/HiraethTranslation[madara]'; -import p_57 from '@plugins/english/HotNovelPub[hotnovelpub]'; -import p_58 from '@plugins/english/indraTranslations'; -import p_59 from '@plugins/english/inkitt'; -import p_60 from '@plugins/english/inoveltranslation'; -import p_61 from '@plugins/english/Ippotranslations[lightnovelwp]'; -import p_62 from '@plugins/english/KDTNovels[lightnovelwp]'; -import p_63 from '@plugins/english/KeopiTranslations[lightnovelwp]'; -import p_64 from '@plugins/english/KnoxT[lightnovelwp]'; -import p_65 from '@plugins/english/LazyGirlTranslations[lightnovelwp]'; -import p_66 from '@plugins/english/leafstudio'; -import p_67 from '@plugins/english/LibRead[readnovelfull]'; -import p_68 from '@plugins/english/LightNovelCave[lightnovelworld]'; -import p_69 from '@plugins/english/LightNovelHeaven[madara]'; -import p_70 from '@plugins/english/LightNovelPlus[readnovelfull]'; -import p_71 from '@plugins/english/LightNovelPubVip[lightnovelworld]'; -import p_72 from '@plugins/english/lightnoveltranslation'; -import p_73 from '@plugins/english/LightNovelUpdates[madara]'; -import p_74 from '@plugins/english/LilyontheValley[fictioneer]'; -import p_75 from '@plugins/english/lnmtl'; -import p_76 from '@plugins/english/Ltnovel[readwn]'; -import p_77 from '@plugins/english/LulloBox[madara]'; -import p_78 from '@plugins/english/LunarLetters[madara]'; -import p_79 from '@plugins/english/Meownovel[madara]'; -import p_80 from '@plugins/english/MoonlightNovels[lightnovelwp]'; -import p_81 from '@plugins/english/MostNovel[madara]'; -import p_82 from '@plugins/english/MTLNovel[madara]'; -import p_83 from '@plugins/english/MTLNovel[mtlnovel]'; -import p_84 from '@plugins/english/mtlreader'; -import p_85 from '@plugins/english/mvlempyr'; -import p_86 from '@plugins/english/MysticalSeries[madara]'; -import p_87 from '@plugins/english/NeoSekaiTranslations[madara]'; -import p_88 from '@plugins/english/NitroManga[madara]'; -import p_89 from '@plugins/english/NobleMTL[lightnovelwp]'; -import p_90 from '@plugins/english/NoiceTranslations[madara]'; -import p_91 from '@plugins/english/NovelBin[readnovelfull]'; -import p_92 from '@plugins/english/novelbuddy'; -import p_93 from '@plugins/english/NovelCool[novelcool]'; -import p_94 from '@plugins/english/novelfire'; -import p_95 from '@plugins/english/NovelFull[readnovelfull]'; -import p_96 from '@plugins/english/novelhall'; -import p_97 from '@plugins/english/novelhi'; -import p_98 from '@plugins/english/novelight'; -import p_99 from '@plugins/english/NovelLib[fictioneer]'; -import p_100 from '@plugins/english/NovelMultiverse[madara]'; -import p_101 from '@plugins/english/NovelNinja[madara]'; -import p_102 from '@plugins/english/NovelOnline'; -import p_103 from '@plugins/english/novelrest'; -import p_104 from '@plugins/english/NovelsKnight[lightnovelwp]'; -import p_105 from '@plugins/english/NovelTranslate[madara]'; -import p_106 from '@plugins/english/novelupdates'; -import p_107 from '@plugins/english/PandaMachineTranslations[lightnovelwp]'; -import p_108 from '@plugins/english/PastelTales[madara]'; -import p_109 from '@plugins/english/pawread'; -import p_110 from '@plugins/english/PenguinSquad[fictioneer]'; -import p_111 from '@plugins/english/Prizma[fictioneer]'; -import p_112 from '@plugins/english/rainofsnow'; -import p_113 from '@plugins/english/Ranobes[ranobes]'; -import p_114 from '@plugins/english/Ranovel[madara]'; -import p_115 from '@plugins/english/ReadFanfic[madara]'; -import p_116 from '@plugins/english/readfrom'; -import p_117 from '@plugins/english/ReadNovelFull[readnovelfull]'; -import p_118 from '@plugins/english/relibrary'; -import p_119 from '@plugins/english/RequiemTranslations[lightnovelwp]'; -import p_120 from '@plugins/english/royalroad'; -import p_121 from '@plugins/english/SalmonLatte[madara]'; -import p_122 from '@plugins/english/scribblehub'; -import p_123 from '@plugins/english/SleepyTranslations[madara]'; -import p_124 from '@plugins/english/SonicMTL[madara]'; -import p_125 from '@plugins/english/SrankManga[madara]'; -import p_126 from '@plugins/english/StorySeedling'; -import p_127 from '@plugins/english/SweetEscape[madara]'; -import p_128 from '@plugins/english/SystemTranslation[lightnovelwp]'; -import p_129 from '@plugins/english/TranslatinOtaku[madara]'; -import p_130 from '@plugins/english/TranslationWeaver[lightnovelwp]'; -import p_131 from '@plugins/english/UniversalNovel[lightnovelwp]'; -import p_132 from '@plugins/english/VandyTranslate[lightnovelwp]'; -import p_133 from '@plugins/english/VioletLily[madara]'; -import p_134 from '@plugins/english/vynovel'; -import p_135 from '@plugins/english/wct'; -import p_136 from '@plugins/english/webnovel'; -import p_137 from '@plugins/english/WebNovelLover[madara]'; -import p_138 from '@plugins/english/WebNovelPub[lightnovelworld]'; -import p_139 from '@plugins/english/WebNovelTranslation[madara]'; -import p_140 from '@plugins/english/WhiteMoonlightNovels[lightnovelwp]'; -import p_141 from '@plugins/english/WooksTeahouse[madara]'; -import p_142 from '@plugins/english/WordExcerpt[madara]'; -import p_143 from '@plugins/english/wtrlab'; -import p_144 from '@plugins/english/Wuxiabox[readwn]'; -import p_145 from '@plugins/english/Wuxiafox[readwn]'; -import p_146 from '@plugins/english/WuxiaSpace[readwn]'; -import p_147 from '@plugins/english/WuxiaV[readwn]'; -import p_148 from '@plugins/english/wuxiaworld'; -import p_149 from '@plugins/english/WuxiaWorldSite[madara]'; -import p_150 from '@plugins/english/ZetroTranslation[madara]'; -import p_151 from '@plugins/french/chireads'; -import p_152 from '@plugins/french/harkeneliwood'; -import p_153 from '@plugins/french/kisswood'; -import p_154 from '@plugins/french/LighNovelFR[lightnovelwp]'; -import p_155 from '@plugins/french/MassNovel[madara]'; -import p_156 from '@plugins/french/MTLNovel(FR)[mtlnovel]'; +import p_22 from '@plugins/english/ArcaneTranslations[lightnovelwp]'; +import p_23 from '@plugins/english/BelleReservoir[madara]'; +import p_24 from '@plugins/english/BoxNovel[readnovelfull]'; +import p_25 from '@plugins/english/CPUnovel[lightnovelwp]'; +import p_26 from '@plugins/english/CitrusAurora[madara]'; +import p_27 from '@plugins/english/CoralBoutique[madara]'; +import p_28 from '@plugins/english/DaoNovel[madara]'; +import p_29 from '@plugins/english/DaoTranslate[lightnovelwp]'; +import p_30 from '@plugins/english/DaoistQuest[fictioneer]'; +import p_31 from '@plugins/english/DearestRosalie[fictioneer]'; +import p_32 from '@plugins/english/DragonTea[madara]'; +import p_33 from '@plugins/english/Dragonholic[madara]'; +import p_34 from '@plugins/english/DuskBlossoms[madara]'; +import p_35 from '@plugins/english/ElloTL[lightnovelwp]'; +import p_36 from '@plugins/english/Eternalune[madara]'; +import p_37 from '@plugins/english/EtudeTranslations[madara]'; +import p_38 from '@plugins/english/FanNovel[readwn]'; +import p_39 from '@plugins/english/FansMTL[readwn]'; +import p_40 from '@plugins/english/FansTranslations[madara]'; +import p_41 from '@plugins/english/FirstKissNovel[madara]'; +import p_42 from '@plugins/english/Foxaholic[madara]'; +import p_43 from '@plugins/english/FreeWebNovel[readnovelfull]'; +import p_44 from '@plugins/english/GalaxyTranslations[madara]'; +import p_45 from '@plugins/english/Guavaread[madara]'; +import p_46 from '@plugins/english/HiraethTranslation[madara]'; +import p_47 from '@plugins/english/HotNovelPub[hotnovelpub]'; +import p_48 from '@plugins/english/Ippotranslations[lightnovelwp]'; +import p_49 from '@plugins/english/KDTNovels[lightnovelwp]'; +import p_50 from '@plugins/english/KeopiTranslations[lightnovelwp]'; +import p_51 from '@plugins/english/KnoxT[lightnovelwp]'; +import p_52 from '@plugins/english/LazyGirlTranslations[lightnovelwp]'; +import p_53 from '@plugins/english/LibRead[readnovelfull]'; +import p_54 from '@plugins/english/LightNovelCave[lightnovelworld]'; +import p_55 from '@plugins/english/LightNovelHeaven[madara]'; +import p_56 from '@plugins/english/LightNovelPlus[readnovelfull]'; +import p_57 from '@plugins/english/LightNovelPubVip[lightnovelworld]'; +import p_58 from '@plugins/english/LightNovelUpdates[madara]'; +import p_59 from '@plugins/english/LilyontheValley[fictioneer]'; +import p_60 from '@plugins/english/Ltnovel[readwn]'; +import p_61 from '@plugins/english/LulloBox[madara]'; +import p_62 from '@plugins/english/LunarLetters[madara]'; +import p_63 from '@plugins/english/MTLNovel[madara]'; +import p_64 from '@plugins/english/MTLNovel[mtlnovel]'; +import p_65 from '@plugins/english/Meownovel[madara]'; +import p_66 from '@plugins/english/MoonlightNovels[lightnovelwp]'; +import p_67 from '@plugins/english/MostNovel[madara]'; +import p_68 from '@plugins/english/MysticalSeries[madara]'; +import p_69 from '@plugins/english/NeoSekaiTranslations[madara]'; +import p_70 from '@plugins/english/NitroManga[madara]'; +import p_71 from '@plugins/english/NobleMTL[lightnovelwp]'; +import p_72 from '@plugins/english/NoiceTranslations[madara]'; +import p_73 from '@plugins/english/NovelBin[readnovelfull]'; +import p_74 from '@plugins/english/NovelCool[novelcool]'; +import p_75 from '@plugins/english/NovelFull[readnovelfull]'; +import p_76 from '@plugins/english/NovelLib[fictioneer]'; +import p_77 from '@plugins/english/NovelMultiverse[madara]'; +import p_78 from '@plugins/english/NovelNinja[madara]'; +import p_79 from '@plugins/english/NovelOnline'; +import p_80 from '@plugins/english/NovelTranslate[madara]'; +import p_81 from '@plugins/english/NovelsKnight[lightnovelwp]'; +import p_82 from '@plugins/english/PandaMachineTranslations[lightnovelwp]'; +import p_83 from '@plugins/english/PastelTales[madara]'; +import p_84 from '@plugins/english/PenguinSquad[fictioneer]'; +import p_85 from '@plugins/english/Prizma[fictioneer]'; +import p_86 from '@plugins/english/Ranobes[ranobes]'; +import p_87 from '@plugins/english/Ranovel[madara]'; +import p_88 from '@plugins/english/ReadFanfic[madara]'; +import p_89 from '@plugins/english/ReadNovelFull[readnovelfull]'; +import p_90 from '@plugins/english/RequiemTranslations[lightnovelwp]'; +import p_91 from '@plugins/english/SalmonLatte[madara]'; +import p_92 from '@plugins/english/SleepyTranslations[madara]'; +import p_93 from '@plugins/english/SonicMTL[madara]'; +import p_94 from '@plugins/english/SrankManga[madara]'; +import p_95 from '@plugins/english/StorySeedling'; +import p_96 from '@plugins/english/SweetEscape[madara]'; +import p_97 from '@plugins/english/SystemTranslation[lightnovelwp]'; +import p_98 from '@plugins/english/TranslatinOtaku[madara]'; +import p_99 from '@plugins/english/TranslationWeaver[lightnovelwp]'; +import p_100 from '@plugins/english/UniversalNovel[lightnovelwp]'; +import p_101 from '@plugins/english/VandyTranslate[lightnovelwp]'; +import p_102 from '@plugins/english/VioletLily[madara]'; +import p_103 from '@plugins/english/WebNovelLover[madara]'; +import p_104 from '@plugins/english/WebNovelPub[lightnovelworld]'; +import p_105 from '@plugins/english/WebNovelTranslation[madara]'; +import p_106 from '@plugins/english/WhiteMoonlightNovels[lightnovelwp]'; +import p_107 from '@plugins/english/WooksTeahouse[madara]'; +import p_108 from '@plugins/english/WordExcerpt[madara]'; +import p_109 from '@plugins/english/WuxiaSpace[readwn]'; +import p_110 from '@plugins/english/WuxiaV[readwn]'; +import p_111 from '@plugins/english/WuxiaWorldSite[madara]'; +import p_112 from '@plugins/english/Wuxiabox[readwn]'; +import p_113 from '@plugins/english/Wuxiafox[readwn]'; +import p_114 from '@plugins/english/ZetroTranslation[madara]'; +import p_115 from '@plugins/english/ao3'; +import p_116 from '@plugins/english/chrysanthemumgarden'; +import p_117 from '@plugins/english/crimsonscrolls'; +import p_118 from '@plugins/english/divinedaolibrary'; +import p_119 from '@plugins/english/dreambigtl'; +import p_120 from '@plugins/english/faqwikius'; +import p_121 from '@plugins/english/fenrirrealm'; +import p_122 from '@plugins/english/fictionzone'; +import p_123 from '@plugins/english/foxteller'; +import p_124 from '@plugins/english/genesis'; +import p_125 from '@plugins/english/indraTranslations'; +import p_126 from '@plugins/english/inkitt'; +import p_127 from '@plugins/english/inoveltranslation'; +import p_128 from '@plugins/english/leafstudio'; +import p_129 from '@plugins/english/lightnoveltranslation'; +import p_130 from '@plugins/english/lnmtl'; +import p_131 from '@plugins/english/mvlempyr'; +import p_132 from '@plugins/english/novelbuddy'; +import p_133 from '@plugins/english/novelfire'; +import p_134 from '@plugins/english/novelhall'; +import p_135 from '@plugins/english/novelhi'; +import p_136 from '@plugins/english/novelight'; +import p_137 from '@plugins/english/novelrest'; +import p_138 from '@plugins/english/novelupdates'; +import p_139 from '@plugins/english/pawread'; +import p_140 from '@plugins/english/rainofsnow'; +import p_141 from '@plugins/english/readfrom'; +import p_142 from '@plugins/english/relibrary'; +import p_143 from '@plugins/english/royalroad'; +import p_144 from '@plugins/english/scribblehub'; +import p_145 from '@plugins/english/vynovel'; +import p_146 from '@plugins/english/wct'; +import p_147 from '@plugins/english/webnovel'; +import p_148 from '@plugins/english/wtrlab'; +import p_149 from '@plugins/english/wuxiaworld'; +import p_150 from '@plugins/french/LighNovelFR[lightnovelwp]'; +import p_151 from '@plugins/french/MTLNovel(FR)[mtlnovel]'; +import p_152 from '@plugins/french/MassNovel[madara]'; +import p_153 from '@plugins/french/WorldNovel[madara]'; +import p_154 from '@plugins/french/chireads'; +import p_155 from '@plugins/french/harkeneliwood'; +import p_156 from '@plugins/french/kisswood'; import p_157 from '@plugins/french/noveldeglace'; import p_158 from '@plugins/french/novhell'; import p_159 from '@plugins/french/warriorlegendtrad'; -import p_160 from '@plugins/french/WorldNovel[madara]'; -import p_161 from '@plugins/french/wuxialnscantrad'; -import p_162 from '@plugins/french/xiaowaz'; -import p_163 from '@plugins/indonesian/BacaLightNovel[lightnovelwp]'; -import p_164 from '@plugins/indonesian/indowebnovel'; -import p_165 from '@plugins/indonesian/MeioNovel[madara]'; -import p_166 from '@plugins/indonesian/MTLNovel(ID)[mtlnovel]'; -import p_167 from '@plugins/indonesian/NovelBookID[madara]'; -import p_168 from '@plugins/indonesian/sakuranovel'; -import p_169 from '@plugins/indonesian/SekteNovel[lightnovelwp]'; -import p_170 from '@plugins/indonesian/Vanovel[madara]'; -import p_171 from '@plugins/indonesian/WBNovel[madara]'; +import p_160 from '@plugins/french/wuxialnscantrad'; +import p_161 from '@plugins/french/xiaowaz'; +import p_162 from '@plugins/indonesian/BacaLightNovel[lightnovelwp]'; +import p_163 from '@plugins/indonesian/MTLNovel(ID)[mtlnovel]'; +import p_164 from '@plugins/indonesian/MeioNovel[madara]'; +import p_165 from '@plugins/indonesian/NovelBookID[madara]'; +import p_166 from '@plugins/indonesian/SekteNovel[lightnovelwp]'; +import p_167 from '@plugins/indonesian/Vanovel[madara]'; +import p_168 from '@plugins/indonesian/WBNovel[madara]'; +import p_169 from '@plugins/indonesian/indowebnovel'; +import p_170 from '@plugins/indonesian/sakuranovel'; +import p_171 from '@plugins/japanese/Syosetu'; import p_172 from '@plugins/japanese/kakuyomu'; -import p_173 from '@plugins/japanese/Syosetu'; -import p_174 from '@plugins/korean/Agitoon'; -import p_175 from '@plugins/korean/FortuneEternal[madara]'; -import p_176 from '@plugins/multi/komga'; -import p_177 from '@plugins/polish/novelki'; -import p_178 from '@plugins/portuguese/BetterNovels[lightnovelwp]'; -import p_179 from '@plugins/portuguese/blogdoamonnovels'; -import p_180 from '@plugins/portuguese/CentralNovel[lightnovelwp]'; -import p_181 from '@plugins/portuguese/illusia'; -import p_182 from '@plugins/portuguese/Kiniga[madara]'; -import p_183 from '@plugins/portuguese/LaNovels[hotnovelpub]'; -import p_184 from '@plugins/portuguese/LightNovelBrasil[lightnovelwp]'; -import p_185 from '@plugins/portuguese/MTLNovel(PT)[mtlnovel]'; -import p_186 from '@plugins/portuguese/novelmania'; -import p_187 from '@plugins/portuguese/tsundoku'; -import p_188 from '@plugins/russian/authortoday'; -import p_189 from '@plugins/russian/Bllate[rulate]'; -import p_190 from '@plugins/russian/Bookhamster[ifreedom]'; -import p_191 from '@plugins/russian/bookriver'; -import p_192 from '@plugins/russian/Erolate[rulate]'; -import p_193 from '@plugins/russian/EzNovels[hotnovelpub]'; -import p_194 from '@plugins/russian/ficbook'; -import p_195 from '@plugins/russian/jaomix'; -import p_196 from '@plugins/russian/MTLNovel(RU)[mtlnovel]'; -import p_197 from '@plugins/russian/neobook'; -import p_198 from '@plugins/russian/NovelCool(RU)[novelcool]'; -import p_199 from '@plugins/russian/novelTL'; -import p_200 from '@plugins/russian/ranobehub'; -import p_201 from '@plugins/russian/ranobelib'; -import p_202 from '@plugins/russian/ranoberf'; -import p_203 from '@plugins/russian/Ranobes(RU)[ranobes]'; +import p_173 from '@plugins/korean/Agitoon'; +import p_174 from '@plugins/korean/FortuneEternal[madara]'; +import p_175 from '@plugins/multi/komga'; +import p_176 from '@plugins/polish/novelki'; +import p_177 from '@plugins/portuguese/BetterNovels[lightnovelwp]'; +import p_178 from '@plugins/portuguese/CentralNovel[lightnovelwp]'; +import p_179 from '@plugins/portuguese/Kiniga[madara]'; +import p_180 from '@plugins/portuguese/LaNovels[hotnovelpub]'; +import p_181 from '@plugins/portuguese/LightNovelBrasil[lightnovelwp]'; +import p_182 from '@plugins/portuguese/MTLNovel(PT)[mtlnovel]'; +import p_183 from '@plugins/portuguese/blogdoamonnovels'; +import p_184 from '@plugins/portuguese/illusia'; +import p_185 from '@plugins/portuguese/novelmania'; +import p_186 from '@plugins/portuguese/tsundoku'; +import p_187 from '@plugins/russian/Bllate[rulate]'; +import p_188 from '@plugins/russian/Bookhamster[ifreedom]'; +import p_189 from '@plugins/russian/Erolate[rulate]'; +import p_190 from '@plugins/russian/EzNovels[hotnovelpub]'; +import p_191 from '@plugins/russian/MTLNovel(RU)[mtlnovel]'; +import p_192 from '@plugins/russian/NovelCool(RU)[novelcool]'; +import p_193 from '@plugins/russian/Ranobes(RU)[ranobes]'; +import p_194 from '@plugins/russian/Rulate[rulate]'; +import p_195 from '@plugins/russian/authortoday'; +import p_196 from '@plugins/russian/bookriver'; +import p_197 from '@plugins/russian/ficbook'; +import p_198 from '@plugins/russian/jaomix'; +import p_199 from '@plugins/russian/neobook'; +import p_200 from '@plugins/russian/novelTL'; +import p_201 from '@plugins/russian/ranobehub'; +import p_202 from '@plugins/russian/ranobelib'; +import p_203 from '@plugins/russian/ranoberf'; import p_204 from '@plugins/russian/renovels'; -import p_205 from '@plugins/russian/Rulate[rulate]'; -import p_206 from '@plugins/russian/topliba'; -import p_207 from '@plugins/russian/zelluloza'; -import p_208 from '@plugins/russian/СвободныйМирРанобэ[ifreedom]'; -import p_209 from '@plugins/spanish/AllNovelRead[lightnovelwp]'; -import p_210 from '@plugins/spanish/AnimesHoy12[madara]'; -import p_211 from '@plugins/spanish/hasutl'; -import p_212 from '@plugins/spanish/LightNovelDaily[hotnovelpub]'; -import p_213 from '@plugins/spanish/MTLNovel(ES)[mtlnovel]'; -import p_214 from '@plugins/spanish/NOVA'; -import p_215 from '@plugins/spanish/novelasligera'; -import p_216 from '@plugins/spanish/novelawuxia'; -import p_217 from '@plugins/spanish/novelyra'; -import p_218 from '@plugins/spanish/oasistranslations'; -import p_219 from '@plugins/spanish/PanchoTranslations[madara]'; -import p_220 from '@plugins/spanish/skynovels'; -import p_221 from '@plugins/spanish/TC&Sega[lightnovelwp]'; -import p_222 from '@plugins/spanish/TraduccionesAmistosas[madara]'; -import p_223 from '@plugins/spanish/tunovelaligera'; -import p_224 from '@plugins/spanish/yukitls'; -import p_225 from '@plugins/thai/NovelLucky[madara]'; -import p_226 from '@plugins/thai/NovelPDF[madara]'; -import p_227 from '@plugins/turkish/ArazNovel[madara]'; -import p_228 from '@plugins/turkish/EKTAPLAR[madara]'; -import p_229 from '@plugins/turkish/epiknovel'; -import p_230 from '@plugins/turkish/kakikata[madara]'; -import p_231 from '@plugins/turkish/KodeksLibrary[lightnovelwp]'; -import p_232 from '@plugins/turkish/MangaTR'; -import p_233 from '@plugins/turkish/NABSCANS[madara]'; -import p_234 from '@plugins/turkish/Namevt[lightnovelwp]'; -import p_235 from '@plugins/turkish/Noveloku[madara]'; -import p_236 from '@plugins/turkish/NovelTR[lightnovelwp]'; -import p_237 from '@plugins/turkish/RagnarScans[madara]'; -import p_238 from '@plugins/turkish/ThNovels[hotnovelpub]'; -import p_239 from '@plugins/turkish/TurkceLightNovels[madara]'; -import p_240 from '@plugins/turkish/WebNovelOku[madara]'; -import p_241 from '@plugins/ukrainian/bakainua'; -import p_242 from '@plugins/ukrainian/smakolykytl'; +import p_205 from '@plugins/russian/topliba'; +import p_206 from '@plugins/russian/zelluloza'; +import p_207 from '@plugins/russian/СвободныйМирРанобэ[ifreedom]'; +import p_208 from '@plugins/spanish/AllNovelRead[lightnovelwp]'; +import p_209 from '@plugins/spanish/AnimesHoy12[madara]'; +import p_210 from '@plugins/spanish/LightNovelDaily[hotnovelpub]'; +import p_211 from '@plugins/spanish/MTLNovel(ES)[mtlnovel]'; +import p_212 from '@plugins/spanish/NOVA'; +import p_213 from '@plugins/spanish/PanchoTranslations[madara]'; +import p_214 from '@plugins/spanish/TC&Sega[lightnovelwp]'; +import p_215 from '@plugins/spanish/TraduccionesAmistosas[madara]'; +import p_216 from '@plugins/spanish/hasutl'; +import p_217 from '@plugins/spanish/novelasligera'; +import p_218 from '@plugins/spanish/novelawuxia'; +import p_219 from '@plugins/spanish/novelyra'; +import p_220 from '@plugins/spanish/oasistranslations'; +import p_221 from '@plugins/spanish/skynovels'; +import p_222 from '@plugins/spanish/tunovelaligera'; +import p_223 from '@plugins/spanish/yukitls'; +import p_224 from '@plugins/thai/NovelLucky[madara]'; +import p_225 from '@plugins/thai/NovelPDF[madara]'; +import p_226 from '@plugins/turkish/ArazNovel[madara]'; +import p_227 from '@plugins/turkish/EKTAPLAR[madara]'; +import p_228 from '@plugins/turkish/KodeksLibrary[lightnovelwp]'; +import p_229 from '@plugins/turkish/MangaTR'; +import p_230 from '@plugins/turkish/NABSCANS[madara]'; +import p_231 from '@plugins/turkish/Namevt[lightnovelwp]'; +import p_232 from '@plugins/turkish/NovelTR[lightnovelwp]'; +import p_233 from '@plugins/turkish/Noveloku[madara]'; +import p_234 from '@plugins/turkish/RagnarScans[madara]'; +import p_235 from '@plugins/turkish/ThNovels[hotnovelpub]'; +import p_236 from '@plugins/turkish/TurkceLightNovels[madara]'; +import p_237 from '@plugins/turkish/WebNovelOku[madara]'; +import p_238 from '@plugins/turkish/epiknovel'; +import p_239 from '@plugins/turkish/kakikata[madara]'; +import p_240 from '@plugins/ukrainian/bakainua'; +import p_241 from '@plugins/ukrainian/smakolykytl'; +import p_242 from '@plugins/vietnamese/LNHako'; import p_243 from '@plugins/vietnamese/lightnovelvn'; -import p_244 from '@plugins/vietnamese/LNHako'; -import p_245 from '@plugins/vietnamese/nettruyen'; +import p_244 from '@plugins/vietnamese/nettruyen'; const PLUGINS: Plugin.PluginBase[] = [ p_0, @@ -492,6 +491,5 @@ const PLUGINS: Plugin.PluginBase[] = [ p_242, p_243, p_244, - p_245, ]; export default PLUGINS; diff --git a/plugins/japanese/Syosetu.ts b/plugins/japanese/Syosetu.ts index d45d93dee..61691a115 100644 --- a/plugins/japanese/Syosetu.ts +++ b/plugins/japanese/Syosetu.ts @@ -1,4 +1,4 @@ -import { load as loadCheerio } from 'cheerio'; +import { CheerioAPI, load as loadCheerio } from 'cheerio'; import { fetchApi } from '@libs/fetch'; import { Plugin } from '@/types/plugin'; import { defaultCover } from '@libs/defaultCover'; @@ -14,7 +14,7 @@ class Syosetu implements Plugin.PluginBase { icon = 'src/jp/syosetu/icon.png'; site = 'https://yomou.syosetu.com/'; novelPrefix = 'https://ncode.syosetu.com'; - version = '1.1.2'; + version = '1.1.3'; headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', @@ -71,7 +71,7 @@ class Syosetu implements Plugin.PluginBase { return novels; } private async parseChaptersFromPage( - loadedCheerio: cheerio.CheerioAPI, + loadedCheerio: CheerioAPI, ): Promise { const chapters: Plugin.ChapterItem[] = []; diff --git a/plugins/multisrc/fictioneer/custom/lillyonthevalley/chapterTransform.js b/plugins/multisrc/fictioneer/custom/lillyonthevalley/chapterTransform.js index 77d900f49..ef3cddbe6 100644 --- a/plugins/multisrc/fictioneer/custom/lillyonthevalley/chapterTransform.js +++ b/plugins/multisrc/fictioneer/custom/lillyonthevalley/chapterTransform.js @@ -6,7 +6,7 @@ if (scriptContent) { const gibMatch = scriptContent.match(/var gib = (\[.*?\])/); if (gibMatch) { - const gibArray = eval(gibMatch[1]) as string[]; + const gibArray = eval(gibMatch[1]); gibArray.forEach(cssClass => { loadedCheerio(`.${cssClass}`).remove(); }); @@ -33,4 +33,4 @@ .replace(/\u2060/g, '') .replace(/­/g, '') // ­ .replace(/[\u202F\u2007\u200B]/g, '') || '' - ); \ No newline at end of file + ); diff --git a/plugins/multisrc/fictioneer/template.ts b/plugins/multisrc/fictioneer/template.ts index 2ac2688a7..268fdf7db 100644 --- a/plugins/multisrc/fictioneer/template.ts +++ b/plugins/multisrc/fictioneer/template.ts @@ -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 { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; @@ -17,7 +17,7 @@ export type FictioneerMetadata = { options: FictioneerOptions; }; -class FictioneerPlugin implements Plugin.PluginBase { +export class FictioneerPlugin implements Plugin.PluginBase { id: string; name: string; icon: string; @@ -32,16 +32,38 @@ class FictioneerPlugin implements Plugin.PluginBase { this.icon = `multisrc/fictioneer/${metadata.id.toLowerCase()}/icon.png`; this.site = metadata.sourceSite; const versionIncrements = metadata.options?.versionIncrements || 0; - this.version = `1.0.${0 + versionIncrements}`; + this.version = `1.1.${0 + versionIncrements}`; 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( pageNo: number, - { - showLatestNovels, - filters, - }: Plugin.PopularNovelsOptions, + // { + // showLatestNovels, + // filters, + // }: Plugin.PopularNovelsOptions, ): Promise { const req = await fetchApi( this.site + @@ -53,23 +75,10 @@ class FictioneerPlugin implements Plugin.PluginBase { const body = await req.text(); const loadedCheerio = loadCheerio(body); - return loadedCheerio( + return this.parseNovels( + loadedCheerio, '#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 { @@ -94,6 +103,8 @@ class FictioneerPlugin implements Plugin.PluginBase { .map((i, el) => loadedCheerio(el).text()) .toArray() .join(','); + + loadedCheerio('section.story__summary .related-stories-block').remove(); novel.summary = loadedCheerio('section.story__summary').text(); novel.chapters = loadedCheerio('li.chapter-group__list-item._publish') @@ -108,15 +119,12 @@ class FictioneerPlugin implements Plugin.PluginBase { ) .map((i, el) => { const chapterName = loadedCheerio(el).find('a').text(); - const chapterUrl = loadedCheerio(el) - .find('a') - .attr('href') - ?.replace(this.site + '/', '') - .replace(/\/$/, ''); + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + if (!chapterUrl) return; return { name: chapterName, - path: chapterUrl!, + path: new URL(chapterUrl, this.site).pathname.substring(1), }; }) .toArray(); @@ -152,23 +160,12 @@ class FictioneerPlugin implements Plugin.PluginBase { const body = await req.text(); const loadedCheerio = loadCheerio(body); - return loadedCheerio('#search-result-list > 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(); + return this.parseNovels( + loadedCheerio, + '#search-result-list > li > div > div', + ); } - resolveUrl = (path: string, isNovel?: boolean) => - this.site + '/' + path + '/'; + // resolveUrl = (path: string, isNovel?: boolean) => + // this.site + '/' + path + '/'; } diff --git a/plugins/multisrc/hotnovelpub/filter_refresh.ts b/plugins/multisrc/hotnovelpub/filter_refresh.ts index 3d6f01d37..fdcde0e2e 100644 --- a/plugins/multisrc/hotnovelpub/filter_refresh.ts +++ b/plugins/multisrc/hotnovelpub/filter_refresh.ts @@ -3,9 +3,10 @@ import * as cheerio from 'cheerio'; import * as path from 'path'; import list from './sources.json' with { type: 'json' }; import { HotNovelPubMetadata } from './template'; +import { Filters } from '@libs/filterInputs'; async function getFilters(sources: HotNovelPubMetadata) { - const filters: any = { + const filters: Filters = { sort: { type: 'Picker', label: 'Order', @@ -70,7 +71,7 @@ async function start() { const result = []; for (const sources of list) { 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; result.push(sources); } diff --git a/plugins/multisrc/hotnovelpub/generator.js b/plugins/multisrc/hotnovelpub/generator.js index c8c89ae3f..7daca0601 100644 --- a/plugins/multisrc/hotnovelpub/generator.js +++ b/plugins/multisrc/hotnovelpub/generator.js @@ -48,10 +48,7 @@ const generator = function generator(metadata) { const pluginScript = ` ${HotNovelPubTemplate} -const plugin = new HotNovelPubPlugin(${JSON.stringify(metadata).replace( - /"type":"([^"]+)"/g, - '"type":FilterTypes.$1', - )}); +const plugin = new HotNovelPubPlugin(${JSON.stringify(metadata)}); export default plugin; `.trim(); diff --git a/plugins/multisrc/hotnovelpub/template.ts b/plugins/multisrc/hotnovelpub/template.ts index 5fa1246c6..158952883 100644 --- a/plugins/multisrc/hotnovelpub/template.ts +++ b/plugins/multisrc/hotnovelpub/template.ts @@ -1,5 +1,5 @@ import { fetchApi } from '@libs/fetch'; -import { Filters, FilterTypes } from '@libs/filterInputs'; +import { Filters } from '@libs/filterInputs'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; @@ -15,7 +15,7 @@ type HotNovelPubOptions = { lang?: string; }; -class HotNovelPubPlugin implements Plugin.PluginBase { +export class HotNovelPubPlugin implements Plugin.PluginBase { id: string; name: string; icon: string; @@ -38,7 +38,10 @@ class HotNovelPubPlugin implements Plugin.PluginBase { async popularNovels( pageNo: number, - { filters, showLatestNovels }: Plugin.PopularNovelsOptions, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions, ): Promise { let url = this.apiSite + '/books/'; url += showLatestNovels ? 'new' : filters?.sort?.value || 'hot'; @@ -52,7 +55,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase { headers: { lang: this.lang, }, - }).then(res => res.json()); + }).then((res: Response) => res.json()); const novels: Plugin.NovelItem[] = []; if (result.status && result.data.books.data?.length) { @@ -75,7 +78,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase { lang: this.lang, }, }, - ).then(res => res.json()); + ).then((res: Response) => res.json()); const novel: Plugin.SourceNovel = { name: json.data.book.name, @@ -110,8 +113,8 @@ class HotNovelPubPlugin implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.resolveUrl(chapterPath)).then(res => - res.text(), + const body = await fetchApi(this.resolveUrl(chapterPath)).then( + (res: Response) => res.text(), ); let chapterText = @@ -144,7 +147,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase { }, method: 'POST', body: JSON.stringify({ key_search: searchTerm }), - }).then(res => res.json()); + }).then((res: Response) => res.json()); const novels: Plugin.NovelItem[] = []; if (result.status && result.data.books?.length) { diff --git a/plugins/multisrc/ifreedom/generator.js b/plugins/multisrc/ifreedom/generator.js index a09249eba..032768446 100644 --- a/plugins/multisrc/ifreedom/generator.js +++ b/plugins/multisrc/ifreedom/generator.js @@ -21,10 +21,7 @@ const generator = function generator(metadata) { const pluginScript = ` ${IfreedomTemplate} -const plugin = new IfreedomPlugin(${JSON.stringify(metadata).replace( - /"type":"([^"]+)"/g, - '"type":FilterTypes.$1', - )}); +const plugin = new IfreedomPlugin(${JSON.stringify(metadata)}); export default plugin; `.trim(); diff --git a/plugins/multisrc/ifreedom/template.ts b/plugins/multisrc/ifreedom/template.ts index 2a41c61d8..372224807 100644 --- a/plugins/multisrc/ifreedom/template.ts +++ b/plugins/multisrc/ifreedom/template.ts @@ -1,5 +1,5 @@ import { fetchApi } from '@libs/fetch'; -import { Filters, FilterTypes } from '@libs/filterInputs'; +import { Filters, FilterToValues } from '@libs/filterInputs'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; import { Parser } from 'htmlparser2'; @@ -12,7 +12,7 @@ export type IfreedomMetadata = { filters?: Filters; }; -class IfreedomPlugin implements Plugin.PluginBase { +export class IfreedomPlugin implements Plugin.PluginBase { id: string; name: string; icon: string; @@ -25,14 +25,14 @@ class IfreedomPlugin implements Plugin.PluginBase { this.name = metadata.sourceName; this.icon = `multisrc/ifreedom/${metadata.id.toLowerCase()}/icon.png`; this.site = metadata.sourceSite; - this.version = '1.1.0'; + this.version = '1.1.1'; this.filters = metadata.filters; } parseNovels(url: string) { return fetchApi(url) - .then(res => res.text()) - .then(html => { + .then((res: Response) => res.text()) + .then((html: string) => { const novels: Plugin.NovelItem[] = []; let tempNovel = {} as Plugin.NovelItem; let isInsideNovelCard = false; @@ -83,7 +83,8 @@ class IfreedomPlugin implements Plugin.PluginBase { ): Promise { 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[string]; if (Array.isArray(value) && value.length) { url += `&${type}[]=${value.join(`&${type}[]=`)}`; } @@ -94,7 +95,9 @@ class IfreedomPlugin implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - 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 = { path: novelPath, name: '', @@ -288,7 +291,7 @@ class IfreedomPlugin implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then(res => + const body = await fetchApi(this.site + chapterPath).then((res: Response) => res.text(), ); @@ -328,7 +331,7 @@ class IfreedomPlugin implements Plugin.PluginBase { async searchNovels( searchTerm: string, - page: number = 1, + page = 1, ): Promise { const url = `${this.site}/vse-knigi/?searchname=${encodeURIComponent(searchTerm)}&bpage=${page}`; return this.parseNovels(url); @@ -357,7 +360,7 @@ function parseStatus(statusString: string): string { return NovelStatus.Unknown; } -function parseDate(dateString: string = ''): string | null { +function parseDate(dateString = ''): string | null { const months: Record = { января: 1, февраля: 2, diff --git a/plugins/multisrc/lightnovelworld/template.ts b/plugins/multisrc/lightnovelworld/template.ts index 45c743233..162de9366 100644 --- a/plugins/multisrc/lightnovelworld/template.ts +++ b/plugins/multisrc/lightnovelworld/template.ts @@ -14,10 +14,10 @@ export type LightNovelWorldMetadata = { sourceSite: string; sourceName: string; options?: LightNovelWorldOptions; - filters?: any; + filters?: Filters; }; -class LightNovelWorld implements Plugin.PagePlugin { +export class LightNovelWorld implements Plugin.PagePlugin { id: string; name: string; site: string; @@ -52,7 +52,7 @@ class LightNovelWorld implements Plugin.PagePlugin { link += `${filters.status.value}/`; link += page; - const body = await fetchApi(link).then(r => r.text()); + const body = await fetchApi(link).then((r: Response) => r.text()); const loadedCheerio = parseHTML(body); @@ -84,7 +84,9 @@ class LightNovelWorld implements Plugin.PagePlugin { async parseNovel( novelPath: string, ): Promise { - 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 totalChapters = parseInt( @@ -113,7 +115,7 @@ class LightNovelWorld implements Plugin.PagePlugin { async parsePage(novelPath: string, page: string): Promise { const url = this.site + novelPath + '/chapters/page-' + page; - const body = await fetchApi(url).then(res => res.text()); + const body = await fetchApi(url).then((res: Response) => res.text()); const loadedCheerio = parseHTML(body); const chapter: Plugin.ChapterItem[] = []; loadedCheerio('.chapter-list li').each(function () { @@ -144,7 +146,9 @@ class LightNovelWorld implements Plugin.PagePlugin { } async parseChapter(chapterPath: string): Promise { - 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); @@ -156,7 +160,7 @@ class LightNovelWorld implements Plugin.PagePlugin { async searchNovels(searchTerm: string): Promise { const url = `${this.site}lnsearchlive`; const link = `${this.site}search`; - const response = await fetchApi(link).then(r => r.text()); + const response = await fetchApi(link).then((r: Response) => r.text()); const token = parseHTML(response); const verifytoken = token('#novelSearchForm > input').attr('value'); @@ -167,7 +171,7 @@ class LightNovelWorld implements Plugin.PagePlugin { method: 'POST', headers: { LNRequestVerifyToken: verifytoken! }, body: formData, - }).then(r => r.json()); + }).then((r: Response) => r.json()); const novels: Plugin.NovelItem[] = []; diff --git a/plugins/multisrc/lightnovelwp/template.ts b/plugins/multisrc/lightnovelwp/template.ts index a51f23915..8234b5cf4 100644 --- a/plugins/multisrc/lightnovelwp/template.ts +++ b/plugins/multisrc/lightnovelwp/template.ts @@ -21,10 +21,10 @@ export type LightNovelWPMetadata = { sourceSite: string; sourceName: string; options?: LightNovelWPOptions; - filters?: any; + filters?: Filters; }; -class LightNovelWPPlugin implements Plugin.PluginBase { +export class LightNovelWPPlugin implements Plugin.PluginBase { id: string; name: string; icon: string; @@ -34,7 +34,7 @@ class LightNovelWPPlugin implements Plugin.PluginBase { filters?: Filters; hideLocked = storage.get('hideLocked'); - pluginSettings?: Record; + pluginSettings?: Filters; constructor(metadata: LightNovelWPMetadata) { this.id = metadata.id; @@ -42,7 +42,7 @@ class LightNovelWPPlugin implements Plugin.PluginBase { this.icon = `multisrc/lightnovelwp/${metadata.id.toLowerCase()}/icon.png`; this.site = metadata.sourceSite; 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.filters = metadata.filters satisfies Filters; @@ -99,12 +99,12 @@ class LightNovelWPPlugin implements Plugin.PluginBase { const articles = html.match(//g) || []; articles.forEach(article => { const [, novelUrl, novelName] = - article.match(/]*?src="([^\"]*)"[^>]*?(?: data-src="([^\"]*)")?[^>]*>/, + /]*?src="([^"]*)"[^>]*?(?: data-src="([^"]*)")?[^>]*>/, ) || []; let novelPath; diff --git a/plugins/multisrc/madara/sources.json b/plugins/multisrc/madara/sources.json index ec00c4664..eeb1bf52a 100644 --- a/plugins/multisrc/madara/sources.json +++ b/plugins/multisrc/madara/sources.json @@ -39,7 +39,9 @@ "sourceName": "DaoNovel", "options": { "useNewChapterEndpoint": true, - "versionIncrements": 1 + "versionIncrements": 2, + "down": true, + "downSince": 1777153113000 } }, { @@ -150,7 +152,10 @@ "sourceSite": "https://novel4up.com/", "sourceName": "Novel4Up", "options": { - "lang": "Arabic" + "lang": "Arabic", + "versionIncrements": 1, + "down": true, + "downSince": 1777153113000 } }, { @@ -420,7 +425,10 @@ "sourceName": "Belle Reservoir", "options": { "useNewChapterEndpoint": true, - "lang": "English" + "versionIncrements": 1, + "lang": "English", + "down": true, + "downSince": 1777153113000 } }, { @@ -439,7 +447,10 @@ "sourceName": "Coral Boutique", "options": { "useNewChapterEndpoint": true, - "lang": "English" + "lang": "English", + "versionIncrements": 1, + "down": true, + "downSince": 1777153113000 } }, { diff --git a/plugins/multisrc/madara/template.ts b/plugins/multisrc/madara/template.ts index 13f6172f7..b22f927b7 100644 --- a/plugins/multisrc/madara/template.ts +++ b/plugins/multisrc/madara/template.ts @@ -1,7 +1,8 @@ import { fetchApi } from '@libs/fetch'; import { Filters } from '@libs/filterInputs'; 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 { NovelStatus } from '@libs/novelStatus'; import dayjs from 'dayjs'; @@ -24,10 +25,10 @@ export type MadaraMetadata = { sourceSite: string; sourceName: string; options?: MadaraOptions; - filters?: any; + filters?: Filters; }; -class MadaraPlugin implements Plugin.PluginBase { +export class MadaraPlugin implements Plugin.PluginBase { id: string; name: string; icon: string; @@ -37,7 +38,7 @@ class MadaraPlugin implements Plugin.PluginBase { filters?: Filters | undefined; hideLocked = storage.get('hideLocked'); - pluginSettings?: Record; + pluginSettings?: Filters; constructor(metadata: MadaraMetadata) { this.id = metadata.id; @@ -45,7 +46,7 @@ class MadaraPlugin implements Plugin.PluginBase { this.icon = `multisrc/madara/${metadata.id.toLowerCase()}/icon.png`; this.site = metadata.sourceSite; const versionIncrements = metadata.options?.versionIncrements || 0; - this.version = `1.0.${8 + versionIncrements}`; + this.version = `1.0.${9 + versionIncrements}`; this.options = metadata.options; this.filters = metadata.filters; @@ -302,7 +303,7 @@ class MadaraPlugin implements Plugin.PluginBase { html = await fetchApi(this.site + novelPath + 'ajax/chapters/', { method: 'POST', referrer: this.site + novelPath, - }).then(res => res.text()); + }).then((res: Response) => res.text()); } else { const novelId = 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', { method: 'POST', body: formData, - }).then(res => res.text()); + }).then((res: Response) => res.text()); } if (html !== '0') { diff --git a/plugins/multisrc/mtlnovel/generator.js b/plugins/multisrc/mtlnovel/generator.js index e911eb7e3..b058f439b 100644 --- a/plugins/multisrc/mtlnovel/generator.js +++ b/plugins/multisrc/mtlnovel/generator.js @@ -13,7 +13,9 @@ export const generateAll = function () { readFileSync(`${__dirname}/filters/mtlnovel.json`, 'utf-8'), ); source.filters = filters; - } catch (e) {} + } catch (e) { + // for the linter + } console.log( `[mtlnovel] Generating: ${source.id}`.padEnd(35), source.filters ? '🔎with filters🔍' : '🚫 no filters 🚫', diff --git a/plugins/multisrc/mtlnovel/template.ts b/plugins/multisrc/mtlnovel/template.ts index e0a4437d0..3d28ae4a8 100644 --- a/plugins/multisrc/mtlnovel/template.ts +++ b/plugins/multisrc/mtlnovel/template.ts @@ -14,10 +14,10 @@ export type MTLNovelMetadata = { sourceSite: string; sourceName: string; options?: MTLNovelOptions; - filters?: any; + filters?: Filters; }; -class MTLNovelPlugin implements Plugin.PluginBase { +export class MTLNovelPlugin implements Plugin.PluginBase { id: string; name: string; icon: string; diff --git a/plugins/multisrc/novelcool/template.ts b/plugins/multisrc/novelcool/template.ts index 80df52114..cd8547390 100644 --- a/plugins/multisrc/novelcool/template.ts +++ b/plugins/multisrc/novelcool/template.ts @@ -17,7 +17,7 @@ export type NovelCoolMetadata = { options: NovelCoolOptions; }; -class NovelCoolPlugin implements Plugin.PluginBase { +export class NovelCoolPlugin implements Plugin.PluginBase { id: string; name: string; icon: string; @@ -78,7 +78,7 @@ class NovelCoolPlugin implements Plugin.PluginBase { page_size: '20', }).toString(), }, - ).then(res => res.json()); + ).then((res: Response) => res.json()); const novels: Plugin.NovelItem[] = []; list.forEach(novel => @@ -110,7 +110,7 @@ class NovelCoolPlugin implements Plugin.PluginBase { lang: this.options.langCode, }).toString(), }, - ).then(res => res.json()); + ).then((res: Response) => res.json()); const novel: Plugin.SourceNovel = { path: novelPath, @@ -142,7 +142,7 @@ class NovelCoolPlugin implements Plugin.PluginBase { lang: this.options.langCode, }).toString(), }, - ).then(res => res.json()); + ).then((res: Response) => res.json()); list.forEach(chapter => { if (!chapter.is_locked) { @@ -179,7 +179,7 @@ class NovelCoolPlugin implements Plugin.PluginBase { lang: this.options.langCode, }).toString(), }, - ).then(res => res.json()); + ).then((res: Response) => res.json()); return info.content; } @@ -207,7 +207,7 @@ class NovelCoolPlugin implements Plugin.PluginBase { page_size: '20', }).toString(), }, - ).then(res => res.json()); + ).then((res: Response) => res.json()); const novels: Plugin.NovelItem[] = []; list.forEach(novel => @@ -266,7 +266,7 @@ type Novel = { last_url?: string; category_str: string; category_list: string[]; - star_list: any[]; + // star_list: any[]; int_mark: string; time: string; show_ads: string; diff --git a/plugins/multisrc/ranobes/template.ts b/plugins/multisrc/ranobes/template.ts index 788ee4932..13d2613f8 100644 --- a/plugins/multisrc/ranobes/template.ts +++ b/plugins/multisrc/ranobes/template.ts @@ -1,5 +1,5 @@ import { Parser } from 'htmlparser2'; -import { fetchApi } from '@libs/fetch'; +import { fetchApi, FetchInit } from '@libs/fetch'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; @@ -15,7 +15,7 @@ export type RanobesMetadata = { options?: RanobesOptions; }; -class RanobesPlugin implements Plugin.PluginBase { +export class RanobesPlugin implements Plugin.PluginBase { id: string; name: string; icon: string; @@ -32,7 +32,7 @@ class RanobesPlugin implements Plugin.PluginBase { this.options = metadata.options as RanobesOptions; } - async safeFecth(url: string, init: any = {}): Promise { + async safeFecth(url: string, init?: FetchInit): Promise { const r = await fetchApi(url, init); if (!r.ok) throw new Error( diff --git a/plugins/multisrc/readnovelfull/template.ts b/plugins/multisrc/readnovelfull/template.ts index 6f58359ad..5afd9eb44 100644 --- a/plugins/multisrc/readnovelfull/template.ts +++ b/plugins/multisrc/readnovelfull/template.ts @@ -1,5 +1,5 @@ import { Parser } from 'htmlparser2'; -import { fetchApi } from '@libs/fetch'; +import { fetchApi, FetchInit } from '@libs/fetch'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; import { Filters } from '@libs/filterInputs'; @@ -35,7 +35,7 @@ export type ReadNovelFullMetadata = { filters?: Filters; }; -class ReadNovelFullPlugin implements Plugin.PluginBase { +export class ReadNovelFullPlugin implements Plugin.PluginBase { id: string; name: string; icon: string; @@ -50,7 +50,7 @@ class ReadNovelFullPlugin implements Plugin.PluginBase { this.icon = `multisrc/readnovelfull/${metadata.id.toLowerCase()}/icon.png`; this.site = metadata.sourceSite; const versionIncrements = metadata.options?.versionIncrements || 0; - this.version = `2.2.${0 + versionIncrements}`; + this.version = `2.2.${1 + versionIncrements}`; this.options = metadata.options; this.filters = metadata.filters; } @@ -145,7 +145,10 @@ class ReadNovelFullPlugin implements Plugin.PluginBase { async popularNovels( pageNo: number, - { filters, showLatestNovels }: Plugin.PopularNovelsOptions, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions, ): Promise { const { 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 => - text.replace(/[&<>"' ]/g, char => escapeMap[char]); + const escapeHtml = (text: string) => + text.replace(/[&<>"'\xA0\u200C]/g, char => escapeMap[char]); const parser = new Parser({ onopentag(name, attribs) { @@ -602,7 +606,7 @@ class ReadNovelFullPlugin implements Plugin.PluginBase { } break; case ParsingState.Chapter: - if (name === 'sub') { + if (name === 'sub' || name === 'iframe') { pushState(ParsingState.Hidden); } else if (name === 'div') { depth++; @@ -655,7 +659,8 @@ class ReadNovelFullPlugin implements Plugin.PluginBase { ontext(text) { 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(); if (state === ParsingState.Hidden) { - if (name === 'sub') { + if (name === 'sub' || name === 'iframe') { popState(); } else if (name === 'div') { depthHide--; @@ -728,7 +733,7 @@ class ReadNovelFullPlugin implements Plugin.PluginBase { const url = `${this.site}${searchPage}${!postSearch ? `?${params.toString()}` : ''}`; - const fetchOptions: RequestInit | undefined = postSearch + const fetchOptions: FetchInit | undefined = postSearch ? { method: 'POST', body: params.toString(), diff --git a/plugins/multisrc/readwn/get_filters.ts b/plugins/multisrc/readwn/get_filters.ts index c1481a4f1..7c42c6a2b 100644 --- a/plugins/multisrc/readwn/get_filters.ts +++ b/plugins/multisrc/readwn/get_filters.ts @@ -2,7 +2,7 @@ require('module-alias/register'); import * as fs from 'fs'; import * as cheerio from 'cheerio'; import * as path from 'path'; -import { FilterTypes, FilterOption } from '@libs/filterInputs'; +import { Filters, FilterTypes, FilterOption } from '@libs/filterInputs'; const type: string[] = ['genres', 'status', 'sort']; async function getFilters(name: string, url: string) { @@ -10,7 +10,7 @@ async function getFilters(name: string, url: string) { res.text(), ); const $: cheerio.CheerioAPI = cheerio.load(html); - const filters: any = { + const filters: Filters = { 'sort': { type: FilterTypes.Picker, label: 'Sort By', @@ -76,9 +76,9 @@ async function getFilters(name: string, url: string) { .map((index, element) => loadedCheerio(element).attr('href')) .get(); // ===================== tags ====================== - for (let i = 0; i < allPage.length; i++) { - console.log('fetch', url + allPage[i]); - const resTags = await fetch(url + allPage[i]).then(res => res.text()); + for (const page of allPage) { + console.log('fetch', url + page); + const resTags = await fetch(url + page).then(res => res.text()); const $: cheerio.CheerioAPI = cheerio.load(resTags); $('.tag-items > li > a').each((index, element) => filters['tags'].options.push({ @@ -97,10 +97,10 @@ async function getFilters(name: string, url: string) { await sleep(3000); console.log( 'fetch', - url + allPage[i].replace('-0.html', `-${pageNo + 1}.html`), + url + page.replace('-0.html', `-${pageNo + 1}.html`), ); 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()); const $: cheerio.CheerioAPI = cheerio.load(resTags); @@ -141,9 +141,9 @@ async function askGetFilter() { async (url: string) => { try { await getFilters(name, url); - } catch (e: any) { + } catch (e: unknown) { console.error('Error while getting filters from', url); - console.log(e.message || e); + console.log(e instanceof Error ? e.message : e); } readline.close(); }, diff --git a/plugins/multisrc/readwn/template.ts b/plugins/multisrc/readwn/template.ts index 4b73f4d46..85c907fc2 100644 --- a/plugins/multisrc/readwn/template.ts +++ b/plugins/multisrc/readwn/template.ts @@ -13,11 +13,11 @@ export type ReadwnMetadata = { id: string; sourceSite: string; sourceName: string; - filters?: any; + filters?: Filters; options?: ReadwnOptions; }; -class ReadwnPlugin implements Plugin.PluginBase { +export class ReadwnPlugin implements Plugin.PluginBase { id: string; name: string; icon: string; @@ -31,13 +31,16 @@ class ReadwnPlugin implements Plugin.PluginBase { this.icon = `multisrc/readwn/${metadata.id.toLowerCase()}/icon.png`; this.site = metadata.sourceSite; const versionIncrements = metadata.options?.versionIncrements || 0; - this.version = `1.0.${2 + versionIncrements}`; + this.version = `1.0.${3 + versionIncrements}`; this.filters = metadata.filters; } async popularNovels( pageNo: number, - { filters, showLatestNovels }: Plugin.PopularNovelsOptions, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions, ): Promise { let url = this.site + '/list/'; url += (filters?.genres?.value || 'all') + '/'; @@ -50,7 +53,7 @@ class ReadwnPlugin implements Plugin.PluginBase { 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 novels: Plugin.NovelItem[] = loadedCheerio('li.novel-item') @@ -68,7 +71,9 @@ class ReadwnPlugin implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then(res => res.text()); + const body = await fetchApi(this.site + novelPath).then((res: Response) => + res.text(), + ); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { @@ -186,7 +191,7 @@ class ReadwnPlugin implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then(res => + const body = await fetchApi(this.site + chapterPath).then((res: Response) => res.text(), ); const loadedCheerio = parseHTML(body); @@ -205,11 +210,11 @@ class ReadwnPlugin implements Plugin.PluginBase { method: 'POST', body: new URLSearchParams({ show: 'title', - tempid: 1, + tempid: '1', tbname: 'news', keyboard: searchTerm, }).toString(), - }).then(res => res.text()); + }).then((res: Response) => res.text()); const loadedCheerio = parseHTML(result); const novels: Plugin.NovelItem[] = loadedCheerio('li.novel-item') diff --git a/plugins/multisrc/rulate/template.ts b/plugins/multisrc/rulate/template.ts index 3649c6d51..a0c3383c3 100644 --- a/plugins/multisrc/rulate/template.ts +++ b/plugins/multisrc/rulate/template.ts @@ -1,5 +1,5 @@ import { fetchApi } from '@libs/fetch'; -import { Filters } from '@libs/filterInputs'; +import { FilterToValues, Filters } from '@libs/filterInputs'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; import dayjs from 'dayjs'; @@ -18,7 +18,7 @@ const headers = { 'accept-encoding': 'gzip', }; -class RulatePlugin implements Plugin.PluginBase { +export class RulatePlugin implements Plugin.PluginBase { id: string; name: string; icon: string; @@ -32,14 +32,14 @@ class RulatePlugin implements Plugin.PluginBase { this.name = metadata.sourceName + ' (API)'; this.icon = `multisrc/rulate/${metadata.id.toLowerCase()}/icon.png`; this.site = metadata.sourceSite; - this.version = '1.0.' + (0 + metadata.versionIncrements); + this.version = '1.0.' + (1 + metadata.versionIncrements); this.filters = metadata.filters; this.key = metadata.key; } parseNovels(url: string) { return fetchApi(url, { headers }) - .then(res => res.json() as Promise) + .then((res: Response) => res.json() as Promise) .then((data: SearchResponse) => { const novels: Plugin.NovelItem[] = []; @@ -59,12 +59,16 @@ class RulatePlugin implements Plugin.PluginBase { async popularNovels( page: number, - { filters, showLatestNovels }: Plugin.PopularNovelsOptions, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions, ): Promise { let url = this.site + '/api3/searchBooks?limit=40&page=' + page; 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[string]; if (value instanceof Array && value.length) { url += '&' + value.map(val => type + '[]=' + val).join('&'); } @@ -76,7 +80,7 @@ class RulatePlugin implements Plugin.PluginBase { async searchNovels( searchTerm: string, - page: number = 1, + page = 1, ): Promise { const url = `${this.site}/api3/searchBooks?t=${encodeURIComponent( searchTerm, @@ -88,14 +92,16 @@ class RulatePlugin implements Plugin.PluginBase { const book = await fetchApi( this.site + '/api3/book?book_id=' + novelPath + '&key=' + this.key, { headers }, - ).then(res => res.json() as Promise); + ).then((res: Response) => res.json() as Promise); const novel: Plugin.SourceNovel = { name: book.response.t_title || book.response.s_title, path: novelPath, cover: book.response.img, 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(','), summary: book.response.description, author: book.response.author, @@ -118,12 +124,12 @@ class RulatePlugin implements Plugin.PluginBase { '&key=' + this.key, { headers }, - ).then(res => res.json() as Promise); + ).then((res: Response) => res.json() as Promise); const chapters: Plugin.ChapterItem[] = []; if (chaptersData.response && Array.isArray(chaptersData.response)) { - chaptersData.response.forEach(chapter => { + chaptersData.response.forEach((chapter: ChapterResponse) => { if (chapter.can_read && chapter.subscription === 0) { chapters.push({ name: chapter.title + (chapter.illustrated ? ' 🖼️' : ''), @@ -150,7 +156,7 @@ class RulatePlugin implements Plugin.PluginBase { '&key=' + this.key, { headers }, - ).then(res => res.json() as Promise); + ).then((res: Response) => res.json() as Promise); return body.response.text; } @@ -158,7 +164,7 @@ class RulatePlugin implements Plugin.PluginBase { this.site + '/book/' + path + (isNovel ? '/' : '/ready_new'); } -interface SearchResponse { +type SearchResponse = { status: string; response: { t_title?: string; @@ -166,9 +172,9 @@ interface SearchResponse { id: number; img: string; }[]; -} +}; -interface BookResponse { +type BookResponse = { response: { t_title?: string; s_title: string; @@ -181,22 +187,24 @@ interface BookResponse { status: string; rate?: { sum: number; count: number }; }; -} +}; -interface ChaptersResponse { - response: { - title: string; - id: number; - ord: number; - cdate: number; - subscription: number; - can_read: boolean; - illustrated?: boolean; - }[]; -} +type ChapterResponse = { + title: string; + id: number; + ord: number; + cdate: number; + subscription: number; + can_read: boolean; + illustrated?: boolean; +}; + +type ChaptersResponse = { + response: ChapterResponse[]; +}; -interface ChapterTextResponse { +type ChapterTextResponse = { response: { text: string; }; -} +}; diff --git a/plugins/russian/authortoday.ts b/plugins/russian/authortoday.ts index 23d2a61ff..2724c8db0 100644 --- a/plugins/russian/authortoday.ts +++ b/plugins/russian/authortoday.ts @@ -11,7 +11,7 @@ class AuthorToday implements Plugin.PluginBase { name = 'Автор Тудей'; icon = 'src/ru/authortoday/icon.png'; site = 'https://author.today'; - version = '1.2.1'; + version = '1.2.2'; private userAgent = '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 }, }).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 userId = userRaw === 'null' ? '' : userRaw; diff --git a/plugins/russian/neobook.ts b/plugins/russian/neobook.ts index 3b989c142..fb43382c1 100644 --- a/plugins/russian/neobook.ts +++ b/plugins/russian/neobook.ts @@ -17,15 +17,13 @@ class Neobook implements Plugin.PluginBase { name = 'Neobook'; site = 'https://neobook.org'; apiSite = 'https://api.neobook.org/'; - version = '1.0.2'; + version = '1.0.3'; icon = 'src/ru/neobook/icon.png'; async fetchNovels( page: number, - { - filters, - showLatestNovels, - }: Plugin.PopularNovelsOptions, + showLatestNovels?: boolean, + filters?: Plugin.PopularNovelsOptions['filters'], searchTerm?: string, ): Promise { const formData = new FormData(); @@ -70,17 +68,18 @@ class Neobook implements Plugin.PluginBase { return novels; } - popularNovels = this.fetchNovels; - - async searchNovels( - searchTerm: string, + async popularNovels( page: number, - ): Promise { - const defaultOptions: any = { - filters: undefined, - showLatestNovels: false, - }; - return this.fetchNovels(page, defaultOptions, searchTerm); + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ) { + return this.fetchNovels(page, showLatestNovels, filters); + } + + async searchNovels(searchTerm: string, page: number) { + return this.fetchNovels(page, false, undefined, searchTerm); } async parseNovel(novelPath: string): Promise { @@ -215,8 +214,8 @@ class Neobook implements Plugin.PluginBase { export default new Neobook(); type BundleBooks = { - categories: any[]; - topSection: any[]; + // categories: any[]; + // topSection: any[]; feed: Novels[]; }; @@ -286,7 +285,7 @@ type Novels = { readProgress?: ReadProgress; tags?: string[]; chapters?: Chapter[]; - lastComments?: any[]; + // lastComments?: any[]; bottomSection?: BottomSection; carouselItems?: Novels[]; book?: Novels; @@ -339,7 +338,7 @@ type Data = { dateEdit: string; datePublish: string; attachment: Attachment; - lastComments: any[]; + // lastComments: any[]; }; type NovelsCounters = { @@ -371,12 +370,12 @@ type Login = { grm: string; counters: LoginCounters; user: LoginUser; - pro: any[]; - earn: any[]; - boost: any[]; - deposit: any[]; - affiliate: any[]; - preferences: any[]; + // pro: any[]; + // earn: any[]; + // boost: any[]; + // deposit: any[]; + // affiliate: any[]; + // preferences: any[]; }; type LoginCounters = { diff --git a/plugins/spanish/NOVA.ts b/plugins/spanish/NOVA.ts index c85833789..91f594cc6 100644 --- a/plugins/spanish/NOVA.ts +++ b/plugins/spanish/NOVA.ts @@ -1,261 +1,290 @@ -import { fetchApi } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { NovelStatus } from "@libs/novelStatus"; -import * as cheerio from "cheerio"; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { Element } from 'domhandler'; +import * as cheerio from 'cheerio'; class NovaPlugin implements Plugin.PluginBase { - id = 'nova'; - name = 'NOVA'; - icon = 'src/es/nova/icon.png'; - site = 'https://novelasligeras.net'; - version = '1.1.0'; - - // Regex para parsear títulos de capítulos - private readonly CHAPTER_REGEX = /(Parte \d+) . (.+?): (.+)/; - - // Helper para bypass de imágenes de Cloudflare - private async bypassCloudflareImages( - $: cheerio.CheerioAPI, - $content: cheerio.Cheerio - ): Promise { - $content.find('img').each((i, img) => { - const $img = $(img); - let src = $img.attr('src') || $img.attr('data-src') || $img.attr('data-cfsrc'); - - if (src) { - // Si la imagen tiene atributos de Cloudflare, usar la URL directa - $img.attr('src', src); - $img.removeAttr('data-src'); - $img.removeAttr('data-cfsrc'); - } - }); - - 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(); + id = 'nova'; + name = 'NOVA'; + icon = 'src/es/nova/icon.png'; + site = 'https://novelasligeras.net'; + version = '1.1.1'; + + // Regex para parsear títulos de capítulos + private readonly CHAPTER_REGEX = /(Parte \d+) . (.+?): (.+)/; + + // Helper para bypass de imágenes de Cloudflare + private async bypassCloudflareImages( + $: cheerio.CheerioAPI, + $content: cheerio.Cheerio, + ): Promise { + $content.find('img').each((i, img) => { + const $img = $(img); + const src = + $img.attr('src') || $img.attr('data-src') || $img.attr('data-cfsrc'); + + if (src) { + // Si la imagen tiene atributos de Cloudflare, usar la URL directa + $img.attr('src', src); + $img.removeAttr('data-src'); + $img.removeAttr('data-cfsrc'); + } + }); + + 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 { + // Para la primera página, usar la búsqueda AJAX + if (pageNo === 1) { + return this.searchNovels('', 1); } - - // Método para obtener novelas populares - async popularNovels( - pageNo: number, - options: Plugin.PopularNovelsOptions - ): Promise { - // Para la primera página, usar la búsqueda AJAX - if (pageNo === 1) { - return this.searchNovels('', 1); + + // Para páginas siguientes, usar la paginación normal + const url = `${this.site}/index.php/page/${pageNo}/?post_type=product&orderby=popularity`; + const body = await fetchApi(url).then(res => res.text()); + const $ = cheerio.load(body); + + const novels: Plugin.NovelItem[] = []; + + $('.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 }); + } + }); + + return novels; + } + + // Método para buscar novelas + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + 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 }); } - - // Para páginas siguientes, usar la paginación normal - const url = `${this.site}/index.php/page/${pageNo}/?post_type=product&orderby=popularity`; - const body = await fetchApi(url).then(res => res.text()); - const $ = cheerio.load(body); - - const novels: Plugin.NovelItem[] = []; - - $('.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`; + + 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; - } - - // Método para buscar novelas - async searchNovels( - searchTerm: string, - pageNo: number - ): Promise { - 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`; - - 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; + } } - - // Método para obtener detalles de una novela - async parseNovel(novelPath: string): Promise { - 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 = { - path: novelPath, - name, - cover, - summary, - author, - artist, - status, - chapters - }; - - return novel; + + return novels; + } + + // Método para obtener detalles de una novela + async parseNovel(novelPath: string): Promise { + 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 = ''; + 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; } - - // Método para obtener contenido del capítulo - async parseChapter(chapterPath: string): Promise { - 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; - - if (body.includes('Nadie entra sin permiso en la Gran Tumba de Nazarick')) { - $chapterText = $('#content'); + + // 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 { - $chapterText = $('.wpb_text_column.wpb_content_element > .wpb_wrapper'); + chapterName = `${volume} - ${chapterPartName}`; } - - // Remover anuncios y elementos no deseados - $chapterText.find('center').remove(); - - // Convertir elementos con text-align center a tags
- $chapterText.find('*').each((i, el) => { - const $el = $(el); - const style = $el.attr('style') || ''; - if (/text-align:.?center/.test(style)) { - $el.replaceWith(`
${$el.html()}
`); - } + + chapters.push({ + name: chapterName, + path: chapterPath, + releaseTime: '', + chapterNumber: chapterIndex + 1, }); - - // 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; + + chapterIndex++; + }); + }); + + 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 { + 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; + + 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'); } + + // Remover anuncios y elementos no deseados + $chapterText.find('center').remove(); + + // Convertir elementos con text-align center a tags
+ $chapterText.find('*').each((i, el) => { + const $el = $(el); + const style = $el.attr('style') || ''; + if (/text-align:.?center/.test(style)) { + $el.replaceWith(`
${$el.html()}
`); + } + }); + + // Aplicar bypass de imágenes de Cloudflare + const 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(); diff --git a/plugins/spanish/novelyra.ts b/plugins/spanish/novelyra.ts index 66b321220..87ea19f53 100644 --- a/plugins/spanish/novelyra.ts +++ b/plugins/spanish/novelyra.ts @@ -48,7 +48,7 @@ function parseSpanishTextToISO(text: string) { // --- 3. EVALUAR FECHAS RELATIVAS ("hace...") --- if (textLower.startsWith('hace')) { // Normalizar "un" / "una" a "1" para facilitar el cálculo - let normalized = textLower + const normalized = textLower .replace(/\b(un|una)\b/g, '1') .replace('un momento', '0 segundos'); @@ -99,7 +99,7 @@ class Novelyra implements Plugin.PluginBase { name = 'Novelyra'; icon = 'src/es/novelyra/icon.png'; site = 'https://novelyra.com/'; - version = '1.0.0'; + version = '1.0.1'; filters: Filters = { genres: { type: FilterTypes.Picker, @@ -279,16 +279,18 @@ class Novelyra implements Plugin.PluginBase { } break; case 'tag': - const originalTag = element.tagName; - if (tagsPermisive.includes(originalTag)) { - paragraph.push(loadedCheerio.html(element)); - } else { - if (paragraph.length > 0) { - chapterHtml.push(`

${paragraph.join(' ').trim()}

`); - paragraph = []; - if (originalTag === 'br') break; + { + const originalTag = element.tagName; + if (tagsPermisive.includes(originalTag)) { + paragraph.push(loadedCheerio.html(element)); + } else { + if (paragraph.length > 0) { + chapterHtml.push(`

${paragraph.join(' ').trim()}

`); + paragraph = []; + if (originalTag === 'br') break; + } + chapterHtml.push(loadedCheerio.html(element)); } - chapterHtml.push(loadedCheerio.html(element)); } break; } @@ -301,7 +303,7 @@ class Novelyra implements Plugin.PluginBase { } async searchNovels( searchTerm: string, - pageNo: number, + // pageNo: number, ): Promise { searchTerm = searchTerm.toLowerCase(); diff --git a/plugins/spanish/tunovelaligera.ts b/plugins/spanish/tunovelaligera.ts index 6df6e1b2b..da64de4ad 100644 --- a/plugins/spanish/tunovelaligera.ts +++ b/plugins/spanish/tunovelaligera.ts @@ -10,7 +10,7 @@ class TuNovelaLigera implements Plugin.PagePlugin { name = 'TuNovelaLigera'; icon = 'src/es/tunovelaligera/icon.png'; site = 'https://tunovelaligera.com'; - version = '1.2.0'; + version = '1.2.1'; async sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); @@ -228,16 +228,18 @@ class TuNovelaLigera implements Plugin.PagePlugin { } break; case 'tag': - const originalTag = element.tagName; - if (tagsPermisive.includes(originalTag)) { - paragraph.push(loadedCheerio.html(element)); - } else { - if (paragraph.length > 0) { - chapterHtml.push(`

${paragraph.join(' ').trim()}

`); - paragraph = []; - if (originalTag === 'br') break; + { + const originalTag = element.tagName; + if (tagsPermisive.includes(originalTag)) { + paragraph.push(loadedCheerio.html(element)); + } else { + if (paragraph.length > 0) { + chapterHtml.push(`

${paragraph.join(' ').trim()}

`); + paragraph = []; + if (originalTag === 'br') break; + } + chapterHtml.push(loadedCheerio.html(element)); } - chapterHtml.push(loadedCheerio.html(element)); } break; } diff --git a/plugins/ukrainian/bakainua.ts b/plugins/ukrainian/bakainua.ts index d71757450..af96f1446 100644 --- a/plugins/ukrainian/bakainua.ts +++ b/plugins/ukrainian/bakainua.ts @@ -1,4 +1,4 @@ -import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { load as parseHTML } from 'cheerio'; import { fetchApi } from '@libs/fetch'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; @@ -9,7 +9,7 @@ class BakaInUa implements Plugin.PluginBase { name = 'BakaInUA'; icon = 'src/uk/bakainua/icon.png'; site = 'https://baka.in.ua'; - version = '3.1.6'; + version = '3.1.7'; async popularNovels( pageNo: number, @@ -169,7 +169,9 @@ class BakaInUa implements Plugin.PluginBase { const $ = parseHTML(body); // 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) if (!content.text().trim()) { diff --git a/proxy.ts b/proxy.ts index 1a9adea66..1e55e8639 100644 --- a/proxy.ts +++ b/proxy.ts @@ -26,6 +26,14 @@ const settings: ServerSetting = { }; 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 = ''; req.on('data', chunk => { str += chunk; diff --git a/public/static/src/en/indratranslations/icon.png b/public/static/src/en/indratranslations/icon.png new file mode 100644 index 000000000..32e240db4 Binary files /dev/null and b/public/static/src/en/indratranslations/icon.png differ diff --git a/scripts/check-plugin-sites.js b/scripts/check-plugin-sites.js index f68cebdc5..eeb9f7ee5 100644 --- a/scripts/check-plugin-sites.js +++ b/scripts/check-plugin-sites.js @@ -23,7 +23,6 @@ const results = { let totalSites = 0; let checkedSites = 0; -let activeRequests = 0; function checkSite(url, pluginInfo) { return new Promise(resolve => { @@ -115,7 +114,6 @@ async function processSites(sites) { return; } - activeRequests++; const { url, pluginInfo } = queue.shift(); try { @@ -149,7 +147,6 @@ async function processSites(sites) { `\r✗ ${checkedSites}/${totalSites} - ${pluginInfo.name || url} (Error)`, ); } finally { - activeRequests--; await processNext(); } } diff --git a/scripts/download-plugin-icons.js b/scripts/download-plugin-icons.js index 8afd06063..15b116c37 100644 --- a/scripts/download-plugin-icons.js +++ b/scripts/download-plugin-icons.js @@ -1,4 +1,4 @@ -/* global Buffer */ +// /* global Buffer */ import * as fs from 'fs'; import * as path from 'path'; import sizeOf from 'image-size'; diff --git a/src/components/filters/filters-sheet.tsx b/src/components/filters/filters-sheet.tsx index 96cf62651..8500fbd69 100644 --- a/src/components/filters/filters-sheet.tsx +++ b/src/components/filters/filters-sheet.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, ReactNode } from 'react'; +import React from 'react'; import { Button } from '@/components/ui/button'; import { Sheet, @@ -158,8 +158,6 @@ export function FiltersSheet({ filters, refetch, }: FiltersSheetProps) { - const [filterElements, setFilterElements] = useState(null); - const setFilterWithKey = (key: string, newValue: AnyFilterValue) => setValues(fValues => !fValues @@ -186,15 +184,13 @@ export function FiltersSheet({ } }; - useEffect(() => { - setFilterElements(renderFilters(filters, values, setFilterWithKey)); - }, [values, filters]); - const handleApply = () => { refetch(); onOpenChange(false); }; + const filterElements = renderFilters(filters, values, setFilterWithKey); + return ( diff --git a/src/components/parse-chapter.tsx b/src/components/parse-chapter.tsx index 1ae2e46ee..8d7274eda 100644 --- a/src/components/parse-chapter.tsx +++ b/src/components/parse-chapter.tsx @@ -15,7 +15,7 @@ import { import { useAppStore } from '@/store'; import { usePluginCustomAssets } from '@/hooks/usePluginCustomAssets'; -export default function ParseChapterSection() { +const ParseChapterSection = React.memo(function ParseChapterSection() { const plugin = useAppStore(state => state.plugin); const parseChapterPath = useAppStore(state => state.parseChapterPath); const shouldAutoSubmitChapter = useAppStore( @@ -30,6 +30,15 @@ export default function ParseChapterSection() { const [fetchError, setFetchError] = useState(''); const [showRawHtml, setShowRawHtml] = useState(false); const lastProcessedPath = useRef(); + const [prevPluginId, setPrevPluginId] = useState(); + + if (plugin?.id !== prevPluginId) { + setPrevPluginId(plugin?.id); + setChapterPath(''); + setChapterText(''); + setFetchError(''); + setShowRawHtml(false); + } const { customCSSLoaded, customJSLoaded, customCSSError, customJSError } = usePluginCustomAssets(plugin, chapterText); @@ -56,7 +65,7 @@ export default function ParseChapterSection() { await fetchChapterByPath(chapterPath); }; - const handleKeyPress = (e: React.KeyboardEvent) => { + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && chapterPath.trim()) { fetchChapter(); } @@ -128,7 +137,7 @@ export default function ParseChapterSection() { placeholder="Enter chapter path..." value={chapterPath} onChange={e => setChapterPath(e.target.value)} - onKeyPress={handleKeyPress} + onKeyDown={handleKeyDown} className="flex-1" disabled={!plugin} /> @@ -327,4 +336,6 @@ export default function ParseChapterSection() { ); -} +}); + +export default ParseChapterSection; diff --git a/src/components/parse-novel.tsx b/src/components/parse-novel.tsx index 8541f1e41..4a5dbe779 100644 --- a/src/components/parse-novel.tsx +++ b/src/components/parse-novel.tsx @@ -27,7 +27,7 @@ type ParseNovelSectionProps = { onNavigateToParseChapter?: () => void; }; -export default function ParseNovelSection({ +const ParseNovelSection = React.memo(function ParseNovelSection({ onNavigateToParseChapter, }: ParseNovelSectionProps) { const plugin = useAppStore(state => state.plugin); @@ -46,6 +46,16 @@ export default function ParseNovelSection({ const [currentPage, setCurrentPage] = useState(1); const [fetchError, setFetchError] = useState(''); const lastProcessedPath = useRef(); + const [prevPluginId, setPrevPluginId] = useState(); + + if (plugin?.id !== prevPluginId) { + setPrevPluginId(plugin?.id); + setNovelPath(''); + setSourceNovel(undefined); + setChapters([]); + setCurrentPage(1); + setFetchError(''); + } const { exportEpub, isExporting } = useEpubExport({ plugin: plugin || null, @@ -100,7 +110,7 @@ export default function ParseNovelSection({ } }; - const handleKeyPress = (e: React.KeyboardEvent) => { + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && novelPath.trim()) { fetchNovel(); } @@ -168,7 +178,7 @@ export default function ParseNovelSection({ placeholder="Enter novel path..." value={novelPath} onChange={e => setNovelPath(e.target.value)} - onKeyPress={handleKeyPress} + onKeyDown={handleKeyDown} className="flex-1" disabled={!plugin} /> @@ -551,4 +561,6 @@ export default function ParseNovelSection({ ); -} +}); + +export default ParseNovelSection; diff --git a/src/components/popular-novels.tsx b/src/components/popular-novels.tsx index d27729e52..6dd2994a5 100644 --- a/src/components/popular-novels.tsx +++ b/src/components/popular-novels.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Filter, BookOpen } from 'lucide-react'; import { FiltersSheet } from '@/components/filters/filters-sheet'; @@ -16,7 +16,7 @@ type PopularNovelsSectionProps = { onNavigateToParseNovel?: () => void; }; -export default function PopularNovelsSection({ +const PopularNovelsSection = React.memo(function PopularNovelsSection({ onNavigateToParseNovel, }: PopularNovelsSectionProps) { const plugin = useAppStore(state => state.plugin); @@ -30,14 +30,38 @@ export default function PopularNovelsSection({ const [filterValues, setFilterValues] = useState< FilterToValues | undefined >(); + const [prevPluginId, setPrevPluginId] = useState(); - const fetchNovelsByIndex = async (index: number) => { + if (plugin?.id !== prevPluginId) { + setPrevPluginId(plugin?.id); + setCurrentIndex(0); + setMaxIndex(0); + setNovels([]); + + if (plugin?.filters) { + const filters: FilterToValues = {}; + 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) { setLoading(true); try { const fetchedNovels = await plugin.popularNovels(index, { filters: filterValues || {}, - showLatestNovels: isLatest, + showLatestNovels: latestOverride ?? isLatest, }); if (fetchedNovels.length !== 0) { setCurrentIndex(index); @@ -54,31 +78,13 @@ export default function PopularNovelsSection({ } }; - useEffect(() => { - if (plugin) { - setCurrentIndex(1); - setMaxIndex(1); - fetchNovelsByIndex(1); - } - }, [isLatest]); - - useEffect(() => { - // Reset when changing plugins + const handleIsLatestChange = (latest: boolean) => { + if (isLatest === latest) return; + setIsLatest(latest); setCurrentIndex(0); setMaxIndex(0); setNovels([]); - - if (plugin?.filters) { - const filters: FilterToValues = {}; - 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) => { setParseNovelPath(path, true); @@ -136,7 +142,7 @@ export default function PopularNovelsSection({ isLatest === (option === 'Latest') ? 'default' : 'outline' } className="cursor-pointer" - onClick={() => setIsLatest(option === 'Latest')} + onClick={() => handleIsLatestChange(option === 'Latest')} > {option} @@ -149,7 +155,7 @@ export default function PopularNovelsSection({ min="1" max={maxIndex} value={currentIndex} - onChange={(e: React.ChangeEvent) => { + onChange={e => { const page = parseInt(e.target.value); if (page > 0 && page <= maxIndex) { fetchNovelsByIndex(page); @@ -216,4 +222,6 @@ export default function PopularNovelsSection({ /> ); -} +}); + +export default PopularNovelsSection; diff --git a/src/components/search-novels.tsx b/src/components/search-novels.tsx index 30a4266d2..4245b2e05 100644 --- a/src/components/search-novels.tsx +++ b/src/components/search-novels.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Search as SearchIcon } from 'lucide-react'; import { NovelCard } from '@/components/novel-card'; @@ -14,7 +14,7 @@ type SearchNovelsSectionProps = { onNavigateToParseNovel?: () => void; }; -export default function SearchNovelsSection({ +const SearchNovelsSection = React.memo(function SearchNovelsSection({ onNavigateToParseNovel, }: SearchNovelsSectionProps) { const plugin = useAppStore(state => state.plugin); @@ -24,6 +24,15 @@ export default function SearchNovelsSection({ const [novels, setNovels] = useState([]); const [loading, setLoading] = useState(false); const [fetchError, setFetchError] = useState(''); + const [prevPluginId, setPrevPluginId] = useState(); + + if (plugin?.id !== prevPluginId) { + setPrevPluginId(plugin?.id); + setCurrentPage(1); + setNovels([]); + setSearchTerm(''); + setFetchError(''); + } const fetchNovels = async (page: number) => { if (plugin && searchTerm.trim()) { @@ -44,14 +53,7 @@ export default function SearchNovelsSection({ } }; - useEffect(() => { - setCurrentPage(1); - setNovels([]); - setSearchTerm(''); - setFetchError(''); - }, [plugin]); - - const handleKeyPress = (e: React.KeyboardEvent) => { + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && searchTerm.trim()) { fetchNovels(1); } @@ -86,7 +88,7 @@ export default function SearchNovelsSection({ placeholder="Enter search term..." value={searchTerm} onChange={e => setSearchTerm(e.target.value)} - onKeyPress={handleKeyPress} + onKeyDown={handleKeyDown} className="flex-1" disabled={!plugin} /> @@ -159,4 +161,6 @@ export default function SearchNovelsSection({ ); -} +}); + +export default SearchNovelsSection; diff --git a/src/components/settings.tsx b/src/components/settings.tsx index 7bbad2bdc..c90e28918 100644 --- a/src/components/settings.tsx +++ b/src/components/settings.tsx @@ -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 { Check } from 'lucide-react'; @@ -16,59 +16,75 @@ import { import useDebounce from '@/hooks/useDebounce'; import { FetchMode } from '@/types/types'; -export default function SettingsSection() { - const [cookies, setCookies] = useState(''); - const debouncedCookies = useDebounce(cookies, 500); - const [fetchMode, setFetchMode] = useState(FetchMode.PROXY); - const [useUserAgent, setUseUserAgent] = useState(true); - const [loading, setLoading] = useState(false); - const [alertVisible, setAlertVisible] = useState(false); +const FETCH_MODES = { + [FetchMode.PROXY]: 'Proxy', + [FetchMode.NODE_FETCH]: 'Node Fetch', + [FetchMode.CURL]: 'Curl', +}; + +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(null); + const debouncedCookies = useDebounce(settings.cookies, 500); useEffect(() => { - if (alertVisible) { - const id = setTimeout(() => setAlertVisible(false), 2000); - return () => clearTimeout(id); - } - }, [alertVisible]); + fetch('settings') + .then(res => res.json()) + .then(data => { + const loaded = { + cookies: data.cookies || '', + fetchMode: data.fetchMode ?? FetchMode.PROXY, + useUserAgent: data.useUserAgent ?? true, + }; + setSettings(loaded); + lastSaved.current = loaded; + init.current = true; + }) + .catch(console.error); + }, []); 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', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - cookies: debouncedCookies, - fetchMode: fetchMode, - useUserAgent: useUserAgent === true, - }), + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(current), }) - .then(() => setAlertVisible(true)) - .catch(error => console.error('Failed to save settings:', error)) - .finally(() => setLoading(false)); - }, [debouncedCookies, fetchMode, useUserAgent]); + .then(() => { + lastSaved.current = current; + setStatus('saved'); + setTimeout(() => setStatus('idle'), 2000); + }) + .catch(console.error); + }, [ + debouncedCookies, + settings.fetchMode, + settings.useUserAgent, + settings.cookies, + ]); - const getFetchModeLabel = (mode: FetchMode) => { - switch (mode) { - case FetchMode.PROXY: - return 'Proxy'; - case FetchMode.NODE_FETCH: - return 'Node Fetch'; - case FetchMode.CURL: - return 'Curl'; - default: - return 'Proxy'; - } - }; + const update = ( + k: K, + v: (typeof settings)[K], + ) => setSettings(settings => ({ ...settings, [k]: v })); return (
- {alertVisible && ( + {status === 'saved' && (
- - Settings updated + Settings updated
)} @@ -79,86 +95,61 @@ export default function SettingsSection() { Settings are automatically saved

- {loading && ( + {status === 'loading' && (
Saving...
)}
- {/* Request Configuration Section */} -
-
-
-

- Request Configuration -

-
-
- -
- {/* User Agent */} -
- -
- -
- - -
-
-

- Enable to send your browser's user agent with requests -

-
- - {/* Cookies */} -
- +
+
+ +
setCookies(e.target.value.trim())} - placeholder="Enter cookies (optional)..." - className="font-mono text-xs" + value={navigator.userAgent} + disabled + className="font-mono text-xs flex-1 opacity-60" + title={navigator.userAgent} /> -

- Additional cookies to send with requests (optional) -

+
+ update('useUserAgent', v)} + /> + +
-
- {/* Fetch Settings Section */} -
-
-
-

- Fetch Settings -

-
+
+ + update('cookies', e.target.value.trim())} + placeholder="Enter cookies (optional)..." + className="font-mono text-xs" + /> +

+ Additional cookies to send with requests (optional) +

+ +
-
+
); +}); + +function Section({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( +
+
+
+

+ {title} +

+
+
+
{children}
+
+ ); } + +export default SettingsSection; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index b0c1fc933..bf7d44869 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -25,22 +25,25 @@ const badgeVariants = cva( }, ); -function Badge({ - className, - variant, - asChild = false, - ...props -}: React.ComponentProps<'span'> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : 'span'; +type BadgeProps = React.ComponentPropsWithoutRef<'span'> & + VariantProps & { + asChild?: boolean; + }; - return ( - - ); -} +const Badge = React.forwardRef( + ({ className, variant, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'span'; + + return ( + + ); + }, +); +Badge.displayName = 'Badge'; export { Badge, badgeVariants }; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 08da3e66f..8542771e9 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -36,25 +36,25 @@ const buttonVariants = cva( }, ); -function Button({ - className, - variant, - size, - asChild = false, - ...props -}: React.ComponentProps<'button'> & +type ButtonProps = React.ComponentPropsWithoutRef<'button'> & VariantProps & { asChild?: boolean; - }) { - const Comp = asChild ? Slot : 'button'; + }; - return ( - - ); -} +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + + return ( + + ); + }, +); +Button.displayName = 'Button'; export { Button, buttonVariants }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 850392ea0..8d9dfd8d8 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -2,9 +2,10 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; -function Card({ className, ...props }: React.ComponentProps<'div'>) { - return ( +const Card = React.forwardRef>( + ({ className, ...props }, ref) => (
) { )} {...props} /> - ); -} + ), +); +Card.displayName = 'Card'; -function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ); -} +const CardHeader = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = 'CardHeader'; -function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { - return ( +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => (
- ); -} + ), +); +CardTitle.displayName = 'CardTitle'; -function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ); -} +const CardDescription = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> +>(({ className, ...props }, ref) => ( +
+)); +CardDescription.displayName = 'CardDescription'; -function CardAction({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ); -} +const CardAction = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> +>(({ className, ...props }, ref) => ( +
+)); +CardAction.displayName = 'CardAction'; -function CardContent({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ); -} +const CardContent = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> +>(({ className, ...props }, ref) => ( +
+)); +CardContent.displayName = 'CardContent'; -function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ); -} +const CardFooter = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = 'CardFooter'; export { Card, diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx index 346842484..b6ae45726 100644 --- a/src/components/ui/checkbox.tsx +++ b/src/components/ui/checkbox.tsx @@ -6,27 +6,27 @@ import { CheckIcon } from 'lucide-react'; import { cn } from '@/lib/utils'; -function Checkbox({ - className, - ...props -}: React.ComponentProps) { - return ( - , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + - - - - - ); -} + + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; export { Checkbox }; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 33b39362e..436111a4b 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -2,20 +2,24 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; -function Input({ className, type, ...props }: React.ComponentProps<'input'>) { - return ( - - ); -} +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = 'Input'; export { Input }; diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx index 3f438eaaa..65fc3d6b6 100644 --- a/src/components/ui/label.tsx +++ b/src/components/ui/label.tsx @@ -3,20 +3,20 @@ import * as LabelPrimitive from '@radix-ui/react-label'; import { cn } from '@/lib/utils'; -function Label({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; export { Label }; diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index d8642ed44..1ae483911 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -4,60 +4,47 @@ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'; import { cn } from '@/lib/utils'; -function Select({ - ...props -}: React.ComponentProps) { - return ; -} +const Select = SelectPrimitive.Root; -function SelectGroup({ - ...props -}: React.ComponentProps) { - return ; -} +const SelectGroup = SelectPrimitive.Group; -function SelectValue({ - ...props -}: React.ComponentProps) { - return ; -} +const SelectValue = SelectPrimitive.Value; -function SelectTrigger({ - className, - size = 'default', - children, - ...props -}: React.ComponentProps & { - size?: 'sm' | 'default'; -}) { - return ( - - {children} - - - - - ); -} +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + size?: 'sm' | 'default'; + } +>(({ className, size = 'default', children, ...props }, ref) => ( + + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; -function SelectContent({ - className, - children, - position = 'popper', - align = 'center', - ...props -}: React.ComponentProps) { - return ( +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, children, position = 'popper', align = 'center', ...props }, + ref, + ) => ( - ); -} + ), +); +SelectContent.displayName = SelectPrimitive.Content.displayName; -function SelectLabel({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; -function SelectItem({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ); -} +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; -function SelectSeparator({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; -function SelectScrollUpButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ); -} +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; -function SelectScrollDownButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ); -} +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; export { Select, diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx index 16e5a50ce..d86b9a581 100644 --- a/src/components/ui/skeleton.tsx +++ b/src/components/ui/skeleton.tsx @@ -1,16 +1,18 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; -function Skeleton({ - className, - ...props -}: React.HTMLAttributes) { +const Skeleton = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { return (
); -} +}); +Skeleton.displayName = 'Skeleton'; export { Skeleton }; diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index 97945e9ca..2b567d14e 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -3,62 +3,66 @@ import * as TabsPrimitive from '@radix-ui/react-tabs'; import { cn } from '@/lib/utils'; -function Tabs({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} +const Tabs = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Tabs.displayName = TabsPrimitive.Root.displayName; -function TabsList({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; -function TabsTrigger({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; -function TabsContent({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx index 0243b83ae..785f88245 100644 --- a/src/components/ui/tooltip.tsx +++ b/src/components/ui/tooltip.tsx @@ -5,35 +5,26 @@ import { cn } from '@/lib/utils'; const TooltipProvider = TooltipPrimitive.Provider; -function Tooltip({ - ...props -}: React.ComponentProps) { - return ; -} +const Tooltip = TooltipPrimitive.Root; -function TooltipTrigger({ - ...props -}: React.ComponentProps) { - return ; -} +const TooltipTrigger = TooltipPrimitive.Trigger; -function TooltipContent({ - className, - sideOffset = 4, - ...props -}: React.ComponentProps) { - return ( - - - - ); -} +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/src/hooks/useEpubExport.ts b/src/hooks/useEpubExport.ts index 8a8fad0c9..075390446 100644 --- a/src/hooks/useEpubExport.ts +++ b/src/hooks/useEpubExport.ts @@ -3,12 +3,12 @@ import { toast } from 'sonner'; import { Plugin } from '@/types/plugin'; import { createEpub, downloadBlob } from '@/lib/epub'; -interface UseEpubExportOptions { +type UseEpubExportOptions = { plugin: Plugin.PluginBase | null; sourceNovel: (Plugin.SourceNovel & { totalPages?: number }) | undefined; chapters: Plugin.ChapterItem[]; novelPath: string; -} +}; export function useEpubExport({ plugin, @@ -69,11 +69,12 @@ export function useEpubExport({ description: `0/${allChapters.length} chapters processed`, }); - const chapterContents: Array<{ + type Chapter = { title: string; content: string; path: string; - }> = []; + }; + const chapterContents: Chapter[] = []; for (let i = 0; i < allChapters.length; i++) { const chapter = allChapters[i]; diff --git a/src/hooks/usePluginCustomAssets.ts b/src/hooks/usePluginCustomAssets.ts index e3bcaeb98..dedcd11e5 100644 --- a/src/hooks/usePluginCustomAssets.ts +++ b/src/hooks/usePluginCustomAssets.ts @@ -1,12 +1,12 @@ import { useEffect, useRef, useState } from 'react'; import { Plugin } from '@/types/plugin'; -interface UsePluginCustomAssetsReturn { +type UsePluginCustomAssetsReturn = { customCSSLoaded: boolean; customJSLoaded: boolean; customCSSError: boolean; customJSError: boolean; -} +}; /** * Custom hook to load and manage plugin custom CSS and JS assets diff --git a/src/lib/epub.ts b/src/lib/epub.ts index 00676e2bb..2cd231574 100644 --- a/src/lib/epub.ts +++ b/src/lib/epub.ts @@ -1,19 +1,19 @@ import JSZip from 'jszip'; -import { Plugin } from '@/types/plugin'; +// import { Plugin } from '@/types/plugin'; // apparently unused -export interface EpubOptions { +export type EpubOptions = { title: string; author?: string; description?: string; cover?: string; language?: string; -} +}; -export interface ChapterData { +export type ChapterData = { title: string; content: string; path: string; -} +}; function sanitizeHtml(html: string): string { // Remove script tags and their content diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts index 401d2b69d..40b78cb83 100644 --- a/src/lib/fetch.ts +++ b/src/lib/fetch.ts @@ -2,7 +2,7 @@ import { parse as parseProto } from 'protobufjs'; -type FetchInit = { +export type FetchInit = { headers?: Record | Headers; method?: string; body?: FormData | string; @@ -104,7 +104,7 @@ type ProtoRequestInit = { // merged .proto file proto: string; requestType: string; - requestData?: any; + requestData?: Record; responseType: string; }; @@ -117,11 +117,13 @@ export const fetchProto = async function ( ) { const protoRoot = parseProto(protoInit.proto).root; const RequestMessge = protoRoot.lookupType(protoInit.requestType); - if (RequestMessge.verify(protoInit.requestData)) { + if (RequestMessge.verify(protoInit.requestData || {})) { throw new Error('Invalid Proto'); } // encode request data - const encodedrequest = RequestMessge.encode(protoInit.requestData).finish(); + const encodedrequest = RequestMessge.encode( + protoInit.requestData || {}, + ).finish(); const requestLength = BigInt(encodedrequest.length); const headers = new Uint8Array( Array(5) diff --git a/src/lib/storage.ts b/src/lib/storage.ts index b5df7e069..a0a2e0cae 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -1,5 +1,11 @@ +export type StorageItem = { + created: Date; + value: T; + expires?: number; +}; + class Storage { - private db: Record; + private db: Record; /** * Initializes a new instance of the Storage class. @@ -12,10 +18,10 @@ class Storage { * Sets a key-value pair in storage. * * @param {string} key - The key to set. - * @param {any} value - The value to set. + * @param {T} value - The value to set. * @param {Date | number} [expires] - Optional expiry date or time in milliseconds. */ - set(key: string, value: any, expires?: Date | number): void { + set(key: string, value: T, expires?: Date | number): void { this.db[key] = { created: new Date(), value, @@ -28,10 +34,12 @@ class Storage { * * @param {string} key - The key to retrieve the value for. * @param {boolean} [raw] - Optional flag to return the raw stored item. - * @returns {any} The stored value or undefined if key is not found. + * @returns The stored value or undefined if key is not found. */ - get(key: string, raw?: boolean): any { - const item = this.db[key]; + get(key: string, raw: true): StorageItem | undefined; + get(key: string, raw?: false): T | undefined; + get(key: string, raw?: boolean): T | StorageItem | undefined { + const item = this.db[key] as StorageItem | undefined; if (item?.expires && Date.now() > item.expires) { this.delete(key); return undefined; @@ -69,7 +77,7 @@ class Storage { export const storage = new Storage(); /* -These parameters cannot be implemented in `test-web`. +These parameters cannot be implemented in `test-web`. They are generated in the browser when js-scripts are executed Read more @@ -80,7 +88,7 @@ https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage /** * Represents the structure of a storage object with string keys and values. */ -type StorageObject = Record; +type StorageObject = Record; /** * Represents a simplified version of the browser's localStorage. diff --git a/src/libs/fetch.ts b/src/libs/fetch.ts index 8d63d6008..be73680ab 100644 --- a/src/libs/fetch.ts +++ b/src/libs/fetch.ts @@ -2,4 +2,10 @@ * Backward compatibility for 3.0.0 - Re-exports from new location * TODO: Remove in 4.0.0 */ -export { fetchApi, fetchText, fetchProto, fetchFile } from '../lib/fetch'; +export { + fetchApi, + fetchText, + fetchProto, + fetchFile, + type FetchInit, +} from '../lib/fetch'; diff --git a/src/libs/filterInputs.ts b/src/libs/filterInputs.ts index dbb5bf91e..d934b0b98 100644 --- a/src/libs/filterInputs.ts +++ b/src/libs/filterInputs.ts @@ -8,5 +8,6 @@ export type { Filter, FilterOption, FilterToValues, + FilterValueWithType, AnyFilterValue, } from '../types/filters'; diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 97a9d6781..7bdf690e5 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; import { BookOpen, Search, Settings, Zap } from 'lucide-react'; import PluginHeader from '../components/plugin-header'; @@ -14,62 +14,96 @@ import ParseNovelSection from '@/components/parse-novel'; import SettingsSection from '@/components/settings'; import ParseChapterSection from '@/components/parse-chapter'; -function Home() { +function PluginSidebar() { const { plugin, selectPlugin } = useAppStore(state => state); const [searchQuery, setSearchQuery] = useState(''); - const [activeTab, setActiveTab] = useState('popular'); - const filteredPlugins = useMemo( () => - plugins.filter(plugin => - plugin.name.toLowerCase().includes(searchQuery.toLowerCase()), + plugins.filter(p => + p.name.toLowerCase().includes(searchQuery.toLowerCase()), ), [searchQuery], ); + return ( + + ); +} + +function Home() { + const { plugin } = useAppStore(state => state); + + const [activeTab, setActiveTab] = useState('popular'); + + const handleNavigateToParseNovel = useCallback(() => { + setActiveTab('parse-novel'); + }, []); + + const handleNavigateToParseChapter = useCallback(() => { + setActiveTab('parse-chapter'); + }, []); + return (
- + {/* Main Content */}
@@ -126,19 +160,19 @@ function Home() { setActiveTab('parse-novel')} + onNavigateToParseNovel={handleNavigateToParseNovel} /> setActiveTab('parse-novel')} + onNavigateToParseNovel={handleNavigateToParseNovel} /> setActiveTab('parse-chapter')} + onNavigateToParseChapter={handleNavigateToParseChapter} /> diff --git a/tsconfig.json b/tsconfig.json index ba1dfc913..7d06e7b10 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,7 +37,7 @@ "paths": { "@/*": ["./src/*"], "@plugins/*": ["./plugins/*"], - "@libs/*": ["./src/libs/*"] + "@libs/*": ["./src/libs/*"], } /* Specify a set of entries that re-map imports to additional lookup locations. */, // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ @@ -116,20 +116,17 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */, }, "exclude": [ "./plugins/**/*.broken.ts", "./.js/**/*", "scripts/**", "docs/**", - "./src/*.*", - "./*.*", "public/**", ".github/**", ".husky/**", "node_modules/**", - "./src/**", - "./plugins/multisrc/*" - ] + "./plugins/multisrc/*", + ], }