updates and new extensions
This commit is contained in:
154
anime/AniDream.js
Normal file
154
anime/AniDream.js
Normal 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;
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
188
anime/Anicrush.js
Normal file
188
anime/Anicrush.js
Normal 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;
|
||||
@@ -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 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>/
|
||||
);
|
||||
|
||||
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 {
|
||||
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 }; } })()
|
||||
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});`)();
|
||||
}
|
||||
}
|
||||
|
||||
189
anime/AnimePahe.js
Normal file
189
anime/AnimePahe.js
Normal 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
365
anime/Animekai.js
Normal 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;
|
||||
@@ -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: [{
|
||||
videoSources: [
|
||||
{
|
||||
url: streamSource.file,
|
||||
type: streamSource.type === "hls" ? "m3u8" : "mp4",
|
||||
quality: "auto",
|
||||
subtitles
|
||||
}]
|
||||
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) {
|
||||
|
||||
437
anime/OppaiStream.js
Normal file
437
anime/OppaiStream.js
Normal 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;
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user