180 lines
6.2 KiB
JavaScript
180 lines
6.2 KiB
JavaScript
class AnimeAV1 {
|
|
|
|
constructor() {
|
|
this.type = "anime-board";
|
|
this.version = "1.1"
|
|
this.api = "https://animeav1.com";
|
|
}
|
|
|
|
getSettings() {
|
|
return {
|
|
episodeServers: ["HLS", "HLS-DUB"],
|
|
supportsDub: true,
|
|
};
|
|
}
|
|
|
|
async search(query) {
|
|
const res = await fetch(`${this.api}/api/search`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ query: query.query }),
|
|
});
|
|
|
|
if (!res.ok) return [];
|
|
|
|
const data = await res.json();
|
|
|
|
return data.map(anime => ({
|
|
id: anime.slug,
|
|
title: anime.title,
|
|
url: `${this.api}/anime/${anime.slug}`,
|
|
subOrDub: "both",
|
|
}));
|
|
}
|
|
|
|
async getMetadata(id) {
|
|
const html = await fetch(`${this.api}/media/${id}`).then(r => r.text());
|
|
const parsed = this.parseSvelteData(html);
|
|
const media = parsed.find(x => x?.data?.media)?.data.media ?? {};
|
|
|
|
// IMAGE
|
|
const imageMatch = html.match(/<img[^>]*class="aspect-poster[^"]*"[^>]*src="([^"]+)"/i);
|
|
const image = imageMatch ? imageMatch[1] : null;
|
|
|
|
// BLOCK INFO (STATUS, SEASON, YEAR)
|
|
const infoBlockMatch = html.match(
|
|
/<div class="flex flex-wrap items-center gap-2 text-sm">([\s\S]*?)<\/div>/
|
|
);
|
|
|
|
let status = media.status ?? "Unknown";
|
|
let season = media.seasons ?? null;
|
|
let year = media.startDate ? Number(media.startDate.slice(0, 4)) : null;
|
|
|
|
if (infoBlockMatch) {
|
|
const raw = infoBlockMatch[1];
|
|
|
|
// Extraer spans internos
|
|
const spans = [...raw.matchAll(/<span[^>]*>([^<]+)<\/span>/g)].map(m => m[1].trim());
|
|
|
|
// EJEMPLO:
|
|
// ["TV Anime", "•", "2025", "•", "Temporada Otoño", "•", "En emisión"]
|
|
|
|
const clean = spans.filter(x => x !== "•");
|
|
|
|
// YEAR
|
|
const yearMatch = clean.find(x => /^\d{4}$/.test(x));
|
|
if (yearMatch) year = Number(yearMatch);
|
|
|
|
// SEASON (el que contiene "Temporada")
|
|
const seasonMatch = clean.find(x => x.toLowerCase().includes("temporada"));
|
|
if (seasonMatch) season = seasonMatch;
|
|
|
|
// STATUS (normalmente "En emisión", "Finalizado", etc)
|
|
const statusMatch = clean.find(x =>
|
|
/emisión|finalizado|completado|pausa|cancelado/i.test(x)
|
|
);
|
|
if (statusMatch) status = statusMatch;
|
|
}
|
|
|
|
return {
|
|
title: media.title ?? "Unknown",
|
|
summary: media.synopsis ?? "No summary available",
|
|
episodes: media.episodesCount ?? 0,
|
|
characters: [],
|
|
season,
|
|
status,
|
|
studio: "Unknown",
|
|
score: media.score ?? 0,
|
|
year,
|
|
genres: media.genres?.map(g => g.name) ?? [],
|
|
image
|
|
};
|
|
}
|
|
|
|
|
|
async findEpisodes(id) {
|
|
const html = await fetch(`${this.api}/media/${id}`).then(r => r.text());
|
|
const parsed = this.parseSvelteData(html);
|
|
|
|
const media = parsed.find(x => x?.data?.media)?.data?.media;
|
|
if (!media?.episodes) throw new Error("No se encontró media.episodes");
|
|
|
|
return media.episodes.map((ep, i) => ({
|
|
id: `${media.slug}$${ep.number ?? i + 1}`,
|
|
number: ep.number ?? i + 1,
|
|
title: ep.title ?? `Episode ${ep.number ?? i + 1}`,
|
|
url: `${this.api}/media/${media.slug}/${ep.number ?? i + 1}`,
|
|
}));
|
|
}
|
|
|
|
async findEpisodeServer(episodeOrId, _server) {
|
|
const ep = typeof episodeOrId === "string"
|
|
? (() => { try { return JSON.parse(episodeOrId); } catch { return { id: episodeOrId }; } })()
|
|
: episodeOrId;
|
|
|
|
const pageUrl = ep.url ?? (
|
|
typeof ep.id === "string" && ep.id.includes("$")
|
|
? `${this.api}/media/${ep.id.split("$")[0]}/${ep.number ?? ep.id.split("$")[1]}`
|
|
: undefined
|
|
);
|
|
|
|
if (!pageUrl) throw new Error("No se pudo determinar la URL del episodio.");
|
|
|
|
const html = await fetch(pageUrl, {
|
|
headers: { Cookie: "__ddg1_=;__ddg2_=;" },
|
|
}).then(r => r.text());
|
|
|
|
const parsedData = this.parseSvelteData(html);
|
|
const entry = parsedData.find(x => x?.data?.embeds) || parsedData[3];
|
|
const embeds = entry?.data?.embeds;
|
|
if (!embeds) throw new Error("No se encontraron 'embeds' en los datos del episodio.");
|
|
|
|
const selectedEmbeds =
|
|
_server === "HLS"
|
|
? embeds.SUB ?? []
|
|
: _server === "HLS-DUB"
|
|
? embeds.DUB ?? []
|
|
: (() => { throw new Error(`Servidor desconocido: ${_server}`); })();
|
|
|
|
if (!selectedEmbeds.length)
|
|
throw new Error(`No hay mirrors disponibles para ${_server === "HLS" ? "SUB" : "DUB"}.`);
|
|
|
|
const match = selectedEmbeds.find(m =>
|
|
(m.url || "").includes("zilla-networks.com/play/")
|
|
);
|
|
|
|
if (!match)
|
|
throw new Error(`No se encontró ningún embed de ZillaNetworks en ${_server}.`);
|
|
|
|
return {
|
|
server: _server,
|
|
headers: { Referer: 'null' },
|
|
videoSources: [
|
|
{
|
|
url: match.url.replace("/play/", "/m3u8/"),
|
|
type: "m3u8",
|
|
quality: "auto",
|
|
subtitles: [],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
parseSvelteData(html) {
|
|
const scriptMatch = html.match(/<script[^>]*>\s*({[^<]*__sveltekit_[\s\S]*?)<\/script>/i);
|
|
if (!scriptMatch) throw new Error("No se encontró bloque SvelteKit en el HTML.");
|
|
|
|
const dataMatch = scriptMatch[1].match(/data:\s*(\[[\s\S]*?\])\s*,\s*form:/);
|
|
if (!dataMatch) throw new Error("No se encontró el bloque 'data' en el script SvelteKit.");
|
|
|
|
const jsArray = dataMatch[1];
|
|
try {
|
|
return new Function(`"use strict"; return (${jsArray});`)();
|
|
} catch {
|
|
const cleaned = jsArray.replace(/\bvoid 0\b/g, "null").replace(/undefined/g, "null");
|
|
return new Function(`"use strict"; return (${cleaned});`)();
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = AnimeAV1; |