From a89c3123fc09155f0cba61123e88e3a7939613f2 Mon Sep 17 00:00:00 2001 From: D3ICIDE <183315953+D3ICIDE@users.noreply.github.com> Date: Wed, 23 Sep 2026 00:53:12 +0530 Subject: [PATCH 1/9] Hangul Planet seprate plugin --- plugins/english/HangulPlanet.ts | 144 ++++++++++++++++++ plugins/multisrc/madara/sources.json | 9 +- .../madara => src/en}/hangulplanet/icon.png | Bin 3 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 plugins/english/HangulPlanet.ts rename public/static/{multisrc/madara => src/en}/hangulplanet/icon.png (100%) diff --git a/plugins/english/HangulPlanet.ts b/plugins/english/HangulPlanet.ts new file mode 100644 index 000000000..b627757e9 --- /dev/null +++ b/plugins/english/HangulPlanet.ts @@ -0,0 +1,144 @@ +import { fetchApi } from '@libs/fetch'; +import { Filters } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +class HangulPlanetPlugin implements Plugin.PluginBase { + id = 'hangulplanet'; + name = 'HangulPlanet'; + icon = 'src/en/hangulplanet/icon.png'; + site = 'https://hangulplanet.com'; + version = '1.0.0'; + + /** + * Fetches a URL and loads it into Cheerio, guarding against + * Cloudflare bot-verification / captcha interstitials so a + * "Just a moment..." page doesn't get silently parsed as real content. + */ + async getCheerio(url: string, search = false): Promise { + const r = await fetchApi(url); + if (!r.ok && !search) { + throw new Error( + 'Could not reach site (' + r.status + ') try to open in webview.', + ); + } + const $ = parseHTML(await r.text()); + const title = $('title').text().trim(); + if ( + title === 'Bot Verification' || + title === 'You are being redirected...' || + title === 'Un instant...' || + title === 'Just a moment...' || + title === 'Redirecting...' + ) { + throw new Error('Captcha error, please open in webview'); + } + return $; + } + + private parseNovelCards($: CheerioAPI): Plugin.NovelItem[] { + const novels: Plugin.NovelItem[] = []; + $('a[href^="/novel/"]').each((_, el) => { + const href = $(el).attr('href') || ''; + const img = $(el).find('img.object-cover').first(); + const name = img.attr('alt')?.trim() || ''; + const src = img.attr('src'); + const cover = src ? this.site + src : defaultCover; + if (!href || !name) return; + novels.push({ name, cover, path: href }); + }); + return novels; + } + + async popularNovels( + pageNo: number, + { showLatestNovels }: Plugin.PopularNovelsOptions, + ): Promise { + // Site only has a handful of novels and no pagination has been + // confirmed to exist yet, so anything past page 1 returns empty. + if (pageNo > 1) return []; + + const sort = showLatestNovels ? 'latest' : 'popular'; + const $ = await this.getCheerio(`${this.site}/browse?sort=${sort}`); + return this.parseNovelCards($); + } + + async searchNovels(searchTerm: string): Promise { + const url = `${this.site}/browse?q=${encodeURIComponent(searchTerm)}`; + const $ = await this.getCheerio(url, true); + return this.parseNovelCards($); + } + + async parseNovel(novelPath: string): Promise { + const $ = await this.getCheerio(this.site + novelPath); + console.log( + 'chapter links found:', + $('#chapters a[href*="/chapter-"]').length, + ); + + const coverSrc = $('img.object-cover').first().attr('src'); + const heading = $('h1').first(); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: heading.text().trim(), + author: heading.next('p').text().trim() || undefined, + cover: coverSrc ? this.site + coverSrc : defaultCover, + summary: $('.prose').first().text().trim(), + status: NovelStatus.Unknown, + }; + + const statusText = $('span[data-slot="badge"]').first().text().trim(); + novel.status = statusText.includes('Ongoing') + ? NovelStatus.Ongoing + : statusText.includes('Completed') + ? NovelStatus.Completed + : NovelStatus.Unknown; + + novel.genres = $('a[href*="browse?genre="]') + .map((_, el) => $(el).text().trim()) + .get() + .join(', '); + + const chapters: Plugin.ChapterItem[] = []; + $('#chapters a[href*="/chapter-"]').each((_, el) => { + const href = $(el).attr('href') || ''; + const name = $(el).find('.line-clamp-1').text().trim(); + const chapterNumMatch = href.match(/chapter-(\d+)$/); + const chapterNumber = chapterNumMatch + ? parseInt(chapterNumMatch[1], 10) + : 0; + const releaseTime = $(el).find('time').attr('datetime') || null; + + if (!href) return; + chapters.push({ name, path: href, chapterNumber, releaseTime }); + }); + + novel.chapters = chapters.sort( + (a, b) => (a.chapterNumber ?? 0) - (b.chapterNumber ?? 0), + ); + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const $ = await this.getCheerio(this.site + chapterPath); + + const content = $('article[data-reader-article="true"] .reader-prose'); + + // Drop the translator-credit line (first

Translator: ...

) + content + .find('p') + .first() + .each((_, el) => { + if ($(el).text().trim().startsWith('Translator:')) { + $(el).remove(); + } + }); + + return content.html() || ''; + } +} + +export default new HangulPlanetPlugin(); diff --git a/plugins/multisrc/madara/sources.json b/plugins/multisrc/madara/sources.json index c5d39c9ce..af98e4945 100644 --- a/plugins/multisrc/madara/sources.json +++ b/plugins/multisrc/madara/sources.json @@ -661,14 +661,7 @@ "useNewChapterEndpoint": true } }, - { - "id": "hangulplanet", - "sourceSite": "https://hangulplanet.com/", - "sourceName": "HangulPlanet", - "options": { - "useNewChapterEndpoint": true - } - }, + { "id": "lovelyblossoms", "sourceSite": "https://lovelyblossoms.com/", diff --git a/public/static/multisrc/madara/hangulplanet/icon.png b/public/static/src/en/hangulplanet/icon.png similarity index 100% rename from public/static/multisrc/madara/hangulplanet/icon.png rename to public/static/src/en/hangulplanet/icon.png From de6db53b759ccc9f18e0725ef68a9b6ef8072492 Mon Sep 17 00:00:00 2001 From: D3ICIDE <183315953+D3ICIDE@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:43:55 +0530 Subject: [PATCH 2/9] hangul revamped --- plugins/english/HangulPlanet.ts | 127 +++++++++++++++++++++++++------- 1 file changed, 101 insertions(+), 26 deletions(-) diff --git a/plugins/english/HangulPlanet.ts b/plugins/english/HangulPlanet.ts index b627757e9..e79ded848 100644 --- a/plugins/english/HangulPlanet.ts +++ b/plugins/english/HangulPlanet.ts @@ -10,21 +10,21 @@ class HangulPlanetPlugin implements Plugin.PluginBase { name = 'HangulPlanet'; icon = 'src/en/hangulplanet/icon.png'; site = 'https://hangulplanet.com'; - version = '1.0.0'; + version = '1.0.1'; - /** - * Fetches a URL and loads it into Cheerio, guarding against - * Cloudflare bot-verification / captcha interstitials so a - * "Just a moment..." page doesn't get silently parsed as real content. - */ - async getCheerio(url: string, search = false): Promise { + private async fetchPage( + url: string, + search = false, + ): Promise<{ $: CheerioAPI; text: string }> { const r = await fetchApi(url); if (!r.ok && !search) { throw new Error( 'Could not reach site (' + r.status + ') try to open in webview.', ); } - const $ = parseHTML(await r.text()); + const text = await r.text(); + console.log('response byte length:', text.length); + const $ = parseHTML(text); const title = $('title').text().trim(); if ( title === 'Bot Verification' || @@ -35,6 +35,11 @@ class HangulPlanetPlugin implements Plugin.PluginBase { ) { throw new Error('Captcha error, please open in webview'); } + return { $, text }; + } + + async getCheerio(url: string, search = false): Promise { + const { $ } = await this.fetchPage(url, search); return $; } @@ -56,8 +61,6 @@ class HangulPlanetPlugin implements Plugin.PluginBase { pageNo: number, { showLatestNovels }: Plugin.PopularNovelsOptions, ): Promise { - // Site only has a handful of novels and no pagination has been - // confirmed to exist yet, so anything past page 1 returns empty. if (pageNo > 1) return []; const sort = showLatestNovels ? 'latest' : 'popular'; @@ -71,12 +74,68 @@ class HangulPlanetPlugin implements Plugin.PluginBase { return this.parseNovelCards($); } + /** + IMPORTANT: this site does NOT render its full chapter list as real + DOM elements. Beyond a small SSR-visible slice, the list lives + inside a Next.js RSC ("flight") payload + */ + private parseChaptersFromText( + novelPath: string, + text: string, + chapters: Plugin.ChapterItem[], + seen: Set, + ): number { + let added = 0; + + // Primary: escaped-JSON form from the RSC payload. + const hrefPattern = /\\"href\\":\\"([^"\\]*\/chapter-(\d+))\\"/g; + const WINDOW = 800; + let match: RegExpExecArray | null; + while ((match = hrefPattern.exec(text)) !== null) { + const chapterNumber = parseInt(match[2], 10); + if (seen.has(chapterNumber)) continue; + seen.add(chapterNumber); + + const windowText = text.slice(match.index, match.index + WINDOW); + + const nameMatch = windowText.match( + /\\"line-clamp-1 flex-1 text-sm\\",\\"children\\":\\"([^\\]*)\\"/, + ); + const dateMatch = windowText.match(/\\"dateTime\\":\\"([^\\]*)\\"/); + + chapters.push({ + name: nameMatch ? nameMatch[1] : `Chapter ${chapterNumber}`, + path: match[1], + chapterNumber, + releaseTime: dateMatch ? dateMatch[1] : null, + }); + added++; + } + + const $ = parseHTML(text); + $('#chapters a[href*="/chapter-"]').each((_, el) => { + const href = $(el).attr('href') || ''; + const chapterNumMatch = href.match(/chapter-(\d+)$/); + if (!href || !chapterNumMatch) return; + const chapterNumber = parseInt(chapterNumMatch[1], 10); + if (seen.has(chapterNumber)) return; + seen.add(chapterNumber); + const name = $(el).find('.line-clamp-1').text().trim(); + const releaseTime = $(el).find('time').attr('datetime') || null; + chapters.push({ + name: name || `Chapter ${chapterNumber}`, + path: href, + chapterNumber, + releaseTime, + }); + added++; + }); + + return added; + } + async parseNovel(novelPath: string): Promise { - const $ = await this.getCheerio(this.site + novelPath); - console.log( - 'chapter links found:', - $('#chapters a[href*="/chapter-"]').length, - ); + const { $, text } = await this.fetchPage(this.site + novelPath); const coverSrc = $('img.object-cover').first().attr('src'); const heading = $('h1').first(); @@ -103,18 +162,34 @@ class HangulPlanetPlugin implements Plugin.PluginBase { .join(', '); const chapters: Plugin.ChapterItem[] = []; - $('#chapters a[href*="/chapter-"]').each((_, el) => { - const href = $(el).attr('href') || ''; - const name = $(el).find('.line-clamp-1').text().trim(); - const chapterNumMatch = href.match(/chapter-(\d+)$/); - const chapterNumber = chapterNumMatch - ? parseInt(chapterNumMatch[1], 10) - : 0; - const releaseTime = $(el).find('time').attr('datetime') || null; + const seen = new Set(); - if (!href) return; - chapters.push({ name, path: href, chapterNumber, releaseTime }); - }); + const foundOnPage1 = this.parseChaptersFromText( + novelPath, + text, + chapters, + seen, + ); + console.log('chapters found on page 1 (RSC + DOM):', foundOnPage1); + + const MAX_PAGES = 100; // safety cap so a parsing quirk can't loop forever + let page = 2; + while (page <= MAX_PAGES) { + const { text: pageText } = await this.fetchPage( + `${this.site}${novelPath}?cpage=${page}`, + ); + const added = this.parseChaptersFromText( + novelPath, + pageText, + chapters, + seen, + ); + console.log(`cpage=${page} contributed ${added} new chapters`); + if (added === 0) break; + page++; + } + + console.log('total chapter links found:', chapters.length); novel.chapters = chapters.sort( (a, b) => (a.chapterNumber ?? 0) - (b.chapterNumber ?? 0), From b0204c9b6bb7b254baa5188d5cd1c3d51bb1fc34 Mon Sep 17 00:00:00 2001 From: D3ICIDE <183315953+D3ICIDE@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:52:12 +0530 Subject: [PATCH 3/9] final test --- plugins/english/HangulPlanet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/english/HangulPlanet.ts b/plugins/english/HangulPlanet.ts index e79ded848..7c69f0fc3 100644 --- a/plugins/english/HangulPlanet.ts +++ b/plugins/english/HangulPlanet.ts @@ -10,7 +10,7 @@ class HangulPlanetPlugin implements Plugin.PluginBase { name = 'HangulPlanet'; icon = 'src/en/hangulplanet/icon.png'; site = 'https://hangulplanet.com'; - version = '1.0.1'; + version = '1.0.0'; private async fetchPage( url: string, @@ -207,7 +207,7 @@ class HangulPlanetPlugin implements Plugin.PluginBase { .find('p') .first() .each((_, el) => { - if ($(el).text().trim().startsWith('Translator:')) { + if ($(el).text().trim().startsWith('TL/ED')) { $(el).remove(); } }); From 0e5bb4c4f1fd02624f006010270dae49f219e70b Mon Sep 17 00:00:00 2001 From: D3ICIDE <183315953+D3ICIDE@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:58:41 +0530 Subject: [PATCH 4/9] cleanup --- plugins/english/HangulPlanet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/english/HangulPlanet.ts b/plugins/english/HangulPlanet.ts index 7c69f0fc3..551c33fa1 100644 --- a/plugins/english/HangulPlanet.ts +++ b/plugins/english/HangulPlanet.ts @@ -202,7 +202,7 @@ class HangulPlanetPlugin implements Plugin.PluginBase { const content = $('article[data-reader-article="true"] .reader-prose'); - // Drop the translator-credit line (first

Translator: ...

) + //Remove the TL/ED credit(annoying for TTS) content .find('p') .first() From 8cc191cbe99acc35cbf5e4d938342379575d4ca7 Mon Sep 17 00:00:00 2001 From: D3ICIDE <183315953+D3ICIDE@users.noreply.github.com> Date: Wed, 23 Sep 2026 03:07:46 +0530 Subject: [PATCH 5/9] chore: retrigger CI From 9de96dacac1d16a37087cf1e1d32bf408dd6da3a Mon Sep 17 00:00:00 2001 From: Vaibhav <183315953+D3ICIDE@users.noreply.github.com> Date: Wed, 23 Sep 2026 03:20:06 +0530 Subject: [PATCH 6/9] Update version to 2.3.0 in HangulPlanet plugin Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- plugins/english/HangulPlanet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/english/HangulPlanet.ts b/plugins/english/HangulPlanet.ts index 551c33fa1..b41f6236d 100644 --- a/plugins/english/HangulPlanet.ts +++ b/plugins/english/HangulPlanet.ts @@ -10,7 +10,7 @@ class HangulPlanetPlugin implements Plugin.PluginBase { name = 'HangulPlanet'; icon = 'src/en/hangulplanet/icon.png'; site = 'https://hangulplanet.com'; - version = '1.0.0'; + version = '2.3.0'; private async fetchPage( url: string, From e63a5968164668eca38e7ce43ab679a6757971f5 Mon Sep 17 00:00:00 2001 From: D3ICIDE <183315953+D3ICIDE@users.noreply.github.com> Date: Wed, 23 Sep 2026 03:23:41 +0530 Subject: [PATCH 7/9] fix --- plugins/english/HangulPlanet.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/english/HangulPlanet.ts b/plugins/english/HangulPlanet.ts index 551c33fa1..45027bf71 100644 --- a/plugins/english/HangulPlanet.ts +++ b/plugins/english/HangulPlanet.ts @@ -10,7 +10,7 @@ class HangulPlanetPlugin implements Plugin.PluginBase { name = 'HangulPlanet'; icon = 'src/en/hangulplanet/icon.png'; site = 'https://hangulplanet.com'; - version = '1.0.0'; + version = '2.3.0'; private async fetchPage( url: string, @@ -68,7 +68,11 @@ class HangulPlanetPlugin implements Plugin.PluginBase { return this.parseNovelCards($); } - async searchNovels(searchTerm: string): Promise { + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + if (pageNo > 1) return []; const url = `${this.site}/browse?q=${encodeURIComponent(searchTerm)}`; const $ = await this.getCheerio(url, true); return this.parseNovelCards($); From c54de7edc0a8ebe4e0f0d1542f7438287b501d8e Mon Sep 17 00:00:00 2001 From: D3ICIDE <183315953+D3ICIDE@users.noreply.github.com> Date: Wed, 23 Sep 2026 09:59:11 +0530 Subject: [PATCH 8/9] added headers --- plugins/english/HangulPlanet.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/english/HangulPlanet.ts b/plugins/english/HangulPlanet.ts index 45027bf71..288b04671 100644 --- a/plugins/english/HangulPlanet.ts +++ b/plugins/english/HangulPlanet.ts @@ -12,11 +12,16 @@ class HangulPlanetPlugin implements Plugin.PluginBase { site = 'https://hangulplanet.com'; version = '2.3.0'; + private readonly headers = { + 'User-Agent': + 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36', + 'Accept-Language': 'en-US,en;q=0.9', + }; private async fetchPage( url: string, search = false, ): Promise<{ $: CheerioAPI; text: string }> { - const r = await fetchApi(url); + const r = await fetchApi(url, { headers: this.headers }); if (!r.ok && !search) { throw new Error( 'Could not reach site (' + r.status + ') try to open in webview.', @@ -211,7 +216,7 @@ class HangulPlanetPlugin implements Plugin.PluginBase { .find('p') .first() .each((_, el) => { - if ($(el).text().trim().startsWith('TL/ED')) { + if ($(el).text().trim().startsWith('TL')) { $(el).remove(); } }); From 5c88c9358c45f3626255691d7fc7e81f5015d0b8 Mon Sep 17 00:00:00 2001 From: D3ICIDE <183315953+D3ICIDE@users.noreply.github.com> Date: Wed, 23 Sep 2026 10:03:15 +0530 Subject: [PATCH 9/9] Revert "added headers" This reverts commit c54de7edc0a8ebe4e0f0d1542f7438287b501d8e. --- plugins/english/HangulPlanet.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/plugins/english/HangulPlanet.ts b/plugins/english/HangulPlanet.ts index 288b04671..45027bf71 100644 --- a/plugins/english/HangulPlanet.ts +++ b/plugins/english/HangulPlanet.ts @@ -12,16 +12,11 @@ class HangulPlanetPlugin implements Plugin.PluginBase { site = 'https://hangulplanet.com'; version = '2.3.0'; - private readonly headers = { - 'User-Agent': - 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36', - 'Accept-Language': 'en-US,en;q=0.9', - }; private async fetchPage( url: string, search = false, ): Promise<{ $: CheerioAPI; text: string }> { - const r = await fetchApi(url, { headers: this.headers }); + const r = await fetchApi(url); if (!r.ok && !search) { throw new Error( 'Could not reach site (' + r.status + ') try to open in webview.', @@ -216,7 +211,7 @@ class HangulPlanetPlugin implements Plugin.PluginBase { .find('p') .first() .each((_, el) => { - if ($(el).text().trim().startsWith('TL')) { + if ($(el).text().trim().startsWith('TL/ED')) { $(el).remove(); } });