console.log("[HiAnime-TV] source.js loaded"); class HiAnime { constructor() { console.log("[HiAnime-TV] constructor()"); this.type = "anime-board"; this.version = "1.0"; 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 : ""); } normalizeSeasonParts(title) { const s = this.safeString(title); return s.toLowerCase() .replace(/[^a-z0-9]+/g, "") .replace(/\d+(st|nd|rd|th)/g, (m) => m.replace(/st|nd|rd|th/, "")) .replace(/season|cour|part/g, ""); } _decodeHtml(s) { return this.safeString(s) .replace(/&/g, "&") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/'/g, "'") .replace(/</g, "<") .replace(/>/g, ">") .replace(/&#(\d+);?/g, (m, d) => { const n = parseInt(d, 10); return Number.isFinite(n) ? String.fromCharCode(n) : m; }); } _normalize(str) { return this.safeString(str).toLowerCase().replace(/[^a-z0-9]+/g, ""); } _escapeRe(s) { return this.safeString(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } _fetch(url, opts) { const o = opts || {}; const headers = Object.assign( { "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", "Accept": "text/html,application/json;q=0.9,*/*;q=0.8", "Referer": this.baseUrl + "/" }, o.headers || {} ); const method = o.method || "GET"; const body = (o.body === undefined || o.body === null) ? "" : String(o.body); console.log("[HiAnime-TV] fetch", method, url, "headers=", Object.keys(headers).length, "bodyLen=", body.length); const r = fetch(url, { method, headers, body }); console.log("[HiAnime-TV] fetchResp status=", r.status, "ok=", r.ok); return r; } search(query) { console.log("[HiAnime-TV] search() raw=", query); try { let q = query; if (typeof q === "string") { try { q = JSON.parse(q); } catch (e) { q = { query: query, dub: false, media: { startDate: {} } }; } } q = q || {}; const queryText = this.safeString(q.query || q.title); const dub = !!q.dub; const start = (q.media && q.media.startDate) ? q.media.startDate : {}; const year = start.year || ""; const month = start.month || ""; console.log("[HiAnime-TV] search() parsed queryText=", queryText, "dub=", dub, "year=", year, "month=", month); if (!queryText) return []; const normalize = (str) => this._normalize(str); const fetchMatches = (url) => { console.log("[HiAnime-TV] search() fetchMatches url=", url); const html = this._fetch(url).text(); console.log("[HiAnime-TV] search() htmlLen=", (html || "").length); const regex = /]+title="([^"]+)"[^>]+data-id="(\d+)"/g; const all = [...html.matchAll(regex)]; console.log("[HiAnime-TV] search() matchAll count=", all.length); return all.map(m => { const id = m[3]; const pageUrl = m[1]; const title = this._decodeHtml(m[2]); const jnameRegex = new RegExp( `

[\\s\\S]*?]+href="\\/${this._escapeRe(pageUrl)}[^"]*"[^>]+data-jname="([^"]+)"`, "i" ); const jnameMatch = html.match(jnameRegex); const jname = jnameMatch ? this._decodeHtml(jnameMatch[1]) : null; const imageRegex = new RegExp( `]+data-src="([^"]+)"`, "i" ); const imageMatch = html.match(imageRegex); const image = imageMatch ? imageMatch[1] : null; const normTitleJP = normalize(this.normalizeSeasonParts(jname)); const normTitle = normalize(this.normalizeSeasonParts(title)); return { id, pageUrl, title, image, normTitleJP, normTitle }; }); }; let 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); const matches = fetchMatches(url); if (!matches || matches.length === 0) { console.log("[HiAnime-TV] search() 0 matches"); return []; } const out = matches.map(m => ({ id: `${m.id}/${dub ? "dub" : "sub"}`, title: m.title, image: m.image, url: `${this.baseUrl}/${m.pageUrl}`, 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 { let raw = animeId; if (typeof raw !== "string") raw = JSON.stringify(raw); let id = ""; let subOrDub = "sub"; if (raw.includes("/")) { const parts = raw.split("/"); id = parts[0]; subOrDub = parts[1] || "sub"; } else { try { const parsed = JSON.parse(raw); const v = parsed.id || parsed.animeId || raw; if (String(v).includes("/")) { const parts = String(v).split("/"); id = parts[0]; subOrDub = parts[1] || (parsed.subOrDub || "sub"); } else { id = String(v); subOrDub = parsed.subOrDub || "sub"; } } catch (e) { id = raw; } } console.log("[HiAnime-TV] findEpisodes() parsed id=", id, "subOrDub=", subOrDub); const url = `${this.baseUrl}/ajax/v2/episode/list/${id}`; const res = this._fetch(url, { headers: { "X-Requested-With": "XMLHttpRequest" } }); const json = res.json(); const html = this.safeString(json.html); console.log("[HiAnime-TV] findEpisodes() status=", res.status, "ok=", res.ok, "htmlLen=", html.length); const episodes = []; const regex = /]*class="[^"]*\bep-item\b[^"]*"[^>]*data-number="(\d+)"[^>]*data-id="(\d+)"[^>]*href="([^"]+)"[\s\S]*?
]*title="([^"]+)"/g; let 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]) }); } 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 { let ep = episode; if (typeof ep === "string") { try { ep = JSON.parse(ep); } catch (e) {} } let episodeId = ""; let subOrDub = "sub"; if (typeof ep === "string") { const raw = ep; if (raw.includes("/")) { const parts = raw.split("/"); episodeId = parts[0]; subOrDub = parts[1] || "sub"; } else { episodeId = raw; } } else if (ep && typeof ep === "object") { const rawId = ep.id || ep.episodeId || ""; if (String(rawId).includes("/")) { const parts = String(rawId).split("/"); episodeId = parts[0]; subOrDub = parts[1] || (ep.subOrDub || "sub"); } else { episodeId = String(rawId); subOrDub = ep.subOrDub || "sub"; } } if (!episodeId) throw new Error("Missing episode id"); let 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") { const serversUrl = `${this.baseUrl}/ajax/v2/episode/servers?episodeId=${episodeId}`; console.log("[HiAnime-TV] serversUrl=", serversUrl); const serverJson = this._fetch(serversUrl, { headers: { "X-Requested-With": "XMLHttpRequest" } }).json(); const serverHtml = this.safeString(serverJson.html); console.log("[HiAnime-TV] serverHtmlLen=", serverHtml.length); const regex = new RegExp( `]*class="item server-item"[^>]*data-type="${this._escapeRe(subOrDub)}"[^>]*data-id="(\\d+)"[^>]*>\\s*]*>\\s*${this._escapeRe(serverName)}\\s*`, "i" ); const m = regex.exec(serverHtml); if (!m) throw new Error(`Server "${serverName}" (${subOrDub}) not found`); const serverId = m[1]; console.log("[HiAnime-TV] serverId=", serverId); const sourcesUrl = `${this.baseUrl}/ajax/v2/episode/sources?id=${serverId}`; console.log("[HiAnime-TV] sourcesUrl=", sourcesUrl); const sourcesJson = this._fetch(sourcesUrl, { headers: { "X-Requested-With": "XMLHttpRequest" } }).json(); console.log("[HiAnime-TV] sourcesJson has link=", !!sourcesJson.link); let decryptData = null; let 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..."); const fallbackUrl = `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent(sourcesJson.link)}`; console.log("[HiAnime-TV] fallbackUrl=", fallbackUrl); const 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)); } const sourcesArr = (decryptData && decryptData.sources) ? decryptData.sources : []; const streamSource = sourcesArr.find((s) => s && s.type === "hls" && s.file) || sourcesArr.find((s) => s && s.type === "mp4" && s.file); 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 })); 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 }] }; } 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 = false) { console.log("[HiAnime-TV] extractMegaCloud() embedUrl=", embedUrl, "returnHeaders=", returnHeaders); const url = new URL(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 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36" }; const r = this._fetch(embedUrl, { headers }); const html = r.text(); console.log("[HiAnime-TV] extractMegaCloud() embed status=", r.status, "ok=", r.ok, "htmlLen=", (html || "").length); 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 = [...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 sourcesUrl = `${baseDomain}embed-2/v3/e-1/getSources?id=${fileId}&_k=${nonce}`; console.log("[HiAnime-TV] extractMegaCloud() sourcesUrl=", sourcesUrl); const sr = this._fetch(sourcesUrl, { headers }); const 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");