From 22296962c4a23e330272c0ebd161b1915f488c2c Mon Sep 17 00:00:00 2001 From: MrGus Date: Fri, 26 Dec 2025 18:48:46 +0100 Subject: [PATCH] Update anime/hianime/source.js --- anime/hianime/source.js | 368 +++++++++++++++++++--------------------- 1 file changed, 170 insertions(+), 198 deletions(-) diff --git a/anime/hianime/source.js b/anime/hianime/source.js index 21bc644..ace3776 100644 --- a/anime/hianime/source.js +++ b/anime/hianime/source.js @@ -1,16 +1,14 @@ class HiAnime { constructor() { this.type = "anime-streaming"; - this.version = "1.0.3"; + this.version = "1.0.4"; this.baseUrl = "https://hianime.to"; } getSettings() { return { - episodeServers: ["HD-1", "HD-2", "HD-3"], - supportsSub: true, - supportsDub: true, - supportsHls: true + episodeServers: ["HD-1", "HD-2", "HD-3", "HD-4"], + supportsDub: true }; } @@ -43,8 +41,16 @@ class HiAnime { } } - _safeStr(v) { - return typeof v === "string" ? v : (v == null ? "" : String(v)); + 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(str) { @@ -84,49 +90,22 @@ class HiAnime { return q || {}; } - _norm(s) { - return String(s || "") - .toLowerCase() - .replace(/(season|cour|part|uncensored|movie|ova|ona|special)/g, " ") - .replace(/\d+(st|nd|rd|th)\b/g, (m) => m.replace(/st|nd|rd|th/g, "")) - .replace(/[^a-z0-9\s]+/g, " ") - .replace(/\s+/g, " ") - .trim(); - } + _wantTrackFromEpisode(ep) { + const e = ep || {}; + if (typeof e.dub === "boolean") return e.dub ? "dub" : "sub"; - _levSim(a, b) { - a = String(a || ""); - b = String(b || ""); - if (!a || !b) return 0; + const sod = String(e.subOrDub || "").toLowerCase(); + if (sod === "dub" || sod === "sub") return sod; - const la = a.length, lb = b.length; - const dp = Array.from({ length: la + 1 }, () => new Array(lb + 1).fill(0)); - for (let i = 0; i <= la; i++) dp[i][0] = i; - for (let j = 0; j <= lb; j++) dp[0][j] = j; + const tr = String(e.track || "").toLowerCase(); + if (tr === "dub" || tr === "sub") return tr; - for (let i = 1; i <= la; i++) { - for (let j = 1; j <= lb; j++) { - const cost = a[i - 1] === b[j - 1] ? 0 : 1; - dp[i][j] = Math.min( - dp[i - 1][j] + 1, - dp[i][j - 1] + 1, - dp[i - 1][j - 1] + cost - ); - } - } - const dist = dp[la][lb]; - const maxLen = Math.max(la, lb) || 1; - return 1 - (dist / maxLen); - } + 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; - _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 || ""); + return "sub"; } _extractWatchIdFromUrl(url) { @@ -135,173 +114,170 @@ class HiAnime { return m ? m[1] : ""; } - _parseSearchHtmlToResults(html) { - const out = []; - const h = this._decodeHtml(html || ""); - - const re = /]+href="([^"]+\/watch\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/gi; - let m; - const seen = {}; - while ((m = re.exec(h)) !== null) { - const href = String(m[1] || ""); - const full = href.startsWith("http") ? href : (this.baseUrl + href); - const id = this._extractWatchIdFromUrl(full); - if (!id) continue; - if (seen[id]) continue; - seen[id] = true; - - const inner = String(m[2] || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim(); - const title = inner || ""; - - out.push({ - id: id, - title: title, - url: full - }); - } - return out; - } - - search(a1) { - const arg = this._parseQuery(a1); - const q = this._safeStr(arg.query || arg.q || "").trim(); + const query = this._parseQuery(a1); + + const normalize = (str) => + this.safeString(str).toLowerCase().replace(/[^a-z0-9]+/g, ""); + + const media = query.media || {}; + const start = media.startDate || {}; + const q = this.safeString(query.query || "").trim(); if (!q) return "[]"; - const url = `${this.baseUrl}/ajax/search/suggest?keyword=${encodeURIComponent(q)}`; - const j = this._getJson(url, this._headers()); - - 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); - } else { - const html = String(j.html || j.result || ""); - if (html) results = this._parseSearchHtmlToResults(html); - } - - if (!results || results.length === 0) { - const page = this._getText(`${this.baseUrl}/search?keyword=${encodeURIComponent(q)}`, { - "User-Agent": "Mozilla/5.0", - "Accept": "text/html", - "Referer": this.baseUrl + "/", - "Origin": this.baseUrl - }); - - const re = /href="(\/watch\/[^"]+-\d+)"/gi; - let mm; - const seen = {}; - while ((mm = re.exec(page)) !== null) { - const href = mm[1]; - const full = this.baseUrl + href; - const id = this._extractWatchIdFromUrl(full); - if (!id || seen[id]) continue; - seen[id] = true; - - results.push({ id, title: q, url: full }); - if (results.length >= 20) break; - } - } - - const nq = this._norm(q); - results.forEach((r) => { - const nt = this._norm(r.title || ""); - r._score = this._levSim(nq, nt); + const url = `${this.baseUrl}/search?keyword=${encodeURIComponent(q)}&sy=${encodeURIComponent(String(start.year || ""))}&sm=${encodeURIComponent(String(start.month || ""))}&sort=default`; + const html = this._getText(url, { + "User-Agent": "Mozilla/5.0", + "Accept": "text/html", + "Referer": this.baseUrl + "/", + "Origin": this.baseUrl }); - results.sort((a, b) => (b._score || 0) - (a._score || 0)); - return JSON.stringify(results.map((r) => ({ - id: String(r.id), - title: String(r.title || ""), - url: String(r.url || "") - }))); + const regex = /]+title="([^"]+)"[^>]+data-id="(\d+)"/g; + + const matches = []; + let m; + while ((m = regex.exec(html)) !== null) { + const id = m[3]; + const pageUrl = m[1]; + const title = this._decodeHtml(m[2]); + + const jnameRegex = new RegExp( + `

[\\s\\S]*?]+href="\\/${pageUrl}[^"]*"[^>]+data-jname="([^"]+)"`, + "i" + ); + const jnameMatch = html.match(jnameRegex); + const jname = jnameMatch ? this._decodeHtml(jnameMatch[1]) : ""; + + const imageRegex = new RegExp( + `]+data-src="([^"]+)"`, + "i" + ); + const imageMatch = html.match(imageRegex); + const image = imageMatch ? String(imageMatch[1]) : ""; + + matches.push({ + id, + pageUrl, + title, + image, + normTitleJP: normalize(this.normalizeSeasonParts(jname)), + normTitle: normalize(this.normalizeSeasonParts(title)) + }); + } + + if (matches.length === 0) return "[]"; + + const wantTrack = query.dub ? "dub" : "sub"; + + const out = matches.map(x => ({ + id: String(x.id), + title: String(x.title || ""), + image: String(x.image || ""), + url: `${this.baseUrl}/watch/${x.pageUrl}`, + subOrDub: wantTrack + })); + + return JSON.stringify(out); } findEpisodes(animeId) { - const id = this._safeStr(animeId).trim(); - if (!id) return "[]"; + const raw = String(animeId || "").trim(); + if (!raw) return "[]"; - const j = this._getJson(`${this.baseUrl}/ajax/v2/episode/list/${encodeURIComponent(id)}`, this._headers()); - const html = this._decodeHtml(String(j.html || j.result || "")); + const parts = raw.split("/"); + const id = parts[0]; + const subOrDub = (parts[1] && (parts[1] === "dub" || parts[1] === "sub")) ? parts[1] : "sub"; + const json = this._getJson(`${this.baseUrl}/ajax/v2/episode/list/${encodeURIComponent(id)}`, { + "X-Requested-With": "XMLHttpRequest" + }); + + const html = this._decodeHtml(String(json.html || json.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; + + 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: epId, - number: num, - title: "", - url: "" + id: `${match[2]}/${subOrDub}`, + number: parseInt(match[1], 10), + url: this.baseUrl + match[3], + title: this._decodeHtml(match[4] || "") }); } - if (episodes.length === 0) { - const re2 = /data-episode-id="([^"]+)"[^>]*data-num="([^"]+)"/gi; - while ((m = re2.exec(html)) !== null) { - const epId = String(m[1] || "").trim(); - const numRaw = String(m[2] || "").trim(); - const num = parseFloat(numRaw); - if (!epId || !isFinite(num)) continue; - episodes.push({ id: epId, 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 = {}; } + findEpisodeServer(episodeObj, _server) { + let episode = episodeObj; + if (typeof episode === "string") { + try { episode = JSON.parse(episode); } catch (e) { episode = {}; } + } + episode = episode || {}; + + const idParts = String(episode.id || "").split("/"); + const episodeId = idParts[0]; + if (!episodeId) throw new Error("Missing episode id"); + + const wantTrack = this._wantTrackFromEpisode(episode); + + let serverName = String(_server || "").trim(); + if (!serverName || serverName === "default") serverName = "HD-1"; + + if (serverName === "HD-4") { + throw new Error("HD-4 not implemented"); } - const epId = this._safeStr(ep && ep.id ? ep.id : "").trim(); - if (!epId) throw new Error("Missing episode id"); + const serverJson = this._getJson( + `${this.baseUrl}/ajax/v2/episode/servers?episodeId=${encodeURIComponent(episodeId)}`, + { "X-Requested-With": "XMLHttpRequest" } + ); - const server = String(serverName || "").trim() || "HD-1"; + const serverHtml = this._decodeHtml(String(serverJson.html || serverJson.result || "")); + if (!serverHtml) throw new Error("Empty server list"); - const j = this._getJson(`${this.baseUrl}/ajax/v2/episode/servers?episodeId=${encodeURIComponent(epId)}`, this._headers()); - const html = this._decodeHtml(String(j.html || j.result || "")); + const esc = (s) => String(s || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const strictRe = new RegExp( + `]*class="item\\s+server-item"[^>]*data-type="${esc(wantTrack)}"[^>]*data-id="(\\d+)"[^>]*>[\\s\\S]*?]*>[\\s\\S]*?${esc(serverName)}[\\s\\S]*?<\\/a>`, + "i" + ); - let serverId = ""; - - const re = new RegExp(`data-id="([^"]+)"[^>]*>\\s*${server.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*<`, "i"); - const mm = re.exec(html); - if (mm) serverId = String(mm[1] || "").trim(); + let m = strictRe.exec(serverHtml); + let serverId = m ? String(m[1] || "").trim() : ""; if (!serverId) { - const re2 = /data-id="([^"]+)"/i; - const mm2 = re2.exec(html); - if (mm2) serverId = String(mm2[1] || "").trim(); + const trackAnyRe = new RegExp( + `]*class="item\\s+server-item"[^>]*data-type="${esc(wantTrack)}"[^>]*data-id="(\\d+)"`, + "i" + ); + m = trackAnyRe.exec(serverHtml); + serverId = m ? String(m[1] || "").trim() : ""; } - if (!serverId) throw new Error("No server id found"); + if (!serverId) { + const anyRe = /data-id="(\d+)"/i; + const mm = anyRe.exec(serverHtml); + serverId = mm ? String(mm[1] || "").trim() : ""; + } + + if (!serverId) throw new Error(`Server id not found (track=${wantTrack} server=${serverName})`); const sourcesJson = this._getJson( `${this.baseUrl}/ajax/v2/episode/sources?id=${encodeURIComponent(serverId)}`, { "X-Requested-With": "XMLHttpRequest" } ); - const embed = (sourcesJson && sourcesJson.link) ? String(sourcesJson.link) : ""; - if (!embed) throw new Error("No embed link returned"); + const embedUrl = sourcesJson && sourcesJson.link ? String(sourcesJson.link) : ""; + if (!embedUrl) throw new Error("No embed link returned"); let decryptData = null; let requiredHeaders = {}; try { - decryptData = this.extractMegaCloudSync(embed); + decryptData = this.extractMegaCloudSync(embedUrl); requiredHeaders = (decryptData && decryptData.headersProvided) ? decryptData.headersProvided : {}; } catch (e) { decryptData = null; @@ -309,14 +285,13 @@ class HiAnime { if (!decryptData) { decryptData = this._getJson( - `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent(embed)}`, + `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent(embedUrl)}`, {} ); 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", + "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" }; } @@ -325,14 +300,13 @@ class HiAnime { const sources = decryptData.sources || []; const streamSource = - sources.find((s) => s && s.type === "hls" && s.file) || - sources.find((s) => s && s.type === "mp4" && s.file) || + sources.find((s) => s && String(s.type || "").toLowerCase() === "hls" && s.file) || + sources.find((s) => s && String(s.type || "").toLowerCase() === "mp4" && s.file) || sources.find((s) => s && s.file); if (!streamSource || !streamSource.file) throw new Error("No valid stream file found"); - const tracks = decryptData.tracks || []; - const subtitles = (tracks || []) + const subtitles = (decryptData.tracks || []) .filter((t) => t && String(t.kind || "").toLowerCase() === "captions" && t.file) .map((track, index) => ({ id: `sub-${index}`, @@ -344,16 +318,15 @@ class HiAnime { const outType = (String(streamSource.type || "").toLowerCase() === "hls") ? "m3u8" : "mp4"; return JSON.stringify({ - server: server, + server: serverName, headers: requiredHeaders || {}, - videoSources: [ - { - url: String(streamSource.file), - type: outType, - quality: "auto", - subtitles: subtitles - } - ] + _debug: { scUsed: wantTrack, serverId: serverId }, + videoSources: [{ + url: String(streamSource.file), + type: outType, + quality: "auto", + subtitles + }] }); } @@ -367,12 +340,11 @@ class HiAnime { const baseDomain = `${protocol}://${host}/`; const headers = { - Accept: "*/*", + "Accept": "*/*", "X-Requested-With": "XMLHttpRequest", - Referer: baseDomain, - Origin: `${protocol}://${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" + "Referer": baseDomain, + "Origin": `${protocol}://${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 html = this._getText(embedUrl, headers);