From 341e19afe58b5d5b084eb899ea5f76443b261f77 Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Fri, 25 Sep 2026 20:03:39 +0800 Subject: [PATCH 1/5] fix(indonesian/sakuranovel): update chapter body selector for current site markup The chapter pages now render the body inside .tldariinggrissendiribrojangancopy .entry-content as p.ds-markdown-paragraph elements, so the old div:contains('Daftar Isi') + anchor resolves to nothing and parseChapter returned an empty string ("tidak ada konten yang bisa dibaca"). Extract from the new container, strip the site promo paragraph, fall back to the legacy selector. Closes #2571 Co-Authored-By: Muse Spark --- plugins/indonesian/sakuranovel.ts | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/plugins/indonesian/sakuranovel.ts b/plugins/indonesian/sakuranovel.ts index 6200861cb..849812a70 100644 --- a/plugins/indonesian/sakuranovel.ts +++ b/plugins/indonesian/sakuranovel.ts @@ -8,7 +8,7 @@ class SakuraNovel implements Plugin.PluginBase { name = 'SakuraNovel'; icon = 'src/id/sakuranovel/icon.png'; site = 'https://sakuranovel.id/'; - version = '1.0.1'; + version = '1.0.2'; parseNovels(loadedCheerio: CheerioAPI) { const novels: Plugin.NovelItem[] = []; @@ -137,14 +137,25 @@ class SakuraNovel implements Plugin.PluginBase { const loadedCheerio = parseHTML(body); - const divi = loadedCheerio("div:contains('Daftar Isi') +") - .find('div:first') - .attr('class'); - loadedCheerio(`.${divi}`).remove(); - const chapterText = - loadedCheerio("div:contains('Daftar Isi') +").html() || ''; + const contentSelectors = [ + '.tldariinggrissendiribrojangancopy .entry-content', + '.entry-content', + ]; + for (const selector of contentSelectors) { + const content = loadedCheerio(selector).first(); + if (!content.length) continue; + content.find("p:contains('Baca novel lain di sakuranovel')").remove(); + const chapterText = (content.html() || '').trim(); + if (chapterText) return chapterText; + } - return chapterText; + let paragraphs = ''; + loadedCheerio('p.ds-markdown-paragraph').each((i, el) => { + paragraphs += loadedCheerio(el).toString(); + }); + if (paragraphs.trim()) return paragraphs; + + return loadedCheerio("div:contains('Daftar Isi') +").html() || ''; } async searchNovels( From 31cd43d98e91f9ad47656a9d138ce9e71d64fa22 Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Fri, 25 Sep 2026 20:23:24 +0800 Subject: [PATCH 2/5] fix(indonesian/sakuranovel): restore legacy inner-div cleanup on Daftar Isi fallback On the retained legacy path, strip the first inner div (anti-scrape / navigation markup) before returning the sibling HTML, mirroring the v1.0.1 cleanup. Guarded against multi-class values. Addresses Greptile review on #2573. Co-Authored-By: Muse Spark --- plugins/indonesian/sakuranovel.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/indonesian/sakuranovel.ts b/plugins/indonesian/sakuranovel.ts index 849812a70..fa2e38400 100644 --- a/plugins/indonesian/sakuranovel.ts +++ b/plugins/indonesian/sakuranovel.ts @@ -155,7 +155,10 @@ class SakuraNovel implements Plugin.PluginBase { }); if (paragraphs.trim()) return paragraphs; - return loadedCheerio("div:contains('Daftar Isi') +").html() || ''; + const legacy = loadedCheerio("div:contains('Daftar Isi') +").first(); + const legacyInner = legacy.find('div:first').attr('class'); + if (legacyInner) loadedCheerio(`.${legacyInner.split(' ')[0]}`).remove(); + return legacy.html() || ''; } async searchNovels( From 81e968cf4c8235f7d266e125a8da04a18442d0a5 Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Fri, 25 Sep 2026 20:35:39 +0800 Subject: [PATCH 3/5] fix(indonesian/sakuranovel): exclude promo paragraph in ds-markdown fallback The p.ds-markdown-paragraph fallback concatenated every match, including the site promo line. Skip paragraphs containing 'Baca novel lain di sakuranovel', mirroring the entry-content branches above. Co-Authored-By: Muse Spark --- plugins/indonesian/sakuranovel.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/indonesian/sakuranovel.ts b/plugins/indonesian/sakuranovel.ts index fa2e38400..e485b26a0 100644 --- a/plugins/indonesian/sakuranovel.ts +++ b/plugins/indonesian/sakuranovel.ts @@ -151,7 +151,9 @@ class SakuraNovel implements Plugin.PluginBase { let paragraphs = ''; loadedCheerio('p.ds-markdown-paragraph').each((i, el) => { - paragraphs += loadedCheerio(el).toString(); + const para = loadedCheerio(el); + if (para.text().includes('Baca novel lain di sakuranovel')) return; + paragraphs += para.toString(); }); if (paragraphs.trim()) return paragraphs; From 9db150f17901f7e6a3cae6a603402aca39775311 Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Fri, 25 Sep 2026 21:25:33 +0800 Subject: [PATCH 4/5] fix(indonesian/sakuranovel): name fallback tiers and scope legacy removal Apply Clean Coder review: hoist content selectors and promo text to commented constants, scope the legacy class removal to the chapter container, and use map/get/join for paragraph assembly. Co-Authored-By: firstmate-crewmate --- plugins/indonesian/sakuranovel.ts | 36 ++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/plugins/indonesian/sakuranovel.ts b/plugins/indonesian/sakuranovel.ts index e485b26a0..4ccefebbe 100644 --- a/plugins/indonesian/sakuranovel.ts +++ b/plugins/indonesian/sakuranovel.ts @@ -3,6 +3,15 @@ import { fetchApi } from '@libs/fetch'; import { Plugin } from '@/types/plugin'; import { Filters, FilterTypes } from '@libs/filterInputs'; +// `.tldariinggrissendiribrojangancopy` is the site's own obfuscated content wrapper class, not a typo — keep it in sync with the live markup. +const CHAPTER_CONTENT_SELECTORS = [ + '.tldariinggrissendiribrojangancopy .entry-content', + '.entry-content', +]; + +// Promo paragraph the site injects into chapter bodies; rewordings only need updating here. +const PROMO_TEXT = 'Baca novel lain di sakuranovel'; + class SakuraNovel implements Plugin.PluginBase { id = 'sakura.id'; name = 'SakuraNovel'; @@ -137,29 +146,30 @@ class SakuraNovel implements Plugin.PluginBase { const loadedCheerio = parseHTML(body); - const contentSelectors = [ - '.tldariinggrissendiribrojangancopy .entry-content', - '.entry-content', - ]; - for (const selector of contentSelectors) { + // Tier 1: themed content container — the obfuscated parent class marks the real chapter body when the site serves its full theme markup. + for (const selector of CHAPTER_CONTENT_SELECTORS) { const content = loadedCheerio(selector).first(); if (!content.length) continue; - content.find("p:contains('Baca novel lain di sakuranovel')").remove(); + content.find(`p:contains('${PROMO_TEXT}')`).remove(); const chapterText = (content.html() || '').trim(); if (chapterText) return chapterText; } - let paragraphs = ''; - loadedCheerio('p.ds-markdown-paragraph').each((i, el) => { - const para = loadedCheerio(el); - if (para.text().includes('Baca novel lain di sakuranovel')) return; - paragraphs += para.toString(); - }); + // Tier 2: markdown-rendered chapters expose one paragraph per element. + const paragraphs = loadedCheerio('p.ds-markdown-paragraph') + .map((i, el) => { + const para = loadedCheerio(el); + if (para.text().includes(PROMO_TEXT)) return ''; + return para.toString(); + }) + .get() + .join(''); if (paragraphs.trim()) return paragraphs; + // Tier 3 (last resort): the legacy layout locates the body as the sibling after the 'Daftar Isi' div — fragile by construction since :contains matches every ancestor, so this stays last and scoped to the container only. const legacy = loadedCheerio("div:contains('Daftar Isi') +").first(); const legacyInner = legacy.find('div:first').attr('class'); - if (legacyInner) loadedCheerio(`.${legacyInner.split(' ')[0]}`).remove(); + if (legacyInner) legacy.find(`.${legacyInner.split(' ')[0]}`).remove(); return legacy.html() || ''; } From e9f65cb13f1dbe5800baaebb45fdc1cf7fe4f11d Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Tue, 29 Sep 2026 10:40:08 +0800 Subject: [PATCH 5/5] fix(indonesian/sakuranovel): find body between chapter navs and strip ad scripts On real archived chapter pages (Nov 2024 - Apr 2025), the body sits in a rotating-class wrapper after an empty "please stop scrape my site" decoy, with no .entry-content or ds-markdown paragraphs, so every tier returned whitespace. Take the first non-empty sibling between the top and bottom .entry-pagination instead of the Daftar Isi sibling, and only drop the first inner div when it is an empty ad wrapper (on that layout it is the text itself). Also remove script/style/ins/noscript up front: on the 2023 layout the .entry-content tier returned inline adsbygoogle scripts that v1.0.1 had stripped. --- plugins/indonesian/sakuranovel.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/plugins/indonesian/sakuranovel.ts b/plugins/indonesian/sakuranovel.ts index 4ccefebbe..abac3238e 100644 --- a/plugins/indonesian/sakuranovel.ts +++ b/plugins/indonesian/sakuranovel.ts @@ -145,6 +145,8 @@ class SakuraNovel implements Plugin.PluginBase { const body = await result.text(); const loadedCheerio = parseHTML(body); + // Inline ad blocks (adsbygoogle scripts, slots, sticky-ad styles) sit inside the chapter body on some layouts. + loadedCheerio('script, style, ins, noscript').remove(); // Tier 1: themed content container — the obfuscated parent class marks the real chapter body when the site serves its full theme markup. for (const selector of CHAPTER_CONTENT_SELECTORS) { @@ -166,11 +168,26 @@ class SakuraNovel implements Plugin.PluginBase { .join(''); if (paragraphs.trim()) return paragraphs; - // Tier 3 (last resort): the legacy layout locates the body as the sibling after the 'Daftar Isi' div — fragile by construction since :contains matches every ancestor, so this stays last and scoped to the container only. - const legacy = loadedCheerio("div:contains('Daftar Isi') +").first(); - const legacyInner = legacy.find('div:first').attr('class'); - if (legacyInner) legacy.find(`.${legacyInner.split(' ')[0]}`).remove(); - return legacy.html() || ''; + // Tier 3 (last resort): the body is the first non-empty sibling between the top and bottom chapter navigation. Its wrapper class rotates, and an empty anti-scrape decoy div can sit in between. + const legacy = loadedCheerio('.entry-pagination') + .first() + .nextUntil('.entry-pagination') + .filter((i, el) => { + const sibling = loadedCheerio(el); + return !!sibling.text().trim() || sibling.find('img').length > 0; + }) + .first(); + legacy.find(`p:contains('${PROMO_TEXT}')`).remove(); + // Older layouts open the body with an ad wrapper div whose class repeats around every ad slot; drop it only when it is empty, as on some layouts the first div is the text itself. + const legacyInner = legacy.find('div').first(); + const legacyInnerClass = legacyInner.attr('class'); + if ( + legacyInnerClass && + !legacyInner.text().trim() && + !legacyInner.find('img').length + ) + legacy.find(`.${legacyInnerClass.split(' ')[0]}`).remove(); + return (legacy.html() || '').trim(); } async searchNovels(