updated marketplace and extensions
This commit is contained in:
164
anime/AniZone.js
Normal file
164
anime/AniZone.js
Normal 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;
|
||||
@@ -2,7 +2,7 @@ class asmhentai {
|
||||
constructor() {
|
||||
this.baseUrl = "https://asmhentai.com";
|
||||
this.type = "book-board";
|
||||
this.version = "1.0"
|
||||
this.version = "1.1"
|
||||
this.mediaType = "manga";
|
||||
}
|
||||
|
||||
@@ -47,10 +47,9 @@ class asmhentai {
|
||||
|
||||
if (image.startsWith("//")) image = "https:" + image;
|
||||
|
||||
const genres = $(".tags .tag_list .badge.tag")
|
||||
.map((_, el) => $(el).clone().children().remove().end().text().trim())
|
||||
const genres = $(".tags a.tag")
|
||||
.map((_, el) => $(el).text().trim())
|
||||
.get()
|
||||
.join(", ");
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -67,38 +66,64 @@ class asmhentai {
|
||||
}
|
||||
|
||||
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 [{
|
||||
id: chapterId,
|
||||
title,
|
||||
id: mangaId.toString(),
|
||||
title: "Chapter",
|
||||
number: 1,
|
||||
releaseDate: null,
|
||||
index: 0
|
||||
}];
|
||||
}
|
||||
|
||||
async findChapterPages(chapterId) {
|
||||
const { base, pages, ext } = JSON.parse(
|
||||
Buffer.from(chapterId, "base64").toString("utf8")
|
||||
);
|
||||
const html = await fetch(`${this.baseUrl}/g/${chapterId}/`).then(r => r.text());
|
||||
const $ = this.cheerio.load(html);
|
||||
|
||||
return Array.from({ length: pages }, (_, i) => ({
|
||||
url: `${base}${i + 1}.${ext}`,
|
||||
index: i
|
||||
}));
|
||||
const token = $('meta[name="csrf-token"]').attr("content") || "";
|
||||
const loadId = $("#load_id").val();
|
||||
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
155
book/comix.js
Normal 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
338
book/mangafire.js
Normal 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
182
book/mangapill.js
Normal 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;
|
||||
244
book/nhentai.js
244
book/nhentai.js
@@ -1,182 +1,140 @@
|
||||
class nhentai {
|
||||
class NHentai {
|
||||
constructor() {
|
||||
this.baseUrl = "https://nhentai.net";
|
||||
this.type = "book-board";
|
||||
this.version = "1.1";
|
||||
this.mediaType = "manga";
|
||||
this.version = "1.0"
|
||||
}
|
||||
|
||||
async search(queryObj) {
|
||||
const q = queryObj.query.trim().replace(/\s+/g, "+");
|
||||
const url = q
|
||||
? `${this.baseUrl}/search/?q=${q}`
|
||||
: `${this.baseUrl}/?q=`;
|
||||
shortenTitle(title) {
|
||||
return title.replace(/(\[[^]]*]|[({][^)}]*[)}])/g, "").trim();
|
||||
}
|
||||
|
||||
const { result: data } = await this.scrape(
|
||||
url,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const container = document.querySelector('.container.index-container');
|
||||
if (!container) return {results: [], hasNextPage: false};
|
||||
parseId(str) {
|
||||
return str.replace(/\D/g, "");
|
||||
}
|
||||
|
||||
const galleryEls = container.querySelectorAll('.gallery');
|
||||
const results = [];
|
||||
extractJson(scriptText) {
|
||||
const m = scriptText.match(/JSON\.parse\("([\s\S]*?)"\)/);
|
||||
if (!m) throw new Error("JSON.parse no encontrado");
|
||||
|
||||
galleryEls.forEach(el => {
|
||||
const a = el.querySelector('a.cover');
|
||||
if (!a) return;
|
||||
|
||||
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
|
||||
}
|
||||
const unicodeFixed = m[1].replace(
|
||||
/\\u([0-9A-Fa-f]{4})/g,
|
||||
(_, h) => String.fromCharCode(parseInt(h, 16))
|
||||
);
|
||||
|
||||
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) {
|
||||
const { result: data } = await this.scrape(
|
||||
`${this.baseUrl}/g/${id}/`,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const title = document.querySelector('h1.title .pretty')?.textContent?.trim() || "";
|
||||
const url = `${this.baseUrl}/g/${id}/`;
|
||||
|
||||
const img = document.querySelector('#cover img');
|
||||
const image =
|
||||
img?.dataset?.src ? "https:" + img.dataset.src :
|
||||
img?.src?.startsWith("//") ? "https:" + img.src :
|
||||
img?.src || "";
|
||||
const { result } = await this.scrape(
|
||||
url,
|
||||
page =>
|
||||
page.evaluate(() => {
|
||||
const html = document.documentElement.innerHTML;
|
||||
|
||||
const tagBlock = document.querySelector('.tag-container.field-name');
|
||||
const genres = tagBlock
|
||||
? [...tagBlock.querySelectorAll('.tags .name')].map(x => x.textContent.trim())
|
||||
: [];
|
||||
const script = [...document.querySelectorAll("script")]
|
||||
.find(s =>
|
||||
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 published =
|
||||
timeEl?.getAttribute("datetime") ||
|
||||
timeEl?.textContent?.trim() ||
|
||||
"???";
|
||||
const thumbMatch = html.match(/thumb_cdn_urls:\s*(\[[^\]]*])/);
|
||||
const thumbCdns = thumbMatch ? JSON.parse(thumbMatch[1]) : [];
|
||||
|
||||
return {title, image, genres, published};
|
||||
});
|
||||
},
|
||||
{
|
||||
waitSelector: "#bigcontainer",
|
||||
timeout: 55000
|
||||
}
|
||||
return { script, thumbCdns };
|
||||
})
|
||||
);
|
||||
|
||||
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
|
||||
? new Date(data.published).toLocaleDateString("es-ES")
|
||||
: "???";
|
||||
const data = this.extractJson(result.script);
|
||||
const cdn = result.thumbCdns[0] || "t3.nhentai.net";
|
||||
|
||||
return {
|
||||
id,
|
||||
title: data.title || "",
|
||||
format: "Manga",
|
||||
score: 0,
|
||||
genres: Array.isArray(data.genres) ? data.genres : [],
|
||||
status: "Finished",
|
||||
published: formattedDate,
|
||||
summary: "",
|
||||
id: id.toString(),
|
||||
title: data.title.pretty || data.title.english,
|
||||
format: "MANGA",
|
||||
status: "completed",
|
||||
genres: data.tags
|
||||
.filter(t => t.type === "tag")
|
||||
.map(t => t.name),
|
||||
published: new Date(data.upload_date * 1000).toLocaleDateString(),
|
||||
summary: `Pages: ${data.images.pages.length}\nFavorites: ${data.num_favorites}`,
|
||||
chapters: 1,
|
||||
image: data.image || ""
|
||||
image: `https://${cdn}/galleries/${data.media_id}/cover.webp`
|
||||
};
|
||||
}
|
||||
|
||||
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 [{
|
||||
id: encodedChapterId,
|
||||
title: data.title,
|
||||
id: mangaId.toString(),
|
||||
title: "Chapter",
|
||||
number: 1,
|
||||
releaseDate: null,
|
||||
index: 0,
|
||||
index: 0
|
||||
}];
|
||||
}
|
||||
|
||||
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;
|
||||
const baseUrl = "https://i.nhentai.net/galleries";
|
||||
if (!result?.script) throw new Error("Datos no encontrados");
|
||||
const data = this.extractJson(result.script);
|
||||
const cdn = result.cdns[0];
|
||||
|
||||
return Array.from({ length: pages }, (_, i) => ({
|
||||
url: `${baseUrl}/${hash}/${i + 1}.${ext}`,
|
||||
index: i,
|
||||
headers: { Referer: `https://nhentai.net/g/${hash}/` }
|
||||
}));
|
||||
return data.images.pages.map((p, i) => {
|
||||
const ext = p.t === "j" ? "jpg" : p.t === "p" ? "png" : "webp";
|
||||
return {
|
||||
index: i,
|
||||
url: `https://${cdn}/galleries/${data.media_id}/${i + 1}.${ext}`
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = nhentai;
|
||||
module.exports = NHentai;
|
||||
@@ -3,7 +3,7 @@ class NovelFire {
|
||||
this.baseUrl = "https://novelfire.net";
|
||||
this.type = "book-board";
|
||||
this.mediaType = "ln";
|
||||
this.version = "1.0"
|
||||
this.version = "1.1"
|
||||
}
|
||||
|
||||
async search(queryObj) {
|
||||
@@ -71,18 +71,21 @@ class NovelFire {
|
||||
}
|
||||
|
||||
async findChapters(bookId) {
|
||||
const url = `https://novelfire.net/book/${bookId}/chapters`;
|
||||
const html = await (await fetch(url)).text();
|
||||
|
||||
const chapterUrl = `https://novelfire.net/book/${bookId}/chapter-1`;
|
||||
const html = await (await fetch(chapterUrl)).text();
|
||||
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) => {
|
||||
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 (!postId) throw new Error("post_id not found");
|
||||
|
||||
const params = new URLSearchParams({
|
||||
@@ -100,7 +103,13 @@ class NovelFire {
|
||||
|
||||
const res = await fetch(
|
||||
`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();
|
||||
|
||||
227
book/weebcentral.js
Normal file
227
book/weebcentral.js
Normal 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;
|
||||
@@ -3,11 +3,19 @@
|
||||
"AnimeAV1": {
|
||||
"name": "AnimeAV1",
|
||||
"type": "anime-board",
|
||||
"description": ".",
|
||||
"description": "Anime provider with dubs and hard subs in spanish.",
|
||||
"author": "lenafx",
|
||||
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/AnimeAV1.js",
|
||||
"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": {
|
||||
"name": "Anime Pictures",
|
||||
"type": "book-board",
|
||||
@@ -20,11 +28,12 @@
|
||||
"asmhentai": {
|
||||
"name": "ASM Hentai",
|
||||
"type": "book-board",
|
||||
"description": ".",
|
||||
"description": "Adult manga provider.",
|
||||
"author": "lenafx",
|
||||
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/asmhentai.js",
|
||||
"domain": "https://asmhentai.com/",
|
||||
"nsfw": true
|
||||
"nsfw": true,
|
||||
"broken": true
|
||||
},
|
||||
"gelbooru": {
|
||||
"name": "Gelbooru",
|
||||
@@ -46,7 +55,7 @@
|
||||
"HiAnime": {
|
||||
"name": "HiAnime",
|
||||
"type": "anime-board",
|
||||
"description": ".",
|
||||
"description": "English anime provider with soft subs and dubs",
|
||||
"author": "lenafx",
|
||||
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/HiAnime.js",
|
||||
"domain": "https://hianime.to/"
|
||||
@@ -63,15 +72,47 @@
|
||||
"mangadex": {
|
||||
"name": "Mangadex",
|
||||
"type": "book-board",
|
||||
"description": ".",
|
||||
"description": "English manga provider.",
|
||||
"author": "lenafx",
|
||||
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/mangadex.js",
|
||||
"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": {
|
||||
"name": "Mangapark",
|
||||
"type": "book-board",
|
||||
"description": ".",
|
||||
"description": "English manga provider.",
|
||||
"author": "lenafx",
|
||||
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/mangapark.js",
|
||||
"domain": "https://mangapark.io/"
|
||||
@@ -79,7 +120,7 @@
|
||||
"nhentai": {
|
||||
"name": "nhentai",
|
||||
"type": "book-board",
|
||||
"description": ".",
|
||||
"description": "Adult manga provider.",
|
||||
"author": "lenafx",
|
||||
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/nhentai.js",
|
||||
"domain": "https://nhentai.net/",
|
||||
|
||||
Reference in New Issue
Block a user