Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 99 additions & 60 deletions plugins/english/wtrlab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand Down Expand Up @@ -152,7 +152,9 @@ class WTRLAB implements Plugin.PluginBase {
this.trace = $('meta[name="sentry-trace"]').attr('content') ?? '';
}

async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
async parseNovel(
novelPath: string,
): Promise<Plugin.SourceNovel & { totalPages: number }> {
const body = await fetchApi(this.site + novelPath).then(res => res.text());
const loadedCheerio = parseHTML(body);

Expand All @@ -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) {
Expand All @@ -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:
Expand Down Expand Up @@ -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 = [];
Expand All @@ -384,6 +390,7 @@ class WTRLAB implements Plugin.PluginBase {
});
}

novel.totalPages = Math.max(1, Math.ceil(chapterCount / 250));
novel.chapters = chapters;

return novel;
Expand Down Expand Up @@ -620,57 +627,89 @@ class WTRLAB implements Plugin.PluginBase {
return htmlString;
}

async fetchAllChapters(
async fetchPageChapters(
rawId: number,
pageNumber: number,
totalChapters: number,
slug: string,
): Promise<Plugin.ChapterItem[]> {
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<Plugin.SourcePage> {
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(
Expand Down
36 changes: 26 additions & 10 deletions scripts/build-plugin-manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions scripts/publish-plugins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading