From 72322deaee7416743cfc515280ac0c8657649144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20F=C3=A4hrmann?= Date: Thu, 22 Jan 2026 22:12:22 +0100 Subject: [PATCH] [mangafire] generate 'vrf' tokens (#8400 #8906) https://github.com/dazedcat19/FMD2/commit/26fc9e9649d4e21edc03cf3a37fe24f995fa5917 --- gallery_dl/extractor/mangafire.py | 128 +++++++++++++++++++++++++++++- test/results/mangafire.py | 16 ++-- 2 files changed, 133 insertions(+), 11 deletions(-) diff --git a/gallery_dl/extractor/mangafire.py b/gallery_dl/extractor/mangafire.py index 8db91b32..646729f5 100644 --- a/gallery_dl/extractor/mangafire.py +++ b/gallery_dl/extractor/mangafire.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright 2025 Mike Fährmann +# Copyright 2025-2026 Mike Fährmann # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as @@ -11,6 +11,7 @@ from .common import ChapterExtractor, MangaExtractor from .. import text, exception from ..cache import memcache +import binascii BASE_PATTERN = r"(?:https?://)?(?:www\.)?mangafire\.to" @@ -52,8 +53,9 @@ class MangafireChapterExtractor(MangafireBase, ChapterExtractor): def images(self, page): url = f"{self.root}/ajax/read/{self.type}/{self.chapter_id}" + params = {"vrf": generate_VRF(f"{self.type}@{self.chapter_id}")} headers = {"x-requested-with": "XMLHttpRequest"} - data = self.request_json(url, headers=headers) + data = self.request_json(url, params=params, headers=headers) return [ (image[0], None) @@ -129,8 +131,9 @@ def _manga_info(self, manga_path, page=None): def _manga_chapters(self, manga_info): manga_id, type, lang = manga_info url = f"{self.root}/ajax/read/{manga_id}/{type}/{lang}" + params = {"vrf": generate_VRF(f"{manga_id}@{type}@{lang}")} headers = {"x-requested-with": "XMLHttpRequest"} - data = self.request_json(url, headers=headers) + data = self.request_json(url, params=params, headers=headers) needle = f"{manga_id}/{lang}/" return { @@ -166,3 +169,122 @@ def _chapter_info(info): "title" : text.unescape(text.extr(info, 'title="', '"')), "lang" : lang, } + + +############################################################################### +# VRF generation utils +# +# adapted from dazedcat19/FMD2 +# https://github.com/dazedcat19/FMD2/blob/master/lua/modules/MangaFire.lua + +def generate_VRF(input): + input = text.quote(input).encode() + + for key_b64, seed_b64, prefix_b64, schedule in ( + (key_l, seed_A, prefix_O, schedule_c), + (key_g, seed_V, prefix_v, schedule_y), + (key_B, seed_N, prefix_L, schedule_b), + (key_m, seed_P, prefix_p, schedule_j), + (key_F, seed_k, prefix_W, schedule_e), + ): + input = transform( + rc4(binascii.a2b_base64(key_b64), input), + binascii.a2b_base64(seed_b64), + binascii.a2b_base64(prefix_b64), + schedule, + ) + + return binascii.b2a_base64(bytes(input), newline=False).rstrip( + b"=").replace(b"+", b"-").replace(b"/", b"_") + + +def transform(input, seed, prefix, schedule): + prefix_len = len(prefix) + + out = [] + for idx, c in enumerate(input): + if idx < prefix_len: + out.append(prefix[idx] or 0) + out.append(schedule[idx % 10]((c ^ seed[idx % 32]) & 255) & 255) + return out + + +def rc4(key, input): + lkey = len(key) + + j = 0 + s = list(range(256)) + for i in range(256): + j = (j + s[i] + key[i % lkey]) & 255 + s[i], s[j] = s[j], s[i] + + out = [] + i = j = 0 + for c in input: + i = (i + 1) & 255 + j = (j + s[i]) & 255 + s[i], s[j] = s[j], s[i] + k = s[(s[i] + s[j]) & 255] + out.append(c ^ k) + return out + + +def add8(n): + return lambda c: (c + n) & 255 + + +def sub8(n): + return lambda c: (c - n + 256) & 255 + + +def xor8(n): + return lambda c: (c ^ n) & 255 + + +def rotl8(n): + return lambda c: ((c << n) | (c >> (8 - n))) & 255 + + +def rotr8(n): + return lambda c: ((c >> n) | (c << (8 - n))) & 255 + + +schedule_c = ( + sub8(223), rotr8(4), rotr8(4), add8(234), rotr8(7), + rotr8(2), rotr8(7), sub8(223), rotr8(7), rotr8(6), +) +schedule_y = ( + add8(19), rotr8(7), add8(19), rotr8(6), add8(19), + rotr8(1), add8(19), rotr8(6), rotr8(7), rotr8(4), +) +schedule_b = ( + sub8(223), rotr8(1), add8(19), sub8(223), rotl8(2), + sub8(223), add8(19), rotl8(1), rotl8(2), rotl8(1), +) +schedule_j = ( + add8(19), rotl8(1), rotl8(1), rotr8(1), add8(234), + rotl8(1), sub8(223), rotl8(6), rotl8(4), rotl8(1), +) +schedule_e = ( + rotr8(1), rotl8(1), rotl8(6), rotr8(1), rotl8(2), + rotr8(4), rotl8(1), rotl8(1), sub8(223), rotl8(2), +) + + +key_l = "FgxyJUQDPUGSzwbAq/ToWn4/e8jYzvabE+dLMb1XU1o=" +key_g = "CQx3CLwswJAnM1VxOqX+y+f3eUns03ulxv8Z+0gUyik=" +key_B = "fAS+otFLkKsKAJzu3yU+rGOlbbFVq+u+LaS6+s1eCJs=" +key_m = "Oy45fQVK9kq9019+VysXVlz1F9S1YwYKgXyzGlZrijo=" +key_F = "aoDIdXezm2l3HrcnQdkPJTDT8+W6mcl2/02ewBHfPzg=" + +seed_A = "yH6MXnMEcDVWO/9a6P9W92BAh1eRLVFxFlWTHUqQ474=" +seed_V = "RK7y4dZ0azs9Uqz+bbFB46Bx2K9EHg74ndxknY9uknA=" +seed_N = "rqr9HeTQOg8TlFiIGZpJaxcvAaKHwMwrkqojJCpcvoc=" +seed_P = "/4GPpmZXYpn5RpkP7FC/dt8SXz7W30nUZTe8wb+3xmU=" +seed_k = "wsSGSBXKWA9q1oDJpjtJddVxH+evCfL5SO9HZnUDFU8=" + +prefix_O = "l9PavRg=" +prefix_v = "Ml2v7ag1Jg==" +prefix_L = "i/Va0UxrbMo=" +prefix_p = "WFjKAHGEkQM=" +prefix_W = "5Rr27rWd" diff --git a/test/results/mangafire.py b/test/results/mangafire.py index d96c4517..4f4b0a24 100644 --- a/test/results/mangafire.py +++ b/test/results/mangafire.py @@ -11,7 +11,7 @@ __tests__ = ( { "#url" : "https://mangafire.to/read/moto-saikyou-yuusha-no-saishuushoku.qzq9j/en/chapter-4", "#class" : mangafire.MangafireChapterExtractor, - "#pattern" : r"https://s3\.mfcdn2\.xyz/\w+/h/p\.jpg", + "#pattern" : r"https://20l\.mfcdn2\.xyz/mf/\w+/h/p\.jpg", "#count" : 37, "chapter" : 4, @@ -30,7 +30,7 @@ __tests__ = ( "manga_slug" : "moto-saikyou-yuusha-no-saishuushoku", "published" : "Mar 31, 2022 to ?", "publisher" : ["Wild Hero's"], - "score" : 9.0, + "score" : float, "status" : "Releasing", "title" : "The Former Strongest Hero Makes a Choice", "type" : "Manga", @@ -54,7 +54,7 @@ __tests__ = ( { "#url" : "https://mangafire.to/read/munou-wa-fuyou-to-iware-tokei-tsukai-no-boku-wa-shokuin-guild-kara-oidasareru-mo-dungeon-no-shinbu-de-shin-no-chikara-ni-kakusei-suru-the-comic.6wmv9/en/chapter-14.1", "#class" : mangafire.MangafireChapterExtractor, - "#pattern" : r"https://s\d\.mfcdn\d\.xyz/\w+/h/p\.jpg", + "#pattern" : r"https://\w+\.mfcdn\d\.xyz/mf/\w+/h/p\.jpg", "#count" : 13, "chapter" : 14, @@ -73,7 +73,7 @@ __tests__ = ( "manga_slug" : "munou-wa-fuyou-to-iware-tokei-tsukai-no-boku-wa-shokuin-guild-kara-oidasareru-mo-dungeon-no-shinbu-de-shin-no-chikara-ni-kakusei-suru-the-comic", "published" : "Feb 27, 2023 to ?", "publisher" : ["Comic Ride"], - "score" : 9.0, + "score" : range(5, 10), "status" : "Releasing", "title" : "Part 1 - Karim's Purpose", "type" : "Manga", @@ -96,7 +96,7 @@ __tests__ = ( "#url" : "https://mangafire.to/read/munou-wa-fuyou-to-iware-tokei-tsukai-no-boku-wa-shokuin-guild-kara-oidasareru-mo-dungeon-no-shinbu-de-shin-no-chikara-ni-kakusei-suru-the-comic.6wmv9/en/volume-2", "#comment" : "volume", "#class" : mangafire.MangafireChapterExtractor, - "#pattern" : r"https://s\d\.mfcdn\d\.xyz/\w+/h/p\.jpg", + "#pattern" : r"https://\w+\.mfcdn\d\.xyz/mf/\w+/h/p\.jpg", "#count" : 154, "volume" : 2, @@ -111,7 +111,7 @@ __tests__ = ( "#url" : "https://mangafire.to/manga/my-noble-family-is-headed-for-ruin-so-i-may-as-well-study-magic-in-my-free-timee.xjj0w", "#class" : mangafire.MangafireMangaExtractor, "#pattern" : mangafire.MangafireChapterExtractor.pattern, - "#count" : 31, + "#count" : range(40, 60), "chapter" : range(1, 30), "chapter_id" : int, @@ -156,7 +156,7 @@ __tests__ = ( "#url" : "https://mangafire.to/manga/regressing-as-the-reincarnated-bastard-of-the-sword-clann.90vp0", "#class" : mangafire.MangafireMangaExtractor, "#pattern" : mangafire.MangafireChapterExtractor.pattern, - "#count" : 62, + "#count" : range(70, 120), "author" : (), "chapter" : int, @@ -171,7 +171,7 @@ __tests__ = ( "manga_slug" : "regressing-as-the-reincarnated-bastard-of-the-sword-clann", "published" : "2024 to ?", "publisher" : (), - "score" : 9.0, + "score" : range(8, 10), "status" : "Releasing", "title" : "", "type" : "Manhwa",