class HiAnime { constructor() { this.type = "anime-streaming"; this.version = "1.0.0"; this.baseUrl = "https://hianime.to"; } getSettings() { return { episodeServers: ["HD-1", "HD-2", "HD-3", "HD-4"], supportsSub: true, supportsDub: true, supportsHls: 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, day: 0 } } }; const start = (query.media && query.media.startDate) || { year: 0, month: 0, day: 0 }; const sy = start.year || 0; const sm = start.month || 0; const sd = start.day || 0; const url = `${this.baseUrl}/search?keyword=${encodeURIComponent(query.query)}` + `&sy=${sy}&sm=${sm}` + (sd ? `&sd=${sd}` : ``) + `&sort=default`; const html = this._getText(url, {}); const regex = /]+title="([^"]+)"[^>]+data-id="(\d+)"/g; const matches = [...html.matchAll(regex)].map((m) => { const id = m[3]; const pageUrl = m[1]; const title = m[2]; const imageRegex = new RegExp( `]+data-src="([^"]+)"`, "i" ); const imageMatch = html.match(imageRegex); const image = imageMatch ? imageMatch[1] : null; return { id, pageUrl, title, image }; }); 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}/watch/${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 = /]*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; } 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( `]*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 = 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 } ] }; } 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(/\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 = new HiAnime();