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( /]*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( /]*>\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;