diff --git a/plugins/english/wtrlab.ts b/plugins/english/wtrlab.ts index 50804e00b..71290a4f2 100644 --- a/plugins/english/wtrlab.ts +++ b/plugins/english/wtrlab.ts @@ -4,11 +4,11 @@ import { FilterTypes, Filters } from '@libs/filterInputs'; import { CheerioAPI, load as parseHTML } from 'cheerio'; import { gcm } from '@libs/aes'; -class WTRLAB implements Plugin.PluginBase { +class WTRLAB implements Plugin.PagePlugin { id = 'WTRLAB'; name = 'WTR-LAB'; site = 'https://wtr-lab.com/'; - version = '1.1.4'; + version = '1.1.5'; icon = 'src/en/wtrlab/icon.png'; sourceLang = 'en/'; baggage = ''; @@ -152,7 +152,9 @@ class WTRLAB implements Plugin.PluginBase { this.trace = $('meta[name="sentry-trace"]').attr('content') ?? ''; } - async parseNovel(novelPath: string): Promise { + async parseNovel( + novelPath: string, + ): Promise { const body = await fetchApi(this.site + novelPath).then(res => res.text()); const loadedCheerio = parseHTML(body); @@ -173,10 +175,11 @@ class WTRLAB implements Plugin.PluginBase { let slug: string | null = null; let chapterCount = 0; - const novel: Plugin.SourceNovel = { + const novel: Plugin.SourceNovel & { totalPages: number } = { path: novelPath, name: loadedCheerio('h1.text-uppercase').text(), summary: loadedCheerio('.lead').text().trim(), + totalPages: 0, }; if (nextDataText) { @@ -193,6 +196,7 @@ class WTRLAB implements Plugin.PluginBase { novel.author = serieData.data?.author || ''; rawId = serieData.raw_id || null; slug = serieData.slug || null; + chapterCount = serieData.chapter_count ?? 0; switch (serieData.status) { case 0: @@ -347,31 +351,33 @@ class WTRLAB implements Plugin.PluginBase { slug = urlMatch[2]; } - const chapterCountText = - loadedCheerio('.detail-line:contains("Chapters")').text() || - loadedCheerio('div:contains("Chapters")').text(); - const chapterCountMatch = chapterCountText.match(/(\d+)\s+Chapters?/i); - if (chapterCountMatch) { - chapterCount = parseInt(chapterCountMatch[1]); - } - if (chapterCount === 0 && nextDataText) { - try { - const jsonData = JSON.parse(nextDataText); - - chapterCount = - jsonData?.props?.pageProps?.serie?.serie_data?.chapter_count ?? 0; - } catch (error) { - console.error( - 'Failed to parse chapter_count from __NEXT_DATA__:', - error, - ); + if (chapterCount === 0) { + const chapterCountText = + loadedCheerio('.detail-line:contains("Chapters")').text() || + loadedCheerio('div:contains("Chapters")').text(); + const chapterCountMatch = chapterCountText.match(/(\d+)\s+Chapters?/i); + if (chapterCountMatch) { + chapterCount = parseInt(chapterCountMatch[1]); + } + if (chapterCount === 0 && nextDataText) { + try { + const jsonData = JSON.parse(nextDataText); + + chapterCount = + jsonData?.props?.pageProps?.serie?.serie_data?.chapter_count ?? 0; + } catch (error) { + console.error( + 'Failed to parse chapter_count from __NEXT_DATA__:', + error, + ); + } } } let chapters: Plugin.ChapterItem[] = []; if (rawId && slug && chapterCount > 0) { try { - chapters = await this.fetchAllChapters(rawId, chapterCount, slug); + chapters = await this.fetchPageChapters(rawId, 1, chapterCount, slug); } catch (error) { console.error('Failed to fetch chapters via API:', error); chapters = []; @@ -384,6 +390,7 @@ class WTRLAB implements Plugin.PluginBase { }); } + novel.totalPages = Math.max(1, Math.ceil(chapterCount / 250)); novel.chapters = chapters; return novel; @@ -620,57 +627,89 @@ class WTRLAB implements Plugin.PluginBase { return htmlString; } - async fetchAllChapters( + async fetchPageChapters( rawId: number, + pageNumber: number, totalChapters: number, slug: string, ): Promise { - const allChapters: Plugin.ChapterItem[] = []; const batchSize = 250; + const start = (pageNumber - 1) * batchSize + 1; + const end = Math.min(start + batchSize - 1, totalChapters); - for (let start = 1; start <= totalChapters; start += batchSize) { - const end = Math.min(start + batchSize - 1, totalChapters); + if (start > totalChapters) { + return []; + } - try { - const response = await fetchApi( - `${this.site}api/chapters/${rawId}?start=${start}&end=${end}`, - { - headers: { - ...this.headers, - }, + try { + const response = await fetchApi( + `${this.site}api/chapters/${rawId}?start=${start}&end=${end}`, + { + headers: { + ...this.headers, }, - ); - - const data = await response.json(); - const chapters = data.chapters ?? data.data?.chapters ?? []; - - if (Array.isArray(chapters)) { - const batchChapters: Plugin.ChapterItem[] = chapters.map( - (apiChapter: ApiChapter) => ({ - name: apiChapter.title, - path: `${this.sourceLang}serie-${rawId}/${slug}/chapter-${apiChapter.order}`, - releaseTime: apiChapter.updated_at?.substring(0, 10), - chapterNumber: apiChapter.order, - }), - ); + }, + ); - allChapters.push(...batchChapters); + const data = await response.json(); + const chapters = data.chapters ?? data.data?.chapters ?? []; - if (chapters.length < batchSize) { - break; - } - } else { - break; - } - } catch (error) { - console.error(`Failed to fetch chapters ${start}-${end}:`, error); - continue; + if (Array.isArray(chapters)) { + return chapters.map((apiChapter: ApiChapter) => ({ + name: apiChapter.title, + path: `${this.sourceLang}serie-${rawId}/${slug}/chapter-${apiChapter.order}`, + releaseTime: apiChapter.updated_at?.substring(0, 10), + chapterNumber: apiChapter.order, + })); } + } catch (error) { + console.error(`Failed to fetch page chapters ${start}-${end}:`, error); + } + return []; + } + + async parsePage(novelPath: string, page: string): Promise { + let rawId: number | null = null; + let slug: string | null = null; + + const urlMatch = novelPath.match(/serie-(\d+)\/([^/]+)/); + if (urlMatch) { + rawId = parseInt(urlMatch[1]); + slug = urlMatch[2]; } - return allChapters.sort( - (a, b) => (a.chapterNumber || 0) - (b.chapterNumber || 0), + if (!rawId || !slug) { + throw new Error(`Could not parse rawId or slug from novelPath: ${novelPath}`); + } + + const pageNumber = parseInt(page, 10); + const batchSize = 250; + const start = (pageNumber - 1) * batchSize + 1; + const end = start + batchSize - 1; + + const response = await fetchApi( + `${this.site}api/chapters/${rawId}?start=${start}&end=${end}`, + { + headers: { + ...this.headers, + }, + }, ); + + const data = await response.json(); + const chaptersData = data.chapters ?? data.data?.chapters ?? []; + + let chapters: Plugin.ChapterItem[] = []; + if (Array.isArray(chaptersData)) { + chapters = chaptersData.map((apiChapter: ApiChapter) => ({ + name: apiChapter.title, + path: `${this.sourceLang}serie-${rawId}/${slug}/chapter-${apiChapter.order}`, + releaseTime: apiChapter.updated_at?.substring(0, 10), + chapterNumber: apiChapter.order, + })); + } + + return { chapters }; } async searchNovels( diff --git a/scripts/build-plugin-manifest.js b/scripts/build-plugin-manifest.js index cab5eec47..b01b77462 100644 --- a/scripts/build-plugin-manifest.js +++ b/scripts/build-plugin-manifest.js @@ -25,7 +25,6 @@ const PLUGIN_LINK = `${USER_CONTENT_LINK}/.js/src/plugins`; const DIST_DIR = '.dist'; -let json = []; if (!fs.existsSync(DIST_DIR)) { fs.mkdirSync(DIST_DIR); } @@ -38,20 +37,22 @@ const pluginsWithFiltersPerLanguage = {}; const args = process.argv.slice(2); let ONLY_NEW = args.includes('--only-new'); -let existingPlugins = {}; -if (!fs.existsSync(jsonPath)) ONLY_NEW = false; -if (ONLY_NEW) { +const manifestMap = {}; +if (fs.existsSync(jsonPath)) { try { const existingJson = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')); - json = existingJson; for (const plugin of existingJson) { - existingPlugins[plugin.id] = plugin; + manifestMap[plugin.id] = plugin; } } catch (e) { console.warn('Failed to parse existing plugins.json:', e); } } +if (Object.keys(manifestMap).length === 0) { + ONLY_NEW = false; +} + // Simple semver comparison: "1.2.3" < "1.2.4" function compareVersions(a, b) { const pa = a.split('.').map(Number); @@ -85,7 +86,8 @@ const proxy = createRecursiveProxy(); const _require = () => proxy; -const COMPILED_PLUGIN_DIR = './.js/plugins'; +const DEST_PLUGIN_DIR = './.js/plugins'; +const COMPILED_PLUGIN_DIR = process.env.COMPILED_DIR || DEST_PLUGIN_DIR; for (let language in languages) { console.log( @@ -122,8 +124,8 @@ for (let language in languages) { // --only-new logic if ( ONLY_NEW && - existingPlugins[id] && - compareVersions(existingPlugins[id].version, version) >= 0 + manifestMap[id] && + compareVersions(manifestMap[id].version, version) >= 0 ) { // console.log(` Skipping ${name} (${id}) - not newer`, '\ršŸ”'); return; @@ -147,7 +149,19 @@ for (let language in languages) { } else { pluginSet.add(id); } - json.push(info); + + if (COMPILED_PLUGIN_DIR !== DEST_PLUGIN_DIR) { + const destLangPath = path.join(DEST_PLUGIN_DIR, language.toLowerCase()); + if (!fs.existsSync(destLangPath)) { + fs.mkdirSync(destLangPath, { recursive: true }); + } + fs.copyFileSync( + path.join(langPath, plugin), + path.join(destLangPath, plugin) + ); + } + + manifestMap[id] = info; pluginsPerLanguage[language] += 1; if (filters !== undefined) { @@ -163,6 +177,8 @@ for (let language in languages) { }); } +const json = Object.values(manifestMap); + json.sort((a, b) => { if (a.lang === b.lang) return a.id.localeCompare(b.id); return 0; diff --git a/scripts/publish-plugins.sh b/scripts/publish-plugins.sh index fba363c69..69dbacb1c 100755 --- a/scripts/publish-plugins.sh +++ b/scripts/publish-plugins.sh @@ -32,9 +32,10 @@ if [[ "$1" == "--all-branches" ]]; then npm run clean:multisrc npm run build:multisrc echo "Compiling TypeScript..." - npx tsc --project tsconfig.production.json + npx tsc --project tsconfig.production.json --outDir ./.js-temp/plugins echo "# $branch" >> $GITHUB_STEP_SUMMARY - BRANCH=$dist npm run build:manifest -- --only-new 2>> $GITHUB_STEP_SUMMARY + BRANCH=$dist COMPILED_DIR=./.js-temp/plugins npm run build:manifest -- --only-new 2>> $GITHUB_STEP_SUMMARY + rm -rf .js-temp if [ ! -d ".dist" ] || [ -z "$(ls -A .dist)" ]; then echo "āŒ ERROR: Manifest generation failed - .dist is missing or empty" exit 1