contentapi.ts
Version:
Nodejs library that provides high-level APIs for obtaining information on various entertainment media such as books, movies, comic books, anime, manga, and so on.
563 lines • 27.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const cheerio_1 = require("cheerio");
const models_1 = require("../../models");
const utils_1 = require("../../utils");
const { GenerateToken, DecodeIframeData } = new utils_1.MegaUp();
class AnimeKai extends models_1.AnimeParser {
constructor(customBaseURL) {
super(...arguments);
this.name = 'AnimeKai';
this.baseUrl = 'https://animekai.to';
this.logo = 'https://animekai.to/assets/uploads/37585a39fe8c8d8fafaa2c7bfbf5374ecac859ea6a0288a6da2c61f5.png';
this.classPath = 'ANIME.AnimeKai';
/**
* @param id Anime id
*/
this.fetchAnimeInfo = async (id) => {
var _a;
const info = {
id: id,
title: '',
};
try {
const { data } = await this.client.get(`${this.baseUrl}/watch/${id}`, { headers: this.Headers() });
const $ = (0, cheerio_1.load)(data);
info.title = $('.entity-scroll > .title').text();
info.japaneseTitle = (_a = $('.entity-scroll > .title').attr('data-jp')) === null || _a === void 0 ? void 0 : _a.trim();
info.image = $('div.poster > div >img').attr('src');
info.description = $('.entity-scroll > .desc').text().trim();
// Movie, TV, OVA, ONA, Special, Music
info.type = $('.entity-scroll > .info').children().last().text().toUpperCase();
info.url = `${this.baseUrl}/watch/${id}`;
info.recommendations = [];
$('section.sidebar-section:not(#related-anime) .aitem-col .aitem').each((i, ele) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
const aTag = $(ele);
const id = (_a = aTag.attr('href')) === null || _a === void 0 ? void 0 : _a.replace('/watch/', '');
(_b = info.recommendations) === null || _b === void 0 ? void 0 : _b.push({
id: id,
title: aTag.find('.title').text().trim(),
url: `${this.baseUrl}${aTag.attr('href')}`,
image: (_d = (_c = aTag.attr('style')) === null || _c === void 0 ? void 0 : _c.match(/background-image:\s*url\('(.+?)'\)/)) === null || _d === void 0 ? void 0 : _d[1],
japaneseTitle: (_e = aTag.find('.title').attr('data-jp')) === null || _e === void 0 ? void 0 : _e.trim(),
type: aTag.find('.info').children().last().text().trim(),
sub: parseInt((_f = aTag.find('.info span.sub')) === null || _f === void 0 ? void 0 : _f.text()) || 0,
dub: parseInt((_g = aTag.find('.info span.dub')) === null || _g === void 0 ? void 0 : _g.text()) || 0,
episodes: parseInt((_h = aTag.find('.info').children().eq(-2).text().trim()) !== null && _h !== void 0 ? _h : (_j = aTag.find('.info span.sub')) === null || _j === void 0 ? void 0 : _j.text()) || 0,
});
});
info.relations = [];
$('section#related-anime .tab-body .aitem-col').each((i, ele) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
const card = $(ele);
const aTag = card.find('a.aitem');
const id = (_a = aTag.attr('href')) === null || _a === void 0 ? void 0 : _a.replace('/watch/', '');
(_b = info.relations) === null || _b === void 0 ? void 0 : _b.push({
id: id,
title: aTag.find('.title').text().trim(),
url: `${this.baseUrl}${aTag.attr('href')}`,
image: (_d = (_c = aTag.attr('style')) === null || _c === void 0 ? void 0 : _c.match(/background-image:\s*url\('(.+?)'\)/)) === null || _d === void 0 ? void 0 : _d[1],
japaneseTitle: (_e = aTag.find('.title').attr('data-jp')) === null || _e === void 0 ? void 0 : _e.trim(),
type: card.find('.info').children().eq(-2).text().trim(),
sub: parseInt((_f = card.find('.info span.sub')) === null || _f === void 0 ? void 0 : _f.text()) || 0,
dub: parseInt((_g = card.find('.info span.dub')) === null || _g === void 0 ? void 0 : _g.text()) || 0,
relationType: card.find('.info').children().last().text().trim(),
episodes: parseInt((_h = card.find('.info').children().eq(-3).text().trim()) !== null && _h !== void 0 ? _h : (_j = card.find('.info span.sub')) === null || _j === void 0 ? void 0 : _j.text()) || 0,
});
});
const hasSub = $('.entity-scroll > .info > span.sub').length > 0;
const hasDub = $('.entity-scroll > .info > span.dub').length > 0;
if (hasSub) {
info.subOrDub = models_1.SubOrSub.SUB;
info.hasSub = hasSub;
}
if (hasDub) {
info.subOrDub = models_1.SubOrSub.DUB;
info.hasDub = hasDub;
}
if (hasSub && hasDub) {
info.subOrDub = models_1.SubOrSub.BOTH;
}
info.genres = [];
$('.entity-scroll > .detail')
.find('div:contains("Genres")')
.each(function () {
var _a;
const genre = $(this).text().trim();
if (genre != undefined)
(_a = info.genres) === null || _a === void 0 ? void 0 : _a.push(genre);
});
switch ($('.entity-scroll > .detail').find("div:contains('Status') > span").text().trim()) {
case 'Completed':
info.status = models_1.MediaStatus.COMPLETED;
break;
case 'Releasing':
info.status = models_1.MediaStatus.ONGOING;
break;
case 'Not yet aired':
info.status = models_1.MediaStatus.NOT_YET_AIRED;
break;
default:
info.status = models_1.MediaStatus.UNKNOWN;
break;
}
info.season = $('.entity-scroll > .detail').find("div:contains('Premiered') > span").text().trim();
const ani_id = $('.rate-box#anime-rating').attr('data-id');
const episodesAjax = await this.client.get(`${this.baseUrl}/ajax/episodes/list?ani_id=${ani_id}&_=${GenerateToken(ani_id)}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest',
Referer: `${this.baseUrl}/watch/${id}`,
...this.Headers(),
},
});
const $$ = (0, cheerio_1.load)(episodesAjax.data.result);
info.totalEpisodes = $$('div.eplist > ul > li').length;
info.episodes = [];
$$('div.eplist > ul > li > a').each((i, el) => {
var _a;
const episodeId = `${info.id}$ep=${$$(el).attr('num')}$token=${$$(el).attr('token')}`; //appending token to episode id, as it is required to fetch servers keeping the structure same as other providers
const number = parseInt($$(el).attr('num'));
const title = $$(el).children('span').text().trim();
const url = `${this.baseUrl}/watch/${info.id}${$$(el).attr('href')}ep=${$$(el).attr('num')}`;
const isFiller = $$(el).hasClass('filler');
const isSubbed = number <= (parseInt($('.entity-scroll > .info > span.sub').text().trim()) || 0);
const isDubbed = number <= (parseInt($('.entity-scroll > .info > span.dub').text().trim()) || 0);
(_a = info.episodes) === null || _a === void 0 ? void 0 : _a.push({
id: episodeId,
number: number,
title: title,
isFiller: isFiller,
isSubbed: isSubbed,
isDubbed: isDubbed,
url: url,
});
});
return info;
}
catch (err) {
throw new Error(err.message);
}
};
/**
*
* @param episodeId Episode id
* @param server server type (default `VidCloud`) (optional)
* @param subOrDub sub or dub (default `SubOrSub.SUB`) (optional)
*/
this.fetchEpisodeSources = async (episodeId, server = models_1.StreamingServers.MegaUp, subOrDub = models_1.SubOrSub.SUB) => {
var _a, _b;
if (episodeId.startsWith('http')) {
const serverUrl = new URL(episodeId);
switch (server) {
case models_1.StreamingServers.MegaUp:
return {
headers: { Referer: serverUrl.href },
...(await new utils_1.MegaUp(this.proxyConfig, this.adapter).extract(serverUrl)),
};
default:
return {
headers: { Referer: serverUrl.href },
...(await new utils_1.MegaUp(this.proxyConfig, this.adapter).extract(serverUrl)),
};
}
}
try {
const servers = await this.fetchEpisodeServers(episodeId, subOrDub);
const i = servers.findIndex(s => s.name.toLowerCase().includes(server)); //for now only megaup is available, hence directly using it
if (i === -1) {
throw new Error(`Server ${server} not found`);
}
const serverUrl = new URL(servers[i].url);
const sources = await this.fetchEpisodeSources(serverUrl.href, server, subOrDub);
sources.intro = (_a = servers[i]) === null || _a === void 0 ? void 0 : _a.intro;
sources.outro = (_b = servers[i]) === null || _b === void 0 ? void 0 : _b.outro;
return sources;
}
catch (err) {
throw new Error(err.message);
}
};
/**
* @param url string
*/
this.scrapeCardPage = async (url) => {
var _a, _b;
try {
const res = {
currentPage: 0,
hasNextPage: false,
totalPages: 0,
results: [],
};
const { data } = await this.client.get(url, {
headers: this.Headers(),
});
const $ = (0, cheerio_1.load)(data);
const pagination = $('ul.pagination');
res.currentPage = parseInt(pagination.find('.page-item.active span.page-link').text().trim()) || 0;
const nextPage = (_a = pagination
.find('.page-item.active')
.next()
.find('a.page-link')
.attr('href')) === null || _a === void 0 ? void 0 : _a.split('page=')[1];
if (nextPage != undefined && nextPage != '') {
res.hasNextPage = true;
}
const totalPages = (_b = pagination.find('.page-item:last-child a.page-link').attr('href')) === null || _b === void 0 ? void 0 : _b.split('page=')[1];
if (totalPages === undefined || totalPages === '') {
res.totalPages = res.currentPage;
}
else {
res.totalPages = parseInt(totalPages) || 0;
}
res.results = await this.scrapeCard($);
if (res.results.length === 0) {
res.currentPage = 0;
res.hasNextPage = false;
res.totalPages = 0;
}
return res;
}
catch (err) {
throw new Error('Something went wrong. Please try again later.');
}
};
/**
* @param $ cheerio instance
*/
this.scrapeCard = async ($) => {
try {
const results = [];
$('.aitem').each((i, ele) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
const card = $(ele);
const atag = card.find('div.inner > a');
const id = (_a = atag.attr('href')) === null || _a === void 0 ? void 0 : _a.replace('/watch/', '');
const type = (_b = card.find('.info').children().last()) === null || _b === void 0 ? void 0 : _b.text().trim();
results.push({
id: id,
title: atag.text().trim(),
url: `${this.baseUrl}${atag.attr('href')}`,
image: (_d = (_c = card.find('img')) === null || _c === void 0 ? void 0 : _c.attr('data-src')) !== null && _d !== void 0 ? _d : (_e = card.find('img')) === null || _e === void 0 ? void 0 : _e.attr('src'),
// duration: card.find('.fdi-duration')?.text(),
japaneseTitle: (_g = (_f = card.find('a.title')) === null || _f === void 0 ? void 0 : _f.attr('data-jp')) === null || _g === void 0 ? void 0 : _g.trim(),
type: type,
// nsfw: card.find('.tick-rate')?.text() === '18+' ? true : false,
sub: parseInt((_h = card.find('.info span.sub')) === null || _h === void 0 ? void 0 : _h.text()) || 0,
dub: parseInt((_j = card.find('.info span.dub')) === null || _j === void 0 ? void 0 : _j.text()) || 0,
episodes: parseInt((_k = card.find('.info').children().eq(-2).text().trim()) !== null && _k !== void 0 ? _k : (_l = card.find('.info span.sub')) === null || _l === void 0 ? void 0 : _l.text()) || 0, //if no direct episode count, then just use sub count
});
});
return results;
}
catch (err) {
throw new Error('Something went wrong. Please try again later.');
}
};
/**
* @param episodeId Episode id
* @param subOrDub sub or dub (default `sub`) (optional)
*/
this.fetchEpisodeServers = async (episodeId, subOrDub = models_1.SubOrSub.SUB) => {
if (!episodeId.startsWith(this.baseUrl + '/ajax'))
episodeId = `${this.baseUrl}/ajax/links/list?token=${episodeId.split('$token=')[1]}&_=${GenerateToken(episodeId.split('$token=')[1])}`;
try {
const { data } = await this.client.get(episodeId, { headers: this.Headers() });
const $ = (0, cheerio_1.load)(data.result);
const servers = [];
const serverItems = $(`.server-items.lang-group[data-id="${subOrDub}"] .server`);
await Promise.all(serverItems.map(async (i, server) => {
const id = $(server).attr('data-lid');
const { data } = await this.client.get(`${this.baseUrl}/ajax/links/view?id=${id}&_=${GenerateToken(id)}`, { headers: this.Headers() });
const decodedData = JSON.parse(DecodeIframeData(data.result));
servers.push({
name: `MegaUp ${$(server).text().trim()}`, //megaup is the only server for now
url: decodedData.url,
intro: {
start: decodedData === null || decodedData === void 0 ? void 0 : decodedData.skip.intro[0],
end: decodedData === null || decodedData === void 0 ? void 0 : decodedData.skip.intro[1],
},
outro: {
start: decodedData === null || decodedData === void 0 ? void 0 : decodedData.skip.outro[0],
end: decodedData === null || decodedData === void 0 ? void 0 : decodedData.skip.outro[1],
},
});
}));
return servers;
}
catch (err) {
throw new Error(err.message);
}
};
if (customBaseURL) {
if (customBaseURL.startsWith('http://') || customBaseURL.startsWith('https://')) {
this.baseUrl = customBaseURL;
}
else {
this.baseUrl = `http://${customBaseURL}`;
}
}
else {
this.baseUrl = this.baseUrl;
}
}
/**
* @param query Search query
* @param page Page number (optional)
*/
search(query, page = 1) {
if (0 >= page) {
page = 1;
}
return this.scrapeCardPage(`${this.baseUrl}/browser?keyword=${query.replace(/[\W_]+/g, '+')}&page=${page}`);
}
/**
* @param page number
*/
fetchLatestCompleted(page = 1) {
if (0 >= page) {
page = 1;
}
return this.scrapeCardPage(`${this.baseUrl}/completed?page=${page}`);
}
/**
* @param page number
*/
fetchRecentlyAdded(page = 1) {
if (0 >= page) {
page = 1;
}
return this.scrapeCardPage(`${this.baseUrl}/recent?page=${page}`);
}
/**
* @param page number
*/
fetchRecentlyUpdated(page = 1) {
if (0 >= page) {
page = 1;
}
return this.scrapeCardPage(`${this.baseUrl}/updates?page=${page}`);
}
/**
* @param page number
*/
fetchNewReleases(page = 1) {
if (0 >= page) {
page = 1;
}
return this.scrapeCardPage(`${this.baseUrl}/new-releases?page=${page}`);
}
/**
* @param page number
*/
fetchMovie(page = 1) {
if (0 >= page) {
page = 1;
}
return this.scrapeCardPage(`${this.baseUrl}/movie?page=${page}`);
}
/**
* @param page number
*/
fetchTV(page = 1) {
if (0 >= page) {
page = 1;
}
return this.scrapeCardPage(`${this.baseUrl}/tv?page=${page}`);
}
/**
* @param page number
*/
fetchOVA(page = 1) {
if (0 >= page) {
page = 1;
}
return this.scrapeCardPage(`${this.baseUrl}/ova?page=${page}`);
}
/**
* @param page number
*/
fetchONA(page = 1) {
if (0 >= page) {
page = 1;
}
return this.scrapeCardPage(`${this.baseUrl}/ona?page=${page}`);
}
/**
* @param page number
*/
fetchSpecial(page = 1) {
if (0 >= page) {
page = 1;
}
return this.scrapeCardPage(`${this.baseUrl}/special?page=${page}`);
}
async fetchGenres() {
try {
const res = [];
const { data } = await this.client.get(`${this.baseUrl}/home`, { headers: this.Headers() });
const $ = (0, cheerio_1.load)(data);
const sideBar = $('#menu');
sideBar.find('ul.c4 li a').each((i, ele) => {
const genres = $(ele);
res.push(genres.text().toLowerCase());
});
return res;
}
catch (err) {
throw new Error('Something went wrong. Please try again later.');
}
}
/**
* @param page number
*/
genreSearch(genre, page = 1) {
if (genre == '') {
throw new Error('genre is empty');
}
if (0 >= page) {
page = 1;
}
return this.scrapeCardPage(`${this.baseUrl}/genres/${genre}?page=${page}`);
}
/**
* Fetches the schedule for a given date.
* @param date The date in format 'YYYY-MM-DD'. Defaults to the current date.
* @returns A promise that resolves to an object containing the search results.
*/
async fetchSchedule(date = new Date().toISOString().split('T')[0]) {
try {
const res = {
results: [],
};
const { data } = await this.client.get(`${this.baseUrl}/ajax/schedule/items?tz=5.5&time=${Math.floor(new Date(`${date}T00:00:00Z`).getTime() / 1000)}`, { headers: this.Headers() });
const $ = (0, cheerio_1.load)(data.result);
$('ul.collapsed li').each((i, ele) => {
var _a;
const card = $(ele);
const titleElement = card.find('span.title');
const episodeText = card.find('span').last().text().trim();
res.results.push({
id: (_a = card.find('a').attr('href')) === null || _a === void 0 ? void 0 : _a.split('/')[2], // Extract anime ID
title: titleElement.text().trim(),
japaneseTitle: titleElement.attr('data-jp'),
airingTime: card.find('span.time').text().trim(),
airingEpisode: episodeText.replace('EP ', ''), // Extract episode number
});
});
return res;
}
catch (err) {
throw new Error('Something went wrong. Please try again later.');
}
}
async fetchSpotlight() {
try {
const res = { results: [] };
const { data } = await this.client.get(`${this.baseUrl}/home`, { headers: this.Headers() });
const $ = (0, cheerio_1.load)(data);
$('div.swiper-wrapper > div.swiper-slide').each((i, el) => {
var _a, _b;
const card = $(el);
const titleElement = card.find('div.detail > p.title');
const id = (_a = card.find('div.swiper-ctrl > a.btn').attr('href')) === null || _a === void 0 ? void 0 : _a.replace('/watch/', '');
const img = card.attr('style');
res.results.push({
id: id,
title: titleElement.text(),
japaneseTitle: titleElement.attr('data-jp'),
banner: ((_b = img === null || img === void 0 ? void 0 : img.match(/background-image:\s*url\(["']?(.+?)["']?\)/)) === null || _b === void 0 ? void 0 : _b[1]) || null,
url: `${this.baseUrl}/watch/${id}`,
type: card.find('div.detail > div.info').children().eq(-2).text().trim(),
genres: card
.find('div.detail > div.info')
.children()
.last()
.text()
.trim()
.split(',')
.map(genre => genre.trim()),
releaseDate: card
.find('div.detail > div.mics >div:contains("Release")')
.children('span')
.text()
.trim(),
quality: card.find('div.detail > div.mics >div:contains("Quality")').children('span').text().trim(),
sub: parseInt(card.find('div.detail > div.info > span.sub').text().trim()) || 0,
dub: parseInt(card.find('div.detail > div.info > span.dub').text().trim()) || 0,
description: card.find('div.detail > p.desc').text().trim(),
});
});
return res;
}
catch (error) {
throw new Error('Something went wrong. Please try again later.');
}
}
async fetchSearchSuggestions(query) {
try {
const { data } = await this.client.get(`${this.baseUrl}/ajax/anime/search?keyword=${query.replace(/[\W_]+/g, '+')}`, { headers: this.Headers() });
const $ = (0, cheerio_1.load)(data.result.html);
const res = {
results: [],
};
$('a.aitem').each((i, el) => {
var _a, _b, _c, _d, _e;
const card = $(el);
const image = card.find('.poster img').attr('src');
const titleElement = card.find('.title');
const title = titleElement.text().trim();
const japaneseTitle = titleElement.attr('data-jp');
const id = (_a = card.attr('href')) === null || _a === void 0 ? void 0 : _a.split('/')[2];
const year = card.find('.info').children().eq(-2).text().trim();
const type = card.find('.info').children().eq(-3).text().trim();
const sub = parseInt((_b = card.find('.info span.sub')) === null || _b === void 0 ? void 0 : _b.text()) || 0;
const dub = parseInt((_c = card.find('.info span.dub')) === null || _c === void 0 ? void 0 : _c.text()) || 0;
const episodes = parseInt((_d = card.find('.info').children().eq(-4).text().trim()) !== null && _d !== void 0 ? _d : (_e = card.find('.info span.sub')) === null || _e === void 0 ? void 0 : _e.text()) || 0;
res.results.push({
id: id,
title: title,
url: `${this.baseUrl}/watch/${id}`,
image: image,
japaneseTitle: japaneseTitle || null,
type: type,
year: year,
sub: sub,
dub: dub,
episodes: episodes,
});
});
return res;
}
catch (error) {
throw new Error('Something went wrong. Please try again later.');
}
}
Headers() {
return {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0',
Accept: 'text/html, */*; q=0.01',
'Accept-Language': 'en-US,en;q=0.5',
'Sec-GPC': '1',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
Priority: 'u=0',
Pragma: 'no-cache',
'Cache-Control': 'no-cache',
Referer: `${this.baseUrl}/`,
Cookie: 'usertype=guest; session=hxYne0BNXguMc8zK1FHqQKXPmmoANzBBOuNPM64a; cf_clearance=WfGWV1bKGAaNySbh.yzCyuobBOtjg0ncfPwMhtsvsrs-1737611098-1.2.1.1-zWHcaytuokjFTKbCAxnSPDc_BWAeubpf9TAAVfuJ2vZuyYXByqZBXAZDl_VILwkO5NOLck8N0C4uQr4yGLbXRcZ_7jfWUvfPGayTADQLuh.SH.7bvhC7DmxrMGZ8SW.hGKEQzRJf8N7h6ZZ27GMyqOfz1zfrOiu9W30DhEtW2N7FAXUPrdolyKjCsP1AK3DqsDtYOiiPNLnu47l.zxK80XogfBRQkiGecCBaeDOJHenjn._Zgykkr.F_2bj2C3AS3A5mCpZSlWK5lqhV6jQSQLF9wKWitHye39V.6NoE3RE',
};
}
}
// (async () => {
// const animekai = new AnimeKai();
// const anime = await animekai.search('dandadan');
// const info = await animekai.fetchAnimeInfo('solo-leveling-season-2-arise-from-the-shadow-x7rq');
// console.log(info.episodes);
// const sources = await animekai.fetchEpisodeSources(info?.episodes![0].id!);
// console.log(sources);
// })();
exports.default = AnimeKai;
//# sourceMappingURL=animekai.js.map