From 574eb30d4d5f8d98151113b51e38a1dfb5b3bcfc Mon Sep 17 00:00:00 2001 From: MrGus Date: Sun, 21 Dec 2025 16:16:23 +0100 Subject: [PATCH] Update anime/hianime/source.js --- anime/hianime/source.js | 875 ++++++++++++++++++++++------------------ 1 file changed, 493 insertions(+), 382 deletions(-) diff --git a/anime/hianime/source.js b/anime/hianime/source.js index d329473..ac81aad 100644 --- a/anime/hianime/source.js +++ b/anime/hianime/source.js @@ -1,413 +1,524 @@ class HiAnime { - constructor() { - this.type = "anime-streaming"; - this.version = "1.0"; - this.baseUrl = "https://hianime.to"; - console.log("[HiAnime] Constructor initialized"); - } + constructor() { + this.type = "anime-streaming"; + this.version = "1.0"; + this.baseUrl = "https://hianime.to"; + console.log("[HiAnime] Constructor initialized"); + } + + getSettings() { + console.log("[HiAnime] getSettings called"); + return { + episodeServers: ["HD-1", "HD-2", "HD-3", "HD-4"], + supportsDub: true + }; + } + + safeString(str) { + return typeof str === "string" ? str : ""; + } + + decodeHtml(s) { + return this.safeString(s) + .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, "<") + .replace(/>/g, ">"); + } + + normalizeKey(s) { + return this.decodeHtml(this.safeString(s)) + .toLowerCase() + .replace(/[^a-z0-9]+/g, ""); + } + + scoreTitle(queryKey, title) { + const t = this.normalizeKey(title); + if (!t) return 0; + if (t === queryKey) return 1000; + if (t.includes(queryKey) || queryKey.includes(t)) return 700; + + const qTokens = queryKey.match(/[a-z0-9]+/g) || []; + const tTokens = t.match(/[a-z0-9]+/g) || []; + let inter = 0; + for (const tok of qTokens) { + if (tTokens.includes(tok)) inter++; + } + return inter * 50; + } + + fetchAjax(url, extraHeaders) { + const headers = Object.assign( + { "X-Requested-With": "XMLHttpRequest" }, + extraHeaders || {} + ); + return fetch(url, { headers }); + } + + search(query) { + console.log("[HiAnime] search called with:", JSON.stringify(query)); + + try { + let searchQuery = query; + + if (typeof query === "string") { + try { + searchQuery = JSON.parse(query); + } catch (e) { + searchQuery = { query: query, dub: false }; + } + } + + console.log("[HiAnime] Parsed search query:", JSON.stringify(searchQuery)); + + const queryText = searchQuery.query || searchQuery.title || ""; + if (!queryText) { + console.error("[HiAnime] No query text provided"); + return []; + } + + 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}`; + + console.log("[HiAnime] Fetching URL:", url); + + const response = fetch(url); + console.log("[HiAnime] Fetch response status:", response.status); + + const html = response.text(); + console.log("[HiAnime] HTML length:", html.length); + + const regex = + /]*href="\/watch\/([^"]+)"[^>]*title="([^"]+)"[^>]*data-id="(\d+)"/gi; + const matches = [...html.matchAll(regex)]; + console.log("[HiAnime] Found", matches.length, "raw matches"); + + const dubFlag = !!searchQuery.dub; + const queryKey = this.normalizeKey(queryText); + + let results = matches.map((m, idx) => { + const watchSlug = m[1]; + const title = this.decodeHtml(m[2]); + const idNum = m[3]; + + console.log( + `[HiAnime] Match ${idx + 1}: id=${idNum}, title="${title}"` + ); + + 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; - getSettings() { - console.log("[HiAnime] getSettings called"); return { - episodeServers: ["HD-1", "HD-2", "HD-3", "HD-4"], - supportsDub: true + 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)); + + results = results.map((r) => { + const out = Object.assign({}, r); + delete out._score; + return out; + }); + + console.log( + "[HiAnime] Returning", + results.length, + "results (sorted best-first)" + ); + if (results[0]) + console.log("[HiAnime] Top result:", results[0].id, results[0].title); + + return results; + } catch (error) { + console.error("[HiAnime] search error:", error); + throw error; } + } - search(query) { - console.log("[HiAnime] search called with:", JSON.stringify(query)); - - try { - const normalize = (str) => this.safeString(str).toLowerCase().replace(/[^a-z0-9]+/g, ""); + findEpisodes(animeId) { + console.log("[HiAnime] findEpisodes called with:", animeId); - let searchQuery = query; - if (typeof query === "string") { - try { - searchQuery = JSON.parse(query); - } catch (e) { - searchQuery = { query: query, dub: false }; - } - } + try { + let id, subOrDub; - console.log("[HiAnime] Parsed search query:", JSON.stringify(searchQuery)); - - const queryText = searchQuery.query || searchQuery.title || ""; - if (!queryText) { - console.error("[HiAnime] No query text provided"); - return []; - } - - const media = searchQuery.media || {}; - const startDate = media.startDate || {}; - const year = startDate.year || new Date().getFullYear(); - const month = startDate.month || 1; - - const url = `${this.baseUrl}/search?keyword=${encodeURIComponent(queryText)}&sy=${year}&sm=${month}&sort=default`; - console.log("[HiAnime] Fetching URL:", url); - - const response = fetch(url); - console.log("[HiAnime] Fetch response status:", response.status); - - const html = response.text(); - console.log("[HiAnime] HTML length:", html.length); - - const regex = /]+title="([^"]+)"[^>]+data-id="(\d+)"/g; - const matches = [...html.matchAll(regex)]; - console.log("[HiAnime] Found", matches.length, "raw matches"); - - const results = matches.map((m, idx) => { - const id = m[3]; - const pageUrl = m[1]; - const title = m[2]; - - console.log(`[HiAnime] Match ${idx + 1}: id=${id}, title="${title}"`); - - 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: `${id}/${searchQuery.dub ? "dub" : "sub"}`, - title: title, - image: image, - url: `${this.baseUrl}/${pageUrl}`, - subOrDub: searchQuery.dub ? "dub" : "sub" - }; - }); - - console.log("[HiAnime] Returning", results.length, "results"); - return results; - - } catch (error) { - console.error("[HiAnime] search error:", error); - throw error; + 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"; + } - findEpisodes(animeId) { - console.log("[HiAnime] findEpisodes called with:", 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"; - } - } - } else if (typeof animeId === "object") { - 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("/"); - } + console.log( + "[HiAnime] Parsed episode params: id=", + id, + "subOrDub=", + subOrDub + ); - console.log("[HiAnime] Parsed episode params: id=", id, "subOrDub=", subOrDub); + const url = `${this.baseUrl}/ajax/v2/episode/list/${id}`; + console.log("[HiAnime] Fetching episodes from:", url); - const url = `${this.baseUrl}/ajax/v2/episode/list/${id}`; - console.log("[HiAnime] Fetching episodes from:", url); + const response = this.fetchAjax(url); + console.log("[HiAnime] Episodes fetch status:", response.status); - const response = fetch(url, { - headers: { "X-Requested-With": "XMLHttpRequest" } - }); + const json = response.json(); + console.log("[HiAnime] Episodes JSON keys:", Object.keys(json).join(", ")); - console.log("[HiAnime] Episodes fetch status:", response.status); + const html = this.safeString(json.html); + console.log("[HiAnime] Episodes HTML length:", html.length); - const json = response.json(); - console.log("[HiAnime] Episodes JSON keys:", Object.keys(json).join(", ")); + const episodes = []; - const html = json.html || ""; - console.log("[HiAnime] Episodes HTML length:", html.length); + const regex = + /]*\bep-item\b[^>]*data-number="(\d+)"[^>]*data-id="(\d+)"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi; - const episodes = []; - const regex = /]*class="[^"]*\bep-item\b[^"]*"[^>]*data-number="(\d+)"[^>]*data-id="(\d+)"[^>]*href="([^"]+)"[\s\S]*?
]*title="([^"]+)"/g; + let match; + let matchCount = 0; - let match; - let matchCount = 0; - while ((match = regex.exec(html)) !== null) { - matchCount++; - const episode = { - id: `${match[2]}/${subOrDub}`, - number: parseInt(match[1], 10), - url: this.baseUrl + match[3], - title: match[4], - }; - console.log(`[HiAnime] Episode ${matchCount}: num=${episode.number}, id=${episode.id}, title="${episode.title}"`); - episodes.push(episode); - } + while ((match = regex.exec(html)) !== null) { + matchCount++; - console.log("[HiAnime] Total episodes found:", episodes.length); - return episodes; + const number = parseInt(match[1], 10); + const epId = match[2]; + const href = match[3]; + const inner = match[4] || ""; - } catch (error) { - console.error("[HiAnime] findEpisodes error:", error); - throw error; + 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}`; + + const episode = { + id: `${epId}/${subOrDub}`, + number, + url: this.baseUrl + href, + title + }; + + console.log( + `[HiAnime] Episode ${matchCount}: num=${episode.number}, id=${episode.id}, title="${episode.title}"` + ); + episodes.push(episode); + } + + console.log("[HiAnime] Total episodes found:", episodes.length); + + if (episodes.length === 0) { + console.warn( + "[HiAnime] No episodes parsed. HTML preview:", + html.slice(0, 300) + ); + } + + return episodes; + } catch (error) { + console.error("[HiAnime] findEpisodes error:", error); + throw error; } + } - findEpisodeServer(episode, _server) { - console.log("[HiAnime] findEpisodeServer called with episode:", JSON.stringify(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"; - } - } - } else if (typeof episode === "object") { - const epId = episode.id || episode.episodeId || ""; - if (epId.includes("/")) { - [episodeId, subOrDub] = epId.split("/"); - } else { - episodeId = epId; - subOrDub = episode.subOrDub || "sub"; - } - } else { - episodeId = String(episode); - subOrDub = "sub"; - } + findEpisodeServer(episode, _server) { + console.log( + "[HiAnime] findEpisodeServer called with episode:", + JSON.stringify(episode), + "server:", + _server + ); - if (episodeId && episodeId.includes("/")) { - [episodeId, subOrDub] = episodeId.split("/"); - } + try { + let episodeId, subOrDub; - console.log("[HiAnime] Parsed server params: episodeId=", episodeId, "subOrDub=", subOrDub); - - let serverName = (_server && _server !== "default") ? _server : "HD-1"; - console.log("[HiAnime] Using server:", serverName); - - if (_server === "HD-4") { - console.log("[HiAnime] HD-4 not supported, returning null"); - return null; - } - - const serversUrl = `${this.baseUrl}/ajax/v2/episode/servers?episodeId=${episodeId}`; - console.log("[HiAnime] Fetching servers from:", serversUrl); - - const serverResponse = fetch(serversUrl, { - headers: { "X-Requested-With": "XMLHttpRequest" } - }); - - console.log("[HiAnime] Servers fetch status:", serverResponse.status); - - const serverJson = serverResponse.json(); - const serverHtml = serverJson.html || ""; - console.log("[HiAnime] Server HTML length:", serverHtml.length); - - 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) { - console.error(`[HiAnime] Server "${serverName}" (${subOrDub}) not found in HTML`); - throw new Error(`Server "${serverName}" (${subOrDub}) not found`); - } - - const serverId = match[1]; - console.log("[HiAnime] Found serverId:", serverId); - - const sourcesUrl = `${this.baseUrl}/ajax/v2/episode/sources?id=${serverId}`; - console.log("[HiAnime] Fetching sources from:", sourcesUrl); - - const sourcesResponse = fetch(sourcesUrl, { - headers: { "X-Requested-With": "XMLHttpRequest" } - }); - - console.log("[HiAnime] Sources fetch status:", sourcesResponse.status); - - const sourcesJson = sourcesResponse.json(); - console.log("[HiAnime] Sources JSON link:", sourcesJson.link); - - let decryptData = null; - let requiredHeaders = {}; - - try { - console.log("[HiAnime] Attempting primary decrypter..."); - decryptData = this.extractMegaCloud(sourcesJson.link, true); - if (decryptData && decryptData.headersProvided) { - requiredHeaders = decryptData.headersProvided; - } - console.log("[HiAnime] Primary decrypter succeeded"); - } catch (err) { - console.warn("[HiAnime] Primary decrypter failed:", err); - } - - if (!decryptData) { - console.log("[HiAnime] Trying fallback API..."); - const fallbackUrl = `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent(sourcesJson.link)}`; - const fallbackResponse = fetch(fallbackUrl); - console.log("[HiAnime] Fallback fetch status:", fallbackResponse.status); - - 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 succeeded"); - } - - const streamSource = - (decryptData.sources || []).find((s) => s.type === "hls") || - (decryptData.sources || []).find((s) => s.type === "mp4"); - - if (!streamSource || !streamSource.file) { - console.error("[HiAnime] No valid stream file found in sources"); - throw new Error("No valid stream file found"); - } - - console.log("[HiAnime] Stream URL:", streamSource.file); - - 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, - })); - - console.log("[HiAnime] Found", subtitles.length, "subtitles"); - - const result = { - server: serverName, - headers: requiredHeaders, - videoSources: [{ - url: streamSource.file, - type: streamSource.type === "hls" ? "m3u8" : "mp4", - quality: "auto", - subtitles: subtitles - }] - }; - - console.log("[HiAnime] Returning server result"); - return result; - - } catch (error) { - console.error("[HiAnime] findEpisodeServer error:", error); - throw error; + 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"; + } } - } - - 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, ""); - } - - extractMegaCloud(embedUrl, returnHeaders) { - console.log("[HiAnime] extractMegaCloud called with:", embedUrl); - - try { - 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 response = fetch(embedUrl, { headers: headers }); - console.log("[HiAnime] MegaCloud embed fetch status:", response.status); - - const html = response.text(); - console.log("[HiAnime] MegaCloud HTML length:", html.length); - - const fileIdMatch = html.match(/\s*File\s+#([a-zA-Z0-9]+)\s*-/i); - if (!fileIdMatch) { - console.error("[HiAnime] file_id not found in embed page"); - throw new Error("file_id not found in embed page"); - } - const fileId = fileIdMatch[1]; - console.log("[HiAnime] Found fileId:", fileId); - - let nonce = null; - const match48 = html.match(/\b[a-zA-Z0-9]{48}\b/); - if (match48) { - nonce = match48[0]; - console.log("[HiAnime] Found 48-char nonce"); - } 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]; - console.log("[HiAnime] Found 3x16-char nonce"); - } - } - - if (!nonce) { - console.error("[HiAnime] nonce not found"); - throw new Error("nonce not found"); - } - - const sourcesUrl = `${baseDomain}embed-2/v3/e-1/getSources?id=${fileId}&_k=${nonce}`; - console.log("[HiAnime] Fetching sources from:", sourcesUrl); - - const sourcesResponse = fetch(sourcesUrl, { headers: headers }); - console.log("[HiAnime] Sources fetch status:", sourcesResponse.status); - - const sourcesJson = sourcesResponse.json(); - console.log("[HiAnime] Sources has", (sourcesJson.sources || []).length, "sources"); - - return { - sources: sourcesJson.sources || [], - tracks: sourcesJson.tracks || [], - intro: sourcesJson.intro || null, - outro: sourcesJson.outro || null, - server: sourcesJson.server || null, - headersProvided: returnHeaders ? headers : undefined - }; - - } catch (error) { - console.error("[HiAnime] extractMegaCloud error:", error); - throw error; + } 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 { + episodeId = String(episode); + subOrDub = "sub"; + } + + if (episodeId && episodeId.includes("/")) + [episodeId, subOrDub] = episodeId.split("/"); + + console.log( + "[HiAnime] Parsed server params: episodeId=", + episodeId, + "subOrDub=", + subOrDub + ); + + let serverName = + _server && _server !== "default" ? _server : "HD-1"; + console.log("[HiAnime] Using server:", serverName); + + const serversUrl = `${this.baseUrl}/ajax/v2/episode/servers?episodeId=${episodeId}`; + console.log("[HiAnime] Fetching servers from:", serversUrl); + + const serverResponse = this.fetchAjax(serversUrl); + console.log("[HiAnime] Servers fetch status:", serverResponse.status); + + const serverJson = serverResponse.json(); + const serverHtml = this.safeString(serverJson.html); + console.log("[HiAnime] Server HTML length:", serverHtml.length); + + const escapedName = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + + const blockRe = new RegExp( + `<div[^>]*\\bserver-item\\b[^>]*data-type="${subOrDub}"[^>]*data-id="(\\d+)"[\\s\\S]*?>[\\s\\S]*?${escapedName}[\\s\\S]*?<\\/div>`, + "i" + ); + + let match = blockRe.exec(serverHtml); + + if (!match) { + const altRe = new RegExp( + `data-type="${subOrDub}"[^>]*data-id="(\\d+)"[\\s\\S]*?>[\\s\\S]*?<a[^>]*>[\\s\\S]*?${escapedName}[\\s\\S]*?<\\/a>`, + "i" + ); + match = altRe.exec(serverHtml); + } + + if (!match) { + console.error( + `[HiAnime] Server "${serverName}" (${subOrDub}) not found in HTML` + ); + console.warn( + "[HiAnime] Server HTML preview:", + serverHtml.slice(0, 300) + ); + throw new Error(`Server "${serverName}" (${subOrDub}) not found`); + } + + const serverId = match[1]; + console.log("[HiAnime] Found serverId:", serverId); + + const sourcesUrl = `${this.baseUrl}/ajax/v2/episode/sources?id=${serverId}`; + console.log("[HiAnime] Fetching sources from:", sourcesUrl); + + const sourcesResponse = this.fetchAjax(sourcesUrl); + console.log("[HiAnime] Sources fetch status:", sourcesResponse.status); + + const sourcesJson = sourcesResponse.json(); + console.log("[HiAnime] Sources JSON link:", sourcesJson.link); + + let decryptData = null; + let requiredHeaders = {}; + + try { + console.log("[HiAnime] Attempting primary decrypter..."); + decryptData = this.extractMegaCloud(sourcesJson.link, true); + if (decryptData && decryptData.headersProvided) { + requiredHeaders = decryptData.headersProvided; + } + console.log("[HiAnime] Primary decrypter succeeded"); + } catch (err) { + console.warn("[HiAnime] Primary decrypter failed:", err); + } + + if (!decryptData) { + console.log("[HiAnime] Trying fallback API..."); + const fallbackUrl = `https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent( + sourcesJson.link + )}`; + const fallbackResponse = fetch(fallbackUrl); + console.log("[HiAnime] Fallback fetch status:", fallbackResponse.status); + + 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 succeeded"); + } + + const sourcesArr = decryptData && decryptData.sources ? decryptData.sources : []; + const streamSource = + sourcesArr.find((s) => s && s.type === "hls") || + sourcesArr.find((s) => s && s.type === "mp4"); + + if (!streamSource || !streamSource.file) { + console.error("[HiAnime] No valid stream file found in sources"); + throw new Error("No valid stream file found"); + } + + console.log("[HiAnime] Stream URL:", streamSource.file); + + 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] Found", subtitles.length, "subtitles"); + + const result = { + server: serverName, + headers: requiredHeaders, + videoSources: [ + { + url: streamSource.file, + type: streamSource.type === "hls" ? "m3u8" : "mp4", + quality: "auto", + subtitles + } + ] + }; + + console.log("[HiAnime] Returning server result"); + return result; + } catch (error) { + console.error("[HiAnime] findEpisodeServer error:", error); + throw error; } + } + + extractMegaCloud(embedUrl, returnHeaders) { + console.log("[HiAnime] extractMegaCloud called with:", embedUrl); + + try { + 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 response = fetch(embedUrl, { headers }); + console.log("[HiAnime] MegaCloud embed fetch status:", response.status); + + const html = response.text(); + console.log("[HiAnime] MegaCloud HTML length:", html.length); + + const fileIdMatch = html.match(/<title>\s*File\s+#([a-zA-Z0-9]+)\s*-/i); + if (!fileIdMatch) { + console.error("[HiAnime] file_id not found in embed page"); + throw new Error("file_id not found in embed page"); + } + const fileId = fileIdMatch[1]; + console.log("[HiAnime] Found fileId:", fileId); + + let nonce = null; + const match48 = html.match(/\b[a-zA-Z0-9]{48}\b/); + if (match48) { + nonce = match48[0]; + console.log("[HiAnime] Found 48-char nonce"); + } 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]; + console.log("[HiAnime] Found 3x16-char nonce"); + } + } + + if (!nonce) { + console.error("[HiAnime] nonce not found"); + throw new Error("nonce not found"); + } + + const sourcesUrl = `${baseDomain}embed-2/v3/e-1/getSources?id=${fileId}&_k=${nonce}`; + console.log("[HiAnime] Fetching sources from:", sourcesUrl); + + const sourcesResponse = fetch(sourcesUrl, { headers }); + console.log("[HiAnime] Sources fetch status:", sourcesResponse.status); + + const sourcesJson = sourcesResponse.json(); + console.log( + "[HiAnime] Sources has", + (sourcesJson.sources || []).length, + "sources" + ); + + return { + sources: sourcesJson.sources || [], + tracks: sourcesJson.tracks || [], + intro: sourcesJson.intro || null, + outro: sourcesJson.outro || null, + server: sourcesJson.server || null, + headersProvided: returnHeaders ? headers : undefined + }; + } catch (error) { + console.error("[HiAnime] extractMegaCloud error:", error); + throw error; + } + } } -module.exports = HiAnime; \ No newline at end of file +module.exports = HiAnime;