From 83c51a82da4845c5525724acddfae6c130734b50 Mon Sep 17 00:00:00 2001 From: lenafx Date: Mon, 5 Jan 2026 04:46:26 +0100 Subject: [PATCH] updates and new extensions --- anime/AniDream.js | 154 +++++++++++++++ anime/AniZone.js | 4 +- anime/Anicrush.js | 188 +++++++++++++++++++ anime/AnimeAV1.js | 162 ++++++++-------- anime/AnimePahe.js | 189 +++++++++++++++++++ anime/Animekai.js | 365 ++++++++++++++++++++++++++++++++++++ anime/HiAnime.js | 89 +++++---- anime/OppaiStream.js | 437 +++++++++++++++++++++++++++++++++++++++++++ marketplace.json | 41 ++++ 9 files changed, 1500 insertions(+), 129 deletions(-) create mode 100644 anime/AniDream.js create mode 100644 anime/Anicrush.js create mode 100644 anime/AnimePahe.js create mode 100644 anime/Animekai.js create mode 100644 anime/OppaiStream.js diff --git a/anime/AniDream.js b/anime/AniDream.js new file mode 100644 index 0000000..8b23ded --- /dev/null +++ b/anime/AniDream.js @@ -0,0 +1,154 @@ +class AniDream { + constructor() { + this.baseUrl = "https://anidream.cc"; + this.api = "https://common.anidream.cc/v1"; + this.type = "anime-board"; + this.version = "1.0"; + } + + getSettings() { + return { + episodeServers: ["Default", "Zen"], + supportsDub: false, + }; + } + + async search(queryObj) { + const res = await fetch( + `${this.api}/search?pageSize=8&query=${encodeURIComponent(queryObj.query)}`, + { + headers: { + accept: "*/*", + "user-agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36", + }, + } + ); + + const json = await res.json(); + const series = json?.data?.series ?? []; + + return series.map((s) => ({ + id: s.id, + title: s.title, + url: `https://anidream.cc/series/${s.slug}`, + subOrDub: "sub", + })); + } + + async findEpisodes(id) { + const res = await fetch(`${this.api}/series/${id}`, { + headers: { + accept: "application/json, text/javascript, */*; q=0.01", + "accept-language": "es-ES,es;q=0.9", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", + referer: "https://anidream.cc/", + }, + }); + + if (!res.ok) throw new Error(`Error HTTP ${res.status}`); + + const json = await res.json(); + const episodes = json?.data?.episodes; + + if (!Array.isArray(episodes)) return []; + + return episodes.map((ep) => ({ + id: ep.id, + number: parseInt(ep.number, 10), + title: ep.title, + url: `https://anidream.cc/watch/${ep.slug}`, + })); + } + + parseSubtitles(data) { + if (!data || !Array.isArray(data.subtitles)) return []; + + return data.subtitles.map((s) => { + const cleanLang = (s.language_name ?? "") + .replace(/^Language\s*\(|\)$/g, "") + .trim(); + + return { + id: cleanLang, + url: s.url, + language: `${cleanLang} - ${s.title ?? ""}`, + isDefault: s.is_default ?? false, + }; + }); + } + + async findEpisodeServer(episodeOrId, server) { + const headers = { + "user-agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36", + accept: "*/*", + }; + + // Default + if (server.toLowerCase() === "default") { + const res = await fetch( + `${this.api}/watch/default/${episodeOrId.id}/`, + { headers } + ); + const json = await res.json(); + + if (!json?.data?.m3u8_url) + throw new Error("Stream not found at Default"); + + return { + server: "Default", + headers: {}, + videoSources: [ + { + url: json.data.m3u8_url, + type: "m3u8", + quality: "auto", + subtitles: json.data?.subtitles?.length + ? this.parseSubtitles(json.data) + : [], + }, + ], + }; + } + + // Otros servidores (Zen) + const res = await fetch(`${this.api}/episodes/${episodeOrId.id}/`, { + headers, + }); + const json = await res.json(); + const servers = json?.data?.servers ?? []; + + const target = servers.find( + (s) => s.server.toLowerCase() === server.toLowerCase() + ); + if (!target?.access_id) + throw new Error(`Server ${server} not found`); + + const res2 = await fetch( + `${this.api}/watch/${server}/${target.access_id}/`, + { headers } + ); + const json2 = await res2.json(); + + if (!json2?.data?.m3u8_url) + throw new Error(`Stream not found on ${server}`); + + return { + server, + headers: {}, + videoSources: [ + { + url: json2.data.m3u8_url, + type: "m3u8", + quality: "auto", + subtitles: json2.data?.subtitles?.length + ? this.parseSubtitles(json2.data) + : [], + }, + ], + }; + } +} + +module.exports = AniDream; diff --git a/anime/AniZone.js b/anime/AniZone.js index 673e98d..7aa6078 100644 --- a/anime/AniZone.js +++ b/anime/AniZone.js @@ -1,14 +1,14 @@ class Anizone { constructor() { this.type = "anime-board"; - this.version = "1.1"; + this.version = "1.2"; this.api = "https://anizone.to"; } getSettings() { return { episodeServers: ["HLS"], - supportsDub: true, + supportsDub: false, }; } diff --git a/anime/Anicrush.js b/anime/Anicrush.js new file mode 100644 index 0000000..0e97413 --- /dev/null +++ b/anime/Anicrush.js @@ -0,0 +1,188 @@ +class Anicrush { + + constructor() { + this.baseUrl = "https://anicrush.to"; + this.type = "anime-board"; + this.version = "1.0"; + } + + getSettings() { + return { + episodeServers: ["Southcloud-1", "Southcloud-2", "Southcloud-3"], + supportsDub: true, + }; + } + + async search(query) { + const url = `https://api.anicrush.to/shared/v2/movie/list?keyword=${encodeURIComponent(query.query)}&limit=48&page=1`; + + const json = await fetch(url, { + headers: { + "User-Agent": "Mozilla/5.0", + "X-Site": "anicrush", + }, + }).then(r => r.json()); + + const results = json?.result?.movies ?? []; + + return results.map(m => ({ + id: String(m.id), + title: m.name_english || m.name, + url: `${this.baseUrl}/detail/${m.slug}.${m.id}`, + subOrDub: m.has_dub ? "both" : "sub", + })); + } + + async findEpisodes(id) { + const res = await fetch( + `https://api.anicrush.to/shared/v2/episode/list?_movieId=${id}`, + { headers: { "X-Site": "anicrush" } } + ); + + const json = await res.json(); + const groups = json?.result ?? {}; + const episodes = []; + + for (const group of Object.values(groups)) { + if (!Array.isArray(group)) continue; + for (const ep of group) { + episodes.push({ + id: `${id}$${ep.number}`, + number: ep.number, + title: ep.name_english || `Episode ${ep.number}`, + url: "", + }); + } + } + + return episodes.sort((a, b) => a.number - b.number); + } + + async findEpisodeServer(episode, server, category = "sub") { + const [id] = episode.id.split("$"); + + const serverMap = { + "Southcloud-1": 4, + "Southcloud-2": 1, + "Southcloud-3": 6, + }; + + const sv = serverMap[server] ?? 4; + + const apiUrl = + `https://api.anicrush.to/shared/v2/episode/sources` + + `?_movieId=${id}&ep=${episode.number}&sv=${sv}&sc=${category}`; + + const json = await fetch(apiUrl, { + headers: { + "User-Agent": "Mozilla/5.0", + "X-Site": "anicrush", + }, + }).then(r => r.json()); + + const iframe = json?.result?.link; + if (!iframe) throw new Error("No iframe"); + + let data; + try { + data = await this.extractMegaCloud(iframe); + } catch { + const fallback = await fetch( + `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent(iframe)}` + ); + data = await fallback.json(); + } + + const sources = + data.sources ?? + data.result?.sources ?? + []; + + if (!Array.isArray(sources) || !sources.length) { + throw new Error("No video sources"); + } + + const source = + sources.find(s => s.type === "hls") || + sources.find(s => s.type === "mp4"); + + + if (!source?.file) throw new Error("No stream"); + + const subtitles = (data.tracks || []) + .filter(t => t.kind === "captions") + .map((t, i) => ({ + id: `sub-${i}`, + language: t.label || "Unknown", + url: t.file, + isDefault: !!t.default, + })); + + return { + server, + headers: { + Referer: "https://megacloud.club/", + Origin: "https://megacloud.club", + }, + videoSources: [{ + url: source.file, + type: source.type === "hls" ? "m3u8" : "mp4", + quality: "auto", + subtitles, + subOrDub: category, + }], + }; + } + + async extractMegaCloud(embedUrl) { + const url = new URL(embedUrl); + const baseDomain = `${url.protocol}//${url.host}/`; + + const headers = { + Accept: "*/*", + "X-Requested-With": "XMLHttpRequest", + Referer: baseDomain, + "User-Agent": + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36", + }; + + // 1) Fetch embed page + const html = await fetch(embedUrl, { headers }).then(r => r.text()); + + // 2) Extract file ID + const fileIdMatch = html.match(/\s*File\s+#([a-zA-Z0-9]+)\s*-/i); + if (!fileIdMatch) throw new Error("file_id not found in embed page"); + const fileId = fileIdMatch[1]; + + // 3) Extract nonce + let nonce = null; + + const match48 = html.match(/\b[a-zA-Z0-9]{48}\b/); + if (match48) { + nonce = match48[0]; + } else { + const match3x16 = [...html.matchAll(/["']([A-Za-z0-9]{16})["']/g)]; + if (match3x16.length >= 3) { + nonce = match3x16[0][1] + match3x16[1][1] + match3x16[2][1]; + } + } + + if (!nonce) throw new Error("nonce not found"); + + // 4) Fetch sources + const sourcesJson = await fetch( + `${baseDomain}embed-2/v3/e-1/getSources?id=${fileId}&_k=${nonce}`, + { headers } + ).then(r => r.json()); + + return { + sources: sourcesJson.sources || [], + tracks: sourcesJson.tracks || [], + intro: sourcesJson.intro || null, + outro: sourcesJson.outro || null, + server: sourcesJson.server || null, + }; + } +} + +module.exports = Anicrush; diff --git a/anime/AnimeAV1.js b/anime/AnimeAV1.js index b1f35d0..ef23e69 100644 --- a/anime/AnimeAV1.js +++ b/anime/AnimeAV1.js @@ -1,14 +1,13 @@ class AnimeAV1 { - constructor() { this.type = "anime-board"; - this.version = "1.2" + this.version = "1.3"; this.api = "https://animeav1.com"; } getSettings() { return { - episodeServers: ["HLS", "HLS-DUB"], + episodeServers: ["HLS"], supportsDub: true, }; } @@ -21,10 +20,9 @@ class AnimeAV1 { }); if (!res.ok) return []; - const data = await res.json(); - return data.map(anime => ({ + return data.map((anime) => ({ id: anime.slug, title: anime.title, url: `${this.api}/media/${anime.slug}`, @@ -34,70 +32,37 @@ class AnimeAV1 { } async getMetadata(id) { - const html = await fetch(`${this.api}/media/${id}`).then(r => r.text()); + const html = await fetch(`${this.api}/media/${id}`).then((r) => r.text()); const parsed = this.parseSvelteData(html); - const media = parsed.find(x => x?.data?.media)?.data.media ?? {}; + const media = parsed.find((x) => x?.data?.media)?.data.media ?? {}; - // IMAGE - const imageMatch = html.match(/<img[^>]*class="aspect-poster[^"]*"[^>]*src="([^"]+)"/i); - const image = imageMatch ? imageMatch[1] : null; - - // BLOCK INFO (STATUS, SEASON, YEAR) - const infoBlockMatch = html.match( - /<div class="flex flex-wrap items-center gap-2 text-sm">([\s\S]*?)<\/div>/ + const imageMatch = html.match( + /<img[^>]*class="aspect-poster[^"]*"[^>]*src="([^"]+)"/i ); - - let status = media.status ?? "Unknown"; - let season = media.seasons ?? null; - let year = media.startDate ? Number(media.startDate.slice(0, 4)) : null; - - if (infoBlockMatch) { - const raw = infoBlockMatch[1]; - - // Extraer spans internos - const spans = [...raw.matchAll(/<span[^>]*>([^<]+)<\/span>/g)].map(m => m[1].trim()); - - // EJEMPLO: - // ["TV Anime", "•", "2025", "•", "Temporada Otoño", "•", "En emisión"] - - const clean = spans.filter(x => x !== "•"); - - // YEAR - const yearMatch = clean.find(x => /^\d{4}$/.test(x)); - if (yearMatch) year = Number(yearMatch); - - // SEASON (el que contiene "Temporada") - const seasonMatch = clean.find(x => x.toLowerCase().includes("temporada")); - if (seasonMatch) season = seasonMatch; - - // STATUS (normalmente "En emisión", "Finalizado", etc) - const statusMatch = clean.find(x => - /emisión|finalizado|completado|pausa|cancelado/i.test(x) - ); - if (statusMatch) status = statusMatch; - } + const image = imageMatch ? imageMatch[1] : null; return { title: media.title ?? "Unknown", summary: media.synopsis ?? "No summary available", episodes: media.episodesCount ?? 0, characters: [], - season, - status, + season: media.seasons ?? null, + status: media.status ?? "Unknown", studio: "Unknown", score: media.score ?? 0, - year, - genres: media.genres?.map(g => g.name) ?? [], - image + year: media.startDate + ? Number(media.startDate.slice(0, 4)) + : null, + genres: media.genres?.map((g) => g.name) ?? [], + image, }; } - async findEpisodes(id) { - const html = await fetch(`${this.api}/media/${id}`).then(r => r.text()); + const html = await fetch(`${this.api}/media/${id}`).then((r) => r.text()); const parsed = this.parseSvelteData(html); - const media = parsed.find(x => x?.data?.media)?.data?.media; + const media = parsed.find((x) => x?.data?.media)?.data?.media; if (!media?.episodes) throw new Error("No se encontró media.episodes"); return media.episodes.map((ep, i) => ({ @@ -108,71 +73,94 @@ class AnimeAV1 { })); } - async findEpisodeServer(episodeOrId, _server) { - const ep = typeof episodeOrId === "string" - ? (() => { try { return JSON.parse(episodeOrId); } catch { return { id: episodeOrId }; } })() - : episodeOrId; + async findEpisodeServer(episodeOrId, _server, category = "sub") { + const ep = + typeof episodeOrId === "string" + ? (() => { + try { + return JSON.parse(episodeOrId); + } catch { + return { id: episodeOrId }; + } + })() + : episodeOrId; - const pageUrl = ep.url ?? ( - typeof ep.id === "string" && ep.id.includes("$") - ? `${this.api}/media/${ep.id.split("$")[0]}/${ep.number ?? ep.id.split("$")[1]}` - : undefined - ); + + let pageUrl = ep.url; + + if (!pageUrl && typeof ep.id === "string") { + if (ep.id.includes("$")) { + const [slug, num] = ep.id.split("$"); + pageUrl = `${this.api}/media/${slug}/${ep.number ?? num}`; + } else { + pageUrl = `${this.api}/media/${ep.id}/${ep.number}`; + } + } + + if (!pageUrl) { + throw new Error( + `No se pudo determinar la URL del episodio (id=${ep.id}, number=${ep.number})` + ); + } if (!pageUrl) throw new Error("No se pudo determinar la URL del episodio."); - const html = await fetch(pageUrl, { - headers: { Cookie: "__ddg1_=;__ddg2_=;" }, - }).then(r => r.text()); - + const html = await fetch(pageUrl).then((r) => r.text()); const parsedData = this.parseSvelteData(html); - const entry = parsedData.find(x => x?.data?.embeds) || parsedData[3]; + const entry = parsedData.find((x) => x?.data?.embeds); const embeds = entry?.data?.embeds; - if (!embeds) throw new Error("No se encontraron 'embeds' en los datos del episodio."); + if (!embeds) throw new Error("No embeds encontrados"); - const selectedEmbeds = - _server === "HLS" - ? embeds.SUB ?? [] - : _server === "HLS-DUB" - ? embeds.DUB ?? [] - : (() => { throw new Error(`Servidor desconocido: ${_server}`); })(); + const list = + category === "dub" + ? embeds.DUB + : embeds.SUB; - if (!selectedEmbeds.length) - throw new Error(`No hay mirrors disponibles para ${_server === "HLS" ? "SUB" : "DUB"}.`); + if (!Array.isArray(list)) + throw new Error(`No hay streams ${category.toUpperCase()}`); - const match = selectedEmbeds.find(m => - (m.url || "").includes("zilla-networks.com/play/") + const hls = list.find( + (m) => + m.server === "HLS" && + m.url?.includes("zilla-networks.com/play/") ); - if (!match) - throw new Error(`No se encontró ningún embed de ZillaNetworks en ${_server}.`); + if (!hls) + throw new Error(`No se encontró stream HLS ${category.toUpperCase()}`); return { - server: _server, - headers: { Referer: 'null' }, + server: "HLS", + headers: { Referer: "null" }, videoSources: [ { - url: match.url.replace("/play/", "/m3u8/"), + url: hls.url.replace("/play/", "/m3u8/"), type: "m3u8", quality: "auto", subtitles: [], + subOrDub: category, }, ], }; } parseSvelteData(html) { - const scriptMatch = html.match(/<script[^>]*>\s*({[^<]*__sveltekit_[\s\S]*?)<\/script>/i); - if (!scriptMatch) throw new Error("No se encontró bloque SvelteKit en el HTML."); + const scriptMatch = html.match( + /<script[^>]*>\s*({[^<]*__sveltekit_[\s\S]*?)<\/script>/i + ); + if (!scriptMatch) throw new Error("SvelteKit block not found"); - const dataMatch = scriptMatch[1].match(/data:\s*(\[[\s\S]*?\])\s*,\s*form:/); - if (!dataMatch) throw new Error("No se encontró el bloque 'data' en el script SvelteKit."); + const dataMatch = scriptMatch[1].match( + /data:\s*(\[[\s\S]*?\])\s*,\s*form:/ + ); + if (!dataMatch) throw new Error("SvelteKit data block not found"); const jsArray = dataMatch[1]; try { return new Function(`"use strict"; return (${jsArray});`)(); } catch { - const cleaned = jsArray.replace(/\bvoid 0\b/g, "null").replace(/undefined/g, "null"); + const cleaned = jsArray + .replace(/\bvoid 0\b/g, "null") + .replace(/undefined/g, "null"); return new Function(`"use strict"; return (${cleaned});`)(); } } diff --git a/anime/AnimePahe.js b/anime/AnimePahe.js new file mode 100644 index 0000000..eff5ce1 --- /dev/null +++ b/anime/AnimePahe.js @@ -0,0 +1,189 @@ +class AnimePahe { + constructor() { + this.baseUrl = "https://animepahe.si"; + this.api = "https://animepahe.si"; + this.type = "anime-board"; + this.version = "1.0"; + this.headers = { Referer: "https://kwik.cx" }; + } + + getSettings() { + return { + episodeServers: ["Kwik", "Pahe"], + supportsDub: false, + }; + } + + async search(queryObj) { + const req = await fetch( + `${this.api}/api?m=search&q=${encodeURIComponent(queryObj.query)}`, + { headers: { Cookie: "__ddg1_=;__ddg2_=;" } } + ); + + if (!req.ok) return []; + const data = await req.json(); + if (!data?.data) return []; + + return data.data.map((item) => ({ + id: item.session, + title: item.title, + url: "", + subOrDub: "sub", + })); + } + + async findEpisodes(id) { + let episodes = []; + + const req = await fetch( + `${this.api}${id.includes("-") ? `/anime/${id}` : `/a/${id}`}`, + { headers: { Cookie: "__ddg1_=;__ddg2_=;" } } + ); + + const html = await req.text(); + const $ = this.cheerio.load(html); + + const tempId = $("head > meta[property='og:url']") + .attr("content") + .split("/") + .pop(); + + const pushData = (data) => { + for (const item of data) { + episodes.push({ + id: item.session + "$" + id, + number: item.episode, + title: + item.title && item.title.length > 0 + ? item.title + : "Episode " + item.episode, + url: req.url, + }); + } + }; + + const first = await fetch( + `${this.api}/api?m=release&id=${tempId}&sort=episode_asc&page=1`, + { headers: { Cookie: "__ddg1_=;__ddg2_=;" } } + ).then((r) => r.json()); + + pushData(first.data); + + const pages = Array.from( + { length: first.last_page - 1 }, + (_, i) => i + 2 + ); + + const results = await Promise.all( + pages.map((p) => + fetch( + `${this.api}/api?m=release&id=${tempId}&sort=episode_asc&page=${p}`, + { headers: { Cookie: "__ddg1_=;__ddg2_=;" } } + ).then((r) => r.json()) + ) + ); + + results.forEach((r) => r.data && pushData(r.data)); + + episodes.sort((a, b) => a.number - b.number); + + if (!episodes.length) throw new Error("No episodes found."); + + const lowest = episodes[0].number; + if (lowest > 1) { + episodes.forEach((ep) => (ep.number = ep.number - lowest + 1)); + } + + return episodes.filter((ep) => Number.isInteger(ep.number)); + } + + async findEpisodeServer(episodeOrId, server) { + const [episodeId, animeId] = episodeOrId.id.split("$"); + + const req = await fetch( + `${this.api}/play/${animeId}/${episodeId}`, + { headers: { Cookie: "__ddg1_=;__ddg2_=;" } } + ); + + const html = await req.text(); + const matches = html.match(/https:\/\/kwik\.cx\/e\/\w+/g); + if (!matches) throw new Error("Failed to fetch episode server."); + + const $ = this.cheerio.load(html); + + const sourcePromises = $("button[data-src]") + .toArray() + .map(async (el) => { + const embedUrl = $(el).data("src"); + if (!embedUrl) return null; + + const fansub = $(el).data("fansub"); + const quality = $(el).data("resolution"); + + let label = `${quality}p - ${fansub}`; + if ($(el).data("audio") === "eng") label += " (Eng)"; + if (embedUrl === matches[0]) label += " (default)"; + + try { + const srcReq = await fetch(embedUrl, { + headers: { + Referer: this.headers.Referer, + "user-agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36", + }, + }); + + const srcHtml = await srcReq.text(); + const scripts = srcHtml.match(/eval\(f.+?\}\)\)/g); + if (!scripts) return null; + + for (const s of scripts) { + const m = s.match(/eval(.+)/); + if (!m?.[1]) continue; + + try { + const decoded = eval(m[1]); + const link = decoded.match(/source='(.+?)'/); + if (!link?.[1]) continue; + + const m3u8 = link[1]; + + if (server === "Pahe") { + return { + url: m3u8 + .replace("owocdn.top", "kwik.cx") + .replace("/stream/", "/mp4/") + .replace("/uwu.m3u8", ""), + type: "mp4", + quality: label, + subtitles: [], + }; + } + + return { + url: m3u8, + type: "m3u8", + quality: label, + subtitles: [], + }; + } catch {} + } + return null; + } catch { + return null; + } + }); + + const videoSources = (await Promise.all(sourcePromises)).filter(Boolean); + if (!videoSources.length) + throw new Error(`Failed to extract any sources for ${server}.`); + + return { + server, + headers: this.headers, + videoSources, + }; + } +} + +module.exports = AnimePahe; diff --git a/anime/Animekai.js b/anime/Animekai.js new file mode 100644 index 0000000..da4203b --- /dev/null +++ b/anime/Animekai.js @@ -0,0 +1,365 @@ +class AnimeKai { + constructor() { + this.baseUrl = "https://animekai.to"; + this.type = "anime-board"; + this.version = "1.0"; + this.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"; + } + + getSettings() { + return { + episodeServers: ["Server 1", "Server 2"], + supportsDub: true, + }; + } + + async search(queryObj) { + const query = queryObj.query; + + const dubParam = queryObj.dub || ""; + + const normalizedQuery = this.normalizeQuery(query); + console.log("Normalized Query: " + normalizedQuery); + + const url = `${this.baseUrl}/browser?keyword=${encodeURIComponent(normalizedQuery)}`; + + try { + const data = await this.GETText(url); + const $ = this.cheerio.load(data); + const animes = []; + + $("div.aitem-wrapper>div.aitem").each((_, elem) => { + const el = $(elem); + const linkHref = el.find("a.poster").attr("href"); + const idRaw = linkHref ? linkHref.slice(1) : ""; + const title = el.find("a.title").attr("title") || ""; + const subOrDub = this.isSubOrDubOrBoth(el); + const animeUrl = `${this.baseUrl}/${idRaw}`; + + const fullId = `${idRaw}?dub=${dubParam}`; + + animes.push({ + id: fullId, + title: title, + url: animeUrl, + subOrDub: subOrDub, + }); + }); + + return animes; + } catch (e) { + console.error(e); + return []; + } + } + + async getMetadata(id) { + + const cleanId = id.split('?')[0]; + const url = `${this.baseUrl}/${cleanId}`; + + try { + const data = await this.GETText(url); + const $ = this.cheerio.load(data); + + const title = $("meta[property='og:title']").attr("content") || $("h1").text().trim() || "Unknown Title"; + const image = $("meta[property='og:image']").attr("content") || $("div.poster img").attr("src") || ""; + const summary = $("meta[property='og:description']").attr("content") || $("div.desc").text().trim() || ""; + + return { + title: title, + summary: summary, + episodes: 0, + + image: image, + genres: [], + status: "Unknown" + }; + } catch (e) { + console.error(e); + throw new Error("Failed to get metadata"); + } + } + + async findEpisodes(id) { + const url = `${this.baseUrl}/${id.split('?dub')[0]}`; + const rateBoxIdRegex = /<div class="rate-box"[^>]*data-id="([^"]+)"/; + + try { + const pageHtml = await this.GETText(url); + const idMatch = pageHtml.match(rateBoxIdRegex); + const aniId = idMatch ? idMatch[1] : null; + + if (aniId === null) throw new Error("Anime ID not found"); + + const tokenResp = await this.GETJson(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(aniId)}`); + const token = tokenResp.result; + + const fetchUrlListApi = `${this.baseUrl}/ajax/episodes/list?ani_id=${aniId}&_=${token}`; + const ajaxResult = await this.GETJson(fetchUrlListApi); + + const $ = this.cheerio.load(ajaxResult.result); + + const episodeData = $('ul.range>li>a').map((_, elem) => ({ + name: `Episode ${$(elem).attr('num')}`, + number: parseInt($(elem).attr('num') || "0", 10), + data: $(elem).attr('token'), + title: $(elem).find('span').text().replace(/\s/g, ' ') + })).get(); + + const episodes = await Promise.all( + episodeData.map(async (item) => { + const response = await fetch(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`); + const result = await response.json(); + + const dubPart = id.split('?dub=')[1] || ""; + + return { + id: item.data || "", + number: item.number, + title: item.title, + + url: `${this.baseUrl}/ajax/links/list?token=${item.data}&_=${result.result}?dub=${dubPart}` + }; + }) + ); + + return episodes; + } catch (e) { + throw new Error(e); + } + } + + async findEpisodeServer(episode, serverStr, category = "sub") { + let server = "Server 1"; + if (serverStr && serverStr !== "default") server = serverStr; + + const episodeUrl = episode.url.replace('\u0026', '&').split('?dub')[0]; + const dubRequested = episode.url.split('?dub=')[1]; + + console.log("Episode URL: " + episodeUrl); + + try { + const responseText = await this.GETText(episodeUrl); + const cleanedHtml = this.cleanJsonHtml(responseText); + + const subRegex = /<div class="server-items lang-group" data-id="sub"[^>]*>([\s\S]*?)<\/div>/; + const softsubRegex = /<div class="server-items lang-group" data-id="softsub"[^>]*>([\s\S]*?)<\/div>/; + const dubRegex = /<div class="server-items lang-group" data-id="dub"[^>]*>([\s\S]*?)<\/div>/; + + const subMatch = subRegex.exec(cleanedHtml); + const softsubMatch = softsubRegex.exec(cleanedHtml); + const dubMatch = dubRegex.exec(cleanedHtml); + + const sub = subMatch ? subMatch[1].trim() : ""; + const softsub = softsubMatch ? softsubMatch[1].trim() : ""; + const dub = dubMatch ? dubMatch[1].trim() : ""; + + const serverSpanRegex = server === "Server 1" ? + /<span class="server"[^>]*data-lid="([^"]+)"[^>]*>Server 1<\/span>/ : + /<span class="server"[^>]*data-lid="([^"]+)"[^>]*>Server 2<\/span>/; + + const isDub = category === 'dub' || dubRequested === 'true'; + + const serverIdDub = serverSpanRegex.exec(dub)?.[1]; + const serverIdSoftsub = serverSpanRegex.exec(softsub)?.[1]; + const serverIdSub = serverSpanRegex.exec(sub)?.[1]; + + const tokenRequestData = [ + { name: "Dub", data: serverIdDub }, + { name: "Softsub", data: serverIdSoftsub }, + { name: "Sub", data: serverIdSub } + ].filter(item => item.data !== undefined); + + const tokenResults = await Promise.all( + tokenRequestData.map(async (item) => { + const response = await fetch(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`); + return { name: item.name, data: await response.json() }; + }) + ); + + const serverIdMap = Object.fromEntries(tokenRequestData.map(item => [item.name, item.data])); + + const streamUrls = tokenResults.map((result) => { + return { + type: result.name, + url: `${this.baseUrl}/ajax/links/view?id=${serverIdMap[result.name]}&_=${result.data.result}` + }; + }); + + const decryptedUrls = await processStreams(streamUrls); + + const headers = { + "Referer": "https://animekai.to/", + "User-Agent": this.userAgent + }; + + let streamUrl = ""; + if (isDub && decryptedUrls.Dub) { + streamUrl = decryptedUrls.Dub; + } else { + streamUrl = decryptedUrls.Sub || decryptedUrls.Softsub; + } + + if (!streamUrl) { + throw new Error("Unable to find a valid source"); + } + + const streams = await fetch(streamUrl.replace("/e/", "/media/"), { + headers: headers + }); + + const responseJson = await streams.json(); + const result = responseJson?.result; + + const postData = { + "text": result, + "agent": this.userAgent + }; + + const finalJson = await fetch("https://enc-dec.app/api/dec-mega", { + method: "POST", + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(postData) + }).then(res => res.json()); + + if (!finalJson || finalJson.status !== 200) throw new Error("Failed to decrypt the final stream URL"); + if (!finalJson.result.sources || finalJson.result.sources.length === 0) throw new Error("No video sources found"); + + const m3u8Link = finalJson.result.sources[0].file; + const playlistResponse = await fetch(m3u8Link); + const playlistText = await playlistResponse.text(); + + const regex = /#EXT-X-STREAM-INF:BANDWIDTH=\d+,RESOLUTION=(\d+x\d+)\s*(.*)/g; + const videoSources = []; + + let resolutionMatch; + while ((resolutionMatch = regex.exec(playlistText)) !== null) { + let url = ""; + if (resolutionMatch[2].includes("list")) { + url = `${m3u8Link.split(',')[0]}/${resolutionMatch[2]}`; + } else { + url = `${m3u8Link.split('/list')[0]}/${resolutionMatch[2]}`; + } + + videoSources.push({ + url: url, + type: "m3u8", + quality: resolutionMatch[1].split('x')[1] + 'p', + subtitles: [], + subOrDub: isDub ? "dub" : "sub" + }); + } + + if (videoSources.length === 0) { + videoSources.push({ + url: m3u8Link, + type: "m3u8", + quality: "auto", + subtitles: [], + subOrDub: isDub ? "dub" : "sub" + }); + } + + return { + server: server || "default", + headers: { + "Referer": this.baseUrl, + "User-Agent": this.userAgent + }, + videoSources: videoSources, + }; + + } catch (e) { + console.error(e); + throw new Error(e.message || "Error finding server"); + } + } + + normalizeQuery(query) { + return query + .replace(/\b(\d+)(st|nd|rd|th)\b/g, "$1") + .replace(/\s+/g, " ") + .replace(/(\d+)\s*Season/i, "$1") + .replace(/Season\s*(\d+)/i, "$1") + .trim(); + } + + async _makeRequest(url) { + const response = await fetch(url, { + method: "GET", + headers: { + "DNT": "1", + "User-Agent": this.userAgent, + "Cookie": "__ddg1_=;__ddg2_=;", + }, + }); + if (!response.ok) { + throw new Error(`Failed to fetch: ${response.statusText}`); + } + return response; + } + + async GETText(url) { + const res = await this._makeRequest(url); + return await res.text(); + } + + async GETJson(url) { + const res = await this._makeRequest(url); + return await res.json(); + } + + isSubOrDubOrBoth(elem) { + const sub = elem.find("span.sub").text(); + const dub = elem.find("span.dub").text(); + if (sub !== "" && dub !== "") return "both"; + if (sub !== "") return "sub"; + return "dub"; + } + + cleanJsonHtml(jsonHtml) { + if (!jsonHtml) return ""; + return jsonHtml + .replace(/\\"/g, "\"") + .replace(/\\'/g, "'") + .replace(/\\\\/g, "\\") + .replace(/\\n/g, "\n") + .replace(/\\t/g, "\t") + .replace(/\\r/g, "\r"); + } +} + +async function processStreams(streamUrls) { + const streamResponses = await Promise.all( + streamUrls.map(async ({ type, url }) => { + try { + const json = await fetch(url).then(r => r.json()); + return { type, result: json.result }; + } catch (error) { + console.log(`Error fetching ${type} stream:`, error); + return { type, result: null }; + } + }) + ); + + const decryptResults = await Promise.all( + streamResponses + .filter(item => item.result !== null) + .map(async item => { + const result = await fetch("https://enc-dec.app/api/dec-kai", { + headers: { 'Content-Type': 'application/json' }, + method: "POST", + body: JSON.stringify({ text: item.result }) + }).then(res => res.json()); + + return { [item.type]: result.result.url }; + }) + ); + + return Object.assign({}, ...decryptResults); +} + +module.exports = AnimeKai; \ No newline at end of file diff --git a/anime/HiAnime.js b/anime/HiAnime.js index fc56f13..ac27e76 100644 --- a/anime/HiAnime.js +++ b/anime/HiAnime.js @@ -1,7 +1,7 @@ class HiAnime { constructor() { this.type = "anime-board"; - this.version = "1.0" + this.version = "1.1" this.baseUrl = "https://hianime.to"; } @@ -91,70 +91,74 @@ class HiAnime { return episodes; } - async findEpisodeServer(episode, _server) { - const [id, subOrDub] = episode.id.split("/"); - let serverName = _server !== "default" ? _server : "HD-1"; + async findEpisodeServer(episode, _server, category = "sub") { + const id = episode.id; + const subOrDub = category; // backend manda sub | dub - if (_server === "HD-1" || _server === "HD-2" || _server === "HD-3") { - const serverJson = await fetch(`${this.baseUrl}/ajax/v2/episode/servers?episodeId=${id}`, { - headers: { "X-Requested-With": "XMLHttpRequest" } - }).then(res => res.json()); + const serverName = _server !== "default" ? _server : "HD-1"; + + if (serverName === "HD-1" || serverName === "HD-2" || serverName === "HD-3") { + const serverJson = await fetch( + `${this.baseUrl}/ajax/v2/episode/servers?episodeId=${id}`, + { headers: { "X-Requested-With": "XMLHttpRequest" } } + ).then(res => res.json()); const serverHtml = serverJson.html; + const regex = new RegExp( `<div[^>]*class="item server-item"[^>]*data-type="${subOrDub}"[^>]*data-id="(\\d+)"[^>]*>\\s*<a[^>]*>\\s*${serverName}\\s*</a>`, "i" ); const match = regex.exec(serverHtml); - if (!match) throw new Error(`Server "${serverName}" (${subOrDub}) not found`); + if (!match) + throw new Error(`Server "${serverName}" (${subOrDub}) not found`); const serverId = match[1]; - const sourcesJson = await fetch(`${this.baseUrl}/ajax/v2/episode/sources?id=${serverId}`, { - headers: { "X-Requested-With": "XMLHttpRequest" } - }).then(res => res.json()); + const sourcesJson = await fetch( + `${this.baseUrl}/ajax/v2/episode/sources?id=${serverId}`, + { headers: { "X-Requested-With": "XMLHttpRequest" } } + ).then(res => res.json()); - let decryptData = null; + let decryptData; let requiredHeaders = {}; try { - // Pass true to get headers back decryptData = await this.extractMegaCloud(sourcesJson.link, true); - if (decryptData && decryptData.headersProvided) { + if (decryptData?.headersProvided) { requiredHeaders = decryptData.headersProvided; } - } catch (err) { - console.warn("Primary decrypter failed:", err); + } catch (e) { + console.warn("Primary decrypter failed:", e); } if (!decryptData) { - console.warn("Primary decrypter failed — trying ShadeOfChaos fallback..."); const fallbackRes = await fetch( `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent(sourcesJson.link)}` ); decryptData = await fallbackRes.json(); - - // CRITICAL: Fallback headers must mimic the browser behavior expected by the provider - // These MUST be used by a server-side proxy; the browser player cannot set them. + requiredHeaders = { - "Referer": "https://megacloud.club/", - "Origin": "https://megacloud.club", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36", - "X-Requested-With": "XMLHttpRequest" + Referer: "https://megacloud.club/", + Origin: "https://megacloud.club", + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36", + "X-Requested-With": "XMLHttpRequest", }; } const streamSource = - decryptData.sources.find((s) => s.type === "hls") || - decryptData.sources.find((s) => s.type === "mp4"); + decryptData.sources.find(s => s.type === "hls") || + decryptData.sources.find(s => s.type === "mp4"); - if (!streamSource?.file) throw new Error("No valid stream file found"); + if (!streamSource?.file) + throw new Error("No valid stream file found"); const subtitles = (decryptData.tracks || []) - .filter((t) => t.kind === "captions") - .map((track, index) => ({ - id: `sub-${index}`, + .filter(t => t.kind === "captions") + .map((track, i) => ({ + id: `sub-${i}`, language: track.label || "Unknown", url: track.file, isDefault: !!track.default, @@ -163,18 +167,23 @@ class HiAnime { return { server: serverName, headers: requiredHeaders, - videoSources: [{ - url: streamSource.file, - type: streamSource.type === "hls" ? "m3u8" : "mp4", - quality: "auto", - subtitles - }] + videoSources: [ + { + url: streamSource.file, + type: streamSource.type === "hls" ? "m3u8" : "mp4", + quality: "auto", + subtitles, + subOrDub: category, + }, + ], }; } - else if (_server === "HD-4") { - // Implementation for HD-4 if needed - return null; + + if (serverName === "HD-4") { + throw new Error("HD-4 not implemented"); } + + throw new Error(`Unknown server ${serverName}`); } safeString(str) { diff --git a/anime/OppaiStream.js b/anime/OppaiStream.js new file mode 100644 index 0000000..3757beb --- /dev/null +++ b/anime/OppaiStream.js @@ -0,0 +1,437 @@ +class OppaiStream { + constructor() { + 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.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 = { + Title: 3.6, + MaxScore: 10 + }; + } + + getSettings() { + return { + episodeServers: ["oppai.stream"], + supportsDub: false, + }; + } + + async search(queryObj) { + let tempquery = queryObj.query; + + while (tempquery !== "") { + try { + const url = this.searchBaseUrl + encodeURIComponent(tempquery); + const html = await this.GETText(url); + const $ = this.cheerio.load(html); + + const movies = $("div.in-grid.episode-shown"); + + if (movies.length <= 0) { + if (tempquery.includes(" ")) { + tempquery = tempquery.split(/[\s:']+/).slice(0, -1).join(" "); + continue; + } else { + break; + } + } + + const movieList = []; + movies.each((_, el) => { + const title = $(el).find(".title-ep").text().trim(); + const href = $(el).find("a").attr("href"); + const rawUrl = href ? href.replace("&for=search", "") : ""; + + if (title && rawUrl) { + movieList.push({ Title: title, Url: rawUrl }); + } + }); + + const bestMovie = this.findBestTitle(movieList, queryObj.query); + + 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", + }]; + + } catch (e) { + console.error(e); + return []; + } + } + return []; + } + + async getMetadata(id) { + try { + // Decodificamos el ID para obtener la URL real de OppaiStream + const decodedUrl = decodeURIComponent(id); + const html = await this.GETText(decodedUrl); + const $ = this.cheerio.load(html); + + const title = $("meta[property='og:title']").attr("content") || $("h1").text().trim(); + const image = $("meta[property='og:image']").attr("content") || ""; + const summary = $("meta[property='og:description']").attr("content") || $(".desc").text().trim(); + + return { + title: title, + summary: summary, + episodes: $("div.other-episodes.more-same-eps div.in-grid").length || 0, + image: image, + genres: [], + status: "Unknown" + }; + } catch (e) { + console.error(e); + throw new Error("Failed to get metadata"); + } + } + + async findEpisodes(id) { + if (!id) return []; + + try { + // Decodificamos el ID para obtener la URL real + const decodedUrl = decodeURIComponent(id); + const html = await this.GETText(decodedUrl); + const $ = this.cheerio.load(html); + const episodeDetails = []; + + const eps = $("div.other-episodes.more-same-eps div.in-grid.episode-shown"); + + eps.each((_, el) => { + const elObj = $(el); + const idgt = elObj.attr("idgt"); + + if (idgt) { + const href = elObj.find("a").attr("href"); + const rawEpUrl = href ? href.replace("&for=episode-more", "") : ""; + const title = elObj.find("h5 .title").text().trim(); + const epNum = parseInt(elObj.find("h5 .ep").text().trim(), 10); + + episodeDetails.push({ + // También codificamos el ID del episodio por seguridad + id: encodeURIComponent(rawEpUrl), + number: isNaN(epNum) ? 0 : epNum, + title: title || `Episode ${epNum}`, + url: rawEpUrl, + }); + } + }); + + return episodeDetails; + } catch (e) { + console.error(e); + return []; + } + } + + async findEpisodeServer(episode, serverStr) { + // Decodificamos el ID del episodio (que es la URL) + const serverUrl = decodeURIComponent(episode.id); + const videoSources = []; + + if (serverUrl) { + const result = await this.HandleServerUrl(serverUrl); + + if (Array.isArray(result)) { + videoSources.push(...result); + } else if (result) { + videoSources.push(result); + } + } + + return { + server: serverStr || "oppai.stream", + headers: { + "Referer": this.baseUrl, + "User-Agent": this.userAgent + }, + videoSources: videoSources + }; + } + + async HandleServerUrl(serverUrl) { + try { + const html = await this.GETText(serverUrl); + + let unpacked = ""; + const scriptContents = this.extractScripts(html); + + for (const c of scriptContents) { + let c2 = c; + + for (let j = 0; j < c.length; j += 900) { + c2 = c2.substring(0, j) + "\n" + c2.substring(j); + } + + if (c.includes("eval(function(p,a,c,k,e,d)")) { + console.log("Packed script found."); + const fullRegex = /eval\(function\([^)]*\)\{[\s\S]*?\}\(\s*'([\s\S]*?)'\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*'([\s\S]*?)'\.split\('\|'\)/; + const match = c2.match(fullRegex); + + if (match) { + const packed = match[1]; + const base = parseInt(match[2], 10); + const count = parseInt(match[3], 10); + const dict = match[4].split('|'); + unpacked = this.unpack(packed, base, count, dict); + + unpacked = this.decodeUnpacked(unpacked); + } + } + } + + const m3u8Videos = await this.findMediaUrls("m3u8", html, serverUrl, unpacked); + if (m3u8Videos) return m3u8Videos; + + const mp4Videos = await this.findMediaUrls("mp4", html, serverUrl, unpacked); + if (mp4Videos) return mp4Videos; + + return []; + } catch (e) { + console.error("Error handling server URL:", e); + return []; + } + } + + async findMediaUrls(type, html, serverUrl, unpacked) { + const regex = new RegExp('https?:\\/\\/[^\'"]+\\.' + type + '(?:\\?[^\\s\'"]*)?(?:#[^\\s\'"]*)?', 'g'); + const quotedRegex = new RegExp(`"([^"]+\\.${type})"`, "g"); + + let VideoMatch = html.match(regex) + || (unpacked && unpacked.match(regex)) + || html.match(quotedRegex) + || (unpacked && unpacked.match(quotedRegex)); + + if (VideoMatch) { + + VideoMatch = VideoMatch.map(url => { + let clean = url.replace(/"/g, ""); + if (!clean.startsWith("http")) { + const domain = serverUrl.split("/").slice(0, 3).join("/"); + return `${domain}${clean}`; + } + return clean; + }); + + VideoMatch = [...new Set(VideoMatch)]; + const mainUrl = VideoMatch[0]; + + console.log(`Found ${type} URL:`, mainUrl); + + if (mainUrl.includes(`master.${type}`)) { + try { + const reqHtml = await this.GETText(mainUrl); + const videos = []; + let qual = ""; + let url = ""; + + if (reqHtml.includes("#EXTM3U")) { + const lines = reqHtml.split("\n"); + for (let line of lines) { + if (line.startsWith("#EXT-X-STREAM-INF")) { + qual = line.split("RESOLUTION=")[1]?.split(",")[0] || "unknown"; + + const h = parseInt(qual.split("x")[1]) || 0; + if (h >= 1080) qual = "1080p"; + else if (h >= 720) qual = "720p"; + else if (h >= 480) qual = "480p"; + else if (h >= 360) qual = "360p"; + } else if (line.trim().startsWith("http") || line.trim().endsWith(".m3u8")) { + url = line.trim(); + + if (!url.startsWith("http")) { + const baseUrl = mainUrl.substring(0, mainUrl.lastIndexOf('/') + 1); + url = baseUrl + url; + } + } + + if (url && qual) { + videos.push({ + url: url, + type: type, + quality: qual, + subtitles: [] + }); + url = ""; + qual = ""; + } + } + } + + if (videos.length > 0) { + const subtitles = await this.findSubtitles(html, serverUrl, unpacked); + videos.forEach(v => v.subtitles = subtitles); + return videos; + } + } catch (e) { + console.warn("Failed to parse master playlist", e); + } + } + + const resolutionRegex = /\/(\d{3,4})\//; + + const resolutionMatch = mainUrl.match(resolutionRegex); + const quality = resolutionMatch ? `${resolutionMatch[1]}p` : "unknown"; + + return { + url: mainUrl, + quality: quality, + type: type, + subtitles: await this.findSubtitles(html, serverUrl, unpacked) + }; + } + return undefined; + } + + async findSubtitles(html, serverUrl, unpacked) { + let subtitles = []; + const subtitleRegex = /<track\s+[^>]*src=["']([^"']+\.vtt(?:\?[^"']*)?)["'][^>]*>/gi; + + const extract = (text) => { + const matches = text.matchAll(subtitleRegex); + for (const match of matches) { + const src = match[1]; + let url = src.startsWith("http") ? src : `${serverUrl.split("/").slice(0, 3).join("/")}${src}`; + const langMatch = match[0].match(/(?:label|srclang)=["']?([a-zA-Z\-]{2,})["']?/i); + const lang = langMatch?.[1]?.toLowerCase() || "unknown"; + subtitles.push({ + url, + language: lang, + type: "vtt" + }); + } + }; + + if (html) extract(html); + + if (subtitles.length === 0) { + const rawRegex = /https?:\/\/[^\s'"]+\.vtt(?:\?[^'"\s]*)?/g; + const matches = (html.match(rawRegex) || []).concat(unpacked ? (unpacked.match(rawRegex) || []) : []); + + matches.forEach((url, idx) => { + if (!subtitles.some(s => s.url === url)) { + subtitles.push({ + url: url, + language: "Unknown " + (idx + 1), + type: "vtt" + }); + } + }); + } + return subtitles; + } + + extractScripts(str) { + const results = []; + const openTag = "<script type='text/javascript'>"; + const closeTag = "</script>"; + let pos = 0; + while (pos < str.length) { + const start = str.indexOf(openTag, pos); + if (start === -1) break; + const end = str.indexOf(closeTag, start); + if (end === -1) break; + results.push(str.substring(start + openTag.length, end)); + pos = end + closeTag.length; + } + return results; + } + + unpack(p, a, c, k) { + while (c--) if (k[c]) p = p.replace(new RegExp('\\b' + c.toString(a) + '\\b', 'g'), k[c]); + return p; + } + + decodeUnpacked(str) { + return str.replace(/\\u([\d\w]{4})/gi, (_, grp) => String.fromCharCode(parseInt(grp, 16))) + .replace(/%3C/g, '<').replace(/%3E/g, '>') + .replace(/%3F/g, '?').replace(/%3A/g, ':') + .replace(/%2C/g, ',').replace(/%2F/g, '/') + .replace(/%2B/g, '+').replace(/%20/g, ' ') + .replace(/%21/g, '!').replace(/%22/g, '"') + .replace(/%27/g, "'").replace(/%28/g, '(') + .replace(/%29/g, ')').replace(/%3B/g, ';'); + } + + findBestTitle(movies, query) { + let bestScore = 0; + let bestMovie = undefined; + + for (const movie of movies) { + let score = this.scoreStringMatch(2, movie.Title, query); + console.log(`Movie: ${movie.Title} - Score: ${score}`); + if (score > bestScore) { + bestScore = score; + bestMovie = movie; + } + } + return bestMovie; + } + + scoreStringMatch(weight, text, query) { + if (!text || !query) return 0; + text = text.toLowerCase(); + query = query.toLowerCase(); + + if (text === query) return this.ScoreWeight.MaxScore * weight; + + const textWords = text.split(" "); + const queryWords = query.split(" "); + let score = 0; + + for (const word of queryWords) { + if (textWords.includes(word)) { + score += this.ScoreWeight.MaxScore / textWords.length; + } else { + const similarity = this.getWordSimilarity(word, textWords); + score -= similarity * this.ScoreWeight.MaxScore / textWords.length; + } + } + return score * weight; + } + + getWordSimilarity(word1, words) { + const word1Vector = this.getWordVector(word1); + let maxSimilarity = 0; + for (const word2 of words) { + const word2Vector = this.getWordVector(word2); + const similarity = this.cosineSimilarity(word1Vector, word2Vector); + maxSimilarity = Math.max(maxSimilarity, similarity); + } + return maxSimilarity; + } + + getWordVector(word) { + return Array.from(word).map(char => char.charCodeAt(0)); + } + + cosineSimilarity(vec1, vec2) { + const dotProduct = vec1.reduce((sum, val, i) => sum + val * (vec2[i] || 0), 0); + const magnitude1 = Math.sqrt(vec1.reduce((sum, val) => sum + val * val, 0)); + const magnitude2 = Math.sqrt(vec2.reduce((sum, val) => sum + val * val, 0)); + return (magnitude1 && magnitude2) ? dotProduct / (magnitude1 * magnitude2) : 0; + } + + async GETText(url) { + const response = await fetch(url, { + headers: { + "User-Agent": this.userAgent + } + }); + if (!response.ok) throw new Error(`GETText failed: ${response.status}`); + return await response.text(); + } + +} + +module.exports = OppaiStream; \ No newline at end of file diff --git a/marketplace.json b/marketplace.json index cd2c79d..e8f9101 100644 --- a/marketplace.json +++ b/marketplace.json @@ -1,5 +1,46 @@ { "extensions": { + "Anicrush": { + "name": "Anicrush", + "type": "anime-board", + "description": "Anime streaming provider.", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/Anicrush.js", + "domain": "https://anicrush.to/" + }, + "AniDream": { + "name": "AniDream", + "type": "anime-board", + "description": "Anime streaming provider.", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/AniDream.js", + "domain": "https://anidream.cc/" + }, + "Animekai": { + "name": "Animekai", + "type": "anime-board", + "description": "Anime streaming provider.", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/Animekai.js", + "domain": "https://animekai.to/" + }, + "AnimePahe": { + "name": "AnimePahe", + "type": "anime-board", + "description": "Anime streaming provider.", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/AnimePahe.js", + "domain": "https://animepahe.ru/" + }, + "OppaiStream": { + "name": "OppaiStream", + "type": "anime-board", + "description": "Anime streaming provider.", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/OppaiStream.js", + "domain": "https://oppaistream.to/", + "nsfw": true + }, "AnimeAV1": { "name": "AnimeAV1", "type": "anime-board",