diff --git a/anime/AnimeAV1.js b/anime/AnimeAV1.js index ef23e69..fc9ad6e 100644 --- a/anime/AnimeAV1.js +++ b/anime/AnimeAV1.js @@ -1,7 +1,7 @@ class AnimeAV1 { constructor() { this.type = "anime-board"; - this.version = "1.3"; + this.version = "1.4"; this.api = "https://animeav1.com"; } @@ -12,23 +12,178 @@ class AnimeAV1 { }; } - async search(query) { - const res = await fetch(`${this.api}/api/search`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ query: query.query }), + 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 + }); }); - 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}`, - subOrDub: "both", - image: `https://cdn.animeav1.com/covers/${anime.id}.jpg`, - })); + return results; } async getMetadata(id) { @@ -74,17 +229,9 @@ class AnimeAV1 { } async findEpisodeServer(episodeOrId, _server, category = "sub") { - const ep = - typeof episodeOrId === "string" - ? (() => { - try { - return JSON.parse(episodeOrId); - } catch { - return { id: episodeOrId }; - } - })() - : episodeOrId; - + const ep = typeof episodeOrId === "string" + ? JSON.parse(episodeOrId) + : episodeOrId; let pageUrl = ep.url; @@ -97,36 +244,22 @@ class AnimeAV1 { } } - if (!pageUrl) { - throw new Error( - `No se pudo determinar la URL del episodio (id=${ep.id}, number=${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; + const list = category === "dub" ? embeds.DUB : embeds.SUB; - if (!Array.isArray(list)) - throw new Error(`No hay streams ${category.toUpperCase()}`); + 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/") - ); + 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()}`); + if (!hls) throw new Error(`No se encontró stream HLS ${category.toUpperCase()}`); return { server: "HLS", diff --git a/anime/OppaiStream.js b/anime/OppaiStream.js index 3757beb..f1e533f 100644 --- a/anime/OppaiStream.js +++ b/anime/OppaiStream.js @@ -3,7 +3,7 @@ class OppaiStream { this.baseUrl = "https://oppai.stream"; this.searchBaseUrl = "https://oppai.stream/actions/search.php?order=recent&page=1&limit=35&genres=&blacklist=&studio=&ibt=0&swa=1&text="; this.type = "anime-board"; - this.version = "1.0"; + this.version = "1.2"; this.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36"; this.ScoreWeight = { @@ -19,48 +19,178 @@ class OppaiStream { }; } - async search(queryObj) { - let tempquery = queryObj.query; + getGenreOptions() { + return [ + '4k','ahegao','anal','armpitmasturbation','bdsm','beach','bigboobs', + 'blackhair','blondehair','blowjob','bluehair','bondage','boobjob', + 'brownhair','censored','comedy','cosplay','cowgirl','creampie', + 'darkskin','demon','doggy','dominantgirl','doublepenetration','elf', + 'facial','fantasy','filmed','footjob','futanari','gangbang', + 'girlsonly','glasses','greenhair','gyaru','hd','handjob','harem', + 'horror','incest','inflation','invertednipples','lactation','loli', + 'maid','masturbation','milf','mindbreak','mindcontrol','missionary', + 'monster','ntr','nekomimi','nurse','old','orgy','pov','pinkhair', + 'plot','pregnant','publicsex','purplehair','rape','redhair', + 'reversegangbang','reverserape','rimjob','scat','schoolgirl', + 'shorthair','shota','smallboobs','softcore','succubus','swimsuit', + 'teacher','tentacle','threesome','toys','trap','tripplepenetration', + 'tsundere','uglybastard','uncensored','vampire','vanilla','virgin', + 'watersports','whitehair','x-ray','yaoi','yuri' + ].map(g => ({ value: g, label: g })); + } + + getStudioOptions() { + return [ + "44℃ Baidoku","AT-X","AXsiZ","Alice Soft","Antechinus","An♥Tekinus", + "BOOTLEG","BREAKBOTTLE","Bomb! Cute! Bomb!","Breakbottle","Bunny Walker", + "ChuChu","Collaboration Works","Cotton Doll","Digital Works", + "Global Solutions","HiLLS","Himajin Planning","JapanAnime","Jumondou", + "Kitty Media","Lune Pictures","MS Pictures","Magic Bus","Magin Label", + "Majin Petit","Majin petit","Majin","Mary Jane","Mediabank", + "Milky Animation Label","Mirai Koujou", + "NBCUniversal Entertainment Japan","Natural High","NewGeneration", + "Nippon Columbia","Nur","Office Nobu","Pashima","Pashmina","Passione", + "Peak Hunt","Pink Pineapple","PoRO petit","PoRO","Queen Bee", + "Rabbit Gate","Seven","Shion","Show-Ten","Shueisha","Studio 1st", + "Studio Gokumi","Studio Houkiboshi","Suzuki Mirano","T-Rex", + "TEATRO Nishi Tokyo Studio","TNK","Toranoana","WHITE BEAR","Y.O.U.C", + "YTV","Yomiuri TV Enterprise","ZIZ Entertainment","erozuki" + ].map(s => ({ value: s, label: s })); + } + + getFilters() { + return { + order: { + label: 'Sort By', + type: 'select', + options: [ + { value: 'az', label: 'A-Z' }, + { value: 'za', label: 'Z-A' }, + { value: 'recent', label: 'Recently Released' }, + { value: 'old', label: 'Oldest Releases' }, + { value: 'views', label: 'Most Views' }, + { value: 'rating', label: 'Highest Rated' }, + { value: 'uploaded', label: 'Recently Uploaded' }, + { value: 'random', label: 'Randomize' }, + ] + }, + + // TRI-STATE SIMULADO CON MULTISELECT + genre_include: { + label: 'Genre (Include)', + type: 'multiselect', + options: this.getGenreOptions() + }, + + genre_exclude: { + label: 'Genre (Exclude)', + type: 'multiselect', + options: this.getGenreOptions() + }, + + studio: { + label: 'Studio', + type: 'multiselect', + options: this.getStudioOptions() + } + }; + } + + async search({ query = "", filters }) { + let tempquery = query || ""; + + // 👉 si no hay texto pero sí filtros, hacemos una sola búsqueda + const hasFilters = filters && Object.keys(filters).length > 0; + let firstRun = true; + + while (firstRun || tempquery !== "") { + firstRun = false; - while (tempquery !== "") { try { - const url = this.searchBaseUrl + encodeURIComponent(tempquery); + const params = new URLSearchParams(); + + // SOLO ponemos text si existe + if (tempquery) params.set("text", tempquery); + + if (filters) { + if (filters.order) params.set("order", filters.order); + + if (filters.genre_include) { + const inc = String(filters.genre_include).split(',').map(x => x.trim()).filter(Boolean); + if (inc.length) params.set("genres", inc.join(',')); + } + + if (filters.genre_exclude) { + const exc = String(filters.genre_exclude).split(',').map(x => x.trim()).filter(Boolean); + if (exc.length) params.set("blacklist", exc.join(',')); + } + + if (filters.studio) { + const studios = String(filters.studio).split(',').map(x => x.trim()).filter(Boolean); + if (studios.length) params.set("studio", studios.join(',')); + } + } + + params.set("page", "1"); + params.set("limit", "35"); + + const url = `${this.baseUrl}/actions/search.php?${params.toString()}`; const html = await this.GETText(url); const $ = this.cheerio.load(html); const movies = $("div.in-grid.episode-shown"); + // 👉 si no hay resultados: if (movies.length <= 0) { + // si hay filtros, no hacemos fallback por palabras + if (hasFilters || !tempquery) return []; + + // fallback normal cuando hay texto if (tempquery.includes(" ")) { tempquery = tempquery.split(/[\s:']+/).slice(0, -1).join(" "); continue; - } else { - break; } + return []; } - const movieList = []; + const results = []; movies.each((_, el) => { - const title = $(el).find(".title-ep").text().trim(); - const href = $(el).find("a").attr("href"); + const elObj = $(el); + const title = elObj.find(".title-ep .title").text().trim(); + const href = elObj.find("a").attr("href"); const rawUrl = href ? href.replace("&for=search", "") : ""; + const image = elObj.find(".cover-img-in").attr("src") + || elObj.find(".cover-img-in").attr("original"); if (title && rawUrl) { - movieList.push({ Title: title, Url: rawUrl }); + results.push({ + id: encodeURIComponent(rawUrl), + title, + url: rawUrl, + image, + subOrDub: "sub", + }); } }); - const bestMovie = this.findBestTitle(movieList, queryObj.query); + // 👉 si hay query usamos tu sistema de score + if (query) { + const best = this.findBestTitle( + results.map(r => ({ Title: r.title, Url: r.url, Image: r.image })), + query + ); + if (!best) return []; + return [{ + id: encodeURIComponent(best.Url), + title: best.Title, + url: best.Url, + image: best.Image, + subOrDub: "sub", + }]; + } - if (!bestMovie) return []; - - return [{ - // Codificamos la URL para que sea un ID seguro para la URL de la app - id: encodeURIComponent(bestMovie.Url), - title: bestMovie.Title, - url: bestMovie.Url, - subOrDub: queryObj.dub ? "dub" : "sub", - }]; + // 👉 si NO hay query, devolvemos todo (modo catálogo) + return results; } catch (e) { console.error(e); @@ -74,6 +204,7 @@ class OppaiStream { try { // Decodificamos el ID para obtener la URL real de OppaiStream const decodedUrl = decodeURIComponent(id); + console.log(decodedUrl) const html = await this.GETText(decodedUrl); const $ = this.cheerio.load(html); diff --git a/anime/hentaila.js b/anime/hentaila.js new file mode 100644 index 0000000..41cb912 --- /dev/null +++ b/anime/hentaila.js @@ -0,0 +1,437 @@ +class Hentaila { + constructor() { + this.baseUrl = "https://hentaila.com"; + this.cdnUrl = "https://cdn.hentaila.com"; + this.type = "anime-board"; + this.version = "1.0"; + } + + getFilters() { + return { + sort: { + label: "Ordenar por", + type: "select", + options: [ + { value: "latest_released", label: "Recientes" }, + { value: "popular", label: "Populares" } + ], + default: "latest_released" + }, + genres: { + label: "Géneros", + type: "select", + options: [ + { value: "3d", label: "3D" }, + { value: "ahegao", label: "Ahegao" }, + { value: "anal", label: "Anal" }, + { value: "casadas", label: "Casadas" }, + { value: "chikan", label: "Chikan" }, + { value: "ecchi", label: "Ecchi" }, + { value: "enfermeras", label: "Enfermeras" }, + { value: "futanari", label: "Futanari" }, + { value: "escolares", label: "Escolares" }, + { value: "gore", label: "Gore" }, + { value: "hardcore", label: "Hardcore" }, + { value: "harem", label: "Harem" }, + { value: "incesto", label: "Incesto" }, + { value: "juegos-sexuales", label: "Juegos Sexuales" }, + { value: "milfs", label: "Milfs" }, + { value: "maids", label: "Maids" }, + { value: "netorare", label: "Netorare" }, + { value: "ninfomania", label: "Ninfomanía" }, + { value: "ninjas", label: "Ninjas" }, + { value: "orgias", label: "Orgías" }, + { value: "romance", label: "Romance" }, + { value: "shota", label: "Shota" }, + { value: "softcore", label: "Softcore" }, + { value: "succubus", label: "Succubus" }, + { value: "teacher", label: "Teacher" }, + { value: "tentaculos", label: "Tentáculos" }, + { value: "tetonas", label: "Tetonas" }, + { value: "vanilla", label: "Vanilla" }, + { value: "violacion", label: "Violación" }, + { value: "virgenes", label: "Vírgenes" }, + { value: "yaoi", label: "Yaoi" }, + { value: "yuri", label: "Yuri" }, + { value: "bondage", label: "Bondage" }, + { value: "elfas", label: "Elfas" }, + { value: "petit", label: "Petit" }, + { value: "threesome", label: "Threesome" }, + { value: "paizuri", label: "Paizuri" }, + { value: "gal", label: "Gal" }, + { value: "oyakodon", label: "Oyakodon" } + ] + }, + status: { + label: "Estado", + type: "select", + options: [ + { value: "emision", label: "En Emisión" }, + { value: "finalizado", label: "Finalizado" } + ] + }, + uncensored: { + label: "Sin Censura", + type: "checkbox", + default: false + } + }; + } + + getSettings() { + return { + episodeServers: ["StreamWish", "VidHide"], //"VIP" works but the stream is blocked even with the headers. + supportsDub: false + }; + } + + _resolveRemixData(json) { + if (!json || !json.nodes) return []; + + for (const node of json.nodes) { + if (node && node.uses && node.uses.search_params) { + const data = node.data; + if (!data || data.length === 0) continue; + + const rootConfig = data[0]; + + if (!rootConfig || typeof rootConfig.results !== 'number') continue; + + const resultsIndex = rootConfig.results; + + const animePointers = data[resultsIndex]; + + if (!Array.isArray(animePointers)) continue; + + return animePointers.map(pointer => { + const rawObj = data[pointer]; + + if (!rawObj) return null; + + const realId = data[rawObj.id]; + const title = data[rawObj.title]; + const slug = data[rawObj.slug]; + + // Validación básica + if (!title || !slug) return null; + + return { + id: slug, + title: title, + url: `${this.baseUrl}/media/${slug}`, + image: `${this.cdnUrl}/covers/${realId}.jpg`, + year: null + }; + }).filter(Boolean); + } + } + return []; + } + + async search(queryObj) { + const { query, filters, page } = queryObj; + const pageNum = page || 1; + + let url = `${this.baseUrl}/catalogo/__data.json?page=${pageNum}`; + + if (query && query.trim() !== "") { + url += `&search=${encodeURIComponent(query)}`; + } else { + if (filters.sort) url += `&order=${filters.sort}`; + else url += `&order=latest_released`; + + if (filters.genres) url += `&genre=${filters.genres}`; + + if (filters.status) url += `&status=${filters.status}`; + + if (filters.uncensored) url += `&uncensored=`; + } + + try { + const response = await fetch(url); + const json = await response.json(); + return this._resolveRemixData(json); + } catch (error) { + console.error("Error searching Hentaila:", error); + return []; + } + } + + async getMetadata(id) { + const url = `${this.baseUrl}/media/${id}`; + + try { + const response = await fetch(url); + const html = await response.text(); + const $ = this.cheerio.load(html); + + const title = $(".grid.items-start h1.text-lead").first().text().trim(); + const image = $("img.object-cover.w-full.aspect-poster").first().attr("src"); + const summary = $(".entry.text-lead.text-sm p").text().trim(); + + // Estado + const statusText = $("div.flex.flex-wrap.items-center.text-sm span").text(); + const status = statusText.includes("En emisión") ? "En Emisión" : "Finalizado"; + + // Géneros + const genres = []; + $(".flex-wrap.items-center .btn.btn-xs.rounded-full").each((i, el) => { + const txt = $(el).text().trim(); + if (txt) genres.push(txt); + }); + + const episodeCount = $("article.group\\/item").length; + + return { + title: title, + summary: summary, + status: status, + genres: genres, + image: image, + episodes: episodeCount, + url: url + }; + } catch (error) { + console.error("Error getting metadata:", error); + return null; + } + } + + async findEpisodes(id) { + const url = `${this.baseUrl}/media/${id}`; + + try { + const response = await fetch(url); + const html = await response.text(); + const $ = this.cheerio.load(html); + const episodes = []; + + $("article.group\\/item").each((i, el) => { + const $el = $(el); + + const numberText = $el.find("span.text-lead").text().trim(); + const number = parseFloat(numberText); + + const relativeUrl = $el.find("a").attr("href"); + + const image = $el.find("img").attr("src"); + + if (!isNaN(number) && relativeUrl) { + episodes.push({ + id: JSON.stringify({ slug: id, number: number }), + number: number, + title: `Episodio ${number}`, + url: `${this.baseUrl}${relativeUrl}`, + image: image + }); + } + }); + + return episodes; + + } catch (error) { + console.error("Error finding episodes:", error); + return []; + } + } + + async findEpisodeServer(episodeOrId, _server, category = "sub") { + let slug, number; + + const ep = typeof episodeOrId === "string" + ? JSON.parse(episodeOrId) + : episodeOrId; + + if (ep.id && typeof ep.id === "string" && ep.id.startsWith("{")) { + const p = JSON.parse(ep.id); + slug = p.slug; + number = p.number; + } else { + slug = ep.slug; + number = ep.number; + } + + if (!slug || !number) throw new Error("No se pudo determinar episodio"); + + const url = `${this.baseUrl}/media/${slug}/${number}/__data.json`; + const json = await fetch(url).then(r => r.json()); + + let chosen = null; + const wanted = (_server || "VIP").toLowerCase(); + + if (json.nodes) { + for (const node of json.nodes) { + if (!node?.uses?.params?.includes("number")) continue; + + const data = node.data; + const root = data?.[0]; + if (!root || typeof root.embeds !== "number") continue; + + const embeds = data[root.embeds]; + const listIndex = category === "dub" ? embeds?.DUB : embeds?.SUB; + if (typeof listIndex !== "number") continue; + + const list = data[listIndex]; + if (!Array.isArray(list)) continue; + + for (const i of list) { + const v = data[i]; + const server = data[v.server]; + const link = data[v.url]; + if (!server || !link) continue; + + if (server.toLowerCase() !== wanted) continue; + + let finalUrl = link; + let type = "iframe"; + + // --- VIP → m3u8 directo --- + const serverName = server.toLowerCase(); + + // --- VIP --- + if (serverName === "vip") { + finalUrl = link.replace("/play/", "/m3u8/"); + type = "m3u8"; + } + + // --- STREAMWISH --- + else if (serverName === "streamwish") { + const m3u8 = await this.extractPackedM3U8(link); + if (m3u8) { + finalUrl = m3u8; + type = "m3u8"; + } + } + + else if (serverName === "vidhide") { + const m3u8 = await this.extractPackedM3U8(link); + if (m3u8) { + finalUrl = m3u8; + type = "m3u8"; + } + } + + chosen = { + url: finalUrl, + type, + quality: server, + subtitles: [], + subOrDub: category + }; + break; + } + } + } + + if (!chosen) throw new Error(`No se encontró el server ${_server}`); + + return { + server: _server || "VIP", + headers: { + Referer: "https://hentaila.com/", + Origin: "https://hentaila.com" + }, + videoSources: [chosen] + }; + } + + async extractPackedM3U8(embedUrl) { + + try { + const { result } = await this.scrape(embedUrl, async (page) => { + try { + await page.waitForSelector('script', { state: 'attached', timeout: 5000 }); + } catch (e) {} + + return await page.evaluate(() => { + function unpack(code) { + try { + + const regex = /}\s*\('(.*?)',\s*(\d+),\s*(\d+),\s*'(.*?)'\.split\('\|'\)/; + const m = code.match(regex); + + if (!m) return null; + + let payload = m[1].replace(/\\'/g, "'"); + const radix = parseInt(m[2]); + const count = parseInt(m[3]); + const dict = m[4].split('|'); + + const unbase = (val) => { + const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + if (radix > 62) return parseInt(val, radix); + + const alphabet = chars.slice(0, radix); + let ret = 0; + const reversed = val.split('').reverse().join(''); + for (let i = 0; i < reversed.length; i++) { + const index = alphabet.indexOf(reversed[i]); + if (index === -1) continue; + ret += index * Math.pow(radix, i); + } + return ret; + }; + + return payload.replace(/\b\w+\b/g, (word) => { + const index = unbase(word); + if (dict[index]) return dict[index]; + return word; + }); + } catch (e) { + return "ERROR_IN_UNPACKER: " + e.message; + } + } + + // --- BÚSQUEDA DEL SCRIPT --- + const scripts = Array.from(document.getElementsByTagName('script')); + + for (const script of scripts) { + const content = script.textContent; + if (!content) continue; + + // Buscamos la firma del packer + if (content.includes('eval(function(p,a,c,k,e,d)') || content.includes('eval(function(p,a,c')) { + // Intentamos desempaquetar + const unpacked = unpack(content); + + // Si funcionó y parece contener HTML/JS válido + if (unpacked && unpacked.length > 20 && !unpacked.startsWith("ERROR")) { + return unpacked; + } + } + } + + return "NO_PACKER_FOUND"; + }); + + }, { + waitUntil: "domcontentloaded", + renderWaitTime: 2000 + }); + + if (!result || result === "NO_PACKER_FOUND") { + return null; + } + + if (result.startsWith("ERROR")) { + return null; + } + + const m3u8Regex = /(https?:\/\/[^"']+\.m3u8[^"']*)/; + const match = result.match(m3u8Regex); + + if (match) { + return match[1]; + } else { + console.log("[DEBUG] ⚠️ Script desempaquetado pero SIN m3u8. Dump parcial:", result.substring(0, 100)); + } + + return null; + + } catch (error) { + return null; + } + } +} + +module.exports = Hentaila; \ No newline at end of file diff --git a/anime/missav.js b/anime/missav.js new file mode 100644 index 0000000..9009d04 --- /dev/null +++ b/anime/missav.js @@ -0,0 +1,266 @@ +class MissAV { + constructor() { + this.type = "anime-board"; + this.version = "1.0"; + this.baseUrl = "https://missav.live"; + } + + getSettings() { + return { + supportsDub: false, + episodeServers: ["Default"], + }; + } + + /* ================= FILTERS ================= */ + + getFilters() { + return [ + { + key: "sort", + name: "Sort by", + type: "select", + options: [ + { value: "", label: "Any" }, + { value: "released_at", label: "Release date" }, + { value: "published_at", label: "Recent update" }, + { value: "today_views", label: "Today views" }, + { value: "weekly_views", label: "Weekly views" }, + { value: "monthly_views", label: "Monthly views" }, + { value: "views", label: "Total views" }, + ], + }, + { + key: "genre", + name: "Genres", + type: "select", + options: [ + { value: "", label: "