updates and new extensions
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user