From 5a3d2285050586bf158e5a4d94f2ceaa9f9d78eb Mon Sep 17 00:00:00 2001 From: MrGus Date: Sun, 21 Dec 2025 14:25:26 +0100 Subject: [PATCH] Update anime/hianime/source.js --- anime/hianime/source.js | 448 +++++++++++++++++++--------------------- 1 file changed, 218 insertions(+), 230 deletions(-) diff --git a/anime/hianime/source.js b/anime/hianime/source.js index 06abbde..cff12f2 100644 --- a/anime/hianime/source.js +++ b/anime/hianime/source.js @@ -1,244 +1,232 @@ class HiAnime { - constructor() { - this.type = "anime-streaming"; - this.version = "1.0.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 - }; - } + getSettings() { + return { + episodeServers: ["HD-1", "HD-2", "HD-3", "HD-4"], + supportsDub: true + }; + } - async search(query) { - 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.startDate; + const start = query.media.startDate; + const fetchMatches = async (url) => { + const html = await fetch(url).then(res => res.text()); - const fetchMatches = async (url) => { - const html = await fetch(url).then((res) => res.text()); - 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; + 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 { + 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(); + + 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") { + 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()); return { - id, - pageUrl, - title, - image, - normTitleJP: normalize(this.normalizeSeasonParts(jname)), - normTitle: normalize(this.normalizeSeasonParts(title)) + sources: sourcesJson.sources, + tracks: sourcesJson.tracks || [], + intro: sourcesJson.intro || null, + outro: sourcesJson.outro || null, + server: sourcesJson.server || null, + headersProvided: returnHeaders ? headers : undefined }; - }); - }; - - const url = - `${this.baseUrl}/search?keyword=${encodeURIComponent(query.query)}` + - `&sy=${start.year}&sm=${start.month}&sort=default`; - - const 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; - - 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; - } - - async findEpisodeServer(episode, _server) { - const [id, subOrDub] = episode.id.split("/"); - const serverName = _server !== "default" ? _server : "HD-1"; - - if (_server === "HD-1" || _server === "HD-2" || _server === "HD-3" || _server === "default") { - 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( - `<div[^>]*class="item server-item"[^>]*data-type="${subOrDub}"[^>]*data-id="(\\d+)"[^>]*>\\s*<a[^>]*>\\s*${serverName}\\s*</a>`, - "i" - ); - - const m = regex.exec(serverHtml); - if (!m) throw new Error(`Server "${serverName}" (${subOrDub}) not found`); - - const serverId = m[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 { - decryptData = await this.extractMegaCloud(sourcesJson.link, true); - if (decryptData && decryptData.headersProvided) requiredHeaders = decryptData.headersProvided; - } catch (err) { - // ignore - } - - if (!decryptData) { - const fallbackRes = await fetch( - `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent( - sourcesJson.link - )}` - ); - decryptData = await fallbackRes.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" - }; - } - - 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 - } - ] - }; - } - - if (_server === "HD-4") { - return null; - } - - 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(/<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 = await fetch( - `${baseDomain}embed-2/v3/e-1/getSources?id=${fileId}&_k=${nonce}`, - { headers } - ).then((r) => r.json()); - - 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; +module.exports = HiAnime; \ No newline at end of file