From 3609f4f5b0c2bece5334670c136aa97d88a7182a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20Duy=20To=C3=A0n?= <93266912+tranduytoan@users.noreply.github.com> Date: Wed, 15 Jul 2026 03:02:24 +0700 Subject: [PATCH] fix(hako): Fix Chapter Path Parsing, Decrypt Protected Content, Format Notes, and Remove Suggestion Banners (#2270) * fix(vi/Hako): fix chapter path parsing and decrypt protected chapter content - Fix `parseNovel` extracting full URL instead of relative path for chapters. - Implement decryption logic for protected chapter content (`#chapter-c-protected`) using XOR shuffle, base64 reverse, and plain base64 decoding. * fix(vi/hako): format note tags, suggestion banners, and bump version to 1.1.1 - Format `[note(\d+)]` tags into HTML span/anchor tags for proper rendering (following the original website's logic) - Remove recommendation/suggestion banners at the end of chapters - Bump plugin version to 1.1.1 --- plugins/vietnamese/LNHako.ts | 72 ++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 8 deletions(-) diff --git a/plugins/vietnamese/LNHako.ts b/plugins/vietnamese/LNHako.ts index 042bdcf..547e88f 100644 --- a/plugins/vietnamese/LNHako.ts +++ b/plugins/vietnamese/LNHako.ts @@ -1,5 +1,6 @@ import { fetchApi } from '@libs/fetch'; import { Parser } from 'htmlparser2'; +import { load as parseHTML } from 'cheerio'; import { HTMLParser2Util, Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; import { FilterTypes, Filters } from '@libs/filterInputs'; @@ -19,7 +20,7 @@ class HakoPlugin implements Plugin.PluginBase { name = 'Hako'; icon = 'src/vi/hakolightnovel/icon.png'; site = 'https://ln.hako.vn'; - version = '1.1.0'; + version = '1.1.1'; parseNovels(url: string) { return fetchApi(url) .then(res => res.text()) @@ -223,7 +224,7 @@ class HakoPlugin implements Plugin.PluginBase { this.part++; } this.tempChapter = { - path: attribs['href'], + path: attribs['href']?.replace(/^(https?:\/\/[^/]+)/, ''), name: chapterName, page: this.currentVolume, chapterNumber: chapterNumber, @@ -401,12 +402,47 @@ class HakoPlugin implements Plugin.PluginBase { parseChapter(chapterPath: string): Promise { return fetchApi(this.site + chapterPath) .then(res => res.text()) - .then( - html => - html.match( - /(
[^]+)
[note]', + ); + + protectedEl.replaceWith(decryptedHtml); + } + } + + $('a[href^="/truyen/"]').has('img[src*="chapter-banners"]').remove(); + + return $('#chapter-content').html() || 'Không tìm thấy nội dung'; + }); } searchNovels( searchTerm: string, @@ -495,4 +531,24 @@ class HakoPlugin implements Plugin.PluginBase { } satisfies Filters; } +function decodeBase64(str: string): Uint8Array { + return Uint8Array.from(atob(str), c => c.charCodeAt(0)); +} + +function decryptXor(ciphertext: string, key: string): string { + const data = decodeBase64(ciphertext); + const keyLen = key.length; + const decrypted = data.map((byte, i) => byte ^ key.charCodeAt(i % keyLen)); + return new TextDecoder('utf-8').decode(decrypted); +} + +function decryptBase64Reverse(ciphertext: string): string { + const reversed = ciphertext.split('').reverse().join(''); + return new TextDecoder('utf-8').decode(decodeBase64(reversed)); +} + +function decryptNone(ciphertext: string): string { + return new TextDecoder('utf-8').decode(decodeBase64(ciphertext)); +} + export default new HakoPlugin();