From b820c8bbce4bbcbed8daacfcf7ed4869de2e93ce Mon Sep 17 00:00:00 2001 From: 7ui77 <99854073+7ui77@users.noreply.github.com> Date: Sun, 17 May 2026 09:11:14 +0700 Subject: [PATCH 1/9] fix(novelbuddy): fix watermark regex and bump version to 2.1.2 --- plugins/english/novelbuddy.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/english/novelbuddy.ts b/plugins/english/novelbuddy.ts index b30cf469c..6b93f568c 100644 --- a/plugins/english/novelbuddy.ts +++ b/plugins/english/novelbuddy.ts @@ -9,7 +9,7 @@ class NovelBuddy implements Plugin.PluginBase { name = 'NovelBuddy'; site = 'https://novelbuddy.com/'; api = 'https://api.novelbuddy.com/'; - version = '2.1.1'; + version = '2.1.2'; icon = 'src/en/novelbuddy/icon.png'; parseNovels(body: Response): Plugin.NovelItem[] { @@ -167,7 +167,9 @@ class NovelBuddy implements Plugin.PluginBase { ); // Remove obfuscated freewebnovel watermarks (e.g., free𝑤𝑒𝑏novel.com) - content = content.replace(/free.*?novel\.com/gi, ''); + const fwnRegex = + /(?:𝐟|ᵮ|𝑓|𝒇|𝒻|𝓯|𝔣|𝕗|𝖿|𝗳|𝙛|𝚏|ꬵ|ꞙ|ẝ|𝖋|ⓕ|f|ƒ|ḟ|ʃ|բ|ᶠ|⒡|ſ|ꊰ|ʄ|∱|ᶂ|𝘧|f)(?:𝚛|ꭇ|ᣴ|ℾ|𝚪|𝛤|𝜞|𝝘|𝞒|Ⲅ|Г|Ꮁ|ᒥ|ꭈ|ⲅ|ꮁ|ⓡ|r|ŕ|ṙ|ř|ȑ|ȓ|ṛ|ṝ|ŗ|г|Ր|ɾ|ᥬ|ṟ|ɍ|ʳ|⒭|ɼ|ѓ|ᴦ|ᶉ|𝐫|𝑟|𝒓|𝓇|𝓻|𝔯|𝕣|𝖗|𝗋|𝗿|𝘳|𝙧|ᵲ|ґ|ᵣ|r)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)+(?:𝐰|ꝡ|𝑤|𝒘|𝓌|𝔀|𝔴|𝕨|𝖜|𝗐|𝘄|𝘸|𝙬|𝚠|ա|ẁ|ꮃ|ẃ|ⓦ|⍵|ŵ|ẇ|ẅ|ẘ|ẉ|ⱳ|ὼ|ὠ|ὡ|ὢ|ὣ|ω|ὤ|ὥ|ὦ|ὧ|ῲ|ῳ|ῴ|ῶ|ῷ|Ⱳ|ѡ|ԝ|ᴡ|ώ|ᾠ|ᾡ|ᾡ|ᾢ|ᾣ|ᾤ|ᾥ|ᾦ|ɯ|𝝕|𝟉|𝞏|w)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ꮟ|Ꮟ|𝐛|𝘣|𝒷|𝔟|𝓫|𝖇|𝖻|𝑏|𝙗|𝕓|𝒃|𝗯|𝚋|♭|ᑳ|ᒈ|b|ᖚ|ᕹ|ᕺ|ⓑ|ḃ|ḅ|ҍ|ъ|ḇ|ƃ|ɓ|ƅ|ᖯ|Ƅ|Ь|ᑲ|þ|Ƃ|⒝|Ъ|ᶀ|ᑿ|ᒀ|ᒂ|ᒁ|ᑾ|ь|ƀ|Ҍ|Ѣ|ѣ|ᔎ|b)(?:ո|ռ|ח|𝒏|𝓷|ն|𝑛|𝖓|𝔫|𝗇|𝚗|𝗻|ᥒ|ⓝ|ή|n|ǹ|ᴒ|ń|ñ|ᾗ|η|ṅ|ň|ṇ|ɲ|ņ|ṋ|ṉ|ղ|ຖ|Ռ|ƞ|ŋ|⒩|ภ|ก|ɳ|п|ʼn|л|ԉ|Ƞ|ἠ|ἡ|ῃ|դ|ᾐ|ᾑ|ᾒ|ᾓ|ᾔ|ᾕ|ᾖ|ῄ|ῆ|ῇ|ῂ|ἢ|ἣ|ἤ|ἥ|ἦ|ἧ|ὴ|ή|በ|ቡ|ቢ|ባ|ቤ|ብ|ቦ|ȵ|𝛈|𝜂|𝜼|𝝶|𝞰|𝕟|𝘯|𝐧|𝓃|ᶇ|ᵰ|ᥥ|∩|n)(?:ం|ಂ|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝒐|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝛐|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|o|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|𝘰|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|ⓞ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|೦|൦|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:∨|⌄|⋁|ⅴ|𝐯|𝑣|𝒗|𝓋|𝔳|𝕧|𝖛|𝗏|ꮩ|ሀ|ⓥ|v|𝜐|𝝊|ṽ|ṿ|౮|ง|ѵ|ע|ᴠ|ν|ט|ᵥ|ѷ|៴|ᘁ|𝙫|𝚟|𝛎|𝜈|𝝂|𝝼|𝞶|𝘷|𝘃|𝓿|v)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ⓛ|l|ŀ|ĺ|ľ|ḷ|ḹ|ḷ|ļ|Ӏ|ℓ|ḽ|ḻ|ł|レ|ɭ|ƚ|ɫ|ⱡ|\\|Ɩ|⒧|ʅ|ǀ|ו|ן|Ι|І|||ᶩ|ӏ|𝓘|𝕀|𝖨|𝗜|𝘐|𝐥|𝑙|𝒍|𝓁|𝔩|𝕝|𝖑|𝗅|𝗹|𝘭|𝚕|𝜤|𝝞|ı|𝚤|ɩ|ι|𝛊|𝜄|𝜾|𝞲|I|l)(?:.?(?:🝌|c|ⅽ|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖼|𝗰|𝘤|𝙘|𝚌|ᴄ|ϲ|ⲥ|с|ꮯ|𐐽|ⲥ|𐐽|ꮯ|ĉ|c|ⓒ|ć|č|ċ|ç|ҁ|ƈ|ḉ|ȼ|ↄ|с|ር|ᴄ|ϲ|ҫ|꒝|ς|ɽ|ϛ|𝙲|ᑦ|᧚|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖼|𝗰|𝘤|𝙘|𝚌|₵|🇨|ᥴ|ᒼ|ⅽ|c)(?:ం|ం|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝒐|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝛐|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|o|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|𝘰|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|ⓞ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|告知|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:₥|ᵯ|𝖒|𝐦|𝗆|𝔪|𝕞|𝓂|ⓜ|m|ന|ᙢ|൩|m|ḿ|ṁ|ⅿ|ϻ|ṃ|ጠ|ɱ|៳|ᶆ|𝒎|𝙢|𝓶|𝚖|𝑚|𝗺|᧕|᧗|m))?/g; + content = content.replace(fwnRegex, ''); } return content; From 4fa119d33bc852a174ddb66dd60cc60f0e4ecea2 Mon Sep 17 00:00:00 2001 From: 7ui77 <99854073+7ui77@users.noreply.github.com> Date: Sun, 17 May 2026 09:29:40 +0700 Subject: [PATCH 2/9] fix(novelbuddy): fix watermark regex, remove chapter api fetch and bump version to 2.1.2 --- plugins/english/novelbuddy.ts | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/plugins/english/novelbuddy.ts b/plugins/english/novelbuddy.ts index 6b93f568c..4eaf9c38e 100644 --- a/plugins/english/novelbuddy.ts +++ b/plugins/english/novelbuddy.ts @@ -119,20 +119,7 @@ class NovelBuddy implements Plugin.PluginBase { novel.rating = initialManga.ratingStats.average; } - // Fetch full chapter list from API - const chaptersUrl = `${this.api}titles/${initialManga.id}/chapters`; - const chaptersResponse = await fetchApi(chaptersUrl); - const chaptersJson: ChapterResponse = await chaptersResponse.json(); - - if (chaptersJson?.success && chaptersJson?.data?.chapters) { - novel.chapters = chaptersJson.data.chapters - .map(chapter => ({ - name: chapter.name, - path: new URL(chapter.url, this.site).pathname.substring(1), - releaseTime: chapter.updated_at, - })) - .reverse(); - } else if (initialManga.chapters) { + if (initialManga.chapters) { novel.chapters = initialManga.chapters .map(chapter => ({ name: chapter.name, @@ -168,7 +155,7 @@ class NovelBuddy implements Plugin.PluginBase { // Remove obfuscated freewebnovel watermarks (e.g., free𝑤𝑒𝑏novel.com) const fwnRegex = - /(?:𝐟|ᵮ|𝑓|𝒇|𝒻|𝓯|𝔣|𝕗|𝖿|𝗳|𝙛|𝚏|ꬵ|ꞙ|ẝ|𝖋|ⓕ|f|ƒ|ḟ|ʃ|բ|ᶠ|⒡|ſ|ꊰ|ʄ|∱|ᶂ|𝘧|f)(?:𝚛|ꭇ|ᣴ|ℾ|𝚪|𝛤|𝜞|𝝘|𝞒|Ⲅ|Г|Ꮁ|ᒥ|ꭈ|ⲅ|ꮁ|ⓡ|r|ŕ|ṙ|ř|ȑ|ȓ|ṛ|ṝ|ŗ|г|Ր|ɾ|ᥬ|ṟ|ɍ|ʳ|⒭|ɼ|ѓ|ᴦ|ᶉ|𝐫|𝑟|𝒓|𝓇|𝓻|𝔯|𝕣|𝖗|𝗋|𝗿|𝘳|𝙧|ᵲ|ґ|ᵣ|r)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)+(?:𝐰|ꝡ|𝑤|𝒘|𝓌|𝔀|𝔴|𝕨|𝖜|𝗐|𝘄|𝘸|𝙬|𝚠|ա|ẁ|ꮃ|ẃ|ⓦ|⍵|ŵ|ẇ|ẅ|ẘ|ẉ|ⱳ|ὼ|ὠ|ὡ|ὢ|ὣ|ω|ὤ|ὥ|ὦ|ὧ|ῲ|ῳ|ῴ|ῶ|ῷ|Ⱳ|ѡ|ԝ|ᴡ|ώ|ᾠ|ᾡ|ᾡ|ᾢ|ᾣ|ᾤ|ᾥ|ᾦ|ɯ|𝝕|𝟉|𝞏|w)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ꮟ|Ꮟ|𝐛|𝘣|𝒷|𝔟|𝓫|𝖇|𝖻|𝑏|𝙗|𝕓|𝒃|𝗯|𝚋|♭|ᑳ|ᒈ|b|ᖚ|ᕹ|ᕺ|ⓑ|ḃ|ḅ|ҍ|ъ|ḇ|ƃ|ɓ|ƅ|ᖯ|Ƅ|Ь|ᑲ|þ|Ƃ|⒝|Ъ|ᶀ|ᑿ|ᒀ|ᒂ|ᒁ|ᑾ|ь|ƀ|Ҍ|Ѣ|ѣ|ᔎ|b)(?:ո|ռ|ח|𝒏|𝓷|ն|𝑛|𝖓|𝔫|𝗇|𝚗|𝗻|ᥒ|ⓝ|ή|n|ǹ|ᴒ|ń|ñ|ᾗ|η|ṅ|ň|ṇ|ɲ|ņ|ṋ|ṉ|ղ|ຖ|Ռ|ƞ|ŋ|⒩|ภ|ก|ɳ|п|ʼn|л|ԉ|Ƞ|ἠ|ἡ|ῃ|դ|ᾐ|ᾑ|ᾒ|ᾓ|ᾔ|ᾕ|ᾖ|ῄ|ῆ|ῇ|ῂ|ἢ|ἣ|ἤ|ἥ|ἦ|ἧ|ὴ|ή|በ|ቡ|ቢ|ባ|ቤ|ብ|ቦ|ȵ|𝛈|𝜂|𝜼|𝝶|𝞰|𝕟|𝘯|𝐧|𝓃|ᶇ|ᵰ|ᥥ|∩|n)(?:ం|ಂ|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝒐|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝛐|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|o|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|𝘰|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|ⓞ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|೦|൦|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:∨|⌄|⋁|ⅴ|𝐯|𝑣|𝒗|𝓋|𝔳|𝕧|𝖛|𝗏|ꮩ|ሀ|ⓥ|v|𝜐|𝝊|ṽ|ṿ|౮|ง|ѵ|ע|ᴠ|ν|ט|ᵥ|ѷ|៴|ᘁ|𝙫|𝚟|𝛎|𝜈|𝝂|𝝼|𝞶|𝘷|𝘃|𝓿|v)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ⓛ|l|ŀ|ĺ|ľ|ḷ|ḹ|ḷ|ļ|Ӏ|ℓ|ḽ|ḻ|ł|レ|ɭ|ƚ|ɫ|ⱡ|\\|Ɩ|⒧|ʅ|ǀ|ו|ן|Ι|І|||ᶩ|ӏ|𝓘|𝕀|𝖨|𝗜|𝘐|𝐥|𝑙|𝒍|𝓁|𝔩|𝕝|𝖑|𝗅|𝗹|𝘭|𝚕|𝜤|𝝞|ı|𝚤|ɩ|ι|𝛊|𝜄|𝜾|𝞲|I|l)(?:.?(?:🝌|c|ⅽ|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖼|𝗰|𝘤|𝙘|𝚌|ᴄ|ϲ|ⲥ|с|ꮯ|𐐽|ⲥ|𐐽|ꮯ|ĉ|c|ⓒ|ć|č|ċ|ç|ҁ|ƈ|ḉ|ȼ|ↄ|с|ር|ᴄ|ϲ|ҫ|꒝|ς|ɽ|ϛ|𝙲|ᑦ|᧚|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖼|𝗰|𝘤|𝙘|𝚌|₵|🇨|ᥴ|ᒼ|ⅽ|c)(?:ం|ం|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝒐|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝛐|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|o|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|𝘰|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|ⓞ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|告知|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:₥|ᵯ|𝖒|𝐦|𝗆|𝔪|𝕞|𝓂|ⓜ|m|ന|ᙢ|൩|m|ḿ|ṁ|ⅿ|ϻ|ṃ|ጠ|ɱ|៳|ᶆ|𝒎|𝙢|𝓶|𝚖|𝑚|𝗺|᧕|᧗|m))?/g; + /(?:𝐟|ᵮ|𝑓|𝒇|𝒻|𝓯|𝔣|𝕗|𝖿|𝗳|𝙛|𝚏|ꬵ|ꞙ|ẝ|𝖋|ⓕ|f|ƒ|ḟ|ʃ|բ|ᶠ|⒡|ſ|ꊰ|ʄ|∱|ᶂ|𝘧|f)(?:𝚛|ꭇ|ᣴ|ℾ|𝚪|𝛤|𝜞|𝝘|𝞒|Ⲅ|Г|Ꮁ|ᒥ|ꭈ|ⲅ|ꮁ|ⓡ|r|ŕ|ṙ|ř|ȑ|ȓ|ṛ|ṝ|ŗ|г|Ր|ɾ|ᥬ|ṟ|ɍ|ʳ|⒭|ɼ|ѓ|ᴦ|ᶉ|𝐫|𝑟|𝒓|𝓇|𝓻|𝔯|𝕣|𝖗|𝗋|𝗿|𝘳|𝙧|ᵲ|ґ|ᵣ|r)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)+(?:𝐰|ꝡ|𝑤|𝒘|𝓌|𝔀|𝔴|𝕨|𝖜|𝗐|𝘄|𝘸|𝙬|𝚠|ա|ẁ|ꮃ|ẃ|ⓦ|⍵|ŵ|ẇ|ẅ|ẘ|ẉ|ⱳ|ὼ|ὠ|ὡ|ὢ|ὣ|ω|ὤ|ὥ|ὦ|ὧ|ῲ|ῳ|ῴ|ῶ|ῷ|Ⱳ|ѡ|ԝ|ᴡ|ώ|ᾠ|ᾡ|ᾡ|ᾢ|ᾣ|ᾤ|ᾥ|ᾦ|ɯ|𝝕|𝟉|𝞏|w)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ꮟ|Ꮟ|𝐛|𝘣|𝒷|𝔟|𝓫|𝖇|𝖻|𝑏|𝙗|𝕓|𝒃|𝗯|𝚋|♭|ᑳ|ᒈ|b|ᖚ|ᕹ|ᕺ|ⓑ|ḃ|ḅ|ҍ|ъ|ḇ|ƃ|ɓ|ƅ|ᖯ|Ƅ|Ь|ᑲ|þ|Ƃ|⒝|Ъ|ᶀ|ᑿ|ᒀ|ᒂ|ᒁ|ᑾ|ь|ƀ|Ҍ|Ѣ|ѣ|ᔎ|b)(?:ո|ռ|ח|𝒏|𝓷|ն|𝑛|𝖓|𝔫|𝗇|ն|𝗻|ᥒ|ⓝ|ή|n|ǹ|ᴒ|ń|ñ|ᾗ|η|ṅ|ň|ṇ|ɲ|ņ|ṋ|ṉ|ղ|ຖ|Ռ|ƞ|ŋ|⒩|ภ|ก|ɳ|п|ʼn|л|ԉ|Ƞ|ἠ|ἡ|ῃ|դ|ᾐ|ᾑ|ᾒ|ᾓ|ᾔ|ᾕ|ᾖ|ῄ|ῆ|ῇ|ῂ|ἢ|ἣ|ἤ|ἥ|ἦ|ἧ|ὴ|ή|በ|ቡ|ቢ|ባ|ቤ|ብ|ቦ|ȵ|𝛈|𝜂|𝜼|𝝶|𝞰|𝕟|𝘯|𝐧|𝓃|ᶇ|ᵰ|ᥥ|∩|n)(?:ం|ಂ|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝖔|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝛐|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|o|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|o|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|ⓞ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|告知|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:∨|⌄|⋁|ⅴ|𝐯|𝑣|𝒗|𝓋|𝔳|𝕧|𝖛|𝗏|ꮩ|ሀ|ⓥ|v|𝜐|𝝊|ṽ|ṿ|౮|ง|ѵ|ע|ᴠ|ν|ט|ᵥ|ѷ|៴|ᘁ|𝙫|𝚟|𝛎|𝜈|𝝂|𝝼|𝞶|𝘷|𝘃|𝓿|v)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|✀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ⓛ|l|ŀ|ĺ|ľ|ḷ|ḹ|ḷ|ļ|Ӏ|ℓ|ḽ|ḻ|ł|レ|ɭ|ƚ|ɫ|ⱡ|\\|Ɩ|⒧|ʅ|ǀ|ו|ן|Ι|І|||ᶩ|ӏ|𝓘|𝕀|𝖨|𝗜|𝘐|𝐥|𝑙|𝒍|𝓁|𝔩|𝕝|𝖑|𝗅|𝗹|𝘭|𝚕|𝜤|𝝞|ı|𝚤|ɩ|ι|𝛊|𝜄|𝜾|𝞲|I|l)(?:.?(?:🝌|c|ⅽ|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖼|𝗰|𝘤|𝙘|𝚌|ᴄ|ϲ|ⲥ|с|ꮯ|𐐽|ⲥ|𐐽|ꮯ|ĉ|c|ⓒ|ć|č|ċ|ç|ҁ|ƈ|ḉ|ȼ|ↄ|с|ር|ᴄ|ϲ|ҫ|꒝|ς|ɽ|ϛ|𝙲|ᑦ|᧚|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖼|𝗰|𝘤|𝙘|𝚌|₵|🇨|ᥴ|ᒼ|ⅽ|c)(?:ం|ం|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝖔|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝛐|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|o|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|𝘰|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|ⓞ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|告知|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:₥|ᵯ|𝖒|𝐦|𝗆|𝔪|𝕞|𝓂|ⓜ|m|ന|ᙢ|൩|m|ḿ|ṁ|ⅿ|ϻ|ṃ|ጠ|ɱ|៳|ᶆ|𝒎|𝙢|𝓶|𝚖|𝑚|𝗺|᧕|᧗|m))?/g; content = content.replace(fwnRegex, ''); } From 1f18fcd3977c07cba5bda647a3257d2997b8a67a Mon Sep 17 00:00:00 2001 From: 7ui77 <99854073+7ui77@users.noreply.github.com> Date: Sun, 17 May 2026 10:29:42 +0700 Subject: [PATCH 3/9] fix(proxy): propagate origin status code and handle 304 Not Modified correctly --- proxy.ts | 219 +++++++++++++------------------------------------------ 1 file changed, 52 insertions(+), 167 deletions(-) diff --git a/proxy.ts b/proxy.ts index 1a9adea66..1aaa61da6 100644 --- a/proxy.ts +++ b/proxy.ts @@ -1,5 +1,4 @@ import process from 'node:process'; -import { Buffer } from 'buffer'; import { FetchMode, ServerSetting } from './src/types/types'; import { Connect } from 'vite'; import httpProxy from 'http-proxy'; @@ -51,22 +50,17 @@ const proxySettingMiddleware: Connect.NextHandleFunction = (req, res) => { const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => { const rawUrl = 'https:' + req.url; if (req.headers['access-control-request-method']) { - res.setHeader( - 'access-control-allow-methods', - req.headers['access-control-request-method'], - ); + res.setHeader('access-control-allow-methods', req.headers['access-control-request-method']); delete req.headers['access-control-request-method']; } if (req.headers['access-control-request-headers']) { - res.setHeader( - 'access-control-allow-headers', - req.headers['access-control-request-headers'], - ); + res.setHeader('access-control-allow-headers', req.headers['access-control-request-headers']); delete req.headers['access-control-request-headers']; } res.setHeader('Access-Control-Allow-Origin', settings.CLIENT_HOST); res.setHeader('Access-Control-Allow-Credentials', 'true'); req.headers.referer = rawUrl; + if (req.method === 'OPTIONS') { res.statusCode = 200; res.end(); @@ -74,29 +68,20 @@ const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => { try { const _url = new URL(rawUrl); for (const _header in req.headers) { - if ( - req.headers[_header]?.includes('localhost') || - settings.disAllowedRequestHeaders.includes(_header) - ) { + if (req.headers[_header]?.includes('localhost') || settings.disAllowedRequestHeaders.includes(_header)) { delete req.headers[_header]; } } req.headers['sec-fetch-mode'] = 'cors'; - if (settings.cookies) { - req.headers['cookie'] = settings.cookies; - } - if (!settings.useUserAgent) { - delete req.headers['user-agent']; - } + if (settings.cookies) req.headers['cookie'] = settings.cookies; + if (!settings.useUserAgent) delete req.headers['user-agent']; req.headers.host = _url.host; req.url = _url.toString(); - res.statusCode = 200; proxyRequest(req, res); } catch (err) { - console.log('\x1b[31m', '----------ERRROR----------'); - console.error(err); - console.log('\x1b[31m', '----------ERRROR----------'); + console.error('Proxy logic error:', err); if (!res.closed) { + res.statusCode = 500; res.end(); } } @@ -105,78 +90,41 @@ const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => { const proxyRequest: Connect.SimpleHandleFunction = (req, res) => { const _url = new URL(req.url || ''); - console.log('\x1b[36m', '----------------'); - console.log( - `Making proxy request - at ${new Date().toLocaleTimeString()} - url: ${_url.href} - headers:`, - ); - Object.entries(req.headers).forEach(([name, value]) => { - console.log('\t', '\x1b[32m', name + ':', '\x1b[37m', value); - }); - console.log('\x1b[36m', '----------------'); if (settings.fetchMode === FetchMode.CURL) { - //i mean if it works it works i guess, better than nothing - let curl = `curl '${_url.href}'`; - if (settings.useUserAgent) { - curl += ` -H 'User-Agent: ${req.headers['user-agent']}'`; - } + let curl = `curl -L '${_url.href}'`; + if (settings.useUserAgent) curl += ` -H 'User-Agent: ${req.headers['user-agent']}'`; if (settings.cookies) curl += ` -H 'Cookie: ${settings.cookies}'`; if (req.headers.origin2) curl += ` -H 'Origin: ${req.headers.origin2}'`; - console.log('Running curl command:', curl); - const isWindows = process.platform === 'win32'; - const options = isWindows - ? { - shell: - process.env.BASH_LOCATION || - process.env.ProgramFiles + '\\git\\usr\\bin\\bash.exe', - } - : {}; + const options = isWindows ? { shell: process.env.BASH_LOCATION || process.env.ProgramFiles + '\\git\\usr\\bin\\bash.exe' } : {}; exec(curl, options, (error, stdout, stderr) => { if (error) { - console.error(`exec error: ${error}`); res.statusCode = 500; res.write(`exec error: ${error}`); res.end(); return; } - if (stderr) { - console.error(`stderr: ${stderr}`); - } res.statusCode = 200; res.write(stdout); res.end(); }); } else if (settings.fetchMode === FetchMode.NODE_FETCH) { const headers = new Headers(); - if (settings.useUserAgent) { - headers.append('user-agent', req.headers['user-agent'] as string); - } - if (settings.cookies) { - headers.append('cookie', settings.cookies); - } - if (req.headers.origin2) { - headers.append('origin', req.headers.origin2 as string); - } - fetch(_url.href, { - headers: headers, - }) - .then(async res2 => [res2, await res2.text()] as const) - .then(([res2, text]) => { + if (settings.useUserAgent) headers.append('user-agent', req.headers['user-agent'] as string); + if (settings.cookies) headers.append('cookie', settings.cookies); + if (req.headers.origin2) headers.append('origin', req.headers.origin2 as string); + + fetch(_url.href, { headers }) + .then(async res2 => { res.statusCode = res2.status; res2.headers.forEach((val, key) => { - if ( - !settings.disAllowResponseHeaders.includes(key) && - key !== 'content-encoding' && - key !== 'content-length' - ) { + if (!settings.disAllowResponseHeaders.includes(key) && key !== 'content-encoding' && key !== 'content-length') { res.setHeader(key, val); } }); - res.write(text); + res.write(await res2.text()); res.end(); }) .catch(err => { @@ -185,112 +133,49 @@ const proxyRequest: Connect.SimpleHandleFunction = (req, res) => { res.end(); }); } else if (settings.fetchMode === FetchMode.PROXY) { - proxy.web( - req, - res, - { - target: _url.origin, - selfHandleResponse: true, - followRedirects: true, - }, - err => { - console.error(err); - res.statusCode = 500; - res.end(); - }, - ); + proxy.web(req, res, { target: _url.origin, selfHandleResponse: true, followRedirects: true }, err => { + console.error('Proxy target error:', err); + res.statusCode = 500; + res.end(); + }); } }; proxy.on('proxyRes', function (proxyRes, req, res) { - const statusCode = proxyRes.statusCode; - // Redirect - if ( - statusCode === 301 || - statusCode === 302 || - statusCode === 303 || - statusCode === 307 || - statusCode === 308 - ) { - req.method = 'GET'; - req.headers['content-length'] = '0'; - delete req.headers['content-type']; - // Remove all listeners (=reset events to initial state) - req.removeAllListeners(); - - // Initiate a new proxy request. - proxyRequest(req, res); - return false; - } - - const contentEncoding = proxyRes.headers['content-encoding']; - const isBrotli = - contentEncoding && - (Array.isArray(contentEncoding) - ? contentEncoding.some(enc => enc.includes('br')) - : contentEncoding.includes('br')); - - const isGzip = - contentEncoding && - (Array.isArray(contentEncoding) - ? contentEncoding.some(enc => enc.includes('gzip')) - : contentEncoding.includes('gzip')); - - const isZstd = - contentEncoding && - (Array.isArray(contentEncoding) - ? contentEncoding.some(enc => enc.includes('zstd')) - : contentEncoding.includes('zstd')); + const statusCode = proxyRes.statusCode || 200; + res.statusCode = statusCode; - if (isBrotli || isGzip || isZstd) { - delete proxyRes.headers['content-encoding']; - delete proxyRes.headers['content-length']; - - for (const _header in proxyRes.headers) { - if (!settings.disAllowResponseHeaders.includes(_header)) { - res.setHeader(_header, proxyRes.headers[_header] as string); - } + // Propagate headers but filter restricted ones + Object.keys(proxyRes.headers).forEach(key => { + if (!settings.disAllowResponseHeaders.includes(key) && key !== 'content-encoding' && key !== 'content-length') { + res.setHeader(key, proxyRes.headers[key] as string); } + }); - const chunks: Buffer[] = []; - proxyRes.on('data', chunk => chunks.push(Buffer.from(chunk))); - proxyRes.on('end', async function () { - try { - const buffer = Buffer.concat(chunks); - let decompressed; - - if (isBrotli) { - decompressed = brotliDecompressSync(buffer); - } else if (isZstd) { - decompressed = zstdDecompressSync(buffer); - } else { - decompressed = gunzipSync(buffer); - } + if (statusCode === 304) { + res.end(); + return; + } - res.write(Buffer.from(decompressed)); - res.end(); - } catch (err) { - console.error(err); - res.statusCode = 500; - res.end(`Error decompressing ${isBrotli ? 'Brotli' : 'GZIP'} content`); - } - }); - } else { - for (const _header in proxyRes.headers) { - if (!settings.disAllowResponseHeaders.includes(_header)) { - res.setHeader(_header, proxyRes.headers[_header] as string); + const contentEncoding = proxyRes.headers['content-encoding'] || ''; + const chunks: Buffer[] = []; + proxyRes.on('data', chunk => chunks.push(Buffer.from(chunk))); + proxyRes.on('end', () => { + try { + let buffer = Buffer.concat(chunks); + if (buffer.length > 0) { + if (contentEncoding.includes('br')) buffer = brotliDecompressSync(buffer); + else if (contentEncoding.includes('gzip')) buffer = gunzipSync(buffer); + else if (contentEncoding.includes('zstd')) buffer = zstdDecompressSync(buffer); + res.write(buffer); } - } - for (const _header in settings.disAllowedRequestHeaders) { - delete proxyRes.headers[_header]; - } - proxyRes.on('data', function (chunk) { - res.write(chunk); - }); - proxyRes.on('end', function () { res.end(); - }); - } + } catch (err) { + console.error('Decompression error:', err); + res.statusCode = 500; + res.end('Decompression error'); + } + }); }); export { proxyHandlerMiddle, proxySettingMiddleware }; From 0c01a0d238f3e153155e32f1ce91353dcb9a2297 Mon Sep 17 00:00:00 2001 From: 7ui77 <99854073+7ui77@users.noreply.github.com> Date: Sun, 17 May 2026 10:57:36 +0700 Subject: [PATCH 4/9] Add API call for fetching manga chapters Fetch chapters from API if available, otherwise use initial data. --- plugins/english/novelbuddy.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/plugins/english/novelbuddy.ts b/plugins/english/novelbuddy.ts index 4eaf9c38e..fd815697d 100644 --- a/plugins/english/novelbuddy.ts +++ b/plugins/english/novelbuddy.ts @@ -119,7 +119,19 @@ class NovelBuddy implements Plugin.PluginBase { novel.rating = initialManga.ratingStats.average; } - if (initialManga.chapters) { + const chaptersUrl = `${this.api}titles/${initialManga.id}/chapters`; + const chaptersResponse = await fetchApi(chaptersUrl); + const chaptersJson: ChapterResponse = await chaptersResponse.json(); + + if (chaptersJson?.success && chaptersJson?.data?.chapters) { + novel.chapters = chaptersJson.data.chapters + .map(chapter => ({ + name: chapter.name, + path: new URL(chapter.url, this.site).pathname.substring(1), + releaseTime: chapter.updated_at, + })) + .reverse(); + } else if (initialManga.chapters) { novel.chapters = initialManga.chapters .map(chapter => ({ name: chapter.name, From b7d64138cb9b63f3f0c5a098bef5dfc2d3b1cd15 Mon Sep 17 00:00:00 2001 From: 7ui77 <99854073+7ui77@users.noreply.github.com> Date: Mon, 18 May 2026 06:40:16 +0700 Subject: [PATCH 5/9] revert(novelbuddy): revert novelbuddy to version 2.1.1 and simple watermark removal --- plugins/english/novelbuddy.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/english/novelbuddy.ts b/plugins/english/novelbuddy.ts index fd815697d..b30cf469c 100644 --- a/plugins/english/novelbuddy.ts +++ b/plugins/english/novelbuddy.ts @@ -9,7 +9,7 @@ class NovelBuddy implements Plugin.PluginBase { name = 'NovelBuddy'; site = 'https://novelbuddy.com/'; api = 'https://api.novelbuddy.com/'; - version = '2.1.2'; + version = '2.1.1'; icon = 'src/en/novelbuddy/icon.png'; parseNovels(body: Response): Plugin.NovelItem[] { @@ -119,6 +119,7 @@ class NovelBuddy implements Plugin.PluginBase { novel.rating = initialManga.ratingStats.average; } + // Fetch full chapter list from API const chaptersUrl = `${this.api}titles/${initialManga.id}/chapters`; const chaptersResponse = await fetchApi(chaptersUrl); const chaptersJson: ChapterResponse = await chaptersResponse.json(); @@ -166,9 +167,7 @@ class NovelBuddy implements Plugin.PluginBase { ); // Remove obfuscated freewebnovel watermarks (e.g., free𝑤𝑒𝑏novel.com) - const fwnRegex = - /(?:𝐟|ᵮ|𝑓|𝒇|𝒻|𝓯|𝔣|𝕗|𝖿|𝗳|𝙛|𝚏|ꬵ|ꞙ|ẝ|𝖋|ⓕ|f|ƒ|ḟ|ʃ|բ|ᶠ|⒡|ſ|ꊰ|ʄ|∱|ᶂ|𝘧|f)(?:𝚛|ꭇ|ᣴ|ℾ|𝚪|𝛤|𝜞|𝝘|𝞒|Ⲅ|Г|Ꮁ|ᒥ|ꭈ|ⲅ|ꮁ|ⓡ|r|ŕ|ṙ|ř|ȑ|ȓ|ṛ|ṝ|ŗ|г|Ր|ɾ|ᥬ|ṟ|ɍ|ʳ|⒭|ɼ|ѓ|ᴦ|ᶉ|𝐫|𝑟|𝒓|𝓇|𝓻|𝔯|𝕣|𝖗|𝗋|𝗿|𝘳|𝙧|ᵲ|ґ|ᵣ|r)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)+(?:𝐰|ꝡ|𝑤|𝒘|𝓌|𝔀|𝔴|𝕨|𝖜|𝗐|𝘄|𝘸|𝙬|𝚠|ա|ẁ|ꮃ|ẃ|ⓦ|⍵|ŵ|ẇ|ẅ|ẘ|ẉ|ⱳ|ὼ|ὠ|ὡ|ὢ|ὣ|ω|ὤ|ὥ|ὦ|ὧ|ῲ|ῳ|ῴ|ῶ|ῷ|Ⱳ|ѡ|ԝ|ᴡ|ώ|ᾠ|ᾡ|ᾡ|ᾢ|ᾣ|ᾤ|ᾥ|ᾦ|ɯ|𝝕|𝟉|𝞏|w)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ꮟ|Ꮟ|𝐛|𝘣|𝒷|𝔟|𝓫|𝖇|𝖻|𝑏|𝙗|𝕓|𝒃|𝗯|𝚋|♭|ᑳ|ᒈ|b|ᖚ|ᕹ|ᕺ|ⓑ|ḃ|ḅ|ҍ|ъ|ḇ|ƃ|ɓ|ƅ|ᖯ|Ƅ|Ь|ᑲ|þ|Ƃ|⒝|Ъ|ᶀ|ᑿ|ᒀ|ᒂ|ᒁ|ᑾ|ь|ƀ|Ҍ|Ѣ|ѣ|ᔎ|b)(?:ո|ռ|ח|𝒏|𝓷|ն|𝑛|𝖓|𝔫|𝗇|ն|𝗻|ᥒ|ⓝ|ή|n|ǹ|ᴒ|ń|ñ|ᾗ|η|ṅ|ň|ṇ|ɲ|ņ|ṋ|ṉ|ղ|ຖ|Ռ|ƞ|ŋ|⒩|ภ|ก|ɳ|п|ʼn|л|ԉ|Ƞ|ἠ|ἡ|ῃ|դ|ᾐ|ᾑ|ᾒ|ᾓ|ᾔ|ᾕ|ᾖ|ῄ|ῆ|ῇ|ῂ|ἢ|ἣ|ἤ|ἥ|ἦ|ἧ|ὴ|ή|በ|ቡ|ቢ|ባ|ቤ|ብ|ቦ|ȵ|𝛈|𝜂|𝜼|𝝶|𝞰|𝕟|𝘯|𝐧|𝓃|ᶇ|ᵰ|ᥥ|∩|n)(?:ం|ಂ|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝖔|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝛐|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|o|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|o|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|ⓞ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|告知|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:∨|⌄|⋁|ⅴ|𝐯|𝑣|𝒗|𝓋|𝔳|𝕧|𝖛|𝗏|ꮩ|ሀ|ⓥ|v|𝜐|𝝊|ṽ|ṿ|౮|ง|ѵ|ע|ᴠ|ν|ט|ᵥ|ѷ|៴|ᘁ|𝙫|𝚟|𝛎|𝜈|𝝂|𝝼|𝞶|𝘷|𝘃|𝓿|v)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|✀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ⓛ|l|ŀ|ĺ|ľ|ḷ|ḹ|ḷ|ļ|Ӏ|ℓ|ḽ|ḻ|ł|レ|ɭ|ƚ|ɫ|ⱡ|\\|Ɩ|⒧|ʅ|ǀ|ו|ן|Ι|І|||ᶩ|ӏ|𝓘|𝕀|𝖨|𝗜|𝘐|𝐥|𝑙|𝒍|𝓁|𝔩|𝕝|𝖑|𝗅|𝗹|𝘭|𝚕|𝜤|𝝞|ı|𝚤|ɩ|ι|𝛊|𝜄|𝜾|𝞲|I|l)(?:.?(?:🝌|c|ⅽ|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖼|𝗰|𝘤|𝙘|𝚌|ᴄ|ϲ|ⲥ|с|ꮯ|𐐽|ⲥ|𐐽|ꮯ|ĉ|c|ⓒ|ć|č|ċ|ç|ҁ|ƈ|ḉ|ȼ|ↄ|с|ር|ᴄ|ϲ|ҫ|꒝|ς|ɽ|ϛ|𝙲|ᑦ|᧚|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖼|𝗰|𝘤|𝙘|𝚌|₵|🇨|ᥴ|ᒼ|ⅽ|c)(?:ం|ం|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝖔|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝛐|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|o|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|𝘰|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|ⓞ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|告知|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:₥|ᵯ|𝖒|𝐦|𝗆|𝔪|𝕞|𝓂|ⓜ|m|ന|ᙢ|൩|m|ḿ|ṁ|ⅿ|ϻ|ṃ|ጠ|ɱ|៳|ᶆ|𝒎|𝙢|𝓶|𝚖|𝑚|𝗺|᧕|᧗|m))?/g; - content = content.replace(fwnRegex, ''); + content = content.replace(/free.*?novel\.com/gi, ''); } return content; From c47f58d633a0f8d6cca60c1e6ea3d5916a98d138 Mon Sep 17 00:00:00 2001 From: 7ui77 <99854073+7ui77@users.noreply.github.com> Date: Mon, 18 May 2026 08:27:51 +0700 Subject: [PATCH 6/9] fix(proxy): improve redirect handling, status propagation, and 304 handling Restored verbose logging and implemented robust internal redirect following with infinite loop protection. Verified via web debugger. --- proxy.ts | 123 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 104 insertions(+), 19 deletions(-) diff --git a/proxy.ts b/proxy.ts index 1aaa61da6..0c5a69951 100644 --- a/proxy.ts +++ b/proxy.ts @@ -1,4 +1,5 @@ import process from 'node:process'; +import { Buffer } from 'buffer'; import { FetchMode, ServerSetting } from './src/types/types'; import { Connect } from 'vite'; import httpProxy from 'http-proxy'; @@ -50,11 +51,17 @@ const proxySettingMiddleware: Connect.NextHandleFunction = (req, res) => { const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => { const rawUrl = 'https:' + req.url; if (req.headers['access-control-request-method']) { - res.setHeader('access-control-allow-methods', req.headers['access-control-request-method']); + res.setHeader( + 'access-control-allow-methods', + req.headers['access-control-request-method'], + ); delete req.headers['access-control-request-method']; } if (req.headers['access-control-request-headers']) { - res.setHeader('access-control-allow-headers', req.headers['access-control-request-headers']); + res.setHeader( + 'access-control-allow-headers', + req.headers['access-control-request-headers'], + ); delete req.headers['access-control-request-headers']; } res.setHeader('Access-Control-Allow-Origin', settings.CLIENT_HOST); @@ -68,7 +75,10 @@ const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => { try { const _url = new URL(rawUrl); for (const _header in req.headers) { - if (req.headers[_header]?.includes('localhost') || settings.disAllowedRequestHeaders.includes(_header)) { + if ( + req.headers[_header]?.includes('localhost') || + settings.disAllowedRequestHeaders.includes(_header) + ) { delete req.headers[_header]; } } @@ -79,7 +89,9 @@ const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => { req.url = _url.toString(); proxyRequest(req, res); } catch (err) { - console.error('Proxy logic error:', err); + console.log('\x1b[31m', '----------ERRROR----------'); + console.error(err); + console.log('\x1b[31m', '----------ERRROR----------'); if (!res.closed) { res.statusCode = 500; res.end(); @@ -90,16 +102,34 @@ const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => { const proxyRequest: Connect.SimpleHandleFunction = (req, res) => { const _url = new URL(req.url || ''); + console.log('\x1b[36m', '----------------'); + console.log( + `Making proxy request - at ${new Date().toLocaleTimeString()} + url: ${_url.href} + headers:`, + ); + Object.entries(req.headers).forEach(([name, value]) => { + console.log('\t', '\x1b[32m', name + ':', '\x1b[37m', value); + }); + console.log('\x1b[36m', '----------------'); + if (settings.fetchMode === FetchMode.CURL) { let curl = `curl -L '${_url.href}'`; - if (settings.useUserAgent) curl += ` -H 'User-Agent: ${req.headers['user-agent']}'`; + if (settings.useUserAgent) + curl += ` -H 'User-Agent: ${req.headers['user-agent']}'`; if (settings.cookies) curl += ` -H 'Cookie: ${settings.cookies}'`; if (req.headers.origin2) curl += ` -H 'Origin: ${req.headers.origin2}'`; const isWindows = process.platform === 'win32'; - const options = isWindows ? { shell: process.env.BASH_LOCATION || process.env.ProgramFiles + '\\git\\usr\\bin\\bash.exe' } : {}; + const options = isWindows + ? { + shell: + process.env.BASH_LOCATION || + process.env.ProgramFiles + '\\git\\usr\\bin\\bash.exe', + } + : {}; - exec(curl, options, (error, stdout, stderr) => { + exec(curl, options, (error, stdout) => { if (error) { res.statusCode = 500; res.write(`exec error: ${error}`); @@ -112,15 +142,21 @@ const proxyRequest: Connect.SimpleHandleFunction = (req, res) => { }); } else if (settings.fetchMode === FetchMode.NODE_FETCH) { const headers = new Headers(); - if (settings.useUserAgent) headers.append('user-agent', req.headers['user-agent'] as string); + if (settings.useUserAgent) + headers.append('user-agent', req.headers['user-agent'] as string); if (settings.cookies) headers.append('cookie', settings.cookies); - if (req.headers.origin2) headers.append('origin', req.headers.origin2 as string); - + if (req.headers.origin2) + headers.append('origin', req.headers.origin2 as string); + fetch(_url.href, { headers }) .then(async res2 => { res.statusCode = res2.status; res2.headers.forEach((val, key) => { - if (!settings.disAllowResponseHeaders.includes(key) && key !== 'content-encoding' && key !== 'content-length') { + if ( + !settings.disAllowResponseHeaders.includes(key) && + key !== 'content-encoding' && + key !== 'content-length' + ) { res.setHeader(key, val); } }); @@ -133,21 +169,68 @@ const proxyRequest: Connect.SimpleHandleFunction = (req, res) => { res.end(); }); } else if (settings.fetchMode === FetchMode.PROXY) { - proxy.web(req, res, { target: _url.origin, selfHandleResponse: true, followRedirects: true }, err => { - console.error('Proxy target error:', err); - res.statusCode = 500; - res.end(); - }); + proxy.web( + req, + res, + { target: _url.origin, selfHandleResponse: true, followRedirects: true }, + err => { + console.error('Proxy target error:', err); + res.statusCode = 500; + res.end(); + }, + ); } }; proxy.on('proxyRes', function (proxyRes, req, res) { const statusCode = proxyRes.statusCode || 200; + + // Redirect handling + if ([301, 302, 303, 307, 308].includes(statusCode)) { + const location = proxyRes.headers['location']; + if (location) { + try { + const _url = new URL(req.url || ''); + const redirectUrl = new URL(location, _url.href); + req.url = redirectUrl.toString(); + + // Prevent infinite loops + const reqWithRedirect = req as Connect.IncomingMessage & { + _redirectCount?: number; + }; + const redirectCount = reqWithRedirect._redirectCount || 0; + if (redirectCount >= 5) { + res.statusCode = 508; + res.end('Too many redirects'); + return; + } + reqWithRedirect._redirectCount = redirectCount + 1; + + // Update method for 301/302/303 to GET as per spec + if ([301, 302, 303].includes(statusCode)) { + req.method = 'GET'; + req.headers['content-length'] = '0'; + delete req.headers['content-type']; + } + + req.removeAllListeners(); + proxyRequest(req, res); + return; + } catch (err) { + console.error('Redirect parsing error:', err); + } + } + } + res.statusCode = statusCode; // Propagate headers but filter restricted ones Object.keys(proxyRes.headers).forEach(key => { - if (!settings.disAllowResponseHeaders.includes(key) && key !== 'content-encoding' && key !== 'content-length') { + if ( + !settings.disAllowResponseHeaders.includes(key) && + key !== 'content-encoding' && + key !== 'content-length' + ) { res.setHeader(key, proxyRes.headers[key] as string); } }); @@ -164,9 +247,11 @@ proxy.on('proxyRes', function (proxyRes, req, res) { try { let buffer = Buffer.concat(chunks); if (buffer.length > 0) { - if (contentEncoding.includes('br')) buffer = brotliDecompressSync(buffer); + if (contentEncoding.includes('br')) + buffer = brotliDecompressSync(buffer); else if (contentEncoding.includes('gzip')) buffer = gunzipSync(buffer); - else if (contentEncoding.includes('zstd')) buffer = zstdDecompressSync(buffer); + else if (contentEncoding.includes('zstd')) + buffer = zstdDecompressSync(buffer); res.write(buffer); } res.end(); From a24e637dfa7812778a1e17aabbb150e2ea019dca Mon Sep 17 00:00:00 2001 From: 7ui77 <99854073+7ui77@users.noreply.github.com> Date: Tue, 19 May 2026 05:44:19 +0700 Subject: [PATCH 7/9] fix: type errors for buffer and redact sensitive logs --- proxy.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/proxy.ts b/proxy.ts index 9cd905acd..61fd6a0b9 100644 --- a/proxy.ts +++ b/proxy.ts @@ -117,7 +117,15 @@ const proxyRequest: Connect.SimpleHandleFunction = (req, res) => { headers:`, ); Object.entries(req.headers).forEach(([name, value]) => { - console.log('\t', '\x1b[32m', name + ':', '\x1b[37m', value); + // Redact sensitive headers in logs + const isSensitive = ['cookie', 'authorization'].includes(name.toLowerCase()); + console.log( + '\t', + '\x1b[32m', + name + ':', + '\x1b[37m', + isSensitive ? '[REDACTED]' : value, + ); }); console.log('\x1b[36m', '----------------'); @@ -253,7 +261,8 @@ proxy.on('proxyRes', function (proxyRes, req, res) { proxyRes.on('data', chunk => chunks.push(Buffer.from(chunk))); proxyRes.on('end', () => { try { - let buffer = Buffer.concat(chunks); + // Use 'any' to fix Buffer type mismatch in newer Node environments + let buffer: any = Buffer.concat(chunks); if (buffer.length > 0) { if (contentEncoding.includes('br')) buffer = brotliDecompressSync(buffer); From b413708cbee77d7bbc150c44d52ed14a6e43c1c8 Mon Sep 17 00:00:00 2001 From: 7ui77 <99854073+7ui77@users.noreply.github.com> Date: Tue, 19 May 2026 05:55:55 +0700 Subject: [PATCH 8/9] fix: replace any with strictly typed separate variables --- proxy.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/proxy.ts b/proxy.ts index 61fd6a0b9..6bd0f9793 100644 --- a/proxy.ts +++ b/proxy.ts @@ -261,15 +261,19 @@ proxy.on('proxyRes', function (proxyRes, req, res) { proxyRes.on('data', chunk => chunks.push(Buffer.from(chunk))); proxyRes.on('end', () => { try { - // Use 'any' to fix Buffer type mismatch in newer Node environments - let buffer: any = Buffer.concat(chunks); - if (buffer.length > 0) { - if (contentEncoding.includes('br')) - buffer = brotliDecompressSync(buffer); - else if (contentEncoding.includes('gzip')) buffer = gunzipSync(buffer); - else if (contentEncoding.includes('zstd')) - buffer = zstdDecompressSync(buffer); - res.write(buffer); + const compressedBuffer = Buffer.concat(chunks); + if (compressedBuffer.length > 0) { + let decompressedBuffer: Buffer; + if (contentEncoding.includes('br')) { + decompressedBuffer = brotliDecompressSync(compressedBuffer); + } else if (contentEncoding.includes('gzip')) { + decompressedBuffer = gunzipSync(compressedBuffer); + } else if (contentEncoding.includes('zstd')) { + decompressedBuffer = zstdDecompressSync(compressedBuffer); + } else { + decompressedBuffer = compressedBuffer; + } + res.write(decompressedBuffer); } res.end(); } catch (err) { From facb93a610c9c8232d52b8d94e59526480eb8fc9 Mon Sep 17 00:00:00 2001 From: 7ui77 <99854073+7ui77@users.noreply.github.com> Date: Tue, 19 May 2026 09:22:35 +0700 Subject: [PATCH 9/9] chore(proxy): revert log redaction and run prettier --- proxy.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/proxy.ts b/proxy.ts index 6bd0f9793..7e9ea32dd 100644 --- a/proxy.ts +++ b/proxy.ts @@ -117,15 +117,7 @@ const proxyRequest: Connect.SimpleHandleFunction = (req, res) => { headers:`, ); Object.entries(req.headers).forEach(([name, value]) => { - // Redact sensitive headers in logs - const isSensitive = ['cookie', 'authorization'].includes(name.toLowerCase()); - console.log( - '\t', - '\x1b[32m', - name + ':', - '\x1b[37m', - isSensitive ? '[REDACTED]' : value, - ); + console.log('\t', '\x1b[32m', name + ':', '\x1b[37m', value); }); console.log('\x1b[36m', '----------------');