From 341e19afe58b5d5b084eb899ea5f76443b261f77 Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Fri, 25 Sep 2026 20:03:39 +0800 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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() || ''; }