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
This commit is contained in:
Trần Duy Toàn
2026-07-15 03:02:24 +07:00
committed by GitHub
parent a14df52bee
commit 3609f4f5b0
+64 -8
View File
@@ -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<string> {
return fetchApi(this.site + chapterPath)
.then(res => res.text())
.then(
html =>
html.match(
/(<div id="chapter-content".+?>[^]+)<div style="text-align: center;/,
)?.[1] || 'Không tìm thấy nội dung',
);
.then(html => {
const $ = parseHTML(html);
const protectedEl = $('#chapter-c-protected');
if (protectedEl.length) {
const s = protectedEl.attr('data-s') || 'none';
const k = protectedEl.attr('data-k') || '';
let c: string[] = [];
try {
c = JSON.parse(protectedEl.attr('data-c') || '[]');
} catch {
// ignore
}
if (Array.isArray(c) && c.length > 0) {
c.sort((a, b) => +a.substring(0, 4) - +b.substring(0, 4));
const decryptedChunks = c.map(chunk => {
const ciphertext = chunk.substring(4);
if (s === 'xor_shuffle') {
return decryptXor(ciphertext, k);
} else if (s === 'base64_reverse') {
return decryptBase64Reverse(ciphertext);
} else {
return decryptNone(ciphertext);
}
});
let decryptedHtml = decryptedChunks.join('');
decryptedHtml = decryptedHtml.replace(
/\[note(\d+)\]/gi,
'<span id="anchor-note$1" class="note-icon none-print inline note-tooltip" data-tooltip-content="#note$1 .note-content" data-note-id="note$1"><i class="fas fa-sticky-note"></i></span><a id="anchor-note$1" class="inline-print none" href="#note$1">[note]</a>',
);
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();