class MangaFire { constructor() { this.baseUrl = "https://mangafire.to"; this.type = "book-board"; this.version = "1.0"; this.mediaType = "manga"; } async search(queryObj) { const query = queryObj.query.trim(); const vrf = this.generate(query); const res = await fetch( `${this.baseUrl}/ajax/manga/search?keyword=${query.replaceAll(" ", "+")}&vrf=${vrf}` ); const data = await res.json(); if (!data?.result?.html) return []; const $ = this.cheerio.load(data.result.html); return $("a.unit") .map((_, e) => { const el = $(e); return { id: el.attr("href")?.replace("/manga/", ""), title: el.find("h6").text().trim(), image: el.find("img").attr("src") || null, rating: null, type: "book", }; }) .get(); } async getMetadata(id) { const res = await fetch(`${this.baseUrl}/manga/${id}`); const html = await res.text(); const $ = this.cheerio.load(html); const info = $(".info").first(); const title = info.find("h1[itemprop='name']").text().trim(); const scoreText = info .find("span b") .filter((_, e) => $(e).text().includes("MAL")) .first() .text(); const score = parseFloat(scoreText.replace(/[^\d.]/g, "")) || 0; const genres = $("span:contains('Genres:')") .next("span") .find("a") .map((_, e) => $(e).text().trim()) .get() .join(", "); const status = info.find("p").first().text().trim().toLowerCase(); const published = $("span:contains('Published:')") .next("span") .text() .trim(); const summary = $(".description").text().trim(); const image = $(".poster img[itemprop='image']").attr("src") || null; return { id, title, format: "MANGA", score, genres, status, published, summary, chapters: 0, image, }; } async findChapters(mangaId) { const res = await fetch(`${this.baseUrl}/manga/${mangaId}`); const html = await res.text(); const $ = this.cheerio.load(html); const langs = this.extractLanguageCodes($); const all = []; for (const lang of langs) { const chapters = await this.fetchChaptersForLanguage(mangaId, lang); all.push(...chapters); } return all; } extractLanguageCodes($) { const map = new Map(); $("[data-code][data-title]").each((_, e) => { let code = $(e).attr("data-code")?.toLowerCase() || ""; const title = $(e).attr("data-title") || ""; if (code === "es" && title.includes("LATAM")) code = "es-la"; else if (code === "pt" && title.includes("Br")) code = "pt-br"; map.set(code, code); }); return [...map.values()]; } async fetchChaptersForLanguage(mangaId, lang) { const mangaIdShort = mangaId.split(".").pop(); const vrf = this.generate(mangaIdShort + "@chapter@" + lang); const res = await fetch( `${this.baseUrl}/ajax/read/${mangaIdShort}/chapter/${lang}?vrf=${vrf}` ); const data = await res.json(); if (!data?.result?.html) return []; const $ = this.cheerio.load(data.result.html); const chapters = []; $("a[data-number][data-id]").each((i, e) => { chapters.push({ id: $(e).attr("data-id"), title: $(e).attr("title") || "", number: Number($(e).attr("data-number")) || i + 1, language: this.normalizeLanguageCode(lang), releaseDate: null, index: i, }); }); return chapters.reverse().map((c, i) => ({ ...c, index: i })); } normalizeLanguageCode(lang) { const map = { en: "en", fr: "fr", es: "es", "es-la": "es-419", pt: "pt", "pt-br": "pt-br", ja: "ja", de: "de", it: "it", ru: "ru", ko: "ko", zh: "zh", "zh-cn": "zh-cn", "zh-tw": "zh-tw", ar: "ar", tr: "tr", }; return map[lang] || lang; } async findChapterPages(chapterId) { const vrf = this.generate("chapter@" + chapterId); const res = await fetch( `${this.baseUrl}/ajax/read/chapter/${chapterId}?vrf=${vrf}` ); const data = await res.json(); const images = data?.result?.images; if (!images?.length) return []; return images.map((img, i) => ({ url: img[0], index: i, headers: { Referer: this.baseUrl, }, })); } textEncode(str) { return Uint8Array.from(Buffer.from(str, "utf-8")); } textDecode(bytes) { return Buffer.from(bytes).toString("utf-8"); } atob(data) { return Uint8Array.from(Buffer.from(data, "base64")); } btoa(data) { return Buffer.from(data).toString("base64"); } add8(n) { return (c) => (c + n) & 0xff; } sub8(n) { return (c) => (c - n + 256) & 0xff; } xor8(n) { return (c) => (c ^ n) & 0xff; } rotl8(n) { return (c) => ((c << n) | (c >> (8 - n))) & 0xff; } rotr8(n) { return (c) => ((c >> n) | (c << (8 - n))) & 0xff; } scheduleC = [ this.sub8(223), this.rotr8(4), this.rotr8(4), this.add8(234), this.rotr8(7), this.rotr8(2), this.rotr8(7), this.sub8(223), this.rotr8(7), this.rotr8(6), ]; scheduleY = [ this.add8(19), this.rotr8(7), this.add8(19), this.rotr8(6), this.add8(19), this.rotr8(1), this.add8(19), this.rotr8(6), this.rotr8(7), this.rotr8(4), ]; scheduleB = [ this.sub8(223), this.rotr8(1), this.add8(19), this.sub8(223), this.rotl8(2), this.sub8(223), this.add8(19), this.rotl8(1), this.rotl8(2), this.rotl8(1), ]; scheduleJ = [ this.add8(19), this.rotl8(1), this.rotl8(1), this.rotr8(1), this.add8(234), this.rotl8(1), this.sub8(223), this.rotl8(6), this.rotl8(4), this.rotl8(1), ]; scheduleE = [ this.rotr8(1), this.rotl8(1), this.rotl8(6), this.rotr8(1), this.rotl8(2), this.rotr8(4), this.rotl8(1), this.rotl8(1), this.sub8(223), this.rotl8(2), ]; rc4Keys = { l: "FgxyJUQDPUGSzwbAq/ToWn4/e8jYzvabE+dLMb1XU1o=", g: "CQx3CLwswJAnM1VxOqX+y+f3eUns03ulxv8Z+0gUyik=", B: "fAS+otFLkKsKAJzu3yU+rGOlbbFVq+u+LaS6+s1eCJs=", m: "Oy45fQVK9kq9019+VysXVlz1F9S1YwYKgXyzGlZrijo=", F: "aoDIdXezm2l3HrcnQdkPJTDT8+W6mcl2/02ewBHfPzg=", }; seeds32 = { A: "yH6MXnMEcDVWO/9a6P9W92BAh1eRLVFxFlWTHUqQ474=", V: "RK7y4dZ0azs9Uqz+bbFB46Bx2K9EHg74ndxknY9uknA=", N: "rqr9HeTQOg8TlFiIGZpJaxcvAaKHwMwrkqojJCpcvoc=", P: "/4GPpmZXYpn5RpkP7FC/dt8SXz7W30nUZTe8wb+3xmU=", k: "wsSGSBXKWA9q1oDJpjtJddVxH+evCfL5SO9HZnUDFU8=", }; prefixKeys = { O: "l9PavRg=", v: "Ml2v7ag1Jg==", L: "i/Va0UxrbMo=", p: "WFjKAHGEkQM=", W: "5Rr27rWd", }; rc4(key, input) { const s = new Uint8Array(256); for (let i = 0; i < 256; i++) s[i] = i; let j = 0; for (let i = 0; i < 256; i++) { j = (j + s[i] + key[i % key.length]) & 0xff; [s[i], s[j]] = [s[j], s[i]]; } const output = new Uint8Array(input.length); let i = 0; j = 0; for (let y = 0; y < input.length; y++) { i = (i + 1) & 0xff; j = (j + s[i]) & 0xff; [s[i], s[j]] = [s[j], s[i]]; const k = s[(s[i] + s[j]) & 0xff]; output[y] = input[y] ^ k; } return output; } transform(input, initSeedBytes, prefixKeyBytes, prefixLen, schedule) { const out = []; for (let i = 0; i < input.length; i++) { if (i < prefixLen) { out.push(prefixKeyBytes[i] || 0); } const transformed = schedule[i % 10]((input[i] ^ initSeedBytes[i % 32]) & 0xff) & 0xff; out.push(transformed); } return new Uint8Array(out); } generate(input) { let encodedInput = encodeURIComponent(input); let bytes = this.textEncode(encodedInput); // Etapa 1: RC4 con clave "l" + Transform con schedule_c bytes = this.rc4(this.atob(this.rc4Keys["l"]), bytes); const prefix_O = this.atob(this.prefixKeys["O"]); bytes = this.transform(bytes, this.atob(this.seeds32["A"]), prefix_O, prefix_O.length, this.scheduleC); // Etapa 2: RC4 con clave "g" + Transform con schedule_y bytes = this.rc4(this.atob(this.rc4Keys["g"]), bytes); const prefix_v = this.atob(this.prefixKeys["v"]); bytes = this.transform(bytes, this.atob(this.seeds32["V"]), prefix_v, prefix_v.length, this.scheduleY); // Etapa 3: RC4 con clave "B" + Transform con schedule_b bytes = this.rc4(this.atob(this.rc4Keys["B"]), bytes); const prefix_L = this.atob(this.prefixKeys["L"]); bytes = this.transform(bytes, this.atob(this.seeds32["N"]), prefix_L, prefix_L.length, this.scheduleB); // Etapa 4: RC4 con clave "m" + Transform con schedule_j bytes = this.rc4(this.atob(this.rc4Keys["m"]), bytes); const prefix_p = this.atob(this.prefixKeys["p"]); bytes = this.transform(bytes, this.atob(this.seeds32["P"]), prefix_p, prefix_p.length, this.scheduleJ); // Etapa 5: RC4 con clave "F" + Transform con schedule_e bytes = this.rc4(this.atob(this.rc4Keys["F"]), bytes); const prefix_W = this.atob(this.prefixKeys["W"]); bytes = this.transform(bytes, this.atob(this.seeds32["k"]), prefix_W, prefix_W.length, this.scheduleE); // Base64URL encode return this.btoa(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } } module.exports = MangaFire;