class AnimeKai { constructor() { this.baseUrl = "https://animekai.to"; this.type = "anime-board"; this.version = "1.0"; this.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"; } getSettings() { return { episodeServers: ["Server 1", "Server 2"], supportsDub: true, }; } async search(queryObj) { const query = queryObj.query; const dubParam = queryObj.dub || ""; const normalizedQuery = this.normalizeQuery(query); console.log("Normalized Query: " + normalizedQuery); const url = `${this.baseUrl}/browser?keyword=${encodeURIComponent(normalizedQuery)}`; try { const data = await this.GETText(url); const $ = this.cheerio.load(data); const animes = []; $("div.aitem-wrapper>div.aitem").each((_, elem) => { const el = $(elem); const linkHref = el.find("a.poster").attr("href"); const idRaw = linkHref ? linkHref.slice(1) : ""; const title = el.find("a.title").attr("title") || ""; const subOrDub = this.isSubOrDubOrBoth(el); const animeUrl = `${this.baseUrl}/${idRaw}`; const fullId = `${idRaw}?dub=${dubParam}`; animes.push({ id: fullId, title: title, url: animeUrl, subOrDub: subOrDub, }); }); return animes; } catch (e) { console.error(e); return []; } } async getMetadata(id) { const cleanId = id.split('?')[0]; const url = `${this.baseUrl}/${cleanId}`; try { const data = await this.GETText(url); const $ = this.cheerio.load(data); const title = $("meta[property='og:title']").attr("content") || $("h1").text().trim() || "Unknown Title"; const image = $("meta[property='og:image']").attr("content") || $("div.poster img").attr("src") || ""; const summary = $("meta[property='og:description']").attr("content") || $("div.desc").text().trim() || ""; return { title: title, summary: summary, episodes: 0, image: image, genres: [], status: "Unknown" }; } catch (e) { console.error(e); throw new Error("Failed to get metadata"); } } async findEpisodes(id) { const url = `${this.baseUrl}/${id.split('?dub')[0]}`; const rateBoxIdRegex = /
]*data-id="([^"]+)"/; try { const pageHtml = await this.GETText(url); const idMatch = pageHtml.match(rateBoxIdRegex); const aniId = idMatch ? idMatch[1] : null; if (aniId === null) throw new Error("Anime ID not found"); const tokenResp = await this.GETJson(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(aniId)}`); const token = tokenResp.result; const fetchUrlListApi = `${this.baseUrl}/ajax/episodes/list?ani_id=${aniId}&_=${token}`; const ajaxResult = await this.GETJson(fetchUrlListApi); const $ = this.cheerio.load(ajaxResult.result); const episodeData = $('ul.range>li>a').map((_, elem) => ({ name: `Episode ${$(elem).attr('num')}`, number: parseInt($(elem).attr('num') || "0", 10), data: $(elem).attr('token'), title: $(elem).find('span').text().replace(/\s/g, ' ') })).get(); const episodes = await Promise.all( episodeData.map(async (item) => { const response = await fetch(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`); const result = await response.json(); const dubPart = id.split('?dub=')[1] || ""; return { id: item.data || "", number: item.number, title: item.title, url: `${this.baseUrl}/ajax/links/list?token=${item.data}&_=${result.result}?dub=${dubPart}` }; }) ); return episodes; } catch (e) { throw new Error(e); } } async findEpisodeServer(episode, serverStr, category = "sub") { let server = "Server 1"; if (serverStr && serverStr !== "default") server = serverStr; const episodeUrl = episode.url.replace('\u0026', '&').split('?dub')[0]; const dubRequested = episode.url.split('?dub=')[1]; console.log("Episode URL: " + episodeUrl); try { const responseText = await this.GETText(episodeUrl); const cleanedHtml = this.cleanJsonHtml(responseText); const subRegex = /
]*>([\s\S]*?)<\/div>/; const softsubRegex = /
]*>([\s\S]*?)<\/div>/; const dubRegex = /
]*>([\s\S]*?)<\/div>/; const subMatch = subRegex.exec(cleanedHtml); const softsubMatch = softsubRegex.exec(cleanedHtml); const dubMatch = dubRegex.exec(cleanedHtml); const sub = subMatch ? subMatch[1].trim() : ""; const softsub = softsubMatch ? softsubMatch[1].trim() : ""; const dub = dubMatch ? dubMatch[1].trim() : ""; const serverSpanRegex = server === "Server 1" ? /]*data-lid="([^"]+)"[^>]*>Server 1<\/span>/ : /]*data-lid="([^"]+)"[^>]*>Server 2<\/span>/; const isDub = category === 'dub' || dubRequested === 'true'; const serverIdDub = serverSpanRegex.exec(dub)?.[1]; const serverIdSoftsub = serverSpanRegex.exec(softsub)?.[1]; const serverIdSub = serverSpanRegex.exec(sub)?.[1]; const tokenRequestData = [ { name: "Dub", data: serverIdDub }, { name: "Softsub", data: serverIdSoftsub }, { name: "Sub", data: serverIdSub } ].filter(item => item.data !== undefined); const tokenResults = await Promise.all( tokenRequestData.map(async (item) => { const response = await fetch(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`); return { name: item.name, data: await response.json() }; }) ); const serverIdMap = Object.fromEntries(tokenRequestData.map(item => [item.name, item.data])); const streamUrls = tokenResults.map((result) => { return { type: result.name, url: `${this.baseUrl}/ajax/links/view?id=${serverIdMap[result.name]}&_=${result.data.result}` }; }); const decryptedUrls = await processStreams(streamUrls); const headers = { "Referer": "https://animekai.to/", "User-Agent": this.userAgent }; let streamUrl = ""; if (isDub && decryptedUrls.Dub) { streamUrl = decryptedUrls.Dub; } else { streamUrl = decryptedUrls.Sub || decryptedUrls.Softsub; } if (!streamUrl) { throw new Error("Unable to find a valid source"); } const streams = await fetch(streamUrl.replace("/e/", "/media/"), { headers: headers }); const responseJson = await streams.json(); const result = responseJson?.result; const postData = { "text": result, "agent": this.userAgent }; const finalJson = await fetch("https://enc-dec.app/api/dec-mega", { method: "POST", headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(postData) }).then(res => res.json()); if (!finalJson || finalJson.status !== 200) throw new Error("Failed to decrypt the final stream URL"); if (!finalJson.result.sources || finalJson.result.sources.length === 0) throw new Error("No video sources found"); const m3u8Link = finalJson.result.sources[0].file; const playlistResponse = await fetch(m3u8Link); const playlistText = await playlistResponse.text(); const regex = /#EXT-X-STREAM-INF:BANDWIDTH=\d+,RESOLUTION=(\d+x\d+)\s*(.*)/g; const videoSources = []; let resolutionMatch; while ((resolutionMatch = regex.exec(playlistText)) !== null) { let url = ""; if (resolutionMatch[2].includes("list")) { url = `${m3u8Link.split(',')[0]}/${resolutionMatch[2]}`; } else { url = `${m3u8Link.split('/list')[0]}/${resolutionMatch[2]}`; } videoSources.push({ url: url, type: "m3u8", quality: resolutionMatch[1].split('x')[1] + 'p', subtitles: [], subOrDub: isDub ? "dub" : "sub" }); } if (videoSources.length === 0) { videoSources.push({ url: m3u8Link, type: "m3u8", quality: "auto", subtitles: [], subOrDub: isDub ? "dub" : "sub" }); } return { server: server || "default", headers: { "Referer": this.baseUrl, "User-Agent": this.userAgent }, videoSources: videoSources, }; } catch (e) { console.error(e); throw new Error(e.message || "Error finding server"); } } normalizeQuery(query) { return query .replace(/\b(\d+)(st|nd|rd|th)\b/g, "$1") .replace(/\s+/g, " ") .replace(/(\d+)\s*Season/i, "$1") .replace(/Season\s*(\d+)/i, "$1") .trim(); } async _makeRequest(url) { const response = await fetch(url, { method: "GET", headers: { "DNT": "1", "User-Agent": this.userAgent, "Cookie": "__ddg1_=;__ddg2_=;", }, }); if (!response.ok) { throw new Error(`Failed to fetch: ${response.statusText}`); } return response; } async GETText(url) { const res = await this._makeRequest(url); return await res.text(); } async GETJson(url) { const res = await this._makeRequest(url); return await res.json(); } isSubOrDubOrBoth(elem) { const sub = elem.find("span.sub").text(); const dub = elem.find("span.dub").text(); if (sub !== "" && dub !== "") return "both"; if (sub !== "") return "sub"; return "dub"; } cleanJsonHtml(jsonHtml) { if (!jsonHtml) return ""; return jsonHtml .replace(/\\"/g, "\"") .replace(/\\'/g, "'") .replace(/\\\\/g, "\\") .replace(/\\n/g, "\n") .replace(/\\t/g, "\t") .replace(/\\r/g, "\r"); } } async function processStreams(streamUrls) { const streamResponses = await Promise.all( streamUrls.map(async ({ type, url }) => { try { const json = await fetch(url).then(r => r.json()); return { type, result: json.result }; } catch (error) { console.log(`Error fetching ${type} stream:`, error); return { type, result: null }; } }) ); const decryptResults = await Promise.all( streamResponses .filter(item => item.result !== null) .map(async item => { const result = await fetch("https://enc-dec.app/api/dec-kai", { headers: { 'Content-Type': 'application/json' }, method: "POST", body: JSON.stringify({ text: item.result }) }).then(res => res.json()); return { [item.type]: result.result.url }; }) ); return Object.assign({}, ...decryptResults); } module.exports = AnimeKai;