updates and new extensions
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
437
anime/hentaila.js
Normal 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
266
anime/missav.js
Normal 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
290
anime/rouvideo.js
Normal 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
186
anime/xvideos.js
Normal 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;
|
||||
Reference in New Issue
Block a user