From fbbf0c9f34eb2697a1e218fa619a24179e8ba8cb Mon Sep 17 00:00:00 2001 From: MrGus Date: Wed, 24 Dec 2025 18:42:19 +0100 Subject: [PATCH] Update anime/hianime/source.js --- anime/hianime/source.js | 491 ++++++++++++++++++++++------------------ 1 file changed, 269 insertions(+), 222 deletions(-) diff --git a/anime/hianime/source.js b/anime/hianime/source.js index fc56f13..408ab43 100644 --- a/anime/hianime/source.js +++ b/anime/hianime/source.js @@ -1,236 +1,283 @@ class HiAnime { - constructor() { - this.type = "anime-board"; - this.version = "1.0" - this.baseUrl = "https://hianime.to"; + constructor() { + this.type = "anime-board"; + this.version = "1.0"; + this.baseUrl = "https://hianime.to"; + } + + getSettings() { + return { + episodeServers: ["HD-1", "HD-2", "HD-3", "HD-4"], + supportsDub: true, + }; + } + + _nativeFetch(url, method, headers, body) { + const raw = Native.fetch( + String(url), + method || "GET", + JSON.stringify(headers || {}), + body == null ? "" : String(body) + ); + try { + return JSON.parse(raw || "{}"); + } catch (e) { + return { ok: false, status: 0, headers: {}, body: "" }; + } + } + + _getText(url, headers) { + const res = this._nativeFetch(url, "GET", headers, ""); + return String(res.body || ""); + } + + _getJson(url, headers) { + const res = this._nativeFetch(url, "GET", headers, ""); + try { + return JSON.parse(String(res.body || "{}")); + } catch (e) { + return {}; + } + } + + search(query) { + if (typeof query === "string") { + query = { query, media: { startDate: { year: 0, month: 0 } } }; } - getSettings() { - return { - episodeServers: ["HD-1", "HD-2", "HD-3", "HD-4"], - supportsDub: true - }; - } + const normalize = (str) => + this.safeString(str).toLowerCase().replace(/[^a-z0-9]+/g, ""); - async search(query) { - const normalize = (str) => this.safeString(str).toLowerCase().replace(/[^a-z0-9]+/g, ""); + const start = (query.media && query.media.startDate) || { year: 0, month: 0 }; - const start = query.media.startDate; - const fetchMatches = async (url) => { - const html = await fetch(url).then(res => res.text()); + const fetchMatches = (url) => { + const html = this._getText(url, {}); + const regex = /]+title="([^"]+)"[^>]+data-id="(\d+)"/g; - const regex = /]+title="([^"]+)"[^>]+data-id="(\d+)"/g; + return [...html.matchAll(regex)].map((m) => { + const id = m[3]; + const pageUrl = m[1]; + const title = m[2]; - return [...html.matchAll(regex)].map(m => { - const id = m[3]; - const pageUrl = m[1]; - const title = m[2]; + const jnameRegex = new RegExp( + `

[\\s\\S]*?]+href="\\/${pageUrl}[^"]*"[^>]+data-jname="([^"]+)"`, + "i" + ); + const jnameMatch = html.match(jnameRegex); + const jname = jnameMatch ? jnameMatch[1] : null; - const jnameRegex = new RegExp( - `

[\\s\\S]*?]+href="\\/${pageUrl}[^"]*"[^>]+data-jname="([^"]+)"`, - "i" - ); - const jnameMatch = html.match(jnameRegex); - const jname = jnameMatch ? jnameMatch[1] : null; - - const imageRegex = new RegExp( - `]+data-src="([^"]+)"`, - "i" - ); - const imageMatch = html.match(imageRegex); - const image = imageMatch ? imageMatch[1] : null; - - return { - id, - pageUrl, - title, - image, - normTitleJP: normalize(this.normalizeSeasonParts(jname)), - normTitle: normalize(this.normalizeSeasonParts(title)), - }; - }); - }; - - let url = `${this.baseUrl}/search?keyword=${encodeURIComponent(query.query)}&sy=${start.year}&sm=${start.month}&sort=default`; - let matches = await fetchMatches(url); - - if (matches.length === 0) return []; - - return matches.map(m => ({ - id: `${m.id}/${query.dub ? "dub" : "sub"}`, - title: m.title, - image: m.image, - url: `${this.baseUrl}/${m.pageUrl}`, - subOrDub: query.dub ? "dub" : "sub" - })); - } - - async findEpisodes(animeId) { - const [id, subOrDub] = animeId.split("/"); - const res = await fetch(`${this.baseUrl}/ajax/v2/episode/list/${id}`, { - headers: { "X-Requested-With": "XMLHttpRequest" } - }); - const json = await res.json(); - const html = json.html; - console.log(html) - - - 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: match[4], - }); - } - - return episodes; - } - - async findEpisodeServer(episode, _server) { - const [id, subOrDub] = episode.id.split("/"); - let serverName = _server !== "default" ? _server : "HD-1"; - - if (_server === "HD-1" || _server === "HD-2" || _server === "HD-3") { - const serverJson = await fetch(`${this.baseUrl}/ajax/v2/episode/servers?episodeId=${id}`, { - headers: { "X-Requested-With": "XMLHttpRequest" } - }).then(res => res.json()); - - const serverHtml = serverJson.html; - const regex = new RegExp( - `]*class="item server-item"[^>]*data-type="${subOrDub}"[^>]*data-id="(\\d+)"[^>]*>\\s*]*>\\s*${serverName}\\s*`, - "i" - ); - - const match = regex.exec(serverHtml); - if (!match) throw new Error(`Server "${serverName}" (${subOrDub}) not found`); - - const serverId = match[1]; - - const sourcesJson = await fetch(`${this.baseUrl}/ajax/v2/episode/sources?id=${serverId}`, { - headers: { "X-Requested-With": "XMLHttpRequest" } - }).then(res => res.json()); - - let decryptData = null; - let requiredHeaders = {}; - - try { - // Pass true to get headers back - decryptData = await this.extractMegaCloud(sourcesJson.link, true); - if (decryptData && decryptData.headersProvided) { - requiredHeaders = decryptData.headersProvided; - } - } catch (err) { - console.warn("Primary decrypter failed:", err); - } - - if (!decryptData) { - console.warn("Primary decrypter failed — trying ShadeOfChaos fallback..."); - const fallbackRes = await fetch( - `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent(sourcesJson.link)}` - ); - decryptData = await fallbackRes.json(); - - // CRITICAL: Fallback headers must mimic the browser behavior expected by the provider - // These MUST be used by a server-side proxy; the browser player cannot set them. - 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 streamSource = - decryptData.sources.find((s) => s.type === "hls") || - decryptData.sources.find((s) => s.type === "mp4"); - - if (!streamSource?.file) throw new Error("No valid stream file found"); - - const subtitles = (decryptData.tracks || []) - .filter((t) => t.kind === "captions") - .map((track, index) => ({ - id: `sub-${index}`, - language: track.label || "Unknown", - url: track.file, - isDefault: !!track.default, - })); - - return { - server: serverName, - headers: requiredHeaders, - videoSources: [{ - url: streamSource.file, - type: streamSource.type === "hls" ? "m3u8" : "mp4", - quality: "auto", - subtitles - }] - }; - } - else if (_server === "HD-4") { - // Implementation for HD-4 if needed - return null; - } - } - - 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, ""); - } - - async extractMegaCloud(embedUrl, returnHeaders = false) { - 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 html = await fetch(embedUrl, { headers }).then((r) => r.text()); - 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 sourcesJson = await fetch( - `${baseDomain}embed-2/v3/e-1/getSources?id=${fileId}&_k=${nonce}`, - { headers } - ).then((r) => r.json()); + const imageRegex = new RegExp( + `<a href="/watch/${pageUrl.replace(/\//g, "\\/")}"[\\s\\S]*?<img[^>]+data-src="([^"]+)"`, + "i" + ); + const imageMatch = html.match(imageRegex); + const image = imageMatch ? imageMatch[1] : null; return { - sources: sourcesJson.sources, - tracks: sourcesJson.tracks || [], - intro: sourcesJson.intro || null, - outro: sourcesJson.outro || null, - server: sourcesJson.server || null, - headersProvided: returnHeaders ? headers : undefined + id, + pageUrl, + title, + image, + normTitleJP: normalize(this.normalizeSeasonParts(jname)), + normTitle: normalize(this.normalizeSeasonParts(title)), }; + }); + }; + + const url = + `${this.baseUrl}/search?keyword=${encodeURIComponent(query.query)}` + + `&sy=${start.year || 0}&sm=${start.month || 0}&sort=default`; + + const matches = fetchMatches(url); + if (!matches.length) return []; + + const subOrDub = query.dub ? "dub" : "sub"; + return matches.map((m) => ({ + id: `${m.id}/${subOrDub}`, + title: m.title, + image: m.image, + url: `${this.baseUrl}/${m.pageUrl}`, + subOrDub, + })); + } + + findEpisodes(animeId) { + const parts = String(animeId).split("/"); + const id = parts[0]; + const subOrDub = parts[1] || "sub"; + + const json = this._getJson(`${this.baseUrl}/ajax/v2/episode/list/${id}`, { + "X-Requested-With": "XMLHttpRequest", + }); + + const html = String(json.html || ""); + const episodes = []; + const regex = + /<a[^>]*class="[^"]*\bep-item\b[^"]*"[^>]*data-number="(\d+)"[^>]*data-id="(\d+)"[^>]*href="([^"]+)"[\s\S]*?<div class="ep-name[^"]*"[^>]*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: match[4], + }); } + + return episodes; + } + + findEpisodeServer(episode, _server) { + if (typeof episode === "string") { + try { + episode = JSON.parse(episode); + } catch (e) {} + } + + const parts = String((episode && episode.id) || "").split("/"); + const id = parts[0]; + const subOrDub = parts[1] || "sub"; + const serverName = _server !== "default" ? _server : "HD-1"; + + if (_server === "HD-4") return null; + + const serverJson = this._getJson( + `${this.baseUrl}/ajax/v2/episode/servers?episodeId=${id}`, + { "X-Requested-With": "XMLHttpRequest" } + ); + + const serverHtml = String(serverJson.html || ""); + const regex = new RegExp( + `<div[^>]*class="item server-item"[^>]*data-type="${subOrDub}"[^>]*data-id="(\\d+)"[^>]*>\\s*<a[^>]*>\\s*${serverName}\\s*</a>`, + "i" + ); + + const match = regex.exec(serverHtml); + if (!match) throw new Error(`Server "${serverName}" (${subOrDub}) not found`); + + const serverId = match[1]; + + const sourcesJson = this._getJson( + `${this.baseUrl}/ajax/v2/episode/sources?id=${serverId}`, + { "X-Requested-With": "XMLHttpRequest" } + ); + + let decryptData = null; + let requiredHeaders = {}; + + try { + decryptData = this.extractMegaCloudSync(sourcesJson.link); + requiredHeaders = decryptData.headersProvided || {}; + } catch (e) {} + + if (!decryptData) { + decryptData = this._getJson( + `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent( + sourcesJson.link + )}`, + {} + ); + 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 sources = decryptData.sources || []; + const streamSource = + sources.find((s) => s.type === "hls") || sources.find((s) => s.type === "mp4"); + + if (!streamSource || !streamSource.file) throw new Error("No valid stream file found"); + + const subtitles = (decryptData.tracks || []) + .filter((t) => t.kind === "captions") + .map((track, index) => ({ + id: `sub-${index}`, + language: track.label || "Unknown", + url: track.file, + isDefault: !!track.default, + })); + + return { + server: serverName, + headers: requiredHeaders, + videoSources: [ + { + url: streamSource.file, + type: streamSource.type === "hls" ? "m3u8" : "mp4", + quality: "auto", + subtitles, + }, + ], + }; + } + + 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, ""); + } + + extractMegaCloudSync(embedUrl) { + const s = String(embedUrl); + const mm = s.match(/^(https?):\/\/([^\/]+)(\/.*)?$/i); + if (!mm) throw new Error("Invalid embedUrl: " + s); + + const protocol = mm[1].toLowerCase(); + const host = mm[2]; + const baseDomain = `${protocol}://${host}/`; + + const headers = { + 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", + }; + + const html = this._getText(embedUrl, headers); + + const fileIdMatch = html.match(/<title>\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 sourcesJson = this._getJson( + `${baseDomain}embed-2/v3/e-1/getSources?id=${fileId}&_k=${nonce}`, + headers + ); + + return { + sources: sourcesJson.sources || [], + tracks: sourcesJson.tracks || [], + intro: sourcesJson.intro || null, + outro: sourcesJson.outro || null, + server: sourcesJson.server || null, + headersProvided: headers, + }; + } } -module.exports = HiAnime; \ No newline at end of file +module.exports = HiAnime;