class HiAnime { 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 }; } async search(query) { console.log("[HiAnime] search called with:", JSON.stringify(query)); try { const normalize = (str) => this.safeString(str).toLowerCase().replace(/[^a-z0-9]+/g, ""); 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 || 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; } } async 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("/"); } 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 response = fetch(url, { headers: { "X-Requested-With": "XMLHttpRequest" } }); console.log("[HiAnime] Episodes fetch status:", response.status); const json = response.json(); console.log("[HiAnime] Episodes JSON keys:", Object.keys(json).join(", ")); const html = json.html || ""; console.log("[HiAnime] Episodes HTML length:", html.length); const episodes = []; const regex = /]*class="[^"]*\bep-item\b[^"]*"[^>]*data-number="(\d+)"[^>]*data-id="(\d+)"[^>]*href="([^"]+)"[\s\S]*?
]*title="([^"]+)"/g; 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); } console.log("[HiAnime] Total episodes found:", episodes.length); return episodes; } catch (error) { console.error("[HiAnime] findEpisodes error:", error); throw error; } } async 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"; } 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); 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; } } 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; } } } module.exports = HiAnime;