From dfced2ff0bda4125295a3b28c04e3e8c41ad1e1f Mon Sep 17 00:00:00 2001 From: MrGus Date: Wed, 24 Dec 2025 15:53:44 +0100 Subject: [PATCH] Update anime/hianime/source.js --- anime/hianime/source.js | 480 +++++++++++++++++++++++----------------- 1 file changed, 276 insertions(+), 204 deletions(-) diff --git a/anime/hianime/source.js b/anime/hianime/source.js index 04d40db..5610667 100644 --- a/anime/hianime/source.js +++ b/anime/hianime/source.js @@ -1,11 +1,15 @@ +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 @@ -47,6 +51,7 @@ class HiAnime { } fetchAjax(url, extraHeaders) { + console.log("[HiAnime] fetchAjax url=", url); const headers = Object.assign( { "X-Requested-With": "XMLHttpRequest" }, extraHeaders || {} @@ -55,268 +60,328 @@ class HiAnime { } search(query) { - let searchQuery = 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 }; + if (typeof query === "string") { + try { + searchQuery = JSON.parse(query); + } catch (e) { + searchQuery = { query: query, dub: false }; + } } - } - const queryText = searchQuery.query || searchQuery.title || ""; - if (!queryText) return []; + 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; + 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}`; + let url = `${this.baseUrl}/search?keyword=${encodeURIComponent( + queryText + )}&sort=default`; + if (year) url += `&sy=${year}`; + if (month) url += `&sm=${month}`; - const response = fetch(url); - const html = response.text(); + console.log("[HiAnime] search() url=", url); - const regex = - /]*href="\/watch\/([^"]+)"[^>]*title="([^"]+)"[^>]*data-id="(\d+)"/gi; - const matches = [...html.matchAll(regex)]; + const response = fetch(url); + const html = response.text(); - const dubFlag = !!searchQuery.dub; - const queryKey = this.normalizeKey(queryText); + console.log("[HiAnime] search() htmlLen=", (html || "").length); - let results = matches.map((m) => { - const watchSlug = m[1]; - const title = this.decodeHtml(m[2]); - const idNum = m[3]; + const regex = + /]*href="\/watch\/([^"]+)"[^>]*title="([^"]+)"[^>]*data-id="(\d+)"/gi; + const matches = [...html.matchAll(regex)]; - 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; + console.log("[HiAnime] search() matches=", matches.length); - return { - id: `${idNum}/${dubFlag ? "dub" : "sub"}`, - title, - image, - url: `${this.baseUrl}/watch/${watchSlug}`, - subOrDub: dubFlag ? "dub" : "sub", - _score: this.scoreTitle(queryKey, title) - }; - }); + const dubFlag = !!searchQuery.dub; + const queryKey = this.normalizeKey(queryText); - 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); + let results = matches.map((m) => { + const watchSlug = m[1]; + const title = this.decodeHtml(m[2]); + const idNum = m[3]; - results.sort((a, b) => (b._score || 0) - (a._score || 0)); + 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 results.map((r) => { - const out = Object.assign({}, r); - delete out._score; + 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) { - let id, subOrDub; + 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"; + 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"; } - } 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("/"); + if (id && id.includes("/")) [id, subOrDub] = id.split("/"); - const url = `${this.baseUrl}/ajax/v2/episode/list/${id}`; - const response = this.fetchAjax(url); - const json = response.json(); - const html = this.safeString(json.html); + console.log("[HiAnime] findEpisodes() parsed id=", id, "subOrDub=", subOrDub); - const episodes = []; - const regex = - /]*\bep-item\b[^>]*data-number="(\d+)"[^>]*data-id="(\d+)"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi; + const url = `${this.baseUrl}/ajax/v2/episode/list/${id}`; + console.log("[HiAnime] findEpisodes() url=", url); - 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] || ""; + const response = this.fetchAjax(url); + const json = response.json(); + const html = this.safeString(json.html); - 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]); + 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 + }); } - 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; } - - return episodes; } findEpisodeServer(episode, _server) { - let episodeId, subOrDub; + 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"; + 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 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"; + episodeId = String(episode); + subOrDub = "sub"; } - } else { - episodeId = String(episode); - subOrDub = "sub"; - } - if (episodeId && episodeId.includes("/")) - [episodeId, subOrDub] = episodeId.split("/"); + if (episodeId && episodeId.includes("/")) + [episodeId, subOrDub] = episodeId.split("/"); - let serverName = _server && _server !== "default" ? _server : "HD-1"; + let serverName = _server && _server !== "default" ? _server : "HD-1"; - const serversUrl = `${this.baseUrl}/ajax/v2/episode/servers?episodeId=${episodeId}`; - const serverResponse = this.fetchAjax(serversUrl); - const serverJson = serverResponse.json(); - const serverHtml = this.safeString(serverJson.html); + console.log("[HiAnime] findEpisodeServer() parsed episodeId=", episodeId, "subOrDub=", subOrDub, "serverName=", serverName); - const escapedName = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const serversUrl = `${this.baseUrl}/ajax/v2/episode/servers?episodeId=${episodeId}`; + console.log("[HiAnime] findEpisodeServer() serversUrl=", serversUrl); - const blockRe = new RegExp( - `]*\\bserver-item\\b[^>]*data-type="${subOrDub}"[^>]*data-id="(\\d+)"[\\s\\S]*?>[\\s\\S]*?${escapedName}[\\s\\S]*?<\\/div>`, - "i" - ); + const serverResponse = this.fetchAjax(serversUrl); + const serverJson = serverResponse.json(); + const serverHtml = this.safeString(serverJson.html); - let match = blockRe.exec(serverHtml); + console.log("[HiAnime] findEpisodeServer() serverHtmlLen=", serverHtml.length); - if (!match) { - const altRe = new RegExp( - `data-type="${subOrDub}"[^>]*data-id="(\\d+)"[\\s\\S]*?>[\\s\\S]*?]*>[\\s\\S]*?${escapedName}[\\s\\S]*?<\\/a>`, + 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" ); - match = altRe.exec(serverHtml); - } - if (!match) { - throw new Error(`Server "${serverName}" (${subOrDub}) not found`); - } + let match = blockRe.exec(serverHtml); - const serverId = match[1]; - - const sourcesUrl = `${this.baseUrl}/ajax/v2/episode/sources?id=${serverId}`; - const sourcesResponse = this.fetchAjax(sourcesUrl); - const sourcesJson = sourcesResponse.json(); - - let decryptData = null; - let requiredHeaders = {}; - - try { - decryptData = this.extractMegaCloud(sourcesJson.link, true); - if (decryptData && decryptData.headersProvided) { - requiredHeaders = decryptData.headersProvided; + 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); } - } catch (err) {} - if (!decryptData) { - const fallbackUrl = `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent( - sourcesJson.link - )}`; - const fallbackResponse = fetch(fallbackUrl); - decryptData = fallbackResponse.json(); + if (!match) { + console.error("[HiAnime] findEpisodeServer() server not found:", serverName, subOrDub); + throw new Error(`Server "${serverName}" (${subOrDub}) not found`); + } - 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" - }; - } + const serverId = match[1]; + console.log("[HiAnime] findEpisodeServer() serverId=", serverId); - 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 sourcesUrl = `${this.baseUrl}/ajax/v2/episode/sources?id=${serverId}`; + console.log("[HiAnime] findEpisodeServer() sourcesUrl=", sourcesUrl); - const streamSource = hls || mp4; - if (!streamSource || !streamSource.file) { - throw new Error("No valid stream file found"); - } + const sourcesResponse = this.fetchAjax(sourcesUrl); + const sourcesJson = sourcesResponse.json(); - 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() sourcesJson has link=", !!sourcesJson.link); - return { - server: serverName, - headers: requiredHeaders, - videoSources: [ - { - url: streamSource.file, - type: streamSource.type === "hls" ? "m3u8" : "mp4", - quality: "auto", - subtitles + 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}/`; @@ -332,6 +397,8 @@ class HiAnime { 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"); @@ -354,9 +421,13 @@ class HiAnime { } 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 || [], @@ -369,3 +440,4 @@ class HiAnime { } module.exports = HiAnime; +console.log("[HiAnime] module.exports set to HiAnime class");