updated marketplace and extensions

This commit is contained in:
2026-01-01 20:44:41 +01:00
parent 1f52ac678e
commit ff24046f61
9 changed files with 1285 additions and 186 deletions

164
anime/AniZone.js Normal file
View File

@@ -0,0 +1,164 @@
class Anizone {
constructor() {
this.type = "anime-board";
this.version = "1.0";
this.api = "https://anizone.to";
}
getSettings() {
return {
episodeServers: ["HLS"],
supportsDub: true,
};
}
async search(queryObj) {
const query = queryObj.query ?? "";
const res = await fetch(
`${this.api}/anime?search=${encodeURIComponent(query)}`,
{
headers: {
accept: "*/*",
referer: "https://anizone.to/",
"user-agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
},
}
);
const html = await res.text();
const itemRegex =
/<div[^>]*class="relative overflow-hidden h-26 rounded-lg[\s\S]*?<img[^>]*src="([^"]+)"[^>]*alt="([^"]+)"[\s\S]*?<a[^>]*href="([^"]+)"[^>]*title="([^"]+)"/g;
const results = [];
let match;
while ((match = itemRegex.exec(html)) !== null) {
const [, image, altTitle, href, title] = match;
const animeId = href.split("/").pop();
// detectar sub / dub desde el episodio 1
let subOrDub = "sub";
try {
const epHtml = await fetch(`${this.api}/anime/${animeId}/1`, {
headers: {
referer: "https://anizone.to/",
"user-agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
},
}).then(r => r.text());
const audioMatch = epHtml.match(
/<div class="text-xs flex flex-wrap gap-1">([\s\S]*?)<\/div>/
);
if (audioMatch) {
const block = audioMatch[1];
const hasJP = /Japanese/i.test(block);
const hasOther = /(English|Spanish|French|German|Italian)/i.test(block);
if (hasJP && hasOther) subOrDub = "both";
else if (hasOther) subOrDub = "dub";
}
} catch {}
results.push({
id: animeId,
title: title || altTitle,
image,
url: href,
subOrDub,
});
}
return results;
}
async getMetadata(id) {
// HARDCODED de momento
return {
id,
title: "Unknown",
summary: "",
episodes: 0,
status: "unknown",
season: null,
year: null,
genres: [],
score: 0,
image: null,
};
}
async findEpisodes(id) {
const html = await fetch(`${this.api}/anime/${id}/1`).then(r => r.text());
const regex =
/<a[^>]*href="([^"]*\/anime\/[^"]+?)"[^>]*>\s*<div[^>]*>\s*<div[^>]*class='[^']*min-w-10[^']*'[^>]*>(\d+)<\/div>\s*<div[^>]*class="[^"]*line-clamp-1[^"]*"[^>]*>([^<]+)<\/div>/g;
const episodes = [];
let match;
while ((match = regex.exec(html)) !== null) {
const [, href, num, title] = match;
episodes.push({
id: href.split("/").pop(),
number: Number(num),
title: title.trim(),
url: href,
});
}
return episodes;
}
async findEpisodeServer(episode, server) {
const html = await fetch(episode.url).then(r => r.text());
const srcMatch = html.match(
/<media-player[^>]+src="([^"]+\.m3u8)"/i
);
if (!srcMatch) throw new Error("No m3u8 found");
const masterUrl = srcMatch[1];
const subtitles = [];
const trackRegex =
/<track[^>]+src=([^ >]+)[^>]*label="([^"]+)"[^>]*srclang="([^"]+)"[^>]*(default)?/gi;
let match;
while ((match = trackRegex.exec(html)) !== null) {
const [, src, label, lang, def] = match;
subtitles.push({
id: lang,
url: src,
language: label.trim(),
isDefault: Boolean(def),
});
}
return {
server,
headers: {
Origin: "https://anizone.to",
Referer: "https://anizone.to/",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
},
videoSources: [
{
url: masterUrl,
type: "m3u8",
quality: "auto",
subtitles,
},
],
};
}
}
module.exports = Anizone;

View File

@@ -2,7 +2,7 @@ class asmhentai {
constructor() { constructor() {
this.baseUrl = "https://asmhentai.com"; this.baseUrl = "https://asmhentai.com";
this.type = "book-board"; this.type = "book-board";
this.version = "1.0" this.version = "1.1"
this.mediaType = "manga"; this.mediaType = "manga";
} }
@@ -47,10 +47,9 @@ class asmhentai {
if (image.startsWith("//")) image = "https:" + image; if (image.startsWith("//")) image = "https:" + image;
const genres = $(".tags .tag_list .badge.tag") const genres = $(".tags a.tag")
.map((_, el) => $(el).clone().children().remove().end().text().trim()) .map((_, el) => $(el).text().trim())
.get() .get()
.join(", ");
return { return {
id, id,
@@ -67,38 +66,64 @@ class asmhentai {
} }
async findChapters(mangaId) { async findChapters(mangaId) {
const html = await fetch(`${this.baseUrl}/g/${mangaId}/`).then(r => r.text());
const $ = this.cheerio.load(html);
const title = $("h1").first().text().trim() || "Chapter 1";
let thumb = $(".gallery img").first().attr("data-src") || "";
if (thumb.startsWith("//")) thumb = "https:" + thumb;
const base = thumb.match(/https:\/\/[^\/]+\/\d+\/\d+\//)?.[0];
const pages = parseInt($(".pages").text().match(/\d+/)?.[0] || "0");
const ext = thumb.match(/\.(jpg|png|jpeg|gif)/i)?.[1] || "jpg";
const chapterId = Buffer.from(JSON.stringify({ base, pages, ext })).toString("base64");
return [{ return [{
id: chapterId, id: mangaId.toString(),
title, title: "Chapter",
number: 1, number: 1,
releaseDate: null,
index: 0 index: 0
}]; }];
} }
async findChapterPages(chapterId) { async findChapterPages(chapterId) {
const { base, pages, ext } = JSON.parse( const html = await fetch(`${this.baseUrl}/g/${chapterId}/`).then(r => r.text());
Buffer.from(chapterId, "base64").toString("utf8") const $ = this.cheerio.load(html);
);
return Array.from({ length: pages }, (_, i) => ({ const token = $('meta[name="csrf-token"]').attr("content") || "";
url: `${base}${i + 1}.${ext}`, const loadId = $("#load_id").val();
index: i const loadDir = $("#load_dir").val();
})); const totalPages = $("#t_pages").val() || "0";
const body = new URLSearchParams({
id: loadId,
dir: loadDir,
visible_pages: "0",
t_pages: totalPages,
type: "2",
});
if (token) body.append("_token", token);
const res = await fetch(`${this.baseUrl}/gallery/`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Referer": `${this.baseUrl}/g/${chapterId}/`,
},
body
}).then(r => r.text());
const $$ = this.cheerio.load(res);
return $$("img[data-src], img[src]").get()
.map((el) => {
let url = $$(el).attr("data-src") || $$(el).attr("src");
if (url?.startsWith("//")) url = "https:" + url;
return url;
})
.filter(url => {
// Mantenemos el filtro que te funcionó
return url && url.includes("images.") && !url.includes("/images/");
})
.map((url, i) => {
// Reemplazamos "thumb" por el número del índice + 1
// Ejemplo: .../thumb.jpg -> .../1.jpg
const newUrl = url.replace("thumb", (i + 1).toString());
return {
index: i,
url: newUrl
};
});
} }
} }

155
book/comix.js Normal file
View File

@@ -0,0 +1,155 @@
class Comix {
constructor() {
this.baseUrl = "https://comix.to";
this.apiUrl = "https://comix.to/api/v2";
this.type = "book-board";
this.version = "1.0";
this.mediaType = "manga";
}
get headers() {
return {
"Referer": `${this.baseUrl}/`,
"User-Agent": "Mozilla/5.0"
};
}
async search(queryObj) {
const q = (queryObj.query || "").trim().replace(/\s+/g, "+");
const url = new URL(`${this.apiUrl}/manga`);
if (q) {
url.searchParams.set("keyword", q);
url.searchParams.set("order[relevance]", "desc");
} else {
url.searchParams.set("order[views_30d]", "desc");
}
url.searchParams.set("limit", "50");
url.searchParams.set("page", "1");
const res = await fetch(url, { headers: this.headers });
if (!res.ok) throw new Error(`Search failed: ${res.status}`);
const json = await res.json();
return json.result.items.map(m => ({
id: m.hash_id,
title: m.title,
image: m.poster?.large || null,
rating: m.score ?? null,
type: "book"
}));
}
async getMetadata(id) {
const url = `${this.apiUrl}/manga/${id}?includes[]=genre&includes[]=author&includes[]=artist`;
const res = await fetch(url, { headers: this.headers });
if (!res.ok) throw new Error(`Metadata failed: ${res.status}`);
const { result } = await res.json();
return {
id: result.hash_id,
title: result.title,
format: "MANGA",
score: result.score ?? 0,
genres: result.genres?.map(g => g.name).join(", ") ?? "",
status: result.status ?? "unknown",
published: result.created_at ?? "",
summary: result.description ?? "",
chapters: result.chapters_count ?? 0,
image: result.poster?.large || null
};
}
async getSlug(mangaId) {
const res = await fetch(`${this.apiUrl}/manga/${mangaId}`, {
headers: this.headers
});
if (!res.ok) return "";
const { result } = await res.json();
return result?.slug || "";
}
async findChapters(mangaId) {
const slug = await this.getSlug(mangaId);
if (!slug) return [];
const baseUrl = `${this.apiUrl}/manga/${mangaId}/chapters?order[number]=desc&limit=100`;
const res = await fetch(baseUrl, { headers: this.headers });
if (!res.ok) return [];
const first = await res.json();
const totalPages = first.result.pagination?.last_page || 1;
let all = [...first.result.items];
for (let p = 2; p <= totalPages; p++) {
const r = await fetch(`${baseUrl}&page=${p}`, { headers: this.headers });
if (!r.ok) continue;
const d = await r.json();
if (d?.result?.items) all.push(...d.result.items);
}
const map = new Map();
for (const ch of all) {
if (ch.language !== "en") continue;
const key = ch.number;
if (!map.has(key) || ch.is_official === 1) {
map.set(key, ch);
}
}
return Array.from(map.values())
.sort((a, b) => a.number - b.number)
.map((ch, i) => ({
id: `${mangaId}|${slug}|${ch.chapter_id}|${ch.number}`,
title: ch.name
? `Chapter ${ch.number}${ch.name}`
: `Chapter ${ch.number}`,
number: Number(ch.number),
releaseDate: ch.updated_at ?? null,
index: i
}));
}
async findChapterPages(chapterId) {
const parts = chapterId.split("|");
console.log(parts)
if (parts.length < 4) return [];
const [hashId, slug, chapterRealId, number] = parts;
const readerUrl = `${this.baseUrl}/title/${hashId}-${slug}/${chapterRealId}-chapter-${number}`;
const res = await fetch(readerUrl, { headers: this.headers });
if (!res.ok) return [];
const html = await res.text();
const regex = /["\\]*images["\\]*\s*:\s*(\[[^\]]*\])/s;
const match = html.match(regex);
if (!match?.[1]) return [];
let images;
try {
images = JSON.parse(match[1]);
} catch {
images = JSON.parse(match[1].replace(/\\"/g, '"'));
}
return images.map((img, i) => ({
url: img.url,
index: i,
headers: {
Referer: readerUrl,
"User-Agent": "Mozilla/5.0"
}
}));
}
}
module.exports = Comix;

338
book/mangafire.js Normal file
View File

@@ -0,0 +1,338 @@
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;

182
book/mangapill.js Normal file
View File

@@ -0,0 +1,182 @@
class MangaPill {
constructor() {
this.baseUrl = "https://mangapill.com";
this.type = "book-board";
this.version = "1.0";
this.mediaType = "manga";
}
async fetch(url) {
return fetch(url, {
headers: {
"User-Agent": "Mozilla/5.0",
Referer: this.baseUrl,
},
});
}
async search(queryObj) {
const q = queryObj.query || "";
const res = await this.fetch(
`${this.baseUrl}/search?q=${encodeURIComponent(q)}`
);
const html = await res.text();
const $ = this.cheerio.load(html);
const results = [];
$("div.container div.my-3.justify-end > div").each((_, el) => {
const link = $(el).find("a").attr("href");
if (!link) return;
const id = link.split("/manga/")[1].replace(/\//g, "$");
const title = $(el).find("div > a > div.mt-3").text().trim();
const image = $(el).find("a img").attr("data-src") || null;
results.push({
id,
title,
image,
rating: null,
type: "book",
});
});
return results;
}
async getMetadata(id) {
const uriId = id.replace(/\$/g, "/");
let res = await fetch(`${this.baseUrl}/manga/${uriId}`, {
headers: this.headers,
redirect: "manual",
});
// follow redirect manually
if (res.status === 301 || res.status === 302) {
const loc = res.headers.get("location");
if (loc) {
res = await fetch(`${this.baseUrl}${loc}`, {
headers: this.headers,
});
}
}
if (!res.ok) {
return {
id,
title: "",
format: "MANGA",
score: 0,
genres: "",
status: "unknown",
published: "",
summary: "",
chapters: "???",
image: null,
};
}
const html = await res.text();
const $ = this.cheerio.load(html);
const title = $("h1.font-bold").first().text().trim();
const summary =
$("div.mb-3 p.text-sm").first().text().trim() || "";
const status =
$("label:contains('Status')")
.next("div")
.text()
.trim() || "unknown";
const published =
$("label:contains('Year')")
.next("div")
.text()
.trim() || "";
const genres = [];
$("label:contains('Genres')")
.parent()
.find("a")
.each((_, a) => genres.push($(a).text().trim()));
const image =
$("img[data-src]").first().attr("data-src") || null;
return {
id,
title,
format: "MANGA",
score: 0,
genres: genres.join(", "),
status,
published,
summary,
chapters: "???",
image
};
}
async findChapters(mangaId) {
const uriId = mangaId.replace(/\$/g, "/");
const res = await this.fetch(`${this.baseUrl}/manga/${uriId}`);
const html = await res.text();
const $ = this.cheerio.load(html);
const chapters = [];
$("div#chapters a").each((_, el) => {
const href = $(el).attr("href");
if (!href) return;
const id = href.split("/chapters/")[1].replace(/\//g, "$");
const title = $(el).text().trim();
const match = title.match(/Chapter\s+([\d.]+)/);
const number = match ? Number(match[1]) : 0;
chapters.push({
id,
title,
number,
releaseDate: null,
index: 0,
});
});
chapters.reverse();
chapters.forEach((c, i) => (c.index = i));
return chapters;
}
async findChapterPages(chapterId) {
const uriId = chapterId.replace(/\$/g, "/");
const res = await this.fetch(`${this.baseUrl}/chapters/${uriId}`);
const html = await res.text();
const $ = this.cheerio.load(html);
const pages = [];
$("chapter-page").each((i, el) => {
const img = $(el).find("div picture img").attr("data-src");
if (!img) return;
pages.push({
url: img,
index: i,
headers: {
Referer: "https://mangapill.com/",
Origin: "https://mangapill.com",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
Accept: "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
},
});
});
return pages;
}
}
module.exports = MangaPill;

View File

@@ -1,182 +1,140 @@
class nhentai { class NHentai {
constructor() { constructor() {
this.baseUrl = "https://nhentai.net"; this.baseUrl = "https://nhentai.net";
this.type = "book-board"; this.type = "book-board";
this.version = "1.1";
this.mediaType = "manga"; this.mediaType = "manga";
this.version = "1.0"
} }
async search(queryObj) { shortenTitle(title) {
const q = queryObj.query.trim().replace(/\s+/g, "+"); return title.replace(/(\[[^]]*]|[({][^)}]*[)}])/g, "").trim();
const url = q }
? `${this.baseUrl}/search/?q=${q}`
: `${this.baseUrl}/?q=`;
const { result: data } = await this.scrape( parseId(str) {
url, return str.replace(/\D/g, "");
async (page) => { }
return page.evaluate(() => {
const container = document.querySelector('.container.index-container');
if (!container) return {results: [], hasNextPage: false};
const galleryEls = container.querySelectorAll('.gallery'); extractJson(scriptText) {
const results = []; const m = scriptText.match(/JSON\.parse\("([\s\S]*?)"\)/);
if (!m) throw new Error("JSON.parse no encontrado");
galleryEls.forEach(el => { const unicodeFixed = m[1].replace(
const a = el.querySelector('a.cover'); /\\u([0-9A-Fa-f]{4})/g,
if (!a) return; (_, h) => String.fromCharCode(parseInt(h, 16))
const href = a.getAttribute('href');
const id = href.match(/\d+/)?.[0] || null;
const img = a.querySelector('img.lazyload');
const thumbRaw = img?.dataset?.src || img?.src || "";
const thumb = thumbRaw.startsWith("//") ? "https:" + thumbRaw : thumbRaw;
const coverUrl = thumb.replace("thumb", "cover");
const caption = a.querySelector('.caption');
const title = caption?.textContent.trim() || "";
results.push({
id,
title,
image: coverUrl,
rating: null,
type: "book"
});
});
const hasNextPage = !!document.querySelector('section.pagination a.next');
return {results, hasNextPage};
});
},
{
waitSelector: '.container.index-container',
timeout: 55000
}
); );
return data?.results || []; return JSON.parse(unicodeFixed);
}
async search({ query = "", page = 1 }) {
if (query.startsWith("id:") || (!isNaN(query) && query.length <= 7)) {
return [await this.getMetadata(this.parseId(query))];
}
const url = `${this.baseUrl}/search/?q=${encodeURIComponent(query)}&page=${page}`;
const { result } = await this.scrape(
url,
page =>
page.evaluate(() => document.documentElement.innerHTML),
{ waitSelector: ".gallery" }
);
const $ = this.cheerio.load(result);
return $(".gallery").map((_, el) => ({
id: this.parseId($(el).find("a").attr("href")),
image: $(el).find("img").attr("data-src") || $(el).find("img").attr("src"),
title: this.shortenTitle($(el).find(".caption").text()),
type: "book"
})).get();
} }
async getMetadata(id) { async getMetadata(id) {
const { result: data } = await this.scrape( const url = `${this.baseUrl}/g/${id}/`;
`${this.baseUrl}/g/${id}/`,
async (page) => {
return page.evaluate(() => {
const title = document.querySelector('h1.title .pretty')?.textContent?.trim() || "";
const img = document.querySelector('#cover img'); const { result } = await this.scrape(
const image = url,
img?.dataset?.src ? "https:" + img.dataset.src : page =>
img?.src?.startsWith("//") ? "https:" + img.src : page.evaluate(() => {
img?.src || ""; const html = document.documentElement.innerHTML;
const tagBlock = document.querySelector('.tag-container.field-name'); const script = [...document.querySelectorAll("script")]
const genres = tagBlock .find(s =>
? [...tagBlock.querySelectorAll('.tags .name')].map(x => x.textContent.trim()) s.textContent.includes("JSON.parse") &&
: []; !s.textContent.includes("media_server") &&
!s.textContent.includes("avatar_url")
)?.textContent || null;
const timeEl = document.querySelector('.tag-container.field-name time'); const thumbMatch = html.match(/thumb_cdn_urls:\s*(\[[^\]]*])/);
const published = const thumbCdns = thumbMatch ? JSON.parse(thumbMatch[1]) : [];
timeEl?.getAttribute("datetime") ||
timeEl?.textContent?.trim() ||
"???";
return {title, image, genres, published}; return { script, thumbCdns };
}); })
},
{
waitSelector: "#bigcontainer",
timeout: 55000
}
); );
if (!data) throw new Error(`Fallo al obtener metadatos para ID ${id}`); if (!result?.script) {
throw new Error("Script de datos no encontrado");
}
const formattedDate = data.published const data = this.extractJson(result.script);
? new Date(data.published).toLocaleDateString("es-ES") const cdn = result.thumbCdns[0] || "t3.nhentai.net";
: "???";
return { return {
id, id: id.toString(),
title: data.title || "", title: data.title.pretty || data.title.english,
format: "Manga", format: "MANGA",
score: 0, status: "completed",
genres: Array.isArray(data.genres) ? data.genres : [], genres: data.tags
status: "Finished", .filter(t => t.type === "tag")
published: formattedDate, .map(t => t.name),
summary: "", published: new Date(data.upload_date * 1000).toLocaleDateString(),
summary: `Pages: ${data.images.pages.length}\nFavorites: ${data.num_favorites}`,
chapters: 1, chapters: 1,
image: data.image || "" image: `https://${cdn}/galleries/${data.media_id}/cover.webp`
}; };
} }
async findChapters(mangaId) { async findChapters(mangaId) {
const { result: data } = await this.scrape(
`${this.baseUrl}/g/${mangaId}/`,
async (page) => {
return page.evaluate(() => {
const title = document.querySelector('#info > h1 .pretty')?.textContent?.trim() || "";
const img = document.querySelector('#cover img');
const cover =
img?.dataset?.src ? "https:" + img.dataset.src :
img?.src?.startsWith("//") ? "https:" + img.src :
img?.src || "";
const hash = cover.match(/galleries\/(\d+)\//)?.[1] || null;
const thumbs = document.querySelectorAll('.thumbs img');
const pages = thumbs.length;
const first = thumbs[0];
const s = first?.dataset?.src || first?.src || "";
const ext = s.match(/t\.(\w+)/)?.[1] || "jpg";
const langTag = [...document.querySelectorAll('#tags .tag-container')]
.find(x => x.textContent.includes("Languages:"));
const language = langTag?.querySelector('.tags .name')?.textContent?.trim() || "";
return {title, cover, hash, pages, ext, language};
});
},
{
waitSelector: '#bigcontainer',
timeout: 55000
}
);
if (!data?.hash) throw new Error(`Fallo al obtener hash para ID ${mangaId}`);
const encodedChapterId = Buffer.from(JSON.stringify({
hash: data.hash,
pages: data.pages,
ext: data.ext
})).toString("base64");
return [{ return [{
id: encodedChapterId, id: mangaId.toString(),
title: data.title, title: "Chapter",
number: 1, number: 1,
releaseDate: null, index: 0
index: 0,
}]; }];
} }
async findChapterPages(chapterId) { async findChapterPages(chapterId) {
const decoded = JSON.parse(Buffer.from(chapterId, "base64").toString("utf8")); const url = `${this.baseUrl}/g/${chapterId}/`;
const { result } = await this.scrape(
url,
page =>
page.evaluate(() => {
const html = document.documentElement.innerHTML;
const cdnMatch = html.match(/image_cdn_urls:\s*(\[[^\]]*])/);
const s = [...document.querySelectorAll("script")]
.find(x =>
x.textContent.includes("JSON.parse") &&
!x.textContent.includes("media_server") &&
!x.textContent.includes("avatar_url")
);
return {
script: s?.textContent || null,
cdns: cdnMatch ? JSON.parse(cdnMatch[1]) : ["i.nhentai.net"]
};
})
);
const { hash, pages, ext } = decoded; if (!result?.script) throw new Error("Datos no encontrados");
const baseUrl = "https://i.nhentai.net/galleries"; const data = this.extractJson(result.script);
const cdn = result.cdns[0];
return Array.from({ length: pages }, (_, i) => ({ return data.images.pages.map((p, i) => {
url: `${baseUrl}/${hash}/${i + 1}.${ext}`, const ext = p.t === "j" ? "jpg" : p.t === "p" ? "png" : "webp";
index: i, return {
headers: { Referer: `https://nhentai.net/g/${hash}/` } index: i,
})); url: `https://${cdn}/galleries/${data.media_id}/${i + 1}.${ext}`
};
});
} }
} }
module.exports = nhentai; module.exports = NHentai;

View File

@@ -3,7 +3,7 @@ class NovelFire {
this.baseUrl = "https://novelfire.net"; this.baseUrl = "https://novelfire.net";
this.type = "book-board"; this.type = "book-board";
this.mediaType = "ln"; this.mediaType = "ln";
this.version = "1.0" this.version = "1.1"
} }
async search(queryObj) { async search(queryObj) {
@@ -71,18 +71,21 @@ class NovelFire {
} }
async findChapters(bookId) { async findChapters(bookId) {
const url = `https://novelfire.net/book/${bookId}/chapters`; const chapterUrl = `https://novelfire.net/book/${bookId}/chapter-1`;
const html = await (await fetch(url)).text(); const html = await (await fetch(chapterUrl)).text();
const $ = this.cheerio.load(html); const $ = this.cheerio.load(html);
let postId;
// csrf token
const csrf = $('meta[name="csrf-token"]').attr('content');
if (!csrf) throw new Error("csrf-token not found");
// post_id desde script
let postId;
$("script").each((_, el) => { $("script").each((_, el) => {
const txt = $(el).html() || ""; const txt = $(el).html() || "";
const m = txt.match(/listChapterDataAjax\?post_id=(\d+)/); const m = txt.match(/post_id\s*=\s*parseInt\("(\d+)"\)/);
if (m) postId = m[1]; if (m) postId = m[1];
}); });
if (!postId) throw new Error("post_id not found"); if (!postId) throw new Error("post_id not found");
const params = new URLSearchParams({ const params = new URLSearchParams({
@@ -100,7 +103,13 @@ class NovelFire {
const res = await fetch( const res = await fetch(
`https://novelfire.net/listChapterDataAjax?${params}`, `https://novelfire.net/listChapterDataAjax?${params}`,
{ headers: { "x-requested-with": "XMLHttpRequest" } } {
headers: {
"x-requested-with": "XMLHttpRequest",
"x-csrf-token": csrf,
"referer": chapterUrl
}
}
); );
const json = await res.json(); const json = await res.json();

227
book/weebcentral.js Normal file
View File

@@ -0,0 +1,227 @@
class WeebCentral {
constructor() {
this.baseUrl = "https://weebcentral.com";
this.type = "book-board";
this.version = "1.0";
this.mediaType = "manga";
}
async fetch(url, options = {}) {
return fetch(url, {
...options,
headers: {
"User-Agent": "Mozilla/5.0",
...(options.headers || {}),
},
});
}
async search(queryObj) {
const query = queryObj.query || "";
const form = new URLSearchParams();
form.set("text", query);
const res = await this.fetch(
`${this.baseUrl}/search/simple?location=main`,
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"HX-Request": "true",
"HX-Trigger": "quick-search-input",
"HX-Target": "quick-search-result",
"HX-Current-URL": `${this.baseUrl}/`,
},
body: form.toString(),
}
);
const html = await res.text();
const $ = this.cheerio.load(html);
const results = [];
$("#quick-search-result > div > a").each((_, el) => {
const link = $(el).attr("href");
if (!link) return;
const idMatch = link.match(/\/series\/([^/]+)/);
if (!idMatch) return;
const title = $(el).find(".flex-1").text().trim();
let image =
$(el).find("source").attr("srcset") ||
$(el).find("img").attr("src") ||
null;
results.push({
id: idMatch[1],
title,
image,
rating: null,
type: "book",
});
});
return results;
}
async getMetadata(id) {
const res = await this.fetch(`${this.baseUrl}/series/${id}`, {
headers: { Referer: `${this.baseUrl}/series/${id}` },
});
if (!res.ok) throw new Error("Metadata failed");
const html = await res.text();
const $ = this.cheerio.load(html);
const title =
$("section.md\\:w-8\\/12 h1").first().text().trim() || "";
const genres = [];
$("li strong")
.filter((_, el) => $(el).text().includes("Tags"))
.parent()
.find("a")
.each((_, a) => {
genres.push($(a).text().trim());
});
const status =
$("li strong")
.filter((_, el) => $(el).text().includes("Status"))
.parent()
.find("a")
.first()
.text()
.trim() || "unknown";
const published =
$("li strong")
.filter((_, el) => $(el).text().includes("Released"))
.parent()
.find("span")
.first()
.text()
.trim() || "";
const summary =
$("li strong")
.filter((_, el) => $(el).text().includes("Description"))
.parent()
.find("p")
.text()
.trim() || "";
const image =
$("section.flex picture source").attr("srcset") ||
$("section.flex picture img").attr("src") ||
null;
return {
id,
title,
format: "MANGA",
score: 0,
genres: genres.join(", "),
status,
published,
summary,
chapters: "???",
image,
};
}
async findChapters(mangaId) {
const res = await this.fetch(
`${this.baseUrl}/series/${mangaId}/full-chapter-list`,
{
headers: {
"HX-Request": "true",
"HX-Target": "chapter-list",
"HX-Current-URL": `${this.baseUrl}/series/${mangaId}`,
Referer: `${this.baseUrl}/series/${mangaId}`,
},
}
);
const html = await res.text();
const $ = this.cheerio.load(html);
const chapters = [];
const numRegex = /(\d+(?:\.\d+)?)/;
$("div.flex.items-center").each((_, el) => {
const a = $(el).find("a");
if (!a.length) return;
const href = a.attr("href");
if (!href) return;
const idMatch = href.match(/\/chapters\/([^/]+)/);
if (!idMatch) return;
const title = a.find("span.grow > span").first().text().trim();
const numMatch = title.match(numRegex);
chapters.push({
id: idMatch[1],
title,
number: numMatch ? Number(numMatch[1]) : 0,
releaseDate: null,
index: 0,
});
});
chapters.reverse();
chapters.forEach((c, i) => (c.index = i));
return chapters;
}
async findChapterPages(chapterId) {
const res = await this.fetch(
`${this.baseUrl}/chapters/${chapterId}/images?is_prev=False&reading_style=long_strip`,
{
headers: {
"HX-Request": "true",
"HX-Current-URL": `${this.baseUrl}/chapters/${chapterId}`,
Referer: `${this.baseUrl}/chapters/${chapterId}`,
},
}
);
const html = await res.text();
const $ = this.cheerio.load(html);
const pages = [];
$("section.flex-1 img").each((i, el) => {
const src = $(el).attr("src");
if (src) {
pages.push({
url: src,
index: i,
headers: { Referer: this.baseUrl },
});
}
});
if (pages.length === 0) {
$("img").each((i, el) => {
const src = $(el).attr("src");
if (src) {
pages.push({
url: src,
index: i,
headers: { Referer: this.baseUrl },
});
}
});
}
return pages;
}
}
module.exports = WeebCentral;

View File

@@ -3,11 +3,19 @@
"AnimeAV1": { "AnimeAV1": {
"name": "AnimeAV1", "name": "AnimeAV1",
"type": "anime-board", "type": "anime-board",
"description": ".", "description": "Anime provider with dubs and hard subs in spanish.",
"author": "lenafx", "author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/AnimeAV1.js", "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/AnimeAV1.js",
"domain": "https://animeav1.com/" "domain": "https://animeav1.com/"
}, },
"Anizone": {
"name": "Anizone",
"type": "anime-board",
"description": "Multi language anime provider, soft subs and dubs.",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/AniZone.js",
"domain": "https://anizone.to/"
},
"animepictures": { "animepictures": {
"name": "Anime Pictures", "name": "Anime Pictures",
"type": "book-board", "type": "book-board",
@@ -20,11 +28,12 @@
"asmhentai": { "asmhentai": {
"name": "ASM Hentai", "name": "ASM Hentai",
"type": "book-board", "type": "book-board",
"description": ".", "description": "Adult manga provider.",
"author": "lenafx", "author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/asmhentai.js", "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/asmhentai.js",
"domain": "https://asmhentai.com/", "domain": "https://asmhentai.com/",
"nsfw": true "nsfw": true,
"broken": true
}, },
"gelbooru": { "gelbooru": {
"name": "Gelbooru", "name": "Gelbooru",
@@ -46,7 +55,7 @@
"HiAnime": { "HiAnime": {
"name": "HiAnime", "name": "HiAnime",
"type": "anime-board", "type": "anime-board",
"description": ".", "description": "English anime provider with soft subs and dubs",
"author": "lenafx", "author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/HiAnime.js", "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/HiAnime.js",
"domain": "https://hianime.to/" "domain": "https://hianime.to/"
@@ -63,15 +72,47 @@
"mangadex": { "mangadex": {
"name": "Mangadex", "name": "Mangadex",
"type": "book-board", "type": "book-board",
"description": ".", "description": "English manga provider.",
"author": "lenafx", "author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/mangadex.js", "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/mangadex.js",
"domain": "https://mangadex.org/" "domain": "https://mangadex.org/"
}, },
"Comix": {
"name": "Comix",
"type": "book-board",
"description": "English manga provider.",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/comix.js",
"domain": "https://comix.to/"
},
"MangaPill": {
"name": "MangaPill",
"type": "book-board",
"description": "English manga provider.",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/mangapill.js",
"domain": "https://mangafire.to/"
},
"MangaFire": {
"name": "MangaFire",
"type": "book-board",
"description": "Multi language manga provider",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/mangafire.js",
"domain": "https://mangafire.to/"
},
"WeebCentral": {
"name": "WeebCentral",
"type": "book-board",
"description": "English manga provider.",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/weebcentral.js",
"domain": "https://weebcentral.com/"
},
"mangapark": { "mangapark": {
"name": "Mangapark", "name": "Mangapark",
"type": "book-board", "type": "book-board",
"description": ".", "description": "English manga provider.",
"author": "lenafx", "author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/mangapark.js", "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/mangapark.js",
"domain": "https://mangapark.io/" "domain": "https://mangapark.io/"
@@ -79,7 +120,7 @@
"nhentai": { "nhentai": {
"name": "nhentai", "name": "nhentai",
"type": "book-board", "type": "book-board",
"description": ".", "description": "Adult manga provider.",
"author": "lenafx", "author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/nhentai.js", "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/nhentai.js",
"domain": "https://nhentai.net/", "domain": "https://nhentai.net/",