diff --git a/anime/hianime/source.js b/anime/hianime/source.js index f2d6b65..21bc644 100644 --- a/anime/hianime/source.js +++ b/anime/hianime/source.js @@ -1,7 +1,7 @@ class HiAnime { constructor() { this.type = "anime-streaming"; - this.version = "1.0.2"; + this.version = "1.0.3"; this.baseUrl = "https://hianime.to"; } @@ -14,29 +14,37 @@ class HiAnime { }; } - async _fetch(url, opts) { - if (typeof fetch === "function") return fetch(String(url), opts || {}); + _nativeFetch(url, method, headers, body) { const raw = Native.fetch( String(url), - (opts && opts.method) ? String(opts.method) : "GET", - JSON.stringify((opts && opts.headers) || {}), - (opts && opts.body != null) ? String(opts.body) : "" + method || "GET", + JSON.stringify(headers || {}), + body == null ? "" : String(body) ); - try { return JSON.parse(raw || "{}"); } catch (e) { return { ok: false, status: 0, headers: {}, body: "" }; } + try { + return JSON.parse(raw || "{}"); + } catch (e) { + return { ok: false, status: 0, headers: {}, body: "" }; + } } - async _getText(url, headers) { - const res = await this._fetch(String(url), { method: "GET", headers: headers || {} }); - if (res && typeof res.text === "function") return String(await res.text()); + _getText(url, headers) { + const res = this._nativeFetch(url, "GET", headers, ""); return String(res.body || ""); } - async _getJson(url, headers) { - const res = await this._fetch(String(url), { method: "GET", headers: headers || {} }); - if (res && typeof res.json === "function") { - try { return await res.json(); } catch (e) { return {}; } + _getJson(url, headers) { + const res = this._nativeFetch(url, "GET", headers, ""); + const body = String(res.body || ""); + try { + return JSON.parse(body || "{}"); + } catch (e) { + return { _raw: body }; } - try { return JSON.parse(String(res.body || "{}")); } catch (e) { return {}; } + } + + _safeStr(v) { + return typeof v === "string" ? v : (v == null ? "" : String(v)); } _decodeHtml(str) { @@ -51,7 +59,29 @@ class HiAnime { .replace(/'/g, "'") .replace(/&/g, "&") .replace(/</g, "<") - .replace(/>/g, ">"); + .replace(/>/g, ">") + .replace(///g, "/"); + } + + _headers() { + return { + "User-Agent": "Mozilla/5.0", + "Accept": "*/*", + "Referer": this.baseUrl + "/", + "Origin": this.baseUrl, + "X-Requested-With": "XMLHttpRequest" + }; + } + + _parseQuery(q) { + if (typeof q === "string") { + const s = q.trim(); + if (s.startsWith("{") || s.startsWith("[")) { + try { return JSON.parse(s); } catch (e) { return { query: s }; } + } + return { query: s }; + } + return q || {}; } _norm(s) { @@ -76,272 +106,190 @@ class HiAnime { for (let i = 1; i <= la; i++) { for (let j = 1; j <= lb; j++) { - if (a[i - 1] === b[j - 1]) dp[i][j] = dp[i - 1][j - 1]; - else dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]); + 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); + } + + _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 || ""); + } + + _extractWatchIdFromUrl(url) { + const s = String(url || ""); + const m = s.match(/\/watch\/[^\/]+-(\d+)/i); + 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(); + 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 dist = dp[la][lb]; - const maxLen = Math.max(la, lb) || 1; - return 1 - dist / maxLen; + const nq = this._norm(q); + results.forEach((r) => { + const nt = this._norm(r.title || ""); + r._score = this._levSim(nq, nt); + }); + 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 || "") + }))); } - _parseDate(dateStr) { - const s = String(dateStr || "").trim(); - const m = s.match(/([A-Za-z]+)\s+(\d{1,2}),\s*(\d{4})/); - if (!m) return { year: 0, month: 0, day: 0 }; - const monthMap = { - Jan: 1, Feb: 2, Mar: 3, Apr: 4, May: 5, Jun: 6, - Jul: 7, Aug: 8, Sep: 9, Oct: 10, Nov: 11, Dec: 12 - }; - const month = monthMap[m[1]] || 0; - const day = parseInt(m[2], 10) || 0; - const year = parseInt(m[3], 10) || 0; - return { year, month, day }; - } + findEpisodes(animeId) { + const id = this._safeStr(animeId).trim(); + if (!id) return "[]"; - async _fetchSuggestMatches(queryStr) { - const url = `${this.baseUrl}/ajax/search/suggest?keyword=${encodeURIComponent(queryStr)}`; - const reply = await this._getJson(url, { "X-Requested-With": "XMLHttpRequest" }); - const html = String((reply && reply.html) || ""); + const j = this._getJson(`${this.baseUrl}/ajax/v2/episode/list/${encodeURIComponent(id)}`, this._headers()); + const html = this._decodeHtml(String(j.html || j.result || "")); - const regex = - /[\s\S]*?

]*data-jname="([^"]+)"[^>]*>([^<]+)<\/h3>[\s\S]*?
\s*([^<]+)<\/span>/g; - - const matches = []; - const all = [...html.matchAll(regex)]; - for (let i = 0; i < all.length; i++) { - const pageUrl = String(all[i][1] || "").trim(); - if (!pageUrl || pageUrl.startsWith("search?")) continue; - - const jname = this._decodeHtml(String(all[i][2] || "").trim()); - const title = this._decodeHtml(String(all[i][3] || "").trim()); - const dateStr = String(all[i][4] || "").trim(); - - const startDate = this._parseDate(dateStr); - - const idMatch = pageUrl.match(/-(\d+)$/); - const id = idMatch ? idMatch[1] : pageUrl; - - matches.push({ - id, - pageUrl, - title, - jname, - normTitle: this._norm(title), - normTitleJP: this._norm(jname), - startDate - }); - } - return matches; - } - - async _fallbackSearchPageMatches(queryStr) { - const url = `${this.baseUrl}/search?keyword=${encodeURIComponent(queryStr)}`; - const html = await this._getText(url, {}); - const regex = /]+title="([^"]+)"[^>]+data-id="(\d+)"/g; - - const matches = []; - const all = [...html.matchAll(regex)]; - for (let i = 0; i < all.length; i++) { - const pageUrl = all[i][1]; - const title = this._decodeHtml(all[i][2]); - const id = all[i][3]; - - matches.push({ - id, - pageUrl: "watch/" + pageUrl, - title, - jname: "", - normTitle: this._norm(title), - normTitleJP: "", - startDate: { year: 0, month: 0, day: 0 } - }); - } - return matches; - } - - _titleOk(m, targetNorm, targetNormJP) { - if (!m) return false; - const a = m.normTitle || ""; - const b = m.normTitleJP || ""; - if (!a && !b) return false; - - if (a === targetNorm || b === targetNormJP) return true; - - const inc = - a.includes(targetNorm) || - b.includes(targetNormJP) || - targetNorm.includes(a) || - targetNormJP.includes(b); - - if (inc) return true; - - const simA = this._levSim(a, targetNorm); - const simB = this._levSim(b, targetNormJP); - return (simA >= 0.72) || (simB >= 0.72); - } - - _dateOk(m, start, strictMonth) { - if (!start || !start.year) return true; - const y = (m.startDate && m.startDate.year) || 0; - const mo = (m.startDate && m.startDate.month) || 0; - - if (!y) return false; - if (y !== start.year) return false; - - if (strictMonth) { - if (!start.month) return true; - return mo === start.month; - } - return true; - } - - async search(query) { - if (typeof query === "string") { - query = { query, media: { startDate: { year: 0, month: 0, day: 0 } } }; - } - - const q = String((query && query.query) || "").trim(); - if (!q) return []; - - const media = (query && query.media) || {}; - const start = (media && media.startDate) || { year: 0, month: 0, day: 0 }; - - const targetEn = media.englishTitle || media.english || media.titleEnglish || ""; - const targetRo = media.romajiTitle || media.romaji || media.titleRomaji || ""; - - const targetNormJP = this._norm(this._decodeHtml(targetRo)); - const targetNorm = this._norm(this._decodeHtml(targetEn || targetRo || q)); - - const subOrDub = query && query.dub ? "dub" : "sub"; - - let matches = await this._fetchSuggestMatches(q); - if (!matches.length) return []; - - if (start && start.year) { - let filtered = matches.filter(m => this._titleOk(m, targetNorm, targetNormJP) && this._dateOk(m, start, true)); - if (!filtered.length) filtered = matches.filter(m => this._titleOk(m, targetNorm, targetNormJP) && this._dateOk(m, start, false)); - - if (!filtered.length) return []; - - filtered.sort((a, b) => { - const sa = Math.max(this._levSim(a.normTitle, targetNorm), this._levSim(a.normTitleJP, targetNormJP)); - const sb = Math.max(this._levSim(b.normTitle, targetNorm), this._levSim(b.normTitleJP, targetNormJP)); - if (sb !== sa) return sb - sa; - - const am = (a.startDate && a.startDate.month) || 0; - const bm = (b.startDate && b.startDate.month) || 0; - const tm = start.month || 0; - const da = tm ? Math.abs(am - tm) : 0; - const db = tm ? Math.abs(bm - tm) : 0; - return da - db; - }); - - return filtered.map(m => ({ - id: `${m.id}/${subOrDub}`, - title: m.title, - url: `${this.baseUrl}/${m.pageUrl}`, - subOrDub - })); - } - - const queryNorm = this._norm(q); - - let filtered = matches.filter(m => this._titleOk(m, queryNorm, queryNorm)); - if (!filtered.length) { - const pageMatches = await this._fallbackSearchPageMatches(q); - if (!pageMatches.length) return []; - - filtered = pageMatches.filter(m => { - const a = m.normTitle || ""; - if (!a) return false; - if (a === queryNorm) return true; - if (a.includes(queryNorm) || queryNorm.includes(a)) return true; - return this._levSim(a, queryNorm) >= 0.72; - }); - - if (!filtered.length) return []; - - filtered.sort((a, b) => { - const la = (a.normTitle || "").length; - const lb = (b.normTitle || "").length; - if (la !== lb) return la - lb; - return String(a.title || "").localeCompare(String(b.title || "")); - }); - } else { - filtered.sort((a, b) => { - const sa = Math.max(this._levSim(a.normTitle, queryNorm), this._levSim(a.normTitleJP, queryNorm)); - const sb = Math.max(this._levSim(b.normTitle, queryNorm), this._levSim(b.normTitleJP, queryNorm)); - return sb - sa; - }); - } - - return filtered.map(m => ({ - id: `${m.id}/${subOrDub}`, - title: m.title, - url: `${this.baseUrl}/${m.pageUrl}`, - subOrDub - })); - } - - async findEpisodes(animeId) { - const parts = String(animeId || "").split("/"); - const id = parts[0]; - const subOrDub = parts[1] || "sub"; - - const json = await this._getJson( - `${this.baseUrl}/ajax/v2/episode/list/${encodeURIComponent(id)}`, - { "X-Requested-With": "XMLHttpRequest" } - ); - - const html = String((json && json.html) || ""); const episodes = []; - - const regex = - /]*class="[^"]*\bep-item\b[^"]*"[^>]*data-number="(\d+)"[^>]*data-id="(\d+)"[^>]*href="([^"]+)"[\s\S]*?
]*title="([^"]+)"/g; - + const re = /data-number="([^"]+)"[^>]*data-id="([^"]+)"/gi; let m; - while ((m = regex.exec(html)) !== null) { + 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; episodes.push({ - id: `${m[2]}/${subOrDub}`, - number: parseInt(m[1], 10), - url: this.baseUrl + m[3], - title: this._decodeHtml(m[4] || "") + id: epId, + number: num, + title: "", + url: "" }); } - return episodes; - } - - async findEpisodeServer(episode, _server) { - if (typeof episode === "string") { - try { episode = JSON.parse(episode); } catch (e) {} + 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: "" }); + } } - const parts = String((episode && episode.id) || "").split("/"); - const id = parts[0]; - const subOrDub = parts[1] || "sub"; + episodes.sort((a, b) => (a.number || 0) - (b.number || 0)); + return JSON.stringify(episodes); + } - const serverName = _server && _server !== "default" ? String(_server) : "HD-1"; - if (serverName === "HD-4") return null; + findEpisodeServer(episodeObj, serverName) { + let ep = episodeObj; + if (typeof ep === "string") { + try { ep = JSON.parse(ep); } catch (e) { ep = {}; } + } - const serverJson = await this._getJson( - `${this.baseUrl}/ajax/v2/episode/servers?episodeId=${encodeURIComponent(id)}`, - { "X-Requested-With": "XMLHttpRequest" } - ); - const serverHtml = String((serverJson && serverJson.html) || ""); + const epId = this._safeStr(ep && ep.id ? ep.id : "").trim(); + if (!epId) throw new Error("Missing episode id"); - const regex = new RegExp( - `]*class="item server-item"[^>]*data-type="${subOrDub}"[^>]*data-id="(\\\\d+)"[^>]*>\\\\s*]*>\\\\s*${serverName}\\\\s*`, - "i" - ); + const server = String(serverName || "").trim() || "HD-1"; - const mm = regex.exec(serverHtml); - if (!mm) throw new Error(`Server "${serverName}" (${subOrDub}) not found`); - const serverId = mm[1]; + 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 sourcesJson = await this._getJson( + 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(); + + if (!serverId) { + const re2 = /data-id="([^"]+)"/i; + const mm2 = re2.exec(html); + if (mm2) serverId = String(mm2[1] || "").trim(); + } + + if (!serverId) throw new Error("No server id found"); + + const sourcesJson = this._getJson( `${this.baseUrl}/ajax/v2/episode/sources?id=${encodeURIComponent(serverId)}`, { "X-Requested-With": "XMLHttpRequest" } ); @@ -353,14 +301,14 @@ class HiAnime { let requiredHeaders = {}; try { - decryptData = await this.extractMegaCloud(embed); + decryptData = this.extractMegaCloudSync(embed); requiredHeaders = (decryptData && decryptData.headersProvided) ? decryptData.headersProvided : {}; } catch (e) { decryptData = null; } if (!decryptData) { - decryptData = await this._getJson( + decryptData = this._getJson( `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent(embed)}`, {} ); @@ -373,7 +321,9 @@ class HiAnime { }; } - const sources = (decryptData && decryptData.sources) ? decryptData.sources : []; + if (!decryptData || !decryptData.sources) throw new Error("No video sources from any decrypter"); + + const sources = decryptData.sources || []; const streamSource = sources.find((s) => s && s.type === "hls" && s.file) || sources.find((s) => s && s.type === "mp4" && s.file) || @@ -381,7 +331,7 @@ class HiAnime { if (!streamSource || !streamSource.file) throw new Error("No valid stream file found"); - const tracks = (decryptData && decryptData.tracks) ? decryptData.tracks : []; + const tracks = decryptData.tracks || []; const subtitles = (tracks || []) .filter((t) => t && String(t.kind || "").toLowerCase() === "captions" && t.file) .map((track, index) => ({ @@ -391,24 +341,29 @@ class HiAnime { isDefault: !!track.default })); - return { - server: serverName, + const outType = (String(streamSource.type || "").toLowerCase() === "hls") ? "m3u8" : "mp4"; + + return JSON.stringify({ + server: server, headers: requiredHeaders || {}, videoSources: [ { url: String(streamSource.file), - type: String(streamSource.type || "").toLowerCase() === "hls" ? "m3u8" : "mp4", + type: outType, quality: "auto", - subtitles + subtitles: subtitles } ] - }; + }); } - async extractMegaCloud(embedUrl) { - const u = new URL(String(embedUrl)); - const protocol = String(u.protocol || "https:").replace(":", ""); - const host = String(u.host || ""); + extractMegaCloudSync(embedUrl) { + const s = String(embedUrl || ""); + const mm = s.match(/^(https?):\/\/([^\/]+)(\/.*)?$/i); + if (!mm) throw new Error("Invalid embedUrl"); + + const protocol = mm[1].toLowerCase(); + const host = mm[2]; const baseDomain = `${protocol}://${host}/`; const headers = { @@ -420,25 +375,25 @@ class HiAnime { "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 = await this._getText(embedUrl, headers); + const html = this._getText(embedUrl, headers); 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]; - - if (!nonce) { - const match3x16 = [...html.matchAll(/["']([A-Za-z0-9]{16})["']/g)]; - if (match3x16.length >= 3) nonce = match3x16[0][1] + match3x16[1][1] + match3x16[2][1]; + else { + const match3x16 = []; + const re = /["']([A-Za-z0-9]{16})["']/g; + let m; + while ((m = re.exec(html)) !== null) match3x16.push(m[1]); + if (match3x16.length >= 3) nonce = match3x16[0] + match3x16[1] + match3x16[2]; } - if (!nonce) throw new Error("nonce not found"); - const sourcesJson = await this._getJson( + const sourcesJson = this._getJson( `${baseDomain}embed-2/v3/e-1/getSources?id=${encodeURIComponent(fileId)}&_k=${encodeURIComponent(nonce)}`, headers );