Skip to content
Open
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
59 changes: 51 additions & 8 deletions plugins/indonesian/sakuranovel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@ 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';
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[] = [];
Expand Down Expand Up @@ -136,15 +145,49 @@ class SakuraNovel implements Plugin.PluginBase {
const body = await result.text();

const loadedCheerio = parseHTML(body);
// Inline ad blocks (adsbygoogle scripts, <ins> 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) {
const content = loadedCheerio(selector).first();
if (!content.length) continue;
content.find(`p:contains('${PROMO_TEXT}')`).remove();
const chapterText = (content.html() || '').trim();
if (chapterText) return chapterText;
}

const divi = loadedCheerio("div:contains('Daftar Isi') +")
.find('div:first')
.attr('class');
loadedCheerio(`.${divi}`).remove();
const chapterText =
loadedCheerio("div:contains('Daftar Isi') +").html() || '';
// 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;

return chapterText;
// 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(
Expand Down
Loading