updates and new extensions
This commit is contained in:
365
anime/Animekai.js
Normal file
365
anime/Animekai.js
Normal file
@@ -0,0 +1,365 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user