updates and new extensions

This commit is contained in:
2026-01-13 17:26:06 +01:00
parent 83c51a82da
commit e8d64174fd
15 changed files with 3516 additions and 468 deletions

View File

@@ -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",

View File

@@ -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);

437
anime/hentaila.js Normal file
View File

@@ -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;

266
anime/missav.js Normal file
View File

@@ -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: "<Select>" },
{ value: "en/uncensored-leak", label: "Uncensored Leak" },
{ value: "en/genres/Hd", label: "Hd" },
{ value: "en/genres/Exclusive", label: "Exclusive" },
{ value: "en/genres/Creampie", label: "Creampie" },
{ value: "en/genres/Big%20Breasts", label: "Big Breasts" },
{ value: "en/genres/Individual", label: "Individual" },
{ value: "en/genres/Wife", label: "Wife" },
{ value: "en/genres/Mature%20Woman", label: "Mature Woman" },
{ value: "en/genres/Pretty%20Girl", label: "Pretty Girl" },
{ value: "en/genres/Orgy", label: "Orgy" },
{ value: "en/genres/Lesbian", label: "Lesbian" },
{ value: "en/genres/Ntr", label: "NTR" },
{ value: "en/genres/Cosplay", label: "Cosplay" },
{ value: "en/genres/Uniform", label: "Uniform" },
{ value: "en/genres/Swimsuit", label: "Swimsuit" },
{ value: "en/genres/4K", label: "4K" },
{ value: "en/genres/Vr", label: "VR" },
],
},
{
type: "note",
text: "Genre filters ignored with text search!",
},
];
}
/* ================= SEARCH ================= */
async search(query) {
const filters = query?.filters || {};
const hasText = !!(query?.query && query.query.trim());
let url;
if (hasText) {
url = `${this.baseUrl}/en/search/${encodeURIComponent(query.query)}`;
} else if (filters.genre) {
url = `${this.baseUrl}/${filters.genre}`;
} else {
const params = new URLSearchParams();
if (filters.sort) params.set("sort", filters.sort);
url = `${this.baseUrl}/en?${params.toString()}`;
}
const { result, requests } = await this.scrape(
url,
async (page) => {
const html = await page.content();
const items = await page.$$eval(
'div.thumbnail',
nodes => nodes.map(n => {
const a = n.querySelector('a[href^="https://missav.live/en/"]');
if (!a) return null;
const href = a.getAttribute("href");
const img =
n.querySelector('img')?.getAttribute("data-src") ||
n.querySelector('img')?.getAttribute("src");
const title =
n.querySelector('div.text-sm a')?.textContent?.trim() ||
n.querySelector('a')?.textContent?.trim();
if (!href || !img || !title) return null;
return {
id: href.replace("https://missav.live", ""),
title,
image: img,
url: href,
};
}).filter(Boolean)
);
return items;
},
{
waitUntil: "domcontentloaded",
renderWaitTime: 1500,
scrollToBottom: true,
}
);
return result;
}
/* ================= METADATA ================= */
async getMetadata(animeId) {
const url = animeId.startsWith("http")
? animeId
: this.baseUrl + animeId;
console.log("[MissAV][meta] url =", url);
const { result, requests } = await this.scrape(
url,
async (page) => {
console.log("[MissAV][meta] page loaded");
const htmlSize = await page.content().then(h => h.length);
console.log("[MissAV][meta] html size =", htmlSize);
return await page.evaluate(() => {
const dbg = {};
const h1 = document.querySelector("h1");
dbg.hasH1 = !!h1;
const video = document.querySelector("video.player");
dbg.hasVideo = !!video;
const og = document.querySelector('meta[property="og:image"]');
dbg.hasOg = !!og;
const genreLinks = document.querySelectorAll('a[href^="/en/genres/"]');
dbg.genreCount = genreLinks.length;
const title =
document.querySelector("h1.text-base")?.textContent?.trim() ||
document.querySelector("h1")?.textContent?.trim() ||
"Unknown";
const poster =
video?.getAttribute("data-poster") ||
og?.content ||
null;
const description = "";
const genres = Array.from(genreLinks).map(a =>
a.textContent.trim()
);
return {
dbg,
title,
poster,
description,
genres,
};
});
},
{
waitUntil: "domcontentloaded",
timeout: 20000,
}
);
return {
title: result.title,
image: result.poster,
description: result.description,
genres: result.genres,
};
}
/* ================= EPISODES ================= */
async findEpisodes(animeId) {
// MissAV es 1 video = 1 episodio
return [
{
id: animeId,
number: 1,
title: "Video",
url: animeId.startsWith("http")
? animeId
: this.baseUrl + animeId,
},
];
}
/* ================= SERVERS ================= */
async findEpisodeServer(episode) {
const url = episode.url.startsWith("http")
? episode.url
: this.baseUrl + episode.url;
const { requests } = await this.scrape(
url,
async () => true,
{
waitUntil: "domcontentloaded",
timeout: 20000,
renderWaitTime: 1500,
}
);
const m3u8s = requests
.map(r => r.url)
.filter(u => u.includes(".m3u8"));
if (!m3u8s.length) throw new Error("No m3u8 in network");
// regla:
// - si existe .../playlist.m3u8 -> master
// - si no -> usar video.m3u8 (o el único que haya)
let finalUrl =
m3u8s.find(u => /\/playlist\.m3u8(\?|$)/.test(u)) ||
m3u8s.find(u => /\/video\.m3u8(\?|$)/.test(u)) ||
m3u8s[0];
return {
server: "Default",
videoSources: [
{
url: finalUrl,
type: "m3u8",
quality: "auto",
},
],
};
}
/* ================= UTILS ================= */
safeString(str) {
return typeof str === "string" ? str : "";
}
}
module.exports = MissAV;

290
anime/rouvideo.js Normal file
View File

@@ -0,0 +1,290 @@
class RouVideo {
constructor() {
this.baseUrl = "https://rou.video";
this.apiUrl = "https://rou.video/api";
this.type = "anime-board";
this.version = "1.0";
}
getFilters() {
return {
sort: {
label: "Ordenar por",
type: "select",
options: [
{ value: "createdAt", label: "Recent" },
{ value: "viewCount", label: "Most viewed" },
{ value: "likeCount", label: "Most liked" }
],
default: "createdAt"
},
category: {
label: "Categoría",
type: "select",
options: [
{ value: "all", label: "Todos los videos" },
{ value: "featured", label: "Destacados" },
{ value: "watching", label: "Viendo ahora" },
{ value: "國產AV", label: "Chinese AV" },
{ value: "中文字幕", label: "Chinese Sub" },
{ value: "麻豆傳媒", label: "Madou Media" },
{ value: "自拍流出", label: "Selfie Leaked" },
{ value: "探花", label: "Tanhua" },
{ value: "OnlyFans", label: "OnlyFans" },
{ value: "日本", label: "JAV" }
],
default: "all"
}
};
}
getSettings() {
return {
episodeServers: ["RouVideo"],
supportsDub: false,
};
}
async search(queryObj) {
const { query, filters, page } = queryObj;
const pageNum = page || 1;
const sort = filters?.sort || "createdAt";
const category = filters?.category || "all";
let url;
if (query && query.trim().length > 0) {
url = `${this.baseUrl}/search?q=${encodeURIComponent(query.trim())}&page=${pageNum}`;
if (category !== "all" && category !== "featured" && category !== "watching") {
url += `&t=${encodeURIComponent(category)}`;
}
} else {
if (category === "watching") {
url = `${this.apiUrl}/v/watching`;
} else if (category === "featured") {
url = `${this.baseUrl}/home`;
} else if (category !== "all") {
url = `${this.baseUrl}/t/${encodeURIComponent(category)}?page=${pageNum}&order=${sort}`;
} else {
url = `${this.baseUrl}/v?page=${pageNum}&order=${sort}`;
}
}
try {
if (category === "watching" && !query) {
const response = await this.requestApi(url);
const json = JSON.parse(response);
return json.map(this.parseVideoItem);
}
const response = await this.request(url);
const $ = this.cheerio.load(response);
const nextData = this.extractNextData($);
if (!nextData || !nextData.props || !nextData.props.pageProps) {
return [];
}
const props = nextData.props.pageProps;
let videos = [];
if (props.videos) {
videos = props.videos;
} else if (props.hotSearches && query) {
videos = props.videos || [];
} else if (category === "featured" || url.includes("/home")) {
videos = [
...(props.latestVideos || []),
...(props.hotCNAV || []),
...(props.hot91 || []),
...(props.hotSelfie || [])
];
}
return videos.map(this.parseVideoItem);
} catch (error) {
console.error("Error en search:", error);
return [];
}
}
async getMetadata(id) {
try {
const url = `${this.baseUrl}/v/${id}`;
const response = await this.request(url);
const $ = this.cheerio.load(response);
const nextData = this.extractNextData($);
if (!nextData) return { id, title: "Unknown" };
const video = nextData.props.pageProps.video;
if (!video) return { id, title: "Unknown" };
let descText = "";
if (video.sources && video.sources.length > 0) {
descText += `Resolution: ${video.sources[0].resolution}p\n`;
}
descText += `Duration: ${this.formatDuration(video.duration)}\n`;
descText += `View: ${video.viewCount}`;
if (video.likeCount) descText += ` - Like: ${video.likeCount}`;
if (video.ref) descText += `\nRef: ${video.ref}`;
if (video.description) descText += `\n\n${video.description}`;
return {
id: video.id,
title: video.name,
cover: video.coverImageUrl,
description: descText,
genres: video.tags || [],
author: video.tags?.[0] || "",
status: "Completed",
url: url
};
} catch (error) {
console.error("Error en getMetadata:", error);
return {};
}
}
async findEpisodes(id) {
try {
return [{
id: id,
number: 1,
title: "Movie"
}];
} catch (error) {
return [];
}
}
async findEpisodeServer(episodeInput, server, category = "sub") {
let cleanId = "";
if (typeof episodeInput === 'object' && episodeInput !== null) {
cleanId = episodeInput.id;
} else {
cleanId = episodeInput;
}
if (String(cleanId).includes('/')) {
cleanId = String(cleanId).split('/').pop();
}
console.log(`[RouVideo] Buscando servidor para ID: ${cleanId}`);
const apiUrl = `${this.apiUrl}/v/${cleanId}`;
try {
const req = await fetch(apiUrl, {
method: 'GET',
headers: {
'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': 'application/json, text/plain, */*',
'Host': 'rou.video',
'Origin': 'https://rou.video',
'Referer': 'https://rou.video/'
}
});
if (!req.ok) {
console.error(`[RouVideo] Error HTTP: ${req.status}`);
return { videoSources: [] };
}
const text = await req.text();
if (text.trim().startsWith("<")) {
console.error("[RouVideo] Error: La API devolvió HTML");
return { videoSources: [] };
}
const json = JSON.parse(text);
if (json?.video?.videoUrl) {
console.log("[RouVideo] Video URL encontrado:", json.video.videoUrl);
// Headers necesarios para reproducir el stream
const streamHeaders = {
"Referer": "https://rou.video/",
"Origin": "https://rou.video",
"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"
};
return {
headers: streamHeaders,
videoSources: [{
server: "RouVideo",
url: json.video.videoUrl,
type: "m3u8",
quality: "Auto",
headers: streamHeaders
}]
};
} else {
console.warn("[RouVideo] JSON válido pero sin videoUrl");
return { videoSources: [] };
}
} catch (error) {
console.error("[RouVideo] Error fatal:", error);
return { videoSources: [] };
}
}
extractNextData($) {
try {
const scriptContent = $('#__NEXT_DATA__').html();
if (scriptContent) {
return JSON.parse(scriptContent);
}
} catch (e) {
console.error("Error parsing __NEXT_DATA__", e);
}
return null;
}
parseVideoItem(video) {
return {
id: video.id,
title: video.name,
image: video.coverImageUrl,
};
}
formatDuration(seconds) {
if (!seconds) return "0:00";
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
const mStr = m < 10 && h > 0 ? `0${m}` : m;
const sStr = s < 10 ? `0${s}` : s;
return h > 0 ? `${h}:${mStr}:${sStr}` : `${mStr}:${sStr}`;
}
async request(url) {
const req = await fetch(url, {
headers: {
'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',
'Referer': `${this.baseUrl}/`,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'
}
});
return await req.text();
}
async requestApi(url) {
const req = await fetch(url, {
headers: {
'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',
'Referer': `${this.baseUrl}/`,
'Origin': this.baseUrl,
'Accept': 'application/json, text/plain, */*'
}
});
return await req.text();
}
}
module.exports = RouVideo;

186
anime/xvideos.js Normal file
View File

@@ -0,0 +1,186 @@
class Xvideos {
constructor() {
this.baseUrl = "https://www.xvideos.com";
this.type = "anime-board";
this.version = "1.0";
}
getSettings() {
return {
episodeServers: ["Default"],
supportsDub: false,
};
}
_encodeId(href) {
if (!href) return "";
let id = href.startsWith("/") ? href.substring(1) : href;
return id.replace(/\//g, "___");
}
_decodeId(id) {
if (!id) return "";
return id.replace(/___/g, "/");
}
async search({ query, tag = "", page = 1 }) {
let url;
if (query && query.trim()) {
url = `${this.baseUrl}/?k=${encodeURIComponent(query)}&p=${page}`;
} else if (tag) {
url = `${this.baseUrl}/tags/${tag}/${page}`;
} else {
url = `${this.baseUrl}/new/${page}`;
}
try {
const res = await fetch(url);
const html = await res.text();
const $ = this.cheerio.load(html);
const items = $("div#main div#content div.mozaique.cust-nb-cols > div").toArray();
const results = items.map(el => {
const a = $(el).find("div.thumb-inside div.thumb a");
const img = $(el).find("div.thumb-inside div.thumb a img");
const href = a.attr("href");
if (!href) return null;
const safeId = this._encodeId(href);
const thumbnail = img.attr("data-src") || img.attr("src") || "";
return {
id: safeId,
title: $(el).find("div.thumb-under p.title").text().trim(),
url: this.baseUrl + href,
image: thumbnail,
subOrDub: "sub",
};
}).filter(i => i !== null);
return results;
} catch (e) {
console.error("[ERROR] Fallo en Search:", e);
return [];
}
}
async getMetadata(id) {
const realPath = this._decodeId(id);
const fetchUrl = `${this.baseUrl}/${realPath}`;
try {
const res = await fetch(fetchUrl);
if (res.status === 404) throw new Error("Video no encontrado (404)");
const html = await res.text();
const $ = this.cheerio.load(html);
const title = $("h2.page-title").text().trim();
const genres = $("div.video-metadata ul li a span").toArray().map(e => $(e).text());
let image = "";
image = $('meta[property="og:image"]').attr('content');
if (!image) {
const regex = /html5player\.setThumbUrl\(\s*['"]([^'"]+)['"]/;
const match = html.match(regex);
if (match) image = match[1];
}
return {
title: title || "Unknown Title",
summary: "",
episodes: 1,
characters: [],
season: "",
status: "Completed",
studio: "",
score: null,
year: null,
genres: genres,
image: image || "",
};
} catch (e) {
console.error("[ERROR] Fallo en getMetadata:", e);
throw e;
}
}
async findEpisodes(id) {
return [
{
id: "video_main",
number: 1,
title: "Video",
url: id,
},
];
}
async findEpisodeServer(episode, server, category = "sub") {
const realPath = this._decodeId(episode.url);
const videoUrl = `${this.baseUrl}/${realPath}`;
try {
const res = await fetch(videoUrl);
const html = await res.text();
const videoSources = [];
const extract = (key) => {
const regex = new RegExp(`${key}\\s*\\(\\s*['"]([^'"]+)['"]`);
const match = html.match(regex);
return match ? match[1] : null;
};
const low = extract("html5player.setVideoUrlLow");
const high = extract("html5player.setVideoUrlHigh");
const hls = extract("html5player.setVideoHLS");
if (hls) {
videoSources.push({
url: hls,
type: "m3u8",
quality: "Auto",
subOrDub: category
});
} else if (high) {
videoSources.push({
url: high,
type: "mp4",
quality: "High",
subOrDub: category
});
} else if (low) {
videoSources.push({
url: low,
type: "mp4",
quality: "Low",
subOrDub: category
});
}
if (videoSources.length === 0) throw new Error("No sources found");
return {
server: server || "Default",
headers: { Referer: this.baseUrl },
videoSources: videoSources,
};
} catch (e) {
console.error("[ERROR] Fallo en findEpisodeServer:", e);
throw e;
}
}
}
module.exports = Xvideos;