302 lines
11 KiB
JavaScript
302 lines
11 KiB
JavaScript
class AnimeAV1 {
|
|
constructor() {
|
|
this.type = "anime-board";
|
|
this.version = "1.4";
|
|
this.api = "https://animeav1.com";
|
|
}
|
|
|
|
getSettings() {
|
|
return {
|
|
episodeServers: ["HLS"],
|
|
supportsDub: true,
|
|
};
|
|
}
|
|
|
|
getFilters() {
|
|
return {
|
|
letter: {
|
|
label: 'Letra',
|
|
type: 'select',
|
|
options: [
|
|
{ value: '', label: 'Seleccionar...' },
|
|
{ value: '#', label: '#' },
|
|
...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(l => ({
|
|
value: l,
|
|
label: l
|
|
}))
|
|
]
|
|
},
|
|
|
|
category: {
|
|
label: 'Tipo',
|
|
type: 'multiselect',
|
|
options: [
|
|
{ value: 'tv-anime', label: 'TV Anime' },
|
|
{ value: 'pelicula', label: 'Película' },
|
|
{ value: 'ova', label: 'OVA' },
|
|
{ value: 'ona', label: 'ONA' },
|
|
{ value: 'especial', label: 'Especial' }
|
|
]
|
|
},
|
|
|
|
genre: {
|
|
label: 'Género',
|
|
type: 'multiselect',
|
|
options: [
|
|
{ value: 'accion', label: 'Acción' },
|
|
{ value: 'aventura', label: 'Aventura' },
|
|
{ value: 'ciencia-ficcion', label: 'Ciencia Ficción' },
|
|
{ value: 'comedia', label: 'Comedia' },
|
|
{ value: 'deportes', label: 'Deportes' },
|
|
{ value: 'drama', label: 'Drama' },
|
|
{ value: 'fantasia', label: 'Fantasía' },
|
|
{ value: 'misterio', label: 'Misterio' },
|
|
{ value: 'recuentos-de-la-vida', label: 'Recuentos de la Vida' },
|
|
{ value: 'romance', label: 'Romance' },
|
|
{ value: 'seinen', label: 'Seinen' },
|
|
{ value: 'shoujo', label: 'Shoujo' },
|
|
{ value: 'shounen', label: 'Shounen' },
|
|
{ value: 'sobrenatural', label: 'Sobrenatural' },
|
|
{ value: 'suspenso', label: 'Suspenso' },
|
|
{ value: 'terror', label: 'Terror' },
|
|
{ value: 'artes-marciales', label: 'Artes Marciales' },
|
|
{ value: 'ecchi', label: 'Ecchi' },
|
|
{ value: 'escolares', label: 'Escolares' },
|
|
{ value: 'gore', label: 'Gore' },
|
|
{ value: 'harem', label: 'Harem' },
|
|
{ value: 'historico', label: 'Histórico' },
|
|
{ value: 'isekai', label: 'Isekai' },
|
|
{ value: 'josei', label: 'Josei' },
|
|
{ value: 'magia', label: 'Magia' },
|
|
{ value: 'mecha', label: 'Mecha' },
|
|
{ value: 'militar', label: 'Militar' },
|
|
{ value: 'mitologia', label: 'Mitología' },
|
|
{ value: 'musica', label: 'Música' },
|
|
{ value: 'parodia', label: 'Parodia' },
|
|
{ value: 'psicologico', label: 'Psicológico' },
|
|
{ value: 'superpoderes', label: 'Superpoderes' },
|
|
{ value: 'vampiros', label: 'Vampiros' },
|
|
{ value: 'yuri', label: 'Yuri' },
|
|
{ value: 'yaoi', label: 'Yaoi' }
|
|
]
|
|
},
|
|
|
|
year: {
|
|
label: 'Año (Máximo)',
|
|
type: 'number'
|
|
},
|
|
|
|
status: {
|
|
label: 'Estado',
|
|
type: 'select',
|
|
options: [
|
|
{ value: 'emision', label: 'En emisión' },
|
|
{ value: 'finalizado', label: 'Finalizado' },
|
|
{ value: 'proximamente', label: 'Próximamente' }
|
|
]
|
|
},
|
|
|
|
order: {
|
|
label: 'Ordenar por',
|
|
type: 'select',
|
|
options: [
|
|
{ value: 'default', label: 'Por defecto' },
|
|
{ value: 'updated', label: 'Recientes' },
|
|
{ value: 'likes', label: 'Populares' },
|
|
{ value: 'title', label: 'Alfabético' }
|
|
]
|
|
}
|
|
};
|
|
}
|
|
|
|
async search({ query, filters }) {
|
|
|
|
if (query && (!filters || Object.keys(filters).length === 0)) {
|
|
const res = await fetch(`${this.api}/api/search`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ query }),
|
|
});
|
|
|
|
if (!res.ok) return [];
|
|
const data = await res.json();
|
|
|
|
return data.map(anime => ({
|
|
id: anime.slug,
|
|
title: anime.title,
|
|
url: `${this.api}/media/${anime.slug}`,
|
|
image: `https://cdn.animeav1.com/covers/${anime.id}.jpg`,
|
|
}));
|
|
}
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
if (filters) {
|
|
|
|
if (filters.category) {
|
|
const cats = String(filters.category).split(',');
|
|
cats.forEach(c => {
|
|
if(c.trim()) params.append('category', c.trim());
|
|
});
|
|
}
|
|
|
|
if (filters.genre) {
|
|
const genres = String(filters.genre).split(',');
|
|
genres.forEach(g => {
|
|
if(g.trim()) params.append('genre', g.trim());
|
|
});
|
|
}
|
|
|
|
if (filters.year) params.set('maxYear', String(filters.year));
|
|
if (filters.status) params.set('status', filters.status);
|
|
if (filters.letter) params.set('letter', filters.letter);
|
|
if (filters.order && filters.order !== 'default') params.set('order', filters.order);
|
|
}
|
|
|
|
const url = `${this.api}/catalogo?${params.toString()}`;
|
|
|
|
const res = await fetch(url);
|
|
if (!res.ok) return [];
|
|
|
|
const html = await res.text();
|
|
const $ = this.cheerio.load(html);
|
|
|
|
const results = [];
|
|
|
|
$('article.group\\/item').each((_, el) => {
|
|
const card = $(el);
|
|
|
|
const title = card.find('h3').first().text().trim();
|
|
const href = card.find('a[href^="/media/"]').attr('href');
|
|
const img = card.find('img').first().attr('src');
|
|
|
|
if (!href) return;
|
|
|
|
const slug = href.replace('/media/', '');
|
|
|
|
results.push({
|
|
id: slug,
|
|
title,
|
|
url: `${this.api}${href}`,
|
|
image: img || '',
|
|
year: null
|
|
});
|
|
});
|
|
|
|
return results;
|
|
}
|
|
|
|
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 ?? {};
|
|
|
|
const imageMatch = html.match(
|
|
/<img[^>]*class="aspect-poster[^"]*"[^>]*src="([^"]+)"/i
|
|
);
|
|
const image = imageMatch ? imageMatch[1] : null;
|
|
|
|
return {
|
|
title: media.title ?? "Unknown",
|
|
summary: media.synopsis ?? "No summary available",
|
|
episodes: media.episodesCount ?? 0,
|
|
characters: [],
|
|
season: media.seasons ?? null,
|
|
status: media.status ?? "Unknown",
|
|
studio: "Unknown",
|
|
score: media.score ?? 0,
|
|
year: media.startDate
|
|
? Number(media.startDate.slice(0, 4))
|
|
: null,
|
|
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, category = "sub") {
|
|
const ep = typeof episodeOrId === "string"
|
|
? JSON.parse(episodeOrId)
|
|
: episodeOrId;
|
|
|
|
let pageUrl = ep.url;
|
|
|
|
if (!pageUrl && typeof ep.id === "string") {
|
|
if (ep.id.includes("$")) {
|
|
const [slug, num] = ep.id.split("$");
|
|
pageUrl = `${this.api}/media/${slug}/${ep.number ?? num}`;
|
|
} else {
|
|
pageUrl = `${this.api}/media/${ep.id}/${ep.number}`;
|
|
}
|
|
}
|
|
|
|
if (!pageUrl) throw new Error("No se pudo determinar la URL del episodio.");
|
|
|
|
const html = await fetch(pageUrl).then((r) => r.text());
|
|
const parsedData = this.parseSvelteData(html);
|
|
const entry = parsedData.find((x) => x?.data?.embeds);
|
|
const embeds = entry?.data?.embeds;
|
|
|
|
if (!embeds) throw new Error("No embeds encontrados");
|
|
|
|
const list = category === "dub" ? embeds.DUB : embeds.SUB;
|
|
|
|
if (!Array.isArray(list)) throw new Error(`No hay streams ${category.toUpperCase()}`);
|
|
|
|
const hls = list.find(m => m.server === "HLS" && m.url?.includes("zilla-networks.com/play/"));
|
|
|
|
if (!hls) throw new Error(`No se encontró stream HLS ${category.toUpperCase()}`);
|
|
|
|
return {
|
|
server: "HLS",
|
|
headers: { Referer: "null" },
|
|
videoSources: [
|
|
{
|
|
url: hls.url.replace("/play/", "/m3u8/"),
|
|
type: "m3u8",
|
|
quality: "auto",
|
|
subtitles: [],
|
|
subOrDub: category,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
parseSvelteData(html) {
|
|
const scriptMatch = html.match(
|
|
/<script[^>]*>\s*({[^<]*__sveltekit_[\s\S]*?)<\/script>/i
|
|
);
|
|
if (!scriptMatch) throw new Error("SvelteKit block not found");
|
|
|
|
const dataMatch = scriptMatch[1].match(
|
|
/data:\s*(\[[\s\S]*?\])\s*,\s*form:/
|
|
);
|
|
if (!dataMatch) throw new Error("SvelteKit data block not found");
|
|
|
|
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; |