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;