diff --git a/anime/AniDream.js b/anime/AniDream.js
new file mode 100644
index 0000000..8b23ded
--- /dev/null
+++ b/anime/AniDream.js
@@ -0,0 +1,154 @@
+class AniDream {
+ constructor() {
+ this.baseUrl = "https://anidream.cc";
+ this.api = "https://common.anidream.cc/v1";
+ this.type = "anime-board";
+ this.version = "1.0";
+ }
+
+ getSettings() {
+ return {
+ episodeServers: ["Default", "Zen"],
+ supportsDub: false,
+ };
+ }
+
+ async search(queryObj) {
+ const res = await fetch(
+ `${this.api}/search?pageSize=8&query=${encodeURIComponent(queryObj.query)}`,
+ {
+ headers: {
+ accept: "*/*",
+ "user-agent":
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
+ },
+ }
+ );
+
+ const json = await res.json();
+ const series = json?.data?.series ?? [];
+
+ return series.map((s) => ({
+ id: s.id,
+ title: s.title,
+ url: `https://anidream.cc/series/${s.slug}`,
+ subOrDub: "sub",
+ }));
+ }
+
+ async findEpisodes(id) {
+ const res = await fetch(`${this.api}/series/${id}`, {
+ headers: {
+ accept: "application/json, text/javascript, */*; q=0.01",
+ "accept-language": "es-ES,es;q=0.9",
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
+ referer: "https://anidream.cc/",
+ },
+ });
+
+ if (!res.ok) throw new Error(`Error HTTP ${res.status}`);
+
+ const json = await res.json();
+ const episodes = json?.data?.episodes;
+
+ if (!Array.isArray(episodes)) return [];
+
+ return episodes.map((ep) => ({
+ id: ep.id,
+ number: parseInt(ep.number, 10),
+ title: ep.title,
+ url: `https://anidream.cc/watch/${ep.slug}`,
+ }));
+ }
+
+ parseSubtitles(data) {
+ if (!data || !Array.isArray(data.subtitles)) return [];
+
+ return data.subtitles.map((s) => {
+ const cleanLang = (s.language_name ?? "")
+ .replace(/^Language\s*\(|\)$/g, "")
+ .trim();
+
+ return {
+ id: cleanLang,
+ url: s.url,
+ language: `${cleanLang} - ${s.title ?? ""}`,
+ isDefault: s.is_default ?? false,
+ };
+ });
+ }
+
+ async findEpisodeServer(episodeOrId, server) {
+ const headers = {
+ "user-agent":
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
+ accept: "*/*",
+ };
+
+ // Default
+ if (server.toLowerCase() === "default") {
+ const res = await fetch(
+ `${this.api}/watch/default/${episodeOrId.id}/`,
+ { headers }
+ );
+ const json = await res.json();
+
+ if (!json?.data?.m3u8_url)
+ throw new Error("Stream not found at Default");
+
+ return {
+ server: "Default",
+ headers: {},
+ videoSources: [
+ {
+ url: json.data.m3u8_url,
+ type: "m3u8",
+ quality: "auto",
+ subtitles: json.data?.subtitles?.length
+ ? this.parseSubtitles(json.data)
+ : [],
+ },
+ ],
+ };
+ }
+
+ // Otros servidores (Zen)
+ const res = await fetch(`${this.api}/episodes/${episodeOrId.id}/`, {
+ headers,
+ });
+ const json = await res.json();
+ const servers = json?.data?.servers ?? [];
+
+ const target = servers.find(
+ (s) => s.server.toLowerCase() === server.toLowerCase()
+ );
+ if (!target?.access_id)
+ throw new Error(`Server ${server} not found`);
+
+ const res2 = await fetch(
+ `${this.api}/watch/${server}/${target.access_id}/`,
+ { headers }
+ );
+ const json2 = await res2.json();
+
+ if (!json2?.data?.m3u8_url)
+ throw new Error(`Stream not found on ${server}`);
+
+ return {
+ server,
+ headers: {},
+ videoSources: [
+ {
+ url: json2.data.m3u8_url,
+ type: "m3u8",
+ quality: "auto",
+ subtitles: json2.data?.subtitles?.length
+ ? this.parseSubtitles(json2.data)
+ : [],
+ },
+ ],
+ };
+ }
+}
+
+module.exports = AniDream;
diff --git a/anime/AniZone.js b/anime/AniZone.js
index 673e98d..7aa6078 100644
--- a/anime/AniZone.js
+++ b/anime/AniZone.js
@@ -1,14 +1,14 @@
class Anizone {
constructor() {
this.type = "anime-board";
- this.version = "1.1";
+ this.version = "1.2";
this.api = "https://anizone.to";
}
getSettings() {
return {
episodeServers: ["HLS"],
- supportsDub: true,
+ supportsDub: false,
};
}
diff --git a/anime/Anicrush.js b/anime/Anicrush.js
new file mode 100644
index 0000000..0e97413
--- /dev/null
+++ b/anime/Anicrush.js
@@ -0,0 +1,188 @@
+class Anicrush {
+
+ constructor() {
+ this.baseUrl = "https://anicrush.to";
+ this.type = "anime-board";
+ this.version = "1.0";
+ }
+
+ getSettings() {
+ return {
+ episodeServers: ["Southcloud-1", "Southcloud-2", "Southcloud-3"],
+ supportsDub: true,
+ };
+ }
+
+ async search(query) {
+ const url = `https://api.anicrush.to/shared/v2/movie/list?keyword=${encodeURIComponent(query.query)}&limit=48&page=1`;
+
+ const json = await fetch(url, {
+ headers: {
+ "User-Agent": "Mozilla/5.0",
+ "X-Site": "anicrush",
+ },
+ }).then(r => r.json());
+
+ const results = json?.result?.movies ?? [];
+
+ return results.map(m => ({
+ id: String(m.id),
+ title: m.name_english || m.name,
+ url: `${this.baseUrl}/detail/${m.slug}.${m.id}`,
+ subOrDub: m.has_dub ? "both" : "sub",
+ }));
+ }
+
+ async findEpisodes(id) {
+ const res = await fetch(
+ `https://api.anicrush.to/shared/v2/episode/list?_movieId=${id}`,
+ { headers: { "X-Site": "anicrush" } }
+ );
+
+ const json = await res.json();
+ const groups = json?.result ?? {};
+ const episodes = [];
+
+ for (const group of Object.values(groups)) {
+ if (!Array.isArray(group)) continue;
+ for (const ep of group) {
+ episodes.push({
+ id: `${id}$${ep.number}`,
+ number: ep.number,
+ title: ep.name_english || `Episode ${ep.number}`,
+ url: "",
+ });
+ }
+ }
+
+ return episodes.sort((a, b) => a.number - b.number);
+ }
+
+ async findEpisodeServer(episode, server, category = "sub") {
+ const [id] = episode.id.split("$");
+
+ const serverMap = {
+ "Southcloud-1": 4,
+ "Southcloud-2": 1,
+ "Southcloud-3": 6,
+ };
+
+ const sv = serverMap[server] ?? 4;
+
+ const apiUrl =
+ `https://api.anicrush.to/shared/v2/episode/sources` +
+ `?_movieId=${id}&ep=${episode.number}&sv=${sv}&sc=${category}`;
+
+ const json = await fetch(apiUrl, {
+ headers: {
+ "User-Agent": "Mozilla/5.0",
+ "X-Site": "anicrush",
+ },
+ }).then(r => r.json());
+
+ const iframe = json?.result?.link;
+ if (!iframe) throw new Error("No iframe");
+
+ let data;
+ try {
+ data = await this.extractMegaCloud(iframe);
+ } catch {
+ const fallback = await fetch(
+ `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent(iframe)}`
+ );
+ data = await fallback.json();
+ }
+
+ const sources =
+ data.sources ??
+ data.result?.sources ??
+ [];
+
+ if (!Array.isArray(sources) || !sources.length) {
+ throw new Error("No video sources");
+ }
+
+ const source =
+ sources.find(s => s.type === "hls") ||
+ sources.find(s => s.type === "mp4");
+
+
+ if (!source?.file) throw new Error("No stream");
+
+ const subtitles = (data.tracks || [])
+ .filter(t => t.kind === "captions")
+ .map((t, i) => ({
+ id: `sub-${i}`,
+ language: t.label || "Unknown",
+ url: t.file,
+ isDefault: !!t.default,
+ }));
+
+ return {
+ server,
+ headers: {
+ Referer: "https://megacloud.club/",
+ Origin: "https://megacloud.club",
+ },
+ videoSources: [{
+ url: source.file,
+ type: source.type === "hls" ? "m3u8" : "mp4",
+ quality: "auto",
+ subtitles,
+ subOrDub: category,
+ }],
+ };
+ }
+
+ async extractMegaCloud(embedUrl) {
+ const url = new URL(embedUrl);
+ const baseDomain = `${url.protocol}//${url.host}/`;
+
+ const headers = {
+ Accept: "*/*",
+ "X-Requested-With": "XMLHttpRequest",
+ Referer: baseDomain,
+ "User-Agent":
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36",
+ };
+
+ // 1) Fetch embed page
+ const html = await fetch(embedUrl, { headers }).then(r => r.text());
+
+ // 2) Extract file ID
+ const fileIdMatch = html.match(/
\s*File\s+#([a-zA-Z0-9]+)\s*-/i);
+ if (!fileIdMatch) throw new Error("file_id not found in embed page");
+ const fileId = fileIdMatch[1];
+
+ // 3) Extract nonce
+ let nonce = null;
+
+ const match48 = html.match(/\b[a-zA-Z0-9]{48}\b/);
+ if (match48) {
+ nonce = match48[0];
+ } else {
+ const match3x16 = [...html.matchAll(/["']([A-Za-z0-9]{16})["']/g)];
+ if (match3x16.length >= 3) {
+ nonce = match3x16[0][1] + match3x16[1][1] + match3x16[2][1];
+ }
+ }
+
+ if (!nonce) throw new Error("nonce not found");
+
+ // 4) Fetch sources
+ const sourcesJson = await fetch(
+ `${baseDomain}embed-2/v3/e-1/getSources?id=${fileId}&_k=${nonce}`,
+ { headers }
+ ).then(r => r.json());
+
+ return {
+ sources: sourcesJson.sources || [],
+ tracks: sourcesJson.tracks || [],
+ intro: sourcesJson.intro || null,
+ outro: sourcesJson.outro || null,
+ server: sourcesJson.server || null,
+ };
+ }
+}
+
+module.exports = Anicrush;
diff --git a/anime/AnimeAV1.js b/anime/AnimeAV1.js
index b1f35d0..ef23e69 100644
--- a/anime/AnimeAV1.js
+++ b/anime/AnimeAV1.js
@@ -1,14 +1,13 @@
class AnimeAV1 {
-
constructor() {
this.type = "anime-board";
- this.version = "1.2"
+ this.version = "1.3";
this.api = "https://animeav1.com";
}
getSettings() {
return {
- episodeServers: ["HLS", "HLS-DUB"],
+ episodeServers: ["HLS"],
supportsDub: true,
};
}
@@ -21,10 +20,9 @@ class AnimeAV1 {
});
if (!res.ok) return [];
-
const data = await res.json();
- return data.map(anime => ({
+ return data.map((anime) => ({
id: anime.slug,
title: anime.title,
url: `${this.api}/media/${anime.slug}`,
@@ -34,70 +32,37 @@ class AnimeAV1 {
}
async getMetadata(id) {
- const html = await fetch(`${this.api}/media/${id}`).then(r => r.text());
+ const html = await fetch(`${this.api}/media/${id}`).then((r) => r.text());
const parsed = this.parseSvelteData(html);
- const media = parsed.find(x => x?.data?.media)?.data.media ?? {};
+ const media = parsed.find((x) => x?.data?.media)?.data.media ?? {};
- // IMAGE
- const imageMatch = html.match(/
]*class="aspect-poster[^"]*"[^>]*src="([^"]+)"/i);
- const image = imageMatch ? imageMatch[1] : null;
-
- // BLOCK INFO (STATUS, SEASON, YEAR)
- const infoBlockMatch = html.match(
- /([\s\S]*?)<\/div>/
+ const imageMatch = html.match(
+ /
![]()
]*class="aspect-poster[^"]*"[^>]*src="([^"]+)"/i
);
-
- let status = media.status ?? "Unknown";
- let season = media.seasons ?? null;
- let year = media.startDate ? Number(media.startDate.slice(0, 4)) : null;
-
- if (infoBlockMatch) {
- const raw = infoBlockMatch[1];
-
- // Extraer spans internos
- const spans = [...raw.matchAll(/
]*>([^<]+)<\/span>/g)].map(m => m[1].trim());
-
- // EJEMPLO:
- // ["TV Anime", "•", "2025", "•", "Temporada Otoño", "•", "En emisión"]
-
- const clean = spans.filter(x => x !== "•");
-
- // YEAR
- const yearMatch = clean.find(x => /^\d{4}$/.test(x));
- if (yearMatch) year = Number(yearMatch);
-
- // SEASON (el que contiene "Temporada")
- const seasonMatch = clean.find(x => x.toLowerCase().includes("temporada"));
- if (seasonMatch) season = seasonMatch;
-
- // STATUS (normalmente "En emisión", "Finalizado", etc)
- const statusMatch = clean.find(x =>
- /emisión|finalizado|completado|pausa|cancelado/i.test(x)
- );
- if (statusMatch) status = statusMatch;
- }
+ const image = imageMatch ? imageMatch[1] : null;
return {
title: media.title ?? "Unknown",
summary: media.synopsis ?? "No summary available",
episodes: media.episodesCount ?? 0,
characters: [],
- season,
- status,
+ season: media.seasons ?? null,
+ status: media.status ?? "Unknown",
studio: "Unknown",
score: media.score ?? 0,
- year,
- genres: media.genres?.map(g => g.name) ?? [],
- image
+ year: media.startDate
+ ? Number(media.startDate.slice(0, 4))
+ : null,
+ genres: media.genres?.map((g) => g.name) ?? [],
+ image,
};
}
-
async findEpisodes(id) {
- const html = await fetch(`${this.api}/media/${id}`).then(r => r.text());
+ const html = await fetch(`${this.api}/media/${id}`).then((r) => r.text());
const parsed = this.parseSvelteData(html);
- const media = parsed.find(x => x?.data?.media)?.data?.media;
+ const media = parsed.find((x) => x?.data?.media)?.data?.media;
if (!media?.episodes) throw new Error("No se encontró media.episodes");
return media.episodes.map((ep, i) => ({
@@ -108,71 +73,94 @@ class AnimeAV1 {
}));
}
- async findEpisodeServer(episodeOrId, _server) {
- const ep = typeof episodeOrId === "string"
- ? (() => { try { return JSON.parse(episodeOrId); } catch { return { id: episodeOrId }; } })()
- : episodeOrId;
+ async findEpisodeServer(episodeOrId, _server, category = "sub") {
+ const ep =
+ typeof episodeOrId === "string"
+ ? (() => {
+ try {
+ return JSON.parse(episodeOrId);
+ } catch {
+ return { id: episodeOrId };
+ }
+ })()
+ : episodeOrId;
- const pageUrl = ep.url ?? (
- typeof ep.id === "string" && ep.id.includes("$")
- ? `${this.api}/media/${ep.id.split("$")[0]}/${ep.number ?? ep.id.split("$")[1]}`
- : undefined
- );
+
+ let pageUrl = ep.url;
+
+ if (!pageUrl && typeof ep.id === "string") {
+ if (ep.id.includes("$")) {
+ const [slug, num] = ep.id.split("$");
+ pageUrl = `${this.api}/media/${slug}/${ep.number ?? num}`;
+ } else {
+ pageUrl = `${this.api}/media/${ep.id}/${ep.number}`;
+ }
+ }
+
+ if (!pageUrl) {
+ throw new Error(
+ `No se pudo determinar la URL del episodio (id=${ep.id}, number=${ep.number})`
+ );
+ }
if (!pageUrl) throw new Error("No se pudo determinar la URL del episodio.");
- const html = await fetch(pageUrl, {
- headers: { Cookie: "__ddg1_=;__ddg2_=;" },
- }).then(r => r.text());
-
+ const html = await fetch(pageUrl).then((r) => r.text());
const parsedData = this.parseSvelteData(html);
- const entry = parsedData.find(x => x?.data?.embeds) || parsedData[3];
+ const entry = parsedData.find((x) => x?.data?.embeds);
const embeds = entry?.data?.embeds;
- if (!embeds) throw new Error("No se encontraron 'embeds' en los datos del episodio.");
+ if (!embeds) throw new Error("No embeds encontrados");
- const selectedEmbeds =
- _server === "HLS"
- ? embeds.SUB ?? []
- : _server === "HLS-DUB"
- ? embeds.DUB ?? []
- : (() => { throw new Error(`Servidor desconocido: ${_server}`); })();
+ const list =
+ category === "dub"
+ ? embeds.DUB
+ : embeds.SUB;
- if (!selectedEmbeds.length)
- throw new Error(`No hay mirrors disponibles para ${_server === "HLS" ? "SUB" : "DUB"}.`);
+ if (!Array.isArray(list))
+ throw new Error(`No hay streams ${category.toUpperCase()}`);
- const match = selectedEmbeds.find(m =>
- (m.url || "").includes("zilla-networks.com/play/")
+ const hls = list.find(
+ (m) =>
+ m.server === "HLS" &&
+ m.url?.includes("zilla-networks.com/play/")
);
- if (!match)
- throw new Error(`No se encontró ningún embed de ZillaNetworks en ${_server}.`);
+ if (!hls)
+ throw new Error(`No se encontró stream HLS ${category.toUpperCase()}`);
return {
- server: _server,
- headers: { Referer: 'null' },
+ server: "HLS",
+ headers: { Referer: "null" },
videoSources: [
{
- url: match.url.replace("/play/", "/m3u8/"),
+ url: hls.url.replace("/play/", "/m3u8/"),
type: "m3u8",
quality: "auto",
subtitles: [],
+ subOrDub: category,
},
],
};
}
parseSvelteData(html) {
- const scriptMatch = html.match(/";
+ let pos = 0;
+ while (pos < str.length) {
+ const start = str.indexOf(openTag, pos);
+ if (start === -1) break;
+ const end = str.indexOf(closeTag, start);
+ if (end === -1) break;
+ results.push(str.substring(start + openTag.length, end));
+ pos = end + closeTag.length;
+ }
+ return results;
+ }
+
+ unpack(p, a, c, k) {
+ while (c--) if (k[c]) p = p.replace(new RegExp('\\b' + c.toString(a) + '\\b', 'g'), k[c]);
+ return p;
+ }
+
+ decodeUnpacked(str) {
+ return str.replace(/\\u([\d\w]{4})/gi, (_, grp) => String.fromCharCode(parseInt(grp, 16)))
+ .replace(/%3C/g, '<').replace(/%3E/g, '>')
+ .replace(/%3F/g, '?').replace(/%3A/g, ':')
+ .replace(/%2C/g, ',').replace(/%2F/g, '/')
+ .replace(/%2B/g, '+').replace(/%20/g, ' ')
+ .replace(/%21/g, '!').replace(/%22/g, '"')
+ .replace(/%27/g, "'").replace(/%28/g, '(')
+ .replace(/%29/g, ')').replace(/%3B/g, ';');
+ }
+
+ findBestTitle(movies, query) {
+ let bestScore = 0;
+ let bestMovie = undefined;
+
+ for (const movie of movies) {
+ let score = this.scoreStringMatch(2, movie.Title, query);
+ console.log(`Movie: ${movie.Title} - Score: ${score}`);
+ if (score > bestScore) {
+ bestScore = score;
+ bestMovie = movie;
+ }
+ }
+ return bestMovie;
+ }
+
+ scoreStringMatch(weight, text, query) {
+ if (!text || !query) return 0;
+ text = text.toLowerCase();
+ query = query.toLowerCase();
+
+ if (text === query) return this.ScoreWeight.MaxScore * weight;
+
+ const textWords = text.split(" ");
+ const queryWords = query.split(" ");
+ let score = 0;
+
+ for (const word of queryWords) {
+ if (textWords.includes(word)) {
+ score += this.ScoreWeight.MaxScore / textWords.length;
+ } else {
+ const similarity = this.getWordSimilarity(word, textWords);
+ score -= similarity * this.ScoreWeight.MaxScore / textWords.length;
+ }
+ }
+ return score * weight;
+ }
+
+ getWordSimilarity(word1, words) {
+ const word1Vector = this.getWordVector(word1);
+ let maxSimilarity = 0;
+ for (const word2 of words) {
+ const word2Vector = this.getWordVector(word2);
+ const similarity = this.cosineSimilarity(word1Vector, word2Vector);
+ maxSimilarity = Math.max(maxSimilarity, similarity);
+ }
+ return maxSimilarity;
+ }
+
+ getWordVector(word) {
+ return Array.from(word).map(char => char.charCodeAt(0));
+ }
+
+ cosineSimilarity(vec1, vec2) {
+ const dotProduct = vec1.reduce((sum, val, i) => sum + val * (vec2[i] || 0), 0);
+ const magnitude1 = Math.sqrt(vec1.reduce((sum, val) => sum + val * val, 0));
+ const magnitude2 = Math.sqrt(vec2.reduce((sum, val) => sum + val * val, 0));
+ return (magnitude1 && magnitude2) ? dotProduct / (magnitude1 * magnitude2) : 0;
+ }
+
+ async GETText(url) {
+ const response = await fetch(url, {
+ headers: {
+ "User-Agent": this.userAgent
+ }
+ });
+ if (!response.ok) throw new Error(`GETText failed: ${response.status}`);
+ return await response.text();
+ }
+
+}
+
+module.exports = OppaiStream;
\ No newline at end of file
diff --git a/marketplace.json b/marketplace.json
index cd2c79d..e8f9101 100644
--- a/marketplace.json
+++ b/marketplace.json
@@ -1,5 +1,46 @@
{
"extensions": {
+ "Anicrush": {
+ "name": "Anicrush",
+ "type": "anime-board",
+ "description": "Anime streaming provider.",
+ "author": "lenafx",
+ "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/Anicrush.js",
+ "domain": "https://anicrush.to/"
+ },
+ "AniDream": {
+ "name": "AniDream",
+ "type": "anime-board",
+ "description": "Anime streaming provider.",
+ "author": "lenafx",
+ "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/AniDream.js",
+ "domain": "https://anidream.cc/"
+ },
+ "Animekai": {
+ "name": "Animekai",
+ "type": "anime-board",
+ "description": "Anime streaming provider.",
+ "author": "lenafx",
+ "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/Animekai.js",
+ "domain": "https://animekai.to/"
+ },
+ "AnimePahe": {
+ "name": "AnimePahe",
+ "type": "anime-board",
+ "description": "Anime streaming provider.",
+ "author": "lenafx",
+ "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/AnimePahe.js",
+ "domain": "https://animepahe.ru/"
+ },
+ "OppaiStream": {
+ "name": "OppaiStream",
+ "type": "anime-board",
+ "description": "Anime streaming provider.",
+ "author": "lenafx",
+ "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/OppaiStream.js",
+ "domain": "https://oppaistream.to/",
+ "nsfw": true
+ },
"AnimeAV1": {
"name": "AnimeAV1",
"type": "anime-board",