class HiAnime { constructor() { this.type = "anime-streaming"; this.version = "1.0.5"; this.baseUrl = "https://hianime.to"; } 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(/&/g, "&") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/</g, "<") .replace(/>/g, ">") .replace(/(\d+);/g, (_, n) => { const code = parseInt(n, 10); return isFinite(code) ? String.fromCharCode(code) : ""; }); } _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 || {}; } _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"; } _extractWatchIdFromUrl(url) { const s = String(url || ""); const m = s.match(/\/watch\/[^\/]+-(\d+)/i); return m ? m[1] : ""; } _bestTitle(obj) { if (!obj) return ""; const t = obj.title; if (typeof t === "string") return t; if (t && typeof t === "object") { return String(t.english || t.romaji || t.native || t.userPreferred || ""); } return String(obj.name || obj.english || obj.romaji || ""); } search(a1) { const arg = this._parseQuery(a1); const q = this._safeStr(arg.query || arg.q || "").trim(); if (!q) return "[]"; const subOrDub = this._getSubOrDubFromAny(arg); const url = `${this.baseUrl}/ajax/search/suggest?keyword=${encodeURIComponent(q)}`; const j = this._getJson(url, this._headersJson()); let results = []; if (j && Array.isArray(j.results)) { results = j.results .map((x) => { const title = this._bestTitle(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); return { id, title, url: full }; }) .filter((x) => x.id && x.url); } if (!results.length) { const url2 = `${this.baseUrl}/search?keyword=${encodeURIComponent(q)}`; const html = this._getText(url2, this._headersHtml()); const out = []; const re = /href="(\/watch\/[^"]+)"/gi; const seen = {}; let m; while ((m = re.exec(html)) !== null) { const href = String(m[1] || ""); const full = href.startsWith("http") ? href : (this.baseUrl + href); const id = this._extractWatchIdFromUrl(full); if (!id || seen[id]) continue; seen[id] = true; out.push({ id, title: q, url: full }); if (out.length >= 20) break; } results = out; } const mapped = results.map((r) => ({ id: `${r.id}/${subOrDub}`, title: this._decodeHtml(r.title || ""), url: r.url, subOrDub })); 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(/