class AniCrush { constructor() { this.type = "anime-streaming"; this.version = "1.0.1"; this.baseUrl = "https://anicrush.to"; this.apiBase = "https://api.anicrush.to"; } getSettings() { return { episodeServers: ["Southcloud-1", "Southcloud-2", "Southcloud-3"], supportsSub: true, supportsDub: true, supportsHls: true }; } _nativeFetch(url, method, headers, body) { const raw = Native.fetch( String(url), method || "GET", JSON.stringify(headers || {}), body == null ? "" : String(body) ); try { return JSON.parse(raw || "{}"); } catch (e) { return { ok: false, status: 0, headers: {}, body: "" }; } } _getText(url, headers) { const res = this._nativeFetch(url, "GET", headers, ""); return String(res.body || ""); } _getJson(url, headers) { const res = this._nativeFetch(url, "GET", headers, ""); try { return JSON.parse(String(res.body || "{}")); } catch (e) { return {}; } } _safeStr(v) { return typeof v === "string" ? v : (v == null ? "" : String(v)); } _headers() { return { "User-Agent": "Mozilla/5.0", "Accept": "application/json", "Referer": this.baseUrl + "/", "Origin": this.baseUrl, "X-Site": "anicrush" }; } _parseQuery(q) { if (typeof q === "string") { const s = q.trim(); if (s.startsWith("{") || s.startsWith("[")) { try { return JSON.parse(s); } catch (e) { return { query: s }; } } return { query: s }; } return q || {}; } _stripYearHints(raw) { const s = this._safeStr(raw).trim(); if (!s) return ""; return s .replace(/\((?:19|20)\d{2}\)/g, " ") .replace(/\b(?:19|20)\d{2}\b/g, " ") .replace(/\s+/g, " ") .trim(); } _normalize(title) { return (this._safeStr(this._stripYearHints(title))) .toLowerCase() .replace(/(season|cour|part)/g, "") .replace(/\d+(st|nd|rd|th)/g, (m) => m.replace(/st|nd|rd|th/, "")) .replace(/[^a-z0-9]+/g, "") .replace(/(? m.replace(/st|nd|rd|th/, "")) .replace(/[^a-z0-9]+/g, ""); } _normalizeDate(dateStr) { const s = this._safeStr(dateStr); if (!s) return null; const months = { Jan: "01", Feb: "02", Mar: "03", Apr: "04", May: "05", Jun: "06", Jul: "07", Aug: "08", Sep: "09", Oct: "10", Nov: "11", Dec: "12" }; const m = s.match(/([A-Za-z]+)\s+\d{1,2},\s*(\d{4})/); if (!m) return null; const mm = months[m[1]]; if (!mm) return null; return { year: parseInt(m[2], 10), month: parseInt(mm, 10) }; } _levSim(a, b) { a = this._safeStr(a); b = this._safeStr(b); const lenA = a.length; const lenB = b.length; if (!lenA && !lenB) return 1; if (!lenA || !lenB) return 0; const dp = []; for (let i = 0; i <= lenA; i++) { dp[i] = []; dp[i][0] = i; } for (let j = 0; j <= lenB; j++) dp[0][j] = j; for (let i = 1; i <= lenA; i++) { for (let j = 1; j <= lenB; j++) { if (a[i - 1] === b[j - 1]) dp[i][j] = dp[i - 1][j - 1]; else dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]); } } const distance = dp[lenA][lenB]; const maxLen = Math.max(lenA, lenB); return 1 - distance / maxLen; } search(query) { query = this._parseQuery(query); const q = this._safeStr(query.query).trim(); if (!q) return []; const media = query.media || {}; const start = (media.startDate || {}); const targetNormJP = this._normalize(media.romajiTitle); const targetNorm = media.englishTitle ? this._normalize(media.englishTitle) : targetNormJP; const url = `${this.apiBase}/shared/v2/movie/list?keyword=${encodeURIComponent(q)}&limit=48&page=1`; const json = this._getJson(url, this._headers()); const movies = (((json || {}).result || {}).movies) || []; if (!Array.isArray(movies) || !movies.length) return []; let matches = movies.map((movie) => { const id = movie && movie.id != null ? String(movie.id) : ""; const slug = movie && movie.slug ? String(movie.slug) : ""; if (!id || !slug) return null; const titleJP = movie && movie.name ? String(movie.name) : ""; const titleEN = movie && movie.name_english ? String(movie.name_english) : ""; const title = titleEN || titleJP || "Unknown"; return { id, pageUrl: slug, title, titleJP, normTitleJP: this._normalize(titleJP), normTitle: this._normalize(title), dub: !!(movie && movie.has_dub), startDate: this._normalizeDate(movie && movie.aired_from ? String(movie.aired_from) : "") }; }).filter(Boolean); if (query.dub) matches = matches.filter(m => m.dub); let filtered = matches.filter(m => { const titleMatch = (m.normTitle === targetNorm) || (m.normTitleJP === targetNormJP); const dateMatch = (m.startDate && m.startDate.year === start.year) && (m.startDate && m.startDate.month === start.month); return titleMatch && dateMatch; }); if (!filtered.length) { filtered = matches.filter(m => { const titleMatch = (m.normTitle === targetNorm) || (m.normTitleJP === targetNormJP); const dateMatch = (m.startDate && m.startDate.year === start.year); return titleMatch && dateMatch; }); } if (!filtered.length) { filtered = matches.filter(m => { const titleMatch = m.normTitle.includes(targetNorm) || m.normTitleJP.includes(targetNormJP) || targetNorm.includes(m.normTitle) || targetNormJP.includes(m.normTitleJP) || this._levSim(m.normTitle, targetNorm) > 0.7 || this._levSim(m.normTitleJP, targetNormJP) > 0.7; const dateMatch = (m.startDate && m.startDate.year === start.year) && (m.startDate && m.startDate.month === start.month); return titleMatch && dateMatch; }); } if (!filtered.length) { filtered = matches.filter(m => { const titleMatch = m.normTitle.includes(targetNorm) || m.normTitleJP.includes(targetNormJP) || targetNorm.includes(m.normTitle) || targetNormJP.includes(m.normTitleJP) || this._levSim(m.normTitle, targetNorm) > 0.7 || this._levSim(m.normTitleJP, targetNormJP) > 0.7; const dateMatch = (m.startDate && m.startDate.year === start.year); return titleMatch && dateMatch; }); } let results = filtered.map(m => ({ id: `${m.id}/${query.dub ? "dub" : "sub"}`, title: m.title, url: `${this.baseUrl}/detail/${m.pageUrl}.${m.id}`, subOrDub: query.dub ? "dub" : "sub" })); if (!media.startDate || !media.startDate.year) { const qn = this._normalizeTitle(q); filtered = matches.filter(m => { const a = this._normalizeTitle(m.title); const aj = this._normalizeTitle(m.titleJP); return ( a === qn || aj === qn || a.includes(qn) || aj.includes(qn) || qn.includes(a) || qn.includes(aj) ); }); filtered.sort((a, b) => { const A = this._normalizeTitle(a.title); const B = this._normalizeTitle(b.title); if (A.length !== B.length) return A.length - B.length; return A.localeCompare(B); }); results = filtered.map(m => ({ id: `${m.id}/${query.dub ? "dub" : "sub"}`, title: m.title, url: `${this.baseUrl}/detail/${m.pageUrl}.${m.id}`, subOrDub: query.dub ? "dub" : "sub" })); } return results; } findEpisodes(Id) { const parts = String(Id || "").split("/"); const id = parts[0]; const subOrDub = parts[1] || "sub"; if (!id) throw new Error("Missing id"); const url = `${this.apiBase}/shared/v2/episode/list?_movieId=${encodeURIComponent(id)}`; const epJson = this._getJson(url, this._headers()); const episodeGroups = (epJson && epJson.result) ? epJson.result : {}; const episodes = []; const keys = Object.keys(episodeGroups || {}); for (let i = 0; i < keys.length; i++) { const group = episodeGroups[keys[i]]; if (!Array.isArray(group)) continue; for (let j = 0; j < group.length; j++) { const ep = group[j] || {}; const num = Number(ep.number); if (!Number.isFinite(num)) continue; episodes.push({ id: `${id}/${subOrDub}`, number: num, title: ep.name_english ? String(ep.name_english) : (ep.name ? String(ep.name) : `Episode ${num}`), url: "" }); } } episodes.sort((a, b) => (a.number || 0) - (b.number || 0)); return episodes; } findEpisodeServer(episode, _server) { if (typeof episode === "string") { try { episode = JSON.parse(episode); } catch (e) {} } episode = episode || {}; const parts = String(episode.id || "").split("/"); const id = parts[0]; const subOrDub = parts[1] || "sub"; if (!id) throw new Error("Missing id"); const epNum = Number(episode.number); if (!Number.isFinite(epNum)) throw new Error("Missing episode number"); let server = String(_server || "").trim(); if (!server || server === "default") server = "Southcloud-1"; if (server === "HD-1") server = "Southcloud-1"; if (server === "HD-2") server = "Southcloud-2"; if (server === "HD-3") server = "Southcloud-3"; const serverMap = { "Southcloud-1": 4, "Southcloud-2": 1, "Southcloud-3": 6 }; const sv = serverMap[server] != null ? serverMap[server] : 4; const encryptedLinkUrl = `${this.apiBase}/shared/v2/episode/sources?_movieId=${encodeURIComponent(id)}` + `&ep=${encodeURIComponent(String(epNum))}` + `&sv=${encodeURIComponent(String(sv))}` + `&sc=${encodeURIComponent(String(subOrDub))}`; const json = this._getJson(encryptedLinkUrl, this._headers()); const encryptedIframe = (((json || {}).result || {}).link) ? String(json.result.link) : ""; if (!encryptedIframe) { throw new Error(`Missing encrypted iframe link (server=${server} sv=${sv} sc=${subOrDub})`); } let decryptData = null; let requiredHeaders = null; try { decryptData = this.extractMegaCloudSync(encryptedIframe); requiredHeaders = decryptData && decryptData.headersProvided ? decryptData.headersProvided : null; } catch (e) {} if (!decryptData) { decryptData = this._getJson( `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent(encryptedIframe)}`, {} ); 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" }; } if (!decryptData || !decryptData.sources) throw new Error("No video sources from any decrypter"); const sources = decryptData.sources || []; const streamSource = sources.find((s) => s && s.type === "hls") || sources.find((s) => s && s.type === "mp4"); if (!streamSource || !streamSource.file) throw new Error("No valid stream file found"); const subtitles = (decryptData.tracks || []) .filter((t) => t && t.kind === "captions" && t.file) .map((track, index) => ({ id: `sub-${index}`, language: track.label || "Unknown", url: track.file, isDefault: !!track.default })); const outType = (String(streamSource.type || "") === "hls") ? "m3u8" : "mp4"; return { server: server, headers: requiredHeaders || {}, videoSources: [ { url: streamSource.file, type: outType, quality: "auto", subtitles } ] }; } extractMegaCloudSync(embedUrl) { const s = String(embedUrl || ""); const mm = s.match(/^(https?):\/\/([^\/]+)(\/.*)?$/i); if (!mm) throw new Error("Invalid embedUrl"); const protocol = mm[1].toLowerCase(); const host = mm[2]; const baseDomain = `${protocol}://${host}/`; const headers = { Accept: "*/*", "X-Requested-With": "XMLHttpRequest", Referer: baseDomain, Origin: `${protocol}://${host}`, "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 html = this._getText(embedUrl, headers); 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]; let nonce = null; const match48 = html.match(/\b[a-zA-Z0-9]{48}\b/); if (match48) nonce = match48[0]; else { const match3x16 = []; const re = /["']([A-Za-z0-9]{16})["']/g; let m; while ((m = re.exec(html)) !== null) match3x16.push(m[1]); if (match3x16.length >= 3) nonce = match3x16[0] + match3x16[1] + match3x16[2]; } if (!nonce) throw new Error("nonce not found"); const sourcesJson = this._getJson( `${baseDomain}embed-2/v3/e-1/getSources?id=${fileId}&_k=${nonce}`, headers ); return { sources: sourcesJson.sources || [], tracks: sourcesJson.tracks || [], intro: sourcesJson.intro || null, outro: sourcesJson.outro || null, server: sourcesJson.server || null, headersProvided: headers }; } } module.exports = new AniCrush();