console.log("[HiAnime-TV] source.js loaded"); class HiAnime { constructor() { console.log("[HiAnime-TV] constructor()"); this.type = "anime-board"; this.version = "1.1"; this.baseUrl = "https://hianime.to"; } getSettings() { console.log("[HiAnime-TV] getSettings()"); return { episodeServers: ["HD-1", "HD-2", "HD-3", "HD-4"], supportsDub: true }; } safeString(str) { return (typeof str === "string" ? str : ""); } _decodeHtml(s) { return this.safeString(s) .replace(/&/g, "&") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/'/g, "'") .replace(/</g, "<") .replace(/>/g, ">") .replace(/&#(\d+);?/g, function (m, d) { var n = parseInt(d, 10); return (typeof n === "number" && !isNaN(n)) ? String.fromCharCode(n) : m; }); } _escapeRe(s) { return this.safeString(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } _fetch(url, opts) { var o = opts || {}; var headers = {}; headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"; headers["Accept"] = "text/html,application/json;q=0.9,*/*;q=0.8"; headers["Referer"] = this.baseUrl + "/"; headers["Accept-Language"] = "en-US,en;q=0.9"; if (o.headers && typeof o.headers === "object") { for (var k in o.headers) { if (Object.prototype.hasOwnProperty.call(o.headers, k)) { headers[k] = o.headers[k]; } } } var method = o.method || "GET"; var init = { method: method, headers: headers }; if (method !== "GET" && method !== "HEAD") { var body = (o.body === undefined || o.body === null) ? "" : String(o.body); init.body = body; } console.log("[HiAnime-TV] fetch", method, url, "headers=", Object.keys(headers).length); var r = fetch(url, init); console.log("[HiAnime-TV] fetchResp status=", r.status, "ok=", r.ok); return r; } _isBlockedHtml(html) { var h = this.safeString(html).toLowerCase(); return ( h.indexOf("cloudflare") !== -1 || h.indexOf("just a moment") !== -1 || h.indexOf("cf-challenge") !== -1 || h.indexOf("attention required") !== -1 ); } search(query) { console.log("[HiAnime-TV] search() raw=", query); try { var q = query; if (typeof q === "string") { try { q = JSON.parse(q); } catch (e) { q = { query: query }; } } q = q || {}; var queryText = this.safeString(q.query || q.title); var start = (q.media && q.media.startDate) ? q.media.startDate : {}; var year = start.year || ""; var month = start.month || ""; var dub = !!q.dub; console.log("[HiAnime-TV] search() parsed queryText=", queryText, "year=", year, "month=", month, "dub=", dub); if (!queryText) return []; var url = this.baseUrl + "/search?keyword=" + encodeURIComponent(queryText) + "&sort=default"; if (year) url += "&sy=" + year; if (month) url += "&sm=" + month; console.log("[HiAnime-TV] search() url=", url); var html = this._fetch(url).text(); // sync in host console.log("[HiAnime-TV] search() htmlLen=", (html || "").length); if (this._isBlockedHtml(html)) { throw new Error("HiAnime blocked the request (Cloudflare/bot protection)."); } var cardRegex = /]*href="\/watch\/([^"]+)"[^>]*\btitle="([^"]+)"[^>]*\bdata-id="(\d+)"[^>]*>|]*href="\/watch\/([^"]+)"[^>]*\bdata-id="(\d+)"[^>]*\btitle="([^"]+)"[^>]*>/gi; var out = []; var match; while ((match = cardRegex.exec(html)) !== null) { var pagePath = match[1] || match[4] || ""; var titleRaw = match[2] || match[6] || ""; var id = match[3] || match[5] || ""; if (!pagePath || !id) continue; var title = this._decodeHtml(titleRaw); var image = null; try { var imageRegex = new RegExp( "]*href=\"/watch/" + this._escapeRe(pagePath) + "\"[\\s\\S]*?]+(?:data-src|src)=\"([^\"]+)\"", "i" ); var im = html.match(imageRegex); image = im ? im[1] : null; } catch (e2) {} out.push({ id: String(id) + "/" + (dub ? "dub" : "sub"), title: title, image: image, url: this.baseUrl + "/watch/" + pagePath, subOrDub: dub ? "dub" : "sub" }); } console.log("[HiAnime-TV] search() returning=", out.length, "first=", out[0] ? out[0].title : "none"); return out; } catch (e) { console.error("[HiAnime-TV] search() ERROR", String(e), e && e.stack ? e.stack : ""); throw e; } } findEpisodes(animeId) { console.log("[HiAnime-TV] findEpisodes() animeId=", animeId); try { var raw = animeId; if (typeof raw !== "string") raw = JSON.stringify(raw); var id = ""; var subOrDub = "sub"; if (raw.indexOf("/") !== -1) { var parts = raw.split("/"); id = parts[0]; subOrDub = parts[1] || "sub"; } else { try { var parsed = JSON.parse(raw); var v = parsed.id || parsed.animeId || raw; if (String(v).indexOf("/") !== -1) { var parts2 = String(v).split("/"); id = parts2[0]; subOrDub = parts2[1] || (parsed.subOrDub || "sub"); } else { id = String(v); subOrDub = parsed.subOrDub || "sub"; } } catch (e) { id = raw; } } id = String(id || "").trim(); if (!id) throw new Error("Missing anime id"); console.log("[HiAnime-TV] findEpisodes() parsed id=", id, "subOrDub=", subOrDub); var url = this.baseUrl + "/ajax/v2/episode/list/" + id; var res = this._fetch(url, { headers: { "X-Requested-With": "XMLHttpRequest" } }); var json = res.json(); var html = this.safeString(json.html); console.log("[HiAnime-TV] findEpisodes() status=", res.status, "ok=", res.ok, "htmlLen=", html.length); var episodes = []; var regex = /]*class="[^"]*\bep-item\b[^"]*"[^>]*data-number="(\d+)"[^>]*data-id="(\d+)"[^>]*href="([^"]+)"[\s\S]*?
]*title="([^"]+)"/g; var match; while ((match = regex.exec(html)) !== null) { episodes.push({ id: match[2] + "/" + subOrDub, number: parseInt(match[1], 10), url: this.baseUrl + match[3], title: this._decodeHtml(match[4]), subOrDub: subOrDub }); } console.log("[HiAnime-TV] findEpisodes() returning=", episodes.length, "firstEp=", episodes[0] ? episodes[0].number : "none"); return episodes; } catch (e) { console.error("[HiAnime-TV] findEpisodes() ERROR", String(e), e && e.stack ? e.stack : ""); throw e; } } findEpisodeServer(episode, _server) { console.log("[HiAnime-TV] findEpisodeServer() episode=", episode, "server=", _server); try { var ep = episode; if (typeof ep === "string") { try { ep = JSON.parse(ep); } catch (e) {} } var episodeId = ""; var subOrDub = "sub"; if (typeof ep === "string") { var raw = ep; if (raw.indexOf("/") !== -1) { var parts = raw.split("/"); episodeId = parts[0]; subOrDub = parts[1] || "sub"; } else { episodeId = raw; } } else if (ep && typeof ep === "object") { var rawId = ep.id || ep.episodeId || ""; if (String(rawId).indexOf("/") !== -1) { var parts2 = String(rawId).split("/"); episodeId = parts2[0]; subOrDub = parts2[1] || (ep.subOrDub || "sub"); } else { episodeId = String(rawId); subOrDub = ep.subOrDub || "sub"; } } episodeId = String(episodeId || "").trim(); if (!episodeId) throw new Error("Missing episode id"); var serverName = (_server && _server !== "default") ? String(_server) : "HD-1"; console.log("[HiAnime-TV] parsed episodeId=", episodeId, "subOrDub=", subOrDub, "serverName=", serverName); if (serverName === "HD-1" || serverName === "HD-2" || serverName === "HD-3") { var serversUrl = this.baseUrl + "/ajax/v2/episode/servers?episodeId=" + encodeURIComponent(episodeId); console.log("[HiAnime-TV] serversUrl=", serversUrl); var serverJson = this._fetch(serversUrl, { headers: { "X-Requested-With": "XMLHttpRequest" } }).json(); var serverHtml = this.safeString(serverJson.html); console.log("[HiAnime-TV] serverHtmlLen=", serverHtml.length); var regex = new RegExp( "]*class=\"item server-item\"[^>]*data-type=\"" + this._escapeRe(subOrDub) + "\"[^>]*data-id=\"(\\d+)\"[^>]*>[\\s\\S]*?]*>[\\s\\S]*?" + this._escapeRe(serverName) + "[\\s\\S]*?<\\/a>", "i" ); var m = regex.exec(serverHtml); if (!m) throw new Error("Server \"" + serverName + "\" (" + subOrDub + ") not found"); var serverId = m[1]; console.log("[HiAnime-TV] serverId=", serverId); var sourcesUrl = this.baseUrl + "/ajax/v2/episode/sources?id=" + encodeURIComponent(serverId); console.log("[HiAnime-TV] sourcesUrl=", sourcesUrl); var sourcesJson = this._fetch(sourcesUrl, { headers: { "X-Requested-With": "XMLHttpRequest" } }).json(); console.log("[HiAnime-TV] sourcesJson has link=", !!sourcesJson.link); var decryptData = null; var requiredHeaders = {}; try { decryptData = this.extractMegaCloud(sourcesJson.link, true); if (decryptData && decryptData.headersProvided) requiredHeaders = decryptData.headersProvided; console.log("[HiAnime-TV] extractMegaCloud ok sources=", (decryptData && decryptData.sources ? decryptData.sources.length : 0)); } catch (err) { console.warn("[HiAnime-TV] Primary decrypter failed:", String(err)); } if (!decryptData) { console.warn("[HiAnime-TV] Trying fallback decrypt..."); var fallbackUrl = "https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=" + encodeURIComponent(sourcesJson.link); console.log("[HiAnime-TV] fallbackUrl=", fallbackUrl); var fallbackRes = this._fetch(fallbackUrl); decryptData = fallbackRes.json(); 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" }; console.log("[HiAnime-TV] fallback decrypt sources=", (decryptData && decryptData.sources ? decryptData.sources.length : 0)); } var sourcesArr = (decryptData && decryptData.sources) ? decryptData.sources : []; var streamSource = null; for (var i = 0; i < sourcesArr.length; i++) { var s = sourcesArr[i]; if (s && s.type === "hls" && s.file) { streamSource = s; break; } } if (!streamSource) { for (var j = 0; j < sourcesArr.length; j++) { var s2 = sourcesArr[j]; if (s2 && s2.type === "mp4" && s2.file) { streamSource = s2; break; } } } if (!streamSource || !streamSource.file) throw new Error("No valid stream file found"); var subtitles = []; var tracks = decryptData.tracks || []; for (var k = 0; k < tracks.length; k++) { var track = tracks[k]; if (track && track.kind === "captions" && track.file) { subtitles.push({ id: "sub-" + k, language: track.label || "Unknown", url: track.file, isDefault: !!track.default }); } } console.log("[HiAnime-TV] FINAL stream file=", streamSource.file, "type=", streamSource.type, "subs=", subtitles.length); return { server: serverName, headers: requiredHeaders, videoSources: [{ url: streamSource.file, type: streamSource.type === "hls" ? "m3u8" : "mp4", quality: "auto", subtitles: subtitles }] }; } if (serverName === "HD-4") { console.warn("[HiAnime-TV] HD-4 not implemented"); return null; } console.warn("[HiAnime-TV] Unsupported server=", serverName); return null; } catch (e) { console.error("[HiAnime-TV] findEpisodeServer() ERROR", String(e), e && e.stack ? e.stack : ""); throw e; } } extractMegaCloud(embedUrl, returnHeaders) { console.log("[HiAnime-TV] extractMegaCloud() embedUrl=", embedUrl, "returnHeaders=", returnHeaders); var url = new URL(embedUrl); var baseDomain = url.protocol + "//" + url.host + "/"; var headers = { "Accept": "*/*", "X-Requested-With": "XMLHttpRequest", "Referer": baseDomain, "Origin": url.protocol + "//" + url.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" }; var r = this._fetch(embedUrl, { headers: headers }); var html = r.text(); console.log("[HiAnime-TV] extractMegaCloud() embed status=", r.status, "ok=", r.ok, "htmlLen=", (html || "").length); var fileIdMatch = html.match(/\s*File\s+#([a-zA-Z0-9]+)\s*-/i); if (!fileIdMatch) throw new Error("file_id not found in embed page"); var fileId = fileIdMatch[1]; var nonce = null; var match48 = html.match(/\b[a-zA-Z0-9]{48}\b/); if (match48) { nonce = match48[0]; } else { var regex16 = /["']([A-Za-z0-9]{16})["']/g; var matches = []; var m; while ((m = regex16.exec(html)) !== null) matches.push(m[1]); if (matches.length >= 3) nonce = matches[0] + matches[1] + matches[2]; } if (!nonce) throw new Error("nonce not found"); var sourcesUrl = baseDomain + "embed-2/v3/e-1/getSources?id=" + encodeURIComponent(fileId) + "&_k=" + encodeURIComponent(nonce); console.log("[HiAnime-TV] extractMegaCloud() sourcesUrl=", sourcesUrl); var sr = this._fetch(sourcesUrl, { headers: headers }); var sourcesJson = sr.json(); console.log("[HiAnime-TV] extractMegaCloud() sources status=", sr.status, "ok=", sr.ok, "sources=", (sourcesJson.sources ? sourcesJson.sources.length : 0), "tracks=", (sourcesJson.tracks ? sourcesJson.tracks.length : 0) ); return { sources: sourcesJson.sources || [], tracks: sourcesJson.tracks || [], intro: sourcesJson.intro || null, outro: sourcesJson.outro || null, server: sourcesJson.server || null, headersProvided: returnHeaders ? headers : undefined }; } } module.exports = HiAnime; console.log("[HiAnime-TV] module.exports set");