339 lines
10 KiB
JavaScript
339 lines
10 KiB
JavaScript
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;
|