From 9e7ff49f3e527a95ba3f382c4a358a7b3a0b7bda Mon Sep 17 00:00:00 2001 From: RibatTRW Date: Thu, 24 Sep 2026 19:05:46 +0800 Subject: [PATCH] fix(indonesian/indowebnovel): restore chapter list and chapter text (#2528) --- plugins/indonesian/indowebnovel.ts | 99 +++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 23 deletions(-) diff --git a/plugins/indonesian/indowebnovel.ts b/plugins/indonesian/indowebnovel.ts index 121d4d6..5252854 100644 --- a/plugins/indonesian/indowebnovel.ts +++ b/plugins/indonesian/indowebnovel.ts @@ -9,20 +9,53 @@ class IndoWebNovel implements Plugin.PluginBase { name = 'IndoWebNovel'; icon = 'src/id/indowebnovel/icon.png'; site = 'https://indowebnovel.id/'; - version = '1.2.3'; + version = '1.3.1'; + + private 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', + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + 'Accept-Language': 'id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7', + Referer: 'https://indowebnovel.id/', + }; + + private async fetchPage(url: string) { + const res = await fetchApi(url, { headers: this.headers }); + if (!res.ok) { + throw Object.assign(new Error('Request failed: ' + res.status), { + status: res.status, + }); + } + return res.text(); + } parseNovels(loadedCheerio: CheerioAPI) { const novels: Plugin.NovelItem[] = []; - loadedCheerio('.flexbox2-item').each((i, el) => { - const novelName = loadedCheerio(el) - .find('.flexbox2-title span') - .first() - .text(); - const novelCover = loadedCheerio(el).find('img').attr('src'); - const novelUrl = loadedCheerio(el) - .find('.flexbox2-content > a') - .attr('href'); + // The search/paged listing (`page//?s`) renders `.flexbox2-item` + // cards, while the front page renders `.flexbox3-item` cards (latest + // updates) and `.popular .flexbox-item` cards (ranked list). Accept + // every variant so a listing page never parses to zero novels just + // because the server returned another listing layout. + const items = loadedCheerio('.flexbox2-item').length + ? loadedCheerio('.flexbox2-item') + : loadedCheerio('.flexbox3-item').length + ? loadedCheerio('.flexbox3-item') + : loadedCheerio('.popular .flexbox-item'); + + items.each((i, el) => { + const item = loadedCheerio(el); + const novelName = ( + item.find('.flexbox2-title span').first().text() || + item.find('.title a').first().text() || + item.find('.flexbox-title').first().text() + ).trim(); + const novelCover = item.find('img').attr('src'); + const novelUrl = + item.find('.flexbox2-content > a').attr('href') || + item.find('.flexbox3-content > a').attr('href') || + item.find('a').attr('href'); if (!novelUrl) return; @@ -52,16 +85,14 @@ class IndoWebNovel implements Plugin.PluginBase { link += filters.genre.value.map(i => `&genre[]=${i}`).join(''); */ const link = this.site + `page/${page}/?s`; - const result = await fetchApi(link); - const body = await result.text(); + const body = await this.fetchPage(link); const loadedCheerio = parseHTML(body); return this.parseNovels(loadedCheerio); } async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath); - const body = await result.text(); + const body = await this.fetchPage(this.site + novelPath); const loadedCheerio = parseHTML(body); loadedCheerio('.series-synops div').remove(); @@ -88,16 +119,36 @@ class IndoWebNovel implements Plugin.PluginBase { const chapters: Plugin.ChapterItem[] = []; - loadedCheerio('.series-chapterlist li').each((i, el) => { - const chapterName = loadedCheerio(el).find('a').text().trim(); + loadedCheerio('.series-chapterlists li').each((i, el) => { + // The list entry also holds a `span.date`; drop it so the chapter name + // does not end with the release date. + const chapterName = loadedCheerio(el) + .find('a span') + .not('.date') + .first() + .text() + .replace(/\s+/g, ' ') + .trim(); const chapterUrl = loadedCheerio(el).find('a').attr('href'); if (!chapterUrl) return; - chapters.push({ + const chapter: Plugin.ChapterItem = { name: chapterName, path: chapterUrl.slice(this.site.length), - }); + }; + + // Each entry also holds a `span.date` like "October 23, 2025". + const releaseDate = loadedCheerio(el) + .find('span.date') + .first() + .text() + .trim(); + if (releaseDate && !isNaN(Date.parse(releaseDate))) { + chapter.releaseTime = new Date(releaseDate).toISOString(); + } + + chapters.push(chapter); }); novel.chapters = chapters.reverse(); @@ -106,12 +157,15 @@ class IndoWebNovel implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath); - const body = await result.text(); + const body = await this.fetchPage(this.site + chapterPath); const loadedCheerio = parseHTML(body); - const chapterText = loadedCheerio('.adsads').html() || ''; + // The chapter body is nested in `main .content .container` behind a div + // whose class is a rotating random token (the previous hardcoded name, + // `.adsads`, no longer exists), so use the stable inner `#content` + // element instead of that wrapper class. + const chapterText = loadedCheerio('main #content').html() || ''; return chapterText; } @@ -128,8 +182,7 @@ class IndoWebNovel implements Plugin.PluginBase { link += this.filters.lang.value.map(i => `&country[]=${i}`).join(''); */ const link = this.site + `page/${page}/?s=${searchTerm}`; - const result = await fetchApi(link); - const body = await result.text(); + const body = await this.fetchPage(link); const loadedCheerio = parseHTML(body); return this.parseNovels(loadedCheerio);