class HiAnime { constructor() { this.type = "anime-streaming"; this.version = "1.0.8"; this.baseUrl = "https://hianime.to"; this._dubCache = {}; } getSettings() { return { episodeServers: ["HD-1", "HD-2", "HD-3", "HD-4"], 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)); } _headersHtml() { return { "User-Agent": "Mozilla/5.0", "Accept": "text/html", "Referer": this.baseUrl + "/", "Origin": this.baseUrl, "X-Requested-With": "XMLHttpRequest" }; } _headersJson() { return { "User-Agent": "Mozilla/5.0", "Accept": "application/json", "Referer": this.baseUrl + "/", "Origin": this.baseUrl, "X-Requested-With": "XMLHttpRequest" }; } _decodeHtml(s) { const str = this._safeStr(s); if (!str) return ""; return str .replace(/\\u0026/g, "&") .replace(/&/g, "&") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/'/g, "'") .replace(/</g, "<") .replace(/>/g, ">") .replace(/&#(\d+);/g, (_, n) => { const code = parseInt(n, 10); return isFinite(code) ? String.fromCharCode(code) : ""; }); } _parseArg(a1) { if (typeof a1 === "string") { const s = a1.trim(); if (s.startsWith("{") || s.startsWith("[")) { try { return JSON.parse(s); } catch (e) { return { query: s }; } } return { query: s }; } return a1 || {}; } _getSubOrDubFromAny(obj) { const e = obj || {}; if (typeof e.dub === "boolean") return e.dub ? "dub" : "sub"; const sod = String(e.subOrDub || "").toLowerCase(); if (sod === "dub" || sod === "sub") return sod; const tr = String(e.track || "").toLowerCase(); if (tr === "dub" || tr === "sub") return tr; const id = String(e.id || ""); const parts = id.split("/"); const last = (parts.length >= 2 ? parts[parts.length - 1] : "").toLowerCase(); if (last === "dub" || last === "sub") return last; return "sub"; } _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(); } _normalizeTitle(title) { const t = this._stripYearHints(title); return this._safeStr(t) .toLowerCase() .replace(/(season|cour|part|uncensored)/g, "") .replace(/\d+(st|nd|rd|th)/g, (m) => m.replace(/st|nd|rd|th/, "")) .replace(/[^a-z0-9]+/g, ""); } _levSim(a, b) { a = this._safeStr(a); b = this._safeStr(b); const lenA = a.length, lenB = b.length; if (!lenA && !lenB) return 1; if (!lenA || !lenB) return 0; const dp = new Array(lenA + 1); for (let i = 0; i <= lenA; i++) dp[i] = new Array(lenB + 1).fill(0); for (let i = 0; i <= lenA; 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 dist = dp[lenA][lenB]; const maxLen = Math.max(lenA, lenB); return 1 - dist / maxLen; } _isSingleWordStrictTarget(title) { const n = this._safeStr(title).trim(); if (!n) return false; const parts = n.split(/\s+/).filter(Boolean); if (parts.length !== 1) return false; const base = this._stripYearHints(n); const t = this._normalizeTitle(base); return t.length >= 3 && t.length <= 10; } _bestTitleFromSuggestItem(x) { if (!x) return ""; const t = x.title; if (typeof t === "string") return t; if (t && typeof t === "object") { return String(t.english || t.romaji || t.native || t.userPreferred || ""); } return String(x.name || x.english || x.romaji || ""); } _parseStartDate(dateStr) { const s = this._safeStr(dateStr).trim(); if (!s) return null; const monthMap = { Jan: 1, Feb: 2, Mar: 3, Apr: 4, May: 5, Jun: 6, Jul: 7, Aug: 8, Sep: 9, 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 mon = monthMap[m[1]]; const day = parseInt(m[2], 10); const year = parseInt(m[3], 10); if (!mon || !isFinite(day) || !isFinite(year)) return null; return { year, month: mon, day }; } _extractWatchIdFromUrl(url) { const s = String(url || ""); const m = s.match(/\/watch\/[^\/]+-(\d+)/i); return m ? m[1] : ""; } _extractImageFromHtmlChunk(chunk) { const c = String(chunk || ""); const m = c.match(/]+src="([^"]+)"|]+data-src="([^"]+)"/i); return m ? (m[1] || m[2] || "") : ""; } _fetchSuggestMatches(q) { const url = `${this.baseUrl}/ajax/search/suggest?keyword=${encodeURIComponent(q)}`; const j = this._getJson(url, this._headersJson()); if (j && Array.isArray(j.results)) { return j.results.map((x) => { const title = this._decodeHtml(this._bestTitleFromSuggestItem(x) || String(x.name || "")); const u = x.url ? String(x.url) : ""; const full = u.startsWith("http") ? u : (u ? (this.baseUrl + u) : ""); const id = x.id ? String(x.id) : this._extractWatchIdFromUrl(full); const image = x.image ? String(x.image) : ""; const dateStr = x.released ? String(x.released) : ""; const startDate = this._parseStartDate(dateStr); return { id, title, url: full, image, startDate }; }).filter((x) => x.id && x.url); } const html = this._decodeHtml(String(j.html || j.result || "")); if (!html) return []; const re = /([\s\S]*?)<\/a>/g; const out = []; let m; while ((m = re.exec(html)) !== null) { const pageUrl = String(m[1] || "").trim(); if (!pageUrl || pageUrl.startsWith("search?")) continue; const chunk = String(m[2] || ""); const titleM = chunk.match(/]*class="film-name"[^>]*>([^<]+)<\/h3>/i); const title = this._decodeHtml(titleM ? titleM[1] : ""); const dateM = chunk.match(/
\s*([^<]+)<\/span>/i); const startDate = this._parseStartDate(dateM ? dateM[1] : ""); const fullUrl = `${this.baseUrl}/${pageUrl}`; const idMatch = pageUrl.match(/-(\d+)$/); const id = idMatch ? idMatch[1] : this._extractWatchIdFromUrl(fullUrl); const image = this._extractImageFromHtmlChunk(chunk); if (!id) continue; out.push({ id, title: title || q, url: fullUrl, image, startDate }); } return out; } _fetchFallbackSearchMatches(q) { const url2 = `${this.baseUrl}/search?keyword=${encodeURIComponent(q)}`; const html = this._getText(url2, this._headersHtml()); const out = []; const re = /= 20) break; } return out; } _hasDubForAnimeId(animeId) { const id = String(animeId || "").trim(); if (!id) return false; if (typeof this._dubCache[id] === "boolean") { return this._dubCache[id]; } try { const j = this._getJson( `${this.baseUrl}/ajax/v2/episode/list/${encodeURIComponent(id)}`, this._headersJson() ); const html = this._decodeHtml(String(j.html || j.result || "")); if (!html) { this._dubCache[id] = false; return false; } const re = /data-number="([^"]+)"[^>]*data-id="([^"]+)"/i; const m = re.exec(html); const epId = m ? String(m[2] || "").trim() : ""; if (!epId) { this._dubCache[id] = false; return false; } const j2 = this._getJson( `${this.baseUrl}/ajax/v2/episode/servers?episodeId=${encodeURIComponent(epId)}`, this._headersJson() ); const html2 = this._decodeHtml(String(j2.html || j2.result || "")); if (!html2) { this._dubCache[id] = false; return false; } const has = /data-type\s*=\s*"(?:dub)"/i.test(html2); this._dubCache[id] = !!has; return !!has; } catch (e) { this._dubCache[id] = false; return false; } } search(a1) { const arg = this._parseArg(a1); const q = this._safeStr(arg.query || arg.q || "").trim(); if (!q) return "[]"; const subOrDub = this._getSubOrDubFromAny(arg); const media = arg.media || {}; const start = media.startDate || {}; const targetYear = parseInt(start.year, 10); const targetMonth = parseInt(start.month, 10); const en = this._safeStr(media.englishTitle || media.english || "").trim(); const ro = this._safeStr(media.romajiTitle || media.romaji || "").trim(); const enStripped = this._stripYearHints(en); const roStripped = this._stripYearHints(ro); const targetNormJP = this._normalizeTitle(roStripped); const targetNormEN = this._normalizeTitle(enStripped); const targetNorm = targetNormEN || targetNormJP || this._normalizeTitle(this._stripYearHints(q)); const strictTargetRaw = (this._isSingleWordStrictTarget(enStripped) ? enStripped : (this._isSingleWordStrictTarget(roStripped) ? roStripped : "")); const strictNorm = strictTargetRaw ? this._normalizeTitle(strictTargetRaw) : ""; let matches = this._fetchSuggestMatches(q); if (!matches.length) matches = this._fetchFallbackSearchMatches(q); if (!matches.length) return "[]"; const hasYear = isFinite(targetYear) && targetYear > 0; const hasMonth = isFinite(targetMonth) && targetMonth > 0 && targetMonth <= 12; const withNorm = matches.map((m) => { const title = this._safeStr(m.title); return { id: String(m.id || ""), title, url: String(m.url || ""), image: String(m.image || ""), startDate: m.startDate || null, normTitle: this._normalizeTitle(title) }; }).filter((m) => m.id && m.url); const exactTitleMatch = (m) => { if (!m.normTitle) return false; if (strictNorm) return m.normTitle === strictNorm; return (m.normTitle === targetNorm) || (targetNormJP && m.normTitle === targetNormJP); }; const fuzzyTitleMatch = (m) => { if (!m.normTitle) return false; if (strictNorm) return m.normTitle === strictNorm; const a = m.normTitle; const b = targetNorm; if (a === b) return true; if (a.includes(b) || b.includes(a)) return true; if (targetNormJP) { if (a.includes(targetNormJP) || targetNormJP.includes(a)) return true; if (this._levSim(a, targetNormJP) > 0.72) return true; } return this._levSim(a, b) > 0.72; }; const dateMatchYM = (m) => { if (!hasYear) return true; if (!m.startDate || !m.startDate.year) return false; if (m.startDate.year !== targetYear) return false; if (!hasMonth) return true; return m.startDate.month === targetMonth; }; const dateMatchY = (m) => { if (!hasYear) return true; if (!m.startDate || !m.startDate.year) return false; return m.startDate.year === targetYear; }; let filtered = withNorm.filter((m) => exactTitleMatch(m) && dateMatchYM(m)); if (!filtered.length) filtered = withNorm.filter((m) => exactTitleMatch(m) && dateMatchY(m)); if (!filtered.length) filtered = withNorm.filter((m) => fuzzyTitleMatch(m) && dateMatchYM(m)); if (!filtered.length) filtered = withNorm.filter((m) => fuzzyTitleMatch(m) && dateMatchY(m)); if (!filtered.length && strictNorm) return "[]"; if (!filtered.length) filtered = withNorm.filter((m) => fuzzyTitleMatch(m)); if (!filtered.length) return "[]"; filtered.sort((a, b) => { const A = a.normTitle.length; const B = b.normTitle.length; if (A !== B) return A - B; return a.normTitle.localeCompare(b.normTitle); }); if (subOrDub === "dub") { const top = filtered.slice(0, 8); const ok = []; for (let i = 0; i < top.length; i++) { const r = top[i]; if (this._hasDubForAnimeId(r.id)) ok.push(r); } filtered = ok; if (!filtered.length) return "[]"; } const mapped = filtered.map((r) => ({ id: `${r.id}/${subOrDub}`, title: this._decodeHtml(r.title || ""), image: r.image || "", url: r.url, subOrDub, startDate: r.startDate || null })); return JSON.stringify(mapped); } findEpisodes(animeId) { const parts = String(animeId || "").split("/"); const id = parts[0]; const subOrDub = parts[1] || "sub"; if (!id) return "[]"; const j = this._getJson( `${this.baseUrl}/ajax/v2/episode/list/${encodeURIComponent(id)}`, this._headersJson() ); const html = this._decodeHtml(String(j.html || j.result || "")); const episodes = []; const re = /data-number="([^"]+)"[^>]*data-id="([^"]+)"/gi; let m; while ((m = re.exec(html)) !== null) { const numRaw = String(m[1] || "").trim(); const epId = String(m[2] || "").trim(); const num = parseFloat(numRaw); if (!epId || !isFinite(num)) continue; episodes.push({ id: `${epId}/${subOrDub}`, number: num, title: "", url: "" }); } episodes.sort((a, b) => (a.number || 0) - (b.number || 0)); return JSON.stringify(episodes); } findEpisodeServer(episodeObj, serverName) { let ep = episodeObj; if (typeof ep === "string") { try { ep = JSON.parse(ep); } catch (e) { ep = {}; } } const epIdRaw = this._safeStr(ep && ep.id ? ep.id : "").trim(); if (!epIdRaw) throw new Error("Missing episode id"); const parts = epIdRaw.split("/"); const epId = parts[0]; const subOrDub = (parts[1] || "sub").toLowerCase(); const server = String(serverName || "").trim() || "HD-1"; if (server === "HD-4") return "null"; const j = this._getJson( `${this.baseUrl}/ajax/v2/episode/servers?episodeId=${encodeURIComponent(epId)}`, this._headersJson() ); const html = this._decodeHtml(String(j.html || j.result || "")); const escapedServer = server.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const reExact = new RegExp( `class="item server-item"[^>]*data-type="${subOrDub}"[^>]*data-id="([^"]+)"[^>]*>[\\s\\S]*?>\\s*${escapedServer}\\s*<`, "i" ); const mmExact = reExact.exec(html); let serverId = mmExact ? String(mmExact[1] || "").trim() : ""; if (!serverId) { const reFirst = new RegExp( `class="item server-item"[^>]*data-type="${subOrDub}"[^>]*data-id="([^"]+)"`, "i" ); const mmFirst = reFirst.exec(html); if (mmFirst) serverId = String(mmFirst[1] || "").trim(); } if (!serverId) throw new Error(`No server id found for track=${subOrDub}`); const sourcesJson = this._getJson( `${this.baseUrl}/ajax/v2/episode/sources?id=${encodeURIComponent(serverId)}`, this._headersJson() ); const embed = (sourcesJson && sourcesJson.link) ? String(sourcesJson.link) : ""; if (!embed) throw new Error("No embed link returned"); let decryptData = null; let requiredHeaders = {}; try { decryptData = this.extractMegaCloudSync(embed); requiredHeaders = (decryptData && decryptData.headersProvided) ? decryptData.headersProvided : {}; } catch (e) { decryptData = null; } if (!decryptData) { const fb = this._getJson( `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent(embed)}`, { "User-Agent": "Mozilla/5.0", "Accept": "application/json" } ); decryptData = fb || {}; requiredHeaders = { "Referer": "https://megacloud.club/", "Origin": "https://megacloud.club", "User-Agent": "Mozilla/5.0", "X-Requested-With": "XMLHttpRequest" }; } const sources = Array.isArray(decryptData.sources) ? decryptData.sources : []; const tracks = Array.isArray(decryptData.tracks) ? decryptData.tracks : []; const streamSource = sources.find((s) => s && s.type === "hls" && s.file) || sources.find((s) => s && s.type === "mp4" && s.file) || sources.find((s) => s && s.file); if (!streamSource || !streamSource.file) throw new Error("No valid stream file found"); const subtitles = tracks .filter((t) => t && String(t.kind || "").toLowerCase() === "captions" && t.file) .map((t, index) => ({ id: `sub-${index}`, language: String(t.label || "Unknown"), url: String(t.file), isDefault: !!t.default })); const out = { server: server, headers: requiredHeaders, videoSources: [ { url: String(streamSource.file), type: String(streamSource.type || "").toLowerCase() === "hls" ? "m3u8" : "mp4", quality: "auto", subtitles: subtitles } ] }; return JSON.stringify(out); } extractMegaCloudSync(embedUrl) { const url = new URL(String(embedUrl)); const baseDomain = `${url.protocol}//${url.host}/`; const headers = { "Accept": "*/*", "X-Requested-With": "XMLHttpRequest", "Referer": baseDomain, "Origin": `${url.protocol}//${url.host}`, "User-Agent": "Mozilla/5.0" }; const html = this._getText(String(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]; if (!nonce) { 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"); const sourcesJson = this._getJson( `${baseDomain}embed-2/v3/e-1/getSources?id=${encodeURIComponent(fileId)}&_k=${encodeURIComponent(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 = HiAnime;