class HiAnime { constructor() { this.type = "anime-board"; this.version = "1.1" this.baseUrl = "https://hianime.to"; } 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, ""); const start = query.media.startDate; const fetchMatches = async (url) => { const html = await fetch(url).then(res => res.text()); 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]; 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, category = "sub") { const id = episode.id; const subOrDub = category; // backend manda sub | dub const serverName = _server !== "default" ? _server : "HD-1"; if (serverName === "HD-1" || serverName === "HD-2" || serverName === "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; let requiredHeaders = {}; try { decryptData = await this.extractMegaCloud(sourcesJson.link, true); if (decryptData?.headersProvided) { requiredHeaders = decryptData.headersProvided; } } catch (e) { console.warn("Primary decrypter failed:", e); } 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, i) => ({ id: `sub-${i}`, 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, subOrDub: category, }, ], }; } if (serverName === "HD-4") { throw new Error("HD-4 not implemented"); } throw new Error(`Unknown server ${serverName}`); } 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 { 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;