365 lines
13 KiB
JavaScript
365 lines
13 KiB
JavaScript
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 = /<div class="rate-box"[^>]*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 = /<div class="server-items lang-group" data-id="sub"[^>]*>([\s\S]*?)<\/div>/;
|
|
const softsubRegex = /<div class="server-items lang-group" data-id="softsub"[^>]*>([\s\S]*?)<\/div>/;
|
|
const dubRegex = /<div class="server-items lang-group" data-id="dub"[^>]*>([\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" ?
|
|
/<span class="server"[^>]*data-lid="([^"]+)"[^>]*>Server 1<\/span>/ :
|
|
/<span class="server"[^>]*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; |