Files
WaifuBoard-Extensions/anime/Animekai.js
2026-01-05 04:46:26 +01:00

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;