From da48e9f4f44242ff9f442619eb5f450f3af53d54 Mon Sep 17 00:00:00 2001 From: Yemen Date: Sat, 12 Sep 2026 21:12:32 +0300 Subject: [PATCH 1/3] =?UTF-8?q?feat(ar/mknov):=20add=20=D9=85=D9=85=D9=84?= =?UTF-8?q?=D9=83=D9=87=20=D8=A7=D9=84=D8=B1=D9=88=D8=A7=D9=8A=D8=A7=D8=AA?= =?UTF-8?q?=20plugin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add source plugin for mknov.com (مملكه الروايات), an Arabic novel site. Features: - popularNovels, parseNovel, parseChapter, searchNovels, resolveUrl - Decodes the site's font-cmap obfuscation (19 rotating cipher maps) - Handles Next.js RSC flight payload extraction for chapter content Co-Authored-By: Claude Code --- plugins/arabic/mknov.ts | 1047 +++++++++++++++++++++++++++ public/static/src/ar/mknov/icon.png | Bin 0 -> 7487 bytes 2 files changed, 1047 insertions(+) create mode 100644 plugins/arabic/mknov.ts create mode 100644 public/static/src/ar/mknov/icon.png diff --git a/plugins/arabic/mknov.ts b/plugins/arabic/mknov.ts new file mode 100644 index 000000000..d871c2930 --- /dev/null +++ b/plugins/arabic/mknov.ts @@ -0,0 +1,1047 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; + +type MKWork = { + id: number; + slug: string; + title: string; + titleAr: string; + author?: string | null; + translator?: string | null; + status?: string | null; + year?: number | null; + totalViews?: number; + totalChapters?: number; + genres?: string[]; + description?: string; + image?: string; +}; + +type MKChapter = { + id: number; + chapterNumber: number; + chapterTitle: string; + publishDate?: string; + views?: number; +}; + +type MKVolume = { + volumeNumber: number; + title: string; + chapters: MKChapter[]; +}; + +type MKWorkListResponse = MKWork[]; // /api/works returns an array directly + +/** + * Substitution maps extracted from each `protected-font-N.woff2` + * version that the site rotates through. The chapter RSC content is + * encrypted by mapping each real Arabic letter to a sibling, then + * displayed through a @font-face whose glyph cmap holds the cipher. + * The page tells us which font (and therefore which cipher) applies via + * the `protected-font-N` name in the chapter HTML. + * + * Each map: raw codepoint in the RSC text -> real Arabic letter. + * The site also rotates long dashes `―` per map, omitted here. + */ +const FONT_CMAPS: Record> = { + 1: { + 'آ': 'د', + 'أ': 'إ', + 'ؤ': 'ر', + 'إ': 'ا', + 'ئ': 'غ', + 'ا': 'أ', + 'ب': 'ن', + 'ة': 'ز', + 'ت': 'ض', + 'ث': 'ت', + 'ج': 'ث', + 'ح': 'ع', + 'خ': 'ب', + 'د': 'ة', + 'ذ': 'آ', + 'ر': 'ذ', + 'ز': 'و', + 'س': 'ك', + 'ش': 'ف', + 'ص': 'م', + 'ض': 'ظ', + 'ط': 'س', + 'ظ': 'ق', + 'ع': 'ح', + 'غ': 'ئ', + 'ف': 'ش', + 'ق': 'ي', + 'ك': 'ص', + 'ل': 'ط', + 'م': 'ه', + 'ن': 'ج', + 'ه': 'ل', + 'و': 'ؤ', + 'ي': 'خ', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 2: { + 'آ': 'أ', + 'أ': 'ا', + 'ؤ': 'و', + 'إ': 'ز', + 'ئ': 'ق', + 'ا': 'ؤ', + 'ب': 'غ', + 'ة': 'ذ', + 'ت': 'ئ', + 'ث': 'ظ', + 'ج': 'ب', + 'ح': 'ه', + 'خ': 'ج', + 'د': 'آ', + 'ذ': 'ة', + 'ر': 'إ', + 'ز': 'د', + 'س': 'م', + 'ش': 'ض', + 'ص': 'س', + 'ض': 'ت', + 'ط': 'ح', + 'ظ': 'ف', + 'ع': 'ط', + 'غ': 'خ', + 'ف': 'ي', + 'ق': 'ن', + 'ك': 'ص', + 'ل': 'ع', + 'م': 'ل', + 'ن': 'ش', + 'ه': 'ك', + 'و': 'ر', + 'ي': 'ث', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 3: { + 'آ': 'ز', + 'أ': 'ؤ', + 'ؤ': 'و', + 'إ': 'ة', + 'ئ': 'غ', + 'ا': 'أ', + 'ب': 'ج', + 'ة': 'إ', + 'ت': 'ش', + 'ث': 'ض', + 'ج': 'ق', + 'ح': 'م', + 'خ': 'ئ', + 'د': 'ر', + 'ذ': 'د', + 'ر': 'ا', + 'ز': 'آ', + 'س': 'ه', + 'ش': 'ظ', + 'ص': 'ل', + 'ض': 'خ', + 'ط': 'س', + 'ظ': 'ي', + 'ع': 'ص', + 'غ': 'ن', + 'ف': 'ب', + 'ق': 'ث', + 'ك': 'ع', + 'ل': 'ح', + 'م': 'ط', + 'ن': 'ت', + 'ه': 'ك', + 'و': 'ذ', + 'ي': 'ف', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 4: { + 'آ': 'ذ', + 'أ': 'ة', + 'ؤ': 'أ', + 'إ': 'ر', + 'ئ': 'غ', + 'ا': 'د', + 'ب': 'ش', + 'ة': 'آ', + 'ت': 'ف', + 'ث': 'ض', + 'ج': 'ي', + 'ح': 'ك', + 'خ': 'ج', + 'د': 'و', + 'ذ': 'ز', + 'ر': 'ؤ', + 'ز': 'إ', + 'س': 'ه', + 'ش': 'خ', + 'ص': 'ح', + 'ض': 'ب', + 'ط': 'ل', + 'ظ': 'ن', + 'ع': 'ص', + 'غ': 'ق', + 'ف': 'ت', + 'ق': 'ئ', + 'ك': 'ع', + 'ل': 'س', + 'م': 'ط', + 'ن': 'ظ', + 'ه': 'م', + 'و': 'ا', + 'ي': 'ث', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 5: { + 'آ': 'و', + 'أ': 'ا', + 'ؤ': 'ر', + 'إ': 'ذ', + 'ئ': 'خ', + 'ا': 'د', + 'ب': 'ج', + 'ة': 'إ', + 'ت': 'ث', + 'ث': 'ظ', + 'ج': 'غ', + 'ح': 'ع', + 'خ': 'ش', + 'د': 'ز', + 'ذ': 'آ', + 'ر': 'ؤ', + 'ز': 'ة', + 'س': 'م', + 'ش': 'ت', + 'ص': 'ه', + 'ض': 'ئ', + 'ط': 'ص', + 'ظ': 'ق', + 'ع': 'ط', + 'غ': 'ي', + 'ف': 'ن', + 'ق': 'ض', + 'ك': 'ل', + 'ل': 'ح', + 'م': 'س', + 'ن': 'ب', + 'ه': 'ك', + 'و': 'أ', + 'ي': 'ف', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 6: { + 'آ': 'ؤ', + 'أ': 'ة', + 'ؤ': 'ا', + 'إ': 'آ', + 'ئ': 'خ', + 'ا': 'ز', + 'ب': 'ج', + 'ة': 'أ', + 'ت': 'ب', + 'ث': 'ف', + 'ج': 'ض', + 'ح': 'ط', + 'خ': 'ش', + 'د': 'و', + 'ذ': 'د', + 'ر': 'ذ', + 'ز': 'إ', + 'س': 'ح', + 'ش': 'ق', + 'ص': 'ك', + 'ض': 'ي', + 'ط': 'ص', + 'ظ': 'ئ', + 'ع': 'ه', + 'غ': 'ن', + 'ف': 'ث', + 'ق': 'ظ', + 'ك': 'ل', + 'ل': 'س', + 'م': 'ع', + 'ن': 'ت', + 'ه': 'م', + 'و': 'ر', + 'ي': 'غ', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 7: { + 'آ': 'د', + 'أ': 'ة', + 'ؤ': 'إ', + 'إ': 'ذ', + 'ئ': 'ف', + 'ا': 'ر', + 'ب': 'ي', + 'ة': 'ز', + 'ت': 'ض', + 'ث': 'ج', + 'ج': 'ظ', + 'ح': 'ع', + 'خ': 'ش', + 'د': 'أ', + 'ذ': 'و', + 'ر': 'آ', + 'ز': 'ؤ', + 'س': 'ك', + 'ش': 'ق', + 'ص': 'ح', + 'ض': 'ت', + 'ط': 'ص', + 'ظ': 'غ', + 'ع': 'ل', + 'غ': 'خ', + 'ف': 'ن', + 'ق': 'ب', + 'ك': 'م', + 'ل': 'ط', + 'م': 'ه', + 'ن': 'ث', + 'ه': 'س', + 'و': 'ا', + 'ي': 'ئ', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 8: { + 'آ': 'ا', + 'أ': 'د', + 'ؤ': 'ذ', + 'إ': 'ز', + 'ئ': 'ي', + 'ا': 'إ', + 'ب': 'ش', + 'ة': 'ؤ', + 'ت': 'ث', + 'ث': 'ت', + 'ج': 'ف', + 'ح': 'س', + 'خ': 'ئ', + 'د': 'و', + 'ذ': 'أ', + 'ر': 'ة', + 'ز': 'ر', + 'س': 'ع', + 'ش': 'ق', + 'ص': 'م', + 'ض': 'ظ', + 'ط': 'ل', + 'ظ': 'ب', + 'ع': 'ه', + 'غ': 'ن', + 'ف': 'غ', + 'ق': 'ض', + 'ك': 'ص', + 'ل': 'ط', + 'م': 'ك', + 'ن': 'ج', + 'ه': 'ح', + 'و': 'آ', + 'ي': 'خ', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 9: { + 'آ': 'ؤ', + 'أ': 'د', + 'ؤ': 'و', + 'إ': 'ة', + 'ئ': 'غ', + 'ا': 'ذ', + 'ب': 'ت', + 'ة': 'إ', + 'ت': 'ن', + 'ث': 'ش', + 'ج': 'ث', + 'ح': 'ك', + 'خ': 'ي', + 'د': 'ز', + 'ذ': 'آ', + 'ر': 'أ', + 'ز': 'ا', + 'س': 'م', + 'ش': 'ق', + 'ص': 'س', + 'ض': 'ئ', + 'ط': 'ع', + 'ظ': 'ج', + 'ع': 'ل', + 'غ': 'خ', + 'ف': 'ب', + 'ق': 'ف', + 'ك': 'ه', + 'ل': 'ص', + 'م': 'ح', + 'ن': 'ظ', + 'ه': 'ط', + 'و': 'ر', + 'ي': 'ض', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 10: { + 'آ': 'د', + 'أ': 'آ', + 'ؤ': 'ز', + 'إ': 'ذ', + 'ئ': 'ش', + 'ا': 'ة', + 'ب': 'ف', + 'ة': 'إ', + 'ت': 'ض', + 'ث': 'ج', + 'ج': 'ئ', + 'ح': 'ع', + 'خ': 'ث', + 'د': 'و', + 'ذ': 'ؤ', + 'ر': 'أ', + 'ز': 'ا', + 'س': 'ح', + 'ش': 'ظ', + 'ص': 'م', + 'ض': 'ن', + 'ط': 'ك', + 'ظ': 'ب', + 'ع': 'ط', + 'غ': 'ت', + 'ف': 'خ', + 'ق': 'غ', + 'ك': 'ل', + 'ل': 'ه', + 'م': 'ص', + 'ن': 'ي', + 'ه': 'س', + 'و': 'ر', + 'ي': 'ق', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 11: { + 'آ': 'و', + 'أ': 'ؤ', + 'ؤ': 'ر', + 'إ': 'ز', + 'ئ': 'ف', + 'ا': 'د', + 'ب': 'ج', + 'ة': 'إ', + 'ت': 'ي', + 'ث': 'ق', + 'ج': 'غ', + 'ح': 'ص', + 'خ': 'ن', + 'د': 'آ', + 'ذ': 'ا', + 'ر': 'أ', + 'ز': 'ة', + 'س': 'ح', + 'ش': 'خ', + 'ص': 'ك', + 'ض': 'ت', + 'ط': 'ه', + 'ظ': 'ب', + 'ع': 'ل', + 'غ': 'ض', + 'ف': 'ظ', + 'ق': 'ش', + 'ك': 'م', + 'ل': 'ط', + 'م': 'س', + 'ن': 'ئ', + 'ه': 'ع', + 'و': 'ذ', + 'ي': 'ث', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 12: { + 'آ': 'إ', + 'أ': 'ا', + 'ؤ': 'أ', + 'إ': 'ر', + 'ئ': 'ن', + 'ا': 'آ', + 'ب': 'ض', + 'ة': 'ذ', + 'ت': 'غ', + 'ث': 'ش', + 'ج': 'ق', + 'ح': 'ص', + 'خ': 'ي', + 'د': 'ة', + 'ذ': 'و', + 'ر': 'ز', + 'ز': 'ؤ', + 'س': 'م', + 'ش': 'خ', + 'ص': 'ه', + 'ض': 'ئ', + 'ط': 'ك', + 'ظ': 'ب', + 'ع': 'ل', + 'غ': 'ف', + 'ف': 'ظ', + 'ق': 'ث', + 'ك': 'ح', + 'ل': 'ط', + 'م': 'ع', + 'ن': 'ج', + 'ه': 'س', + 'و': 'د', + 'ي': 'ت', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 13: { + 'آ': 'ر', + 'أ': 'ؤ', + 'ؤ': 'ذ', + 'إ': 'ة', + 'ئ': 'ث', + 'ا': 'آ', + 'ب': 'ئ', + 'ة': 'إ', + 'ت': 'ب', + 'ث': 'ق', + 'ج': 'ش', + 'ح': 'ص', + 'خ': 'ي', + 'د': 'و', + 'ذ': 'د', + 'ر': 'أ', + 'ز': 'ا', + 'س': 'ه', + 'ش': 'ف', + 'ص': 'ع', + 'ض': 'ن', + 'ط': 'ك', + 'ظ': 'ج', + 'ع': 'ط', + 'غ': 'ض', + 'ف': 'ظ', + 'ق': 'ت', + 'ك': 'س', + 'ل': 'م', + 'م': 'ح', + 'ن': 'غ', + 'ه': 'ل', + 'و': 'ز', + 'ي': 'خ', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 14: { + 'آ': 'أ', + 'أ': 'ة', + 'ؤ': 'ر', + 'إ': 'ذ', + 'ئ': 'ي', + 'ا': 'إ', + 'ب': 'ش', + 'ة': 'ا', + 'ت': 'ئ', + 'ث': 'ق', + 'ج': 'ن', + 'ح': 'م', + 'خ': 'ظ', + 'د': 'و', + 'ذ': 'آ', + 'ر': 'ؤ', + 'ز': 'د', + 'س': 'ح', + 'ش': 'ض', + 'ص': 'س', + 'ض': 'ب', + 'ط': 'ص', + 'ظ': 'غ', + 'ع': 'ط', + 'غ': 'ث', + 'ف': 'ج', + 'ق': 'ف', + 'ك': 'ه', + 'ل': 'ك', + 'م': 'ع', + 'ن': 'ت', + 'ه': 'ل', + 'و': 'ز', + 'ي': 'خ', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 15: { + 'آ': 'ز', + 'أ': 'ذ', + 'ؤ': 'د', + 'إ': 'ا', + 'ئ': 'ش', + 'ا': 'إ', + 'ب': 'ت', + 'ة': 'و', + 'ت': 'ظ', + 'ث': 'ق', + 'ج': 'ي', + 'ح': 'ه', + 'خ': 'ئ', + 'د': 'أ', + 'ذ': 'ة', + 'ر': 'ؤ', + 'ز': 'ر', + 'س': 'ع', + 'ش': 'ب', + 'ص': 'ك', + 'ض': 'ث', + 'ط': 'ل', + 'ظ': 'ف', + 'ع': 'م', + 'غ': 'ن', + 'ف': 'غ', + 'ق': 'خ', + 'ك': 'ط', + 'ل': 'ح', + 'م': 'س', + 'ن': 'ج', + 'ه': 'ص', + 'و': 'آ', + 'ي': 'ض', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 16: { + 'آ': 'و', + 'أ': 'ؤ', + 'ؤ': 'ر', + 'إ': 'آ', + 'ئ': 'ت', + 'ا': 'ة', + 'ب': 'غ', + 'ة': 'د', + 'ت': 'ث', + 'ث': 'ظ', + 'ج': 'خ', + 'ح': 'ط', + 'خ': 'ش', + 'د': 'ذ', + 'ذ': 'ز', + 'ر': 'أ', + 'ز': 'إ', + 'س': 'ص', + 'ش': 'ن', + 'ص': 'ك', + 'ض': 'ق', + 'ط': 'ل', + 'ظ': 'ئ', + 'ع': 'ه', + 'غ': 'ج', + 'ف': 'ي', + 'ق': 'ض', + 'ك': 'م', + 'ل': 'س', + 'م': 'ع', + 'ن': 'ف', + 'ه': 'ح', + 'و': 'ا', + 'ي': 'ب', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 17: { + 'آ': 'إ', + 'أ': 'ة', + 'ؤ': 'و', + 'إ': 'آ', + 'ئ': 'ب', + 'ا': 'ؤ', + 'ب': 'ق', + 'ة': 'ز', + 'ت': 'غ', + 'ث': 'ش', + 'ج': 'ث', + 'ح': 'ه', + 'خ': 'ن', + 'د': 'ر', + 'ذ': 'د', + 'ر': 'أ', + 'ز': 'ا', + 'س': 'ك', + 'ش': 'ف', + 'ص': 'ط', + 'ض': 'ي', + 'ط': 'م', + 'ظ': 'ئ', + 'ع': 'س', + 'غ': 'ظ', + 'ف': 'ت', + 'ق': 'خ', + 'ك': 'ل', + 'ل': 'ع', + 'م': 'ص', + 'ن': 'ض', + 'ه': 'ح', + 'و': 'ذ', + 'ي': 'ج', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 18: { + 'آ': 'ا', + 'أ': 'ذ', + 'ؤ': 'آ', + 'إ': 'ر', + 'ئ': 'ض', + 'ا': 'ؤ', + 'ب': 'خ', + 'ة': 'إ', + 'ت': 'ف', + 'ث': 'ئ', + 'ج': 'ظ', + 'ح': 'م', + 'خ': 'ش', + 'د': 'أ', + 'ذ': 'ز', + 'ر': 'و', + 'ز': 'د', + 'س': 'ح', + 'ش': 'ي', + 'ص': 'ط', + 'ض': 'ب', + 'ط': 'ع', + 'ظ': 'ن', + 'ع': 'ه', + 'غ': 'ث', + 'ف': 'ق', + 'ق': 'ج', + 'ك': 'ص', + 'ل': 'س', + 'م': 'ك', + 'ن': 'غ', + 'ه': 'ل', + 'و': 'ة', + 'ي': 'ت', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, + 19: { + 'آ': 'ذ', + 'أ': 'ؤ', + 'ؤ': 'ة', + 'إ': 'ا', + 'ئ': 'خ', + 'ا': 'ز', + 'ب': 'ت', + 'ة': 'د', + 'ت': 'ش', + 'ث': 'ن', + 'ج': 'ب', + 'ح': 'ع', + 'خ': 'ي', + 'د': 'ر', + 'ذ': 'و', + 'ر': 'أ', + 'ز': 'آ', + 'س': 'ل', + 'ش': 'ق', + 'ص': 'ك', + 'ض': 'ظ', + 'ط': 'س', + 'ظ': 'ض', + 'ع': 'ه', + 'غ': 'ث', + 'ف': 'غ', + 'ق': 'ج', + 'ك': 'م', + 'ل': 'ح', + 'م': 'ص', + 'ن': 'ئ', + 'ه': 'ط', + 'و': 'إ', + 'ي': 'ف', + '۱': '١', + '۳': '٣', + '۸': '٨', + '۹': '٩', + }, +}; + +/** Pick the cipher matching the font version found on the page. */ +function pickCmap(fontVersion: number): Record { + return FONT_CMAPS[fontVersion] ?? FONT_CMAPS[14]; +} + +/** Decode a raw T-record payload with the given cipher. */ +function decodeChapter(content: string, cmap: Record): string { + return ( + [...content] + .map(ch => cmap[ch] ?? ch) + .join('') + // CJK / Hangul / Katakana & other scripts the font renders as nothing + .replace(/[぀-ヿ一-鿿가-힯ㇰ-ㇿ㈀-䶿豈-﫿]/g, '') + // Zero-width, format & bidi controls + .replace(/[\u200B-\u200F\u2060-\u2063\u202A-\u202E\u061C\uFEFF]/g, '') + // Arabic diacritics (harakat, sukun, superscript alef) are noise here, + // listed individually so ESLint's no-misleading-character-class stays quiet + .replace(/[\u064B\u064C\u064D\u064E\u064F\u0650\u0651\u0652\u0670]/g, '') + ); +} + +class MKNOV implements Plugin.PluginBase { + id = 'mknov'; + name = 'مملكه الروايات'; + version = '1.0.0'; + icon = 'src/ar/mknov/icon.png'; + site = 'https://mknov.com'; + + private baseUrl = 'https://mknov.com'; + private allWorksCache: MKWork[] | null = null; + private readonly maxOffset = 2000; + + /** Fetch JSON from an endpoint. */ + private async fetchJson(url: string): Promise { + const res = await fetchApi(url); + if (!res.ok) { + throw new Error(`Could not reach site (${res.status})`); + } + return res.json() as Promise; + } + + /** Fetch raw text. */ + private async fetchText(url: string): Promise { + const res = await fetchApi(url); + if (!res.ok) { + throw new Error(`Could not reach site (${res.status})`); + } + return res.text(); + } + + /** /api/works returns a `MKWork[]` array (all works) directly. */ + private async fetchAllWorks(): Promise { + if (this.allWorksCache) { + return this.allWorksCache; + } + const works = await this.fetchJson( + `${this.baseUrl}/api/works?limit=${this.maxOffset}`, + ); + this.allWorksCache = works || []; + return this.allWorksCache; + } + + /** Return only the fields we're sure the site provides. */ + private toNovelItem(work: MKWork): Plugin.NovelItem { + return { + name: work.titleAr || work.title || `رواية ${work.id}`, + path: `/novel/${work.id}`, + cover: work.image || '', + }; + } + + async popularNovels(pageNo: number): Promise { + const limit = 50; + const offset = (pageNo - 1) * limit; + + // Fetching more than the site allows would fail; keep pagination naive. + if (offset >= this.maxOffset) { + return []; + } + + const works = await this.fetchJson( + `${this.baseUrl}/api/works?offset=${offset}&limit=${limit}`, + ); + + return (works || []).map(work => this.toNovelItem(work)); + } + + async parseNovel(novelPath: string): Promise { + const id = novelPath.replace('/novel/', '').replace(/\/$/, ''); + const works = await this.fetchAllWorks(); + const work = works.find(w => String(w.id) === id); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: work?.titleAr || work?.title || '', + cover: work?.image || '', + author: work?.author || undefined, + summary: work?.description || '', + genres: (work?.genres || []).join(', '), + status: (work?.status || '').toLowerCase().includes('مستمر') + ? NovelStatus.Ongoing + : (work?.status || '').toLowerCase().includes('مكتمل') + ? NovelStatus.Completed + : NovelStatus.Unknown, + }; + + // Fetch chapter list + const data = await this.fetchJson<{ volumes?: MKVolume[] }>( + `${this.baseUrl}/api/works/${id}/chapters`, + ); + + const chapters: Plugin.ChapterItem[] = []; + const volumes = data.volumes || []; + + for (const volume of volumes) { + for (const ch of volume.chapters || []) { + chapters.push({ + name: ch.chapterTitle || `الفصل ${ch.chapterNumber}`, + path: `/novel/${id}/chapter/${ch.id}`, + releaseTime: ch.publishDate || '', + chapterNumber: ch.chapterNumber, + }); + } + } + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const url = `${this.baseUrl}${chapterPath}`; + const html = await this.fetchText(url); + + // Extract all RSC flight chunks (self.__next_f.push([1, "..."])) + const allChunks: string[] = []; + const textRe = + /self\.__next_f\.push\(\[\s*1\s*,\s*("(?:[^"\\]|\\.)*")\s*\]/g; + let match; + while ((match = textRe.exec(html)) !== null) { + try { + allChunks.push(JSON.parse(match[1])); + } catch { + // skip unparseable chunks + } + } + + const fullPayload = allChunks.join(''); + + // Chapter content is streamed as a Next.js Flight T-record: + // :T, + const tRe = /([0-9a-f]+):T([0-9a-f]+),/g; + let content = ''; + while ((match = tRe.exec(fullPayload)) !== null) { + const hlen = parseInt(match[2], 16); + const start = match.index + match[0].length; + content += fullPayload.slice(start, start + hlen); + } + + // If no T-record, try a raw "content"-style paragraph block in the payload + if (!content.trim()) { + const contentIdx = fullPayload.indexOf('"content":'); + if (contentIdx > -1) { + const pStart = fullPayload.indexOf('"', contentIdx + 9); + if (pStart > -1) { + let depth = 0; + let inStr = false; + for (let i = pStart + 1; i < fullPayload.length; i++) { + const ch = fullPayload[i]; + if (ch === '\\') { + i++; + continue; + } + if (inStr) { + if (ch === '"') inStr = false; + continue; + } + if (ch === '{') depth++; + else if (ch === '}') { + depth--; + if (depth === 0) { + content = fullPayload.slice(pStart + 1, i); + break; + } + } + } + } + } + } + + if (!content.trim()) { + return '

المحتوى غير متاح.

'; + } + + // The running protected-font version drives which cipher applies. + let fontVer = 0; + const fontMatch = html.match(/protected-font-(\d+)/); + if (fontMatch) { + fontVer = parseInt(fontMatch[1], 10); + } + const cmap = pickCmap(fontVer); + + // Decode the font-cmap obfuscation, then drop any leftover noise. + const cleaned = decodeChapter(content, cmap); + const readable = cleaned.replace(/[^؀-ۿݐ-ݿ\s،.;?!…0-9"'()\-–—:«»]/g, ''); + + // Wrap paragraphs + return readable + .split(/\n{2,}/) + .map(p => p.trim()) + .filter(Boolean) + .map(p => `

${p.replace(/\n/g, '
')}

`) + .join(''); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + if (pageNo !== 1) return []; + + const works = await this.fetchAllWorks(); + const term = searchTerm.trim().toLowerCase(); + + const filtered = works.filter( + w => + w.titleAr?.toLowerCase().includes(term) || + w.title?.toLowerCase().includes(term) || + w.author?.toLowerCase().includes(term), + ); + + return filtered.map(w => this.toNovelItem(w)); + } + + resolveUrl = (path: string): string => this.site + path; +} + +export default new MKNOV(); diff --git a/public/static/src/ar/mknov/icon.png b/public/static/src/ar/mknov/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..486a7e6b400018394a7a45abac41888b37530192 GIT binary patch literal 7487 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 Date: Sun, 13 Sep 2026 08:25:13 +0300 Subject: [PATCH 2/3] fix(ar/mknov): use Array.from for chapter decode TypeScript downlevels [...content] to a __spreadArray helper that requires a real array and throws "Cannot use 'in' operator" on strings during parseChapter. Switch to Array.from which iterates strings safely. Co-Authored-By: Claude Code --- plugins/arabic/mknov.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/arabic/mknov.ts b/plugins/arabic/mknov.ts index d871c2930..227d24a90 100644 --- a/plugins/arabic/mknov.ts +++ b/plugins/arabic/mknov.ts @@ -816,7 +816,9 @@ function pickCmap(fontVersion: number): Record { /** Decode a raw T-record payload with the given cipher. */ function decodeChapter(content: string, cmap: Record): string { return ( - [...content] + // Array.from() rather than `[...content]` — TypeScript downlevels the + // spread to a helper that requires a real array and breaks on strings. + Array.from(content) .map(ch => cmap[ch] ?? ch) .join('') // CJK / Hangul / Katakana & other scripts the font renders as nothing From da473c2495fc42e9f7e2242fdad76f37f1528261 Mon Sep 17 00:00:00 2001 From: Yemen Date: Sun, 13 Sep 2026 08:34:19 +0300 Subject: [PATCH 3/3] chore(ar/mknov): add compiled plugin for immediate testing Co-Authored-By: Claude Code --- .js/src/plugins/arabic/mknov.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 .js/src/plugins/arabic/mknov.js diff --git a/.js/src/plugins/arabic/mknov.js b/.js/src/plugins/arabic/mknov.js new file mode 100644 index 000000000..99c46cf71 --- /dev/null +++ b/.js/src/plugins/arabic/mknov.js @@ -0,0 +1 @@ +var t=this&&this.__awaiter||function(t,e,n,r){return new(n||(n=Promise))((function(o,i){function s(t){try{c(r.next(t))}catch(t){i(t)}}function a(t){try{c(r.throw(t))}catch(t){i(t)}}function c(t){var e;t.done?o(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(s,a)}c((r=r.apply(t,e||[])).next())}))},e=this&&this.__generator||function(t,e){var n,r,o,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]},s=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return s.next=a(0),s.throw=a(1),s.return=a(2),"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(c){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(i=0)),i;)try{if(n=1,r&&(o=2&a[0]?r.return:a[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,a[1])).done)return o;switch(r=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return i.label++,{value:a[1],done:!1};case 5:i.label++,r=a[1],a=[0];continue;case 7:a=i.ops.pop(),i.trys.pop();continue;default:if(!(o=i.trys,(o=o.length>0&&o[o.length-1])||6!==a[0]&&2!==a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]=this.maxOffset?[2,[]]:[4,this.fetchJson("".concat(this.baseUrl,"/api/works?offset=").concat(t,"&limit=").concat(50))];case 1:return[2,(e.sent()||[]).map((function(t){return r.toNovelItem(t)}))]}}))}))},i.prototype.parseNovel=function(n){return t(this,void 0,void 0,(function(){var t,o,i,s,a,c,u,l,h,f,p,v,d;return e(this,(function(e){switch(e.label){case 0:return t=n.replace("/novel/","").replace(/\/$/,""),[4,this.fetchAllWorks()];case 1:return o=e.sent(),i=o.find((function(e){return String(e.id)===t})),s={path:n,name:(null==i?void 0:i.titleAr)||(null==i?void 0:i.title)||"",cover:(null==i?void 0:i.image)||"",author:(null==i?void 0:i.author)||void 0,summary:(null==i?void 0:i.description)||"",genres:((null==i?void 0:i.genres)||[]).join(", "),status:((null==i?void 0:i.status)||"").toLowerCase().includes("مستمر")?r.NovelStatus.Ongoing:((null==i?void 0:i.status)||"").toLowerCase().includes("مكتمل")?r.NovelStatus.Completed:r.NovelStatus.Unknown},[4,this.fetchJson("".concat(this.baseUrl,"/api/works/").concat(t,"/chapters"))];case 2:for(a=e.sent(),c=[],u=a.volumes||[],l=0,h=u;l-1&&(v=c.indexOf('"',p+9))>-1)for(d=0,m=!1,b=v+1;b".concat(t.replace(/\n/g,"
"),"

")})).join("")]):[2,"

المحتوى غير متاح.

"]}}))}))},i.prototype.searchNovels=function(n,r){return t(this,void 0,void 0,(function(){var t,o,i,s=this;return e(this,(function(e){switch(e.label){case 0:return 1!==r?[2,[]]:[4,this.fetchAllWorks()];case 1:return t=e.sent(),o=n.trim().toLowerCase(),i=t.filter((function(t){var e,n,r;return(null===(e=t.titleAr)||void 0===e?void 0:e.toLowerCase().includes(o))||(null===(n=t.title)||void 0===n?void 0:n.toLowerCase().includes(o))||(null===(r=t.author)||void 0===r?void 0:r.toLowerCase().includes(o))})),[2,i.map((function(t){return s.toNovelItem(t)}))]}}))}))},i}();exports.default=new i; \ No newline at end of file