updates and new extensions

This commit is contained in:
2026-01-05 04:46:26 +01:00
parent 5ee2bde49a
commit 83c51a82da
9 changed files with 1500 additions and 129 deletions

154
anime/AniDream.js Normal file
View File

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

View File

@@ -1,14 +1,14 @@
class Anizone { class Anizone {
constructor() { constructor() {
this.type = "anime-board"; this.type = "anime-board";
this.version = "1.1"; this.version = "1.2";
this.api = "https://anizone.to"; this.api = "https://anizone.to";
} }
getSettings() { getSettings() {
return { return {
episodeServers: ["HLS"], episodeServers: ["HLS"],
supportsDub: true, supportsDub: false,
}; };
} }

188
anime/Anicrush.js Normal file
View File

@@ -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(/<title>\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;

View File

@@ -1,14 +1,13 @@
class AnimeAV1 { class AnimeAV1 {
constructor() { constructor() {
this.type = "anime-board"; this.type = "anime-board";
this.version = "1.2" this.version = "1.3";
this.api = "https://animeav1.com"; this.api = "https://animeav1.com";
} }
getSettings() { getSettings() {
return { return {
episodeServers: ["HLS", "HLS-DUB"], episodeServers: ["HLS"],
supportsDub: true, supportsDub: true,
}; };
} }
@@ -21,10 +20,9 @@ class AnimeAV1 {
}); });
if (!res.ok) return []; if (!res.ok) return [];
const data = await res.json(); const data = await res.json();
return data.map(anime => ({ return data.map((anime) => ({
id: anime.slug, id: anime.slug,
title: anime.title, title: anime.title,
url: `${this.api}/media/${anime.slug}`, url: `${this.api}/media/${anime.slug}`,
@@ -34,70 +32,37 @@ class AnimeAV1 {
} }
async getMetadata(id) { 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 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(
const imageMatch = html.match(/<img[^>]*class="aspect-poster[^"]*"[^>]*src="([^"]+)"/i); /<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 image = imageMatch ? imageMatch[1] : null;
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;
}
return { return {
title: media.title ?? "Unknown", title: media.title ?? "Unknown",
summary: media.synopsis ?? "No summary available", summary: media.synopsis ?? "No summary available",
episodes: media.episodesCount ?? 0, episodes: media.episodesCount ?? 0,
characters: [], characters: [],
season, season: media.seasons ?? null,
status, status: media.status ?? "Unknown",
studio: "Unknown", studio: "Unknown",
score: media.score ?? 0, score: media.score ?? 0,
year, year: media.startDate
genres: media.genres?.map(g => g.name) ?? [], ? Number(media.startDate.slice(0, 4))
image : null,
genres: media.genres?.map((g) => g.name) ?? [],
image,
}; };
} }
async findEpisodes(id) { 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 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"); if (!media?.episodes) throw new Error("No se encontró media.episodes");
return media.episodes.map((ep, i) => ({ return media.episodes.map((ep, i) => ({
@@ -108,71 +73,94 @@ class AnimeAV1 {
})); }));
} }
async findEpisodeServer(episodeOrId, _server) { async findEpisodeServer(episodeOrId, _server, category = "sub") {
const ep = typeof episodeOrId === "string" const ep =
? (() => { try { return JSON.parse(episodeOrId); } catch { return { id: episodeOrId }; } })() typeof episodeOrId === "string"
: episodeOrId; ? (() => {
try {
return JSON.parse(episodeOrId);
} catch {
return { id: episodeOrId };
}
})()
: episodeOrId;
const pageUrl = ep.url ?? (
typeof ep.id === "string" && ep.id.includes("$") let pageUrl = ep.url;
? `${this.api}/media/${ep.id.split("$")[0]}/${ep.number ?? ep.id.split("$")[1]}`
: undefined 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."); if (!pageUrl) throw new Error("No se pudo determinar la URL del episodio.");
const html = await fetch(pageUrl, { const html = await fetch(pageUrl).then((r) => r.text());
headers: { Cookie: "__ddg1_=;__ddg2_=;" },
}).then(r => r.text());
const parsedData = this.parseSvelteData(html); 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; 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 = const list =
_server === "HLS" category === "dub"
? embeds.SUB ?? [] ? embeds.DUB
: _server === "HLS-DUB" : embeds.SUB;
? embeds.DUB ?? []
: (() => { throw new Error(`Servidor desconocido: ${_server}`); })();
if (!selectedEmbeds.length) if (!Array.isArray(list))
throw new Error(`No hay mirrors disponibles para ${_server === "HLS" ? "SUB" : "DUB"}.`); throw new Error(`No hay streams ${category.toUpperCase()}`);
const match = selectedEmbeds.find(m => const hls = list.find(
(m.url || "").includes("zilla-networks.com/play/") (m) =>
m.server === "HLS" &&
m.url?.includes("zilla-networks.com/play/")
); );
if (!match) if (!hls)
throw new Error(`No se encontró ningún embed de ZillaNetworks en ${_server}.`); throw new Error(`No se encontró stream HLS ${category.toUpperCase()}`);
return { return {
server: _server, server: "HLS",
headers: { Referer: 'null' }, headers: { Referer: "null" },
videoSources: [ videoSources: [
{ {
url: match.url.replace("/play/", "/m3u8/"), url: hls.url.replace("/play/", "/m3u8/"),
type: "m3u8", type: "m3u8",
quality: "auto", quality: "auto",
subtitles: [], subtitles: [],
subOrDub: category,
}, },
], ],
}; };
} }
parseSvelteData(html) { parseSvelteData(html) {
const scriptMatch = html.match(/<script[^>]*>\s*({[^<]*__sveltekit_[\s\S]*?)<\/script>/i); const scriptMatch = html.match(
if (!scriptMatch) throw new Error("No se encontró bloque SvelteKit en el HTML."); /<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:/); const dataMatch = scriptMatch[1].match(
if (!dataMatch) throw new Error("No se encontró el bloque 'data' en el script SvelteKit."); /data:\s*(\[[\s\S]*?\])\s*,\s*form:/
);
if (!dataMatch) throw new Error("SvelteKit data block not found");
const jsArray = dataMatch[1]; const jsArray = dataMatch[1];
try { try {
return new Function(`"use strict"; return (${jsArray});`)(); return new Function(`"use strict"; return (${jsArray});`)();
} catch { } 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});`)(); return new Function(`"use strict"; return (${cleaned});`)();
} }
} }

189
anime/AnimePahe.js Normal file
View File

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

365
anime/Animekai.js Normal file
View File

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

View File

@@ -1,7 +1,7 @@
class HiAnime { class HiAnime {
constructor() { constructor() {
this.type = "anime-board"; this.type = "anime-board";
this.version = "1.0" this.version = "1.1"
this.baseUrl = "https://hianime.to"; this.baseUrl = "https://hianime.to";
} }
@@ -91,70 +91,74 @@ class HiAnime {
return episodes; return episodes;
} }
async findEpisodeServer(episode, _server) { async findEpisodeServer(episode, _server, category = "sub") {
const [id, subOrDub] = episode.id.split("/"); const id = episode.id;
let serverName = _server !== "default" ? _server : "HD-1"; const subOrDub = category; // backend manda sub | dub
if (_server === "HD-1" || _server === "HD-2" || _server === "HD-3") { const serverName = _server !== "default" ? _server : "HD-1";
const serverJson = await fetch(`${this.baseUrl}/ajax/v2/episode/servers?episodeId=${id}`, {
headers: { "X-Requested-With": "XMLHttpRequest" } if (serverName === "HD-1" || serverName === "HD-2" || serverName === "HD-3") {
}).then(res => res.json()); 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 serverHtml = serverJson.html;
const regex = new RegExp( const regex = new RegExp(
`<div[^>]*class="item server-item"[^>]*data-type="${subOrDub}"[^>]*data-id="(\\d+)"[^>]*>\\s*<a[^>]*>\\s*${serverName}\\s*</a>`, `<div[^>]*class="item server-item"[^>]*data-type="${subOrDub}"[^>]*data-id="(\\d+)"[^>]*>\\s*<a[^>]*>\\s*${serverName}\\s*</a>`,
"i" "i"
); );
const match = regex.exec(serverHtml); 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 serverId = match[1];
const sourcesJson = await fetch(`${this.baseUrl}/ajax/v2/episode/sources?id=${serverId}`, { const sourcesJson = await fetch(
headers: { "X-Requested-With": "XMLHttpRequest" } `${this.baseUrl}/ajax/v2/episode/sources?id=${serverId}`,
}).then(res => res.json()); { headers: { "X-Requested-With": "XMLHttpRequest" } }
).then(res => res.json());
let decryptData = null; let decryptData;
let requiredHeaders = {}; let requiredHeaders = {};
try { try {
// Pass true to get headers back
decryptData = await this.extractMegaCloud(sourcesJson.link, true); decryptData = await this.extractMegaCloud(sourcesJson.link, true);
if (decryptData && decryptData.headersProvided) { if (decryptData?.headersProvided) {
requiredHeaders = decryptData.headersProvided; requiredHeaders = decryptData.headersProvided;
} }
} catch (err) { } catch (e) {
console.warn("Primary decrypter failed:", err); console.warn("Primary decrypter failed:", e);
} }
if (!decryptData) { if (!decryptData) {
console.warn("Primary decrypter failed — trying ShadeOfChaos fallback...");
const fallbackRes = await fetch( const fallbackRes = await fetch(
`https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent(sourcesJson.link)}` `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent(sourcesJson.link)}`
); );
decryptData = await fallbackRes.json(); 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 = { requiredHeaders = {
"Referer": "https://megacloud.club/", Referer: "https://megacloud.club/",
"Origin": "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", "User-Agent":
"X-Requested-With": "XMLHttpRequest" "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 = const streamSource =
decryptData.sources.find((s) => s.type === "hls") || decryptData.sources.find(s => s.type === "hls") ||
decryptData.sources.find((s) => s.type === "mp4"); 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 || []) const subtitles = (decryptData.tracks || [])
.filter((t) => t.kind === "captions") .filter(t => t.kind === "captions")
.map((track, index) => ({ .map((track, i) => ({
id: `sub-${index}`, id: `sub-${i}`,
language: track.label || "Unknown", language: track.label || "Unknown",
url: track.file, url: track.file,
isDefault: !!track.default, isDefault: !!track.default,
@@ -163,18 +167,23 @@ class HiAnime {
return { return {
server: serverName, server: serverName,
headers: requiredHeaders, headers: requiredHeaders,
videoSources: [{ videoSources: [
url: streamSource.file, {
type: streamSource.type === "hls" ? "m3u8" : "mp4", url: streamSource.file,
quality: "auto", type: streamSource.type === "hls" ? "m3u8" : "mp4",
subtitles quality: "auto",
}] subtitles,
subOrDub: category,
},
],
}; };
} }
else if (_server === "HD-4") {
// Implementation for HD-4 if needed if (serverName === "HD-4") {
return null; throw new Error("HD-4 not implemented");
} }
throw new Error(`Unknown server ${serverName}`);
} }
safeString(str) { safeString(str) {

437
anime/OppaiStream.js Normal file
View File

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

View File

@@ -1,5 +1,46 @@
{ {
"extensions": { "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": { "AnimeAV1": {
"name": "AnimeAV1", "name": "AnimeAV1",
"type": "anime-board", "type": "anime-board",