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:
@@ -1,5 +1,6 @@
|
|||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Parser } from 'htmlparser2';
|
import { Parser } from 'htmlparser2';
|
||||||
|
import { load as parseHTML } from 'cheerio';
|
||||||
import { HTMLParser2Util, Plugin } from '@/types/plugin';
|
import { HTMLParser2Util, Plugin } from '@/types/plugin';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
import { FilterTypes, Filters } from '@libs/filterInputs';
|
import { FilterTypes, Filters } from '@libs/filterInputs';
|
||||||
@@ -19,7 +20,7 @@ class HakoPlugin implements Plugin.PluginBase {
|
|||||||
name = 'Hako';
|
name = 'Hako';
|
||||||
icon = 'src/vi/hakolightnovel/icon.png';
|
icon = 'src/vi/hakolightnovel/icon.png';
|
||||||
site = 'https://ln.hako.vn';
|
site = 'https://ln.hako.vn';
|
||||||
version = '1.1.0';
|
version = '1.1.1';
|
||||||
parseNovels(url: string) {
|
parseNovels(url: string) {
|
||||||
return fetchApi(url)
|
return fetchApi(url)
|
||||||
.then(res => res.text())
|
.then(res => res.text())
|
||||||
@@ -223,7 +224,7 @@ class HakoPlugin implements Plugin.PluginBase {
|
|||||||
this.part++;
|
this.part++;
|
||||||
}
|
}
|
||||||
this.tempChapter = {
|
this.tempChapter = {
|
||||||
path: attribs['href'],
|
path: attribs['href']?.replace(/^(https?:\/\/[^/]+)/, ''),
|
||||||
name: chapterName,
|
name: chapterName,
|
||||||
page: this.currentVolume,
|
page: this.currentVolume,
|
||||||
chapterNumber: chapterNumber,
|
chapterNumber: chapterNumber,
|
||||||
@@ -401,12 +402,47 @@ class HakoPlugin implements Plugin.PluginBase {
|
|||||||
parseChapter(chapterPath: string): Promise<string> {
|
parseChapter(chapterPath: string): Promise<string> {
|
||||||
return fetchApi(this.site + chapterPath)
|
return fetchApi(this.site + chapterPath)
|
||||||
.then(res => res.text())
|
.then(res => res.text())
|
||||||
.then(
|
.then(html => {
|
||||||
html =>
|
const $ = parseHTML(html);
|
||||||
html.match(
|
const protectedEl = $('#chapter-c-protected');
|
||||||
/(<div id="chapter-content".+?>[^]+)<div style="text-align: center;/,
|
if (protectedEl.length) {
|
||||||
)?.[1] || 'Không tìm thấy nội dung',
|
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(
|
searchNovels(
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
@@ -495,4 +531,24 @@ class HakoPlugin implements Plugin.PluginBase {
|
|||||||
} satisfies Filters;
|
} 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();
|
export default new HakoPlugin();
|
||||||
|
|||||||
Reference in New Issue
Block a user