console.log("[HiAnime] source.js loaded"); class HiAnime { constructor() { console.log("[HiAnime] constructor()"); this.type = "anime-streaming"; this.version = "1.0"; this.baseUrl = "https://hianime.to"; } getSettings() { console.log("[HiAnime] 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, ">"); } normalizeKey(s) { return this.decodeHtml(this.safeString(s)) .toLowerCase() .replace(/[^a-z0-9]+/g, ""); } scoreTitle(queryKey, title) { const t = this.normalizeKey(title); if (!t) return 0; if (t === queryKey) return 1000; if (t.includes(queryKey) || queryKey.includes(t)) return 700; const qTokens = queryKey.match(/[a-z0-9]+/g) || []; const tTokens = t.match(/[a-z0-9]+/g) || []; let inter = 0; for (const tok of qTokens) { if (tTokens.includes(tok)) inter++; } return inter * 50; } fetchAjax(url, extraHeaders) { console.log("[HiAnime] fetchAjax url=", url); const headers = Object.assign( { "X-Requested-With": "XMLHttpRequest" }, extraHeaders || {} ); return fetch(url, { headers }); } search(query) { console.log("[HiAnime] search() called query=", query); try { let searchQuery = query; if (typeof query === "string") { try { searchQuery = JSON.parse(query); } catch (e) { searchQuery = { query: query, dub: false }; } } const queryText = searchQuery.query || searchQuery.title || ""; if (!queryText) { console.log("[HiAnime] search() empty queryText"); return []; } const media = searchQuery.media || {}; const startDate = media.startDate || {}; const year = startDate.year || null; const month = startDate.month || null; let url = `${this.baseUrl}/search?keyword=${encodeURIComponent( queryText )}&sort=default`; if (year) url += `&sy=${year}`; if (month) url += `&sm=${month}`; console.log("[HiAnime] search() url=", url); const response = fetch(url); const html = response.text(); console.log("[HiAnime] search() htmlLen=", (html || "").length); const regex = /]*href="\/watch\/([^"]+)"[^>]*title="([^"]+)"[^>]*data-id="(\d+)"/gi; const matches = [...html.matchAll(regex)]; console.log("[HiAnime] search() matches=", matches.length); const dubFlag = !!searchQuery.dub; const queryKey = this.normalizeKey(queryText); let results = matches.map((m) => { const watchSlug = m[1]; const title = this.decodeHtml(m[2]); const idNum = m[3]; const imgRe = new RegExp( `]+href="\\/watch\\/${watchSlug .replace(/[.*+?^${}()|[\]\\]/g, "\\$&") }"[\\s\\S]*?]+(?:data-src|src)="([^"]+)"`, "i" ); const imgM = html.match(imgRe); const image = imgM ? imgM[1] : null; return { id: `${idNum}/${dubFlag ? "dub" : "sub"}`, title, image, url: `${this.baseUrl}/watch/${watchSlug}`, subOrDub: dubFlag ? "dub" : "sub", _score: this.scoreTitle(queryKey, title) }; }); const byId = {}; for (const r of results) { const key = r.id; if (!byId[key] || r._score > byId[key]._score) byId[key] = r; } results = Object.values(byId); results.sort((a, b) => (b._score || 0) - (a._score || 0)); const out = results.map((r) => { const o = Object.assign({}, r); delete o._score; return o; }); console.log("[HiAnime] search() returning=", out.length); return out; } catch (e) { console.error("[HiAnime] search() ERROR", String(e), e && e.stack ? e.stack : ""); throw e; } } findEpisodes(animeId) { console.log("[HiAnime] findEpisodes() called animeId=", animeId); try { let id, subOrDub; if (typeof animeId === "string") { if (animeId.includes("/")) { [id, subOrDub] = animeId.split("/"); } else { try { const parsed = JSON.parse(animeId); id = parsed.id || parsed.animeId || animeId; subOrDub = parsed.subOrDub || "sub"; } catch (e) { id = animeId; subOrDub = "sub"; } } } else if (typeof animeId === "object" && animeId) { id = animeId.id || animeId.animeId; subOrDub = animeId.subOrDub || "sub"; } else { id = String(animeId); subOrDub = "sub"; } if (id && id.includes("/")) [id, subOrDub] = id.split("/"); console.log("[HiAnime] findEpisodes() parsed id=", id, "subOrDub=", subOrDub); const url = `${this.baseUrl}/ajax/v2/episode/list/${id}`; console.log("[HiAnime] findEpisodes() url=", url); const response = this.fetchAjax(url); const json = response.json(); const html = this.safeString(json.html); console.log("[HiAnime] findEpisodes() htmlLen=", html.length); const episodes = []; const regex = /]*\bep-item\b[^>]*data-number="(\d+)"[^>]*data-id="(\d+)"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi; let match; while ((match = regex.exec(html)) !== null) { const number = parseInt(match[1], 10); const epId = match[2]; const href = match[3]; const inner = match[4] || ""; let title = ""; const t1 = inner.match(/title="([^"]+)"/i); if (t1) title = this.decodeHtml(t1[1]); if (!title) { const t2 = inner.match(/class="ep-name[^"]*"[^>]*>([^<]+)/i); if (t2) title = this.decodeHtml(t2[1]); } if (!title) title = `Episode ${number}`; episodes.push({ id: `${epId}/${subOrDub}`, number, url: this.baseUrl + href, title }); } console.log("[HiAnime] findEpisodes() returning episodes=", episodes.length); return episodes; } catch (e) { console.error("[HiAnime] findEpisodes() ERROR", String(e), e && e.stack ? e.stack : ""); throw e; } } findEpisodeServer(episode, _server) { console.log("[HiAnime] findEpisodeServer() episode=", episode, "server=", _server); try { let episodeId, subOrDub; if (typeof episode === "string") { if (episode.includes("/")) { [episodeId, subOrDub] = episode.split("/"); } else { try { const parsed = JSON.parse(episode); episodeId = parsed.id || parsed.episodeId || episode; subOrDub = parsed.subOrDub || "sub"; } catch (e) { episodeId = episode; subOrDub = "sub"; } } } else if (typeof episode === "object" && episode) { const epId = episode.id || episode.episodeId || ""; if (epId.includes("/")) { [episodeId, subOrDub] = epId.split("/"); } else { episodeId = epId; subOrDub = episode.subOrDub || "sub"; } } else { episodeId = String(episode); subOrDub = "sub"; } if (episodeId && episodeId.includes("/")) [episodeId, subOrDub] = episodeId.split("/"); let serverName = _server && _server !== "default" ? _server : "HD-1"; console.log("[HiAnime] findEpisodeServer() parsed episodeId=", episodeId, "subOrDub=", subOrDub, "serverName=", serverName); const serversUrl = `${this.baseUrl}/ajax/v2/episode/servers?episodeId=${episodeId}`; console.log("[HiAnime] findEpisodeServer() serversUrl=", serversUrl); const serverResponse = this.fetchAjax(serversUrl); const serverJson = serverResponse.json(); const serverHtml = this.safeString(serverJson.html); console.log("[HiAnime] findEpisodeServer() serverHtmlLen=", serverHtml.length); const escapedName = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const blockRe = new RegExp( `]*\\bserver-item\\b[^>]*data-type="${subOrDub}"[^>]*data-id="(\\d+)"[\\s\\S]*?>[\\s\\S]*?${escapedName}[\\s\\S]*?<\\/div>`, "i" ); let match = blockRe.exec(serverHtml); if (!match) { const altRe = new RegExp( `data-type="${subOrDub}"[^>]*data-id="(\\d+)"[\\s\\S]*?>[\\s\\S]*?]*>[\\s\\S]*?${escapedName}[\\s\\S]*?<\\/a>`, "i" ); match = altRe.exec(serverHtml); } if (!match) { console.error("[HiAnime] findEpisodeServer() server not found:", serverName, subOrDub); throw new Error(`Server "${serverName}" (${subOrDub}) not found`); } const serverId = match[1]; console.log("[HiAnime] findEpisodeServer() serverId=", serverId); const sourcesUrl = `${this.baseUrl}/ajax/v2/episode/sources?id=${serverId}`; console.log("[HiAnime] findEpisodeServer() sourcesUrl=", sourcesUrl); const sourcesResponse = this.fetchAjax(sourcesUrl); const sourcesJson = sourcesResponse.json(); console.log("[HiAnime] findEpisodeServer() 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] findEpisodeServer() megacloud decrypt ok sources=", (decryptData && decryptData.sources ? decryptData.sources.length : 0)); } catch (err) { console.warn("[HiAnime] extractMegaCloud failed, will fallback", String(err)); } if (!decryptData) { const fallbackUrl = `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent( sourcesJson.link )}`; console.log("[HiAnime] fallbackUrl=", fallbackUrl); const fallbackResponse = fetch(fallbackUrl); decryptData = fallbackResponse.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] fallback decrypt sources=", (decryptData && decryptData.sources ? decryptData.sources.length : 0)); } const sourcesArr = decryptData && decryptData.sources ? decryptData.sources : []; const hls = sourcesArr.find((s) => s && s.type === "hls" && s.file); const mp4 = sourcesArr.find((s) => s && s.type === "mp4" && s.file); const streamSource = hls || mp4; if (!streamSource || !streamSource.file) { console.error("[HiAnime] No valid stream file in sourcesArr len=", sourcesArr.length); throw new Error("No valid stream file found"); } const tracksArr = decryptData && decryptData.tracks ? decryptData.tracks : []; const subtitles = tracksArr .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] findEpisodeServer() resolved file=", streamSource.file, "type=", streamSource.type); return { server: serverName, headers: requiredHeaders, videoSources: [ { url: streamSource.file, type: streamSource.type === "hls" ? "m3u8" : "mp4", quality: "auto", subtitles } ] }; } catch (e) { console.error("[HiAnime] findEpisodeServer() ERROR", String(e), e && e.stack ? e.stack : ""); throw e; } } extractMegaCloud(embedUrl, returnHeaders) { console.log("[HiAnime] 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 response = fetch(embedUrl, { headers }); const html = response.text(); console.log("[HiAnime] extractMegaCloud() embed 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] extractMegaCloud() sourcesUrl=", sourcesUrl); const sourcesResponse = fetch(sourcesUrl, { headers }); const sourcesJson = sourcesResponse.json(); console.log("[HiAnime] extractMegaCloud() 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] module.exports set to HiAnime class");