const axios = require('axios');
const cheerio = require('cheerio');
class Otakudesu {
constructor() {
this.inst = axios.create({
baseURL: 'https://uncors.netlify.app/',
params: { destination: 'https://otakudesu.best' }
});
}
async _fetch(path) {
const { data } = await this.inst.get('', { params: { destination: `https://otakudesu.best${path}` } });
return cheerio.load(data);
}
_pagination(html) {
const $ = cheerio.load(html);
const current = parseInt($('.pagination .pagenavix .page-numbers.current').text());
const last = parseInt($('.pagination .pagenavix .page-numbers:last').prev('a.page-numbers').text());
return current ? { current_page: current, last_visible_page: Math.max(current, last) } : false;
}
_mapGenres(html) {
return html.split('</a>').filter(i => i.trim()).map(i => cheerio.load(`${i}</a>`)('a').text());
}
_splitElements(html, delimiter = '</li>') {
return html.split(delimiter).filter(i => i.trim()).map(i => `${i}${delimiter}`);
}
async ongoing(page = 1) {
try {
if (isNaN(page)) throw new Error('Page must be a number.');
const $ = await this._fetch(`/ongoing-anime/page/${page}`);
return {
paginationData: this._pagination($('.pagination').toString()),
ongoing: this._splitElements($('.venutama .rseries .rapi .venz ul li').toString()).map(anime => {
const $a = cheerio.load(anime);
return {
title: $a('.detpost .thumb .thumbz .jdlflm').text(),
cover: $a('.detpost .thumb .thumbz img').attr('src'),
current_episode: $a('.detpost .epz').text().trim(),
release_day: $a('.detpost .epztipe').text().trim(),
newest_release_date: $a('.detpost .newnime').text(),
url: $a('.detpost .thumb a').attr('href')
};
})
};
} catch (error) {
throw new Error(error.message);
}
}
async complete(page = 1) {
try {
if (isNaN(page)) throw new Error('Page must be a number.');
const $ = await this._fetch(`/complete-anime/page/${page}`);
return {
paginationData: this._pagination($('.pagination').toString()),
complete: this._splitElements($('.venutama .rseries .rapi .venz ul li').toString()).map(anime => {
const $a = cheerio.load(anime);
return {
title: $a('.detpost .thumb .thumbz .jdlflm').text(),
cover: $a('.detpost .thumb .thumbz img').attr('src'),
episode_count: $a('.detpost .epz').text().trim().replace(' Episode', ''),
rating: $a('.detpost .epztipe').text().trim(),
last_release_date: $a('.detpost .newnime').text(),
url: $a('.detpost .thumb a').attr('href')
};
})
};
} catch (error) {
throw new Error(error.message);
}
}
async search(query) {
try {
if (!query) throw new Error('Query is required.');
const $ = await this._fetch(`/?s=${query}&post_type=anime`);
return this._splitElements($('.chivsrc li').toString()).map(anime => {
const $a = cheerio.load(anime);
return {
title: $a('h2 a').text().replace(/\bsub(?:title)?[\s-]?(indo(?:nesia)?)\b/gi, '').replace(/\s{2,}/g, ' ').trim(),
cover: $a('img').attr('src'),
genres: this._mapGenres($a('.set:nth-child(3)')?.html()?.toString().replace('<b>Genres</b> : ', '') || ''),
status: $a('.set:nth-child(4)').text()?.replace('Status : ', ''),
rating: $a('.set:last-child').text()?.replace('Rating : ', ''),
url: $a('h2 a').attr('href')
};
});
} catch (error) {
throw new Error(error.message);
}
}
async detail(url) {
try {
if (!url.includes('otakudesu.best')) throw new Error('Invalid URL.');
const $ = await cheerio.load((await this.inst.get('', { params: { destination: url } })).data);
const getText = (sel, rep = '') => $(sel).text()?.replace(rep, '');
const $ep = cheerio.load(`<div>${$('.episodelist').toString()}</div>`);
const list = this._splitElements($ep('.episodelist:nth-child(2) ul').html() || '', '</li>');
if (!list.length) return undefined;
const episode_lists = list.map(ep => {
const $e = cheerio.load(ep);
const title = $e('li span:first a')?.text();
const epNum = title?.replace(/^.*Episode\s+/, '').replace(/\D.*$/, '').trim();
return {
episode: title.replace(/\bsub(?:title)?[\s-]?(indo(?:nesia)?)\b/gi, '').replace(/\s{2,}/g, ' ').trim(),
episode_number: epNum ? parseInt(epNum, 10) : undefined,
url: $e('li span:first a')?.attr('href')
};
}).reverse();
return {
title: getText('.infozin .infozingle p:first span', 'Judul: '),
slug: $('link[rel=canonical]').attr('href')?.replace('https://otakudesu.best/anime/', '').replace('/', ''),
japanese_title: getText('.infozin .infozingle p:nth-child(2) span', 'Japanese: '),
cover: $('.fotoanime img').attr('src'),
rating: getText('.infozin .infozingle p:nth-child(3) span', 'Skor: '),
produser: getText('.infozin .infozingle p:nth-child(4) span', 'Produser: '),
type: getText('.infozin .infozingle p:nth-child(5) span', 'Tipe: '),
status: getText('.infozin .infozingle p:nth-child(6) span', 'Status: '),
episode_count: getText('.infozin .infozingle p:nth-child(7) span', 'Total Episode: '),
duration: getText('.infozin .infozingle p:nth-child(8) span', 'Durasi: '),
release_date: getText('.infozin .infozingle p:nth-child(9) span', 'Tanggal Rilis: '),
studio: getText('.infozin .infozingle p:nth-child(10) span', 'Studio: '),
genres: this._mapGenres($(".infozin .infozingle p:last span a").toString()),
synopsis: $('.sinopc').text().split('<p>').map(i => i.replace('</p>', '\n').replace(' ', '')).join(''),
episode_lists
};
} catch (error) {
throw new Error(error.message);
}
}
async episode(url) {
try {
if (!url.includes('otakudesu.best')) throw new Error('Invalid URL.');
const $ = await cheerio.load((await this.inst.get('', { params: { destination: url } })).data);
const episode = $('.venutama .posttl').text().replace(/\bsub(?:title)?[\s-]?(indo(?:nesia)?)\b/gi, '').replace(/\s{2,}/g, ' ').trim();
if (!episode) return undefined;
const anime = await this.detail(`https://otakudesu.best/anime/${$('.flir a[href*="/anime/"]').attr('href')?.split('/').slice(4).join('/')}`);
return {
episode,
anime_info: {
title: anime.title,
slug: anime.slug,
japanese_title: anime.japanese_title,
genres: anime.genres,
synopsis: anime.synopsis,
episode_lists: anime.episode_lists
},
mirror_urls: await this.getMirror(url)
};
} catch (error) {
throw new Error(error.message);
}
}
async schedule() {
try {
const $ = await this._fetch('/jadwal-rilis');
return $('.kglist321').map((i, el) => ({
day: $(el).find('h2').text().trim(),
anime_list: $(el).find('ul > li').map((j, li) => ({
anime_name: $(li).find('a').text().trim(),
url: $(li).find('a').attr('href') ?? ''
})).get()
})).get();
} catch (error) {
throw new Error(error.message);
}
}
async genreLists() {
try {
const $ = await this._fetch('/genre-list');
return this._splitElements($('#venkonten .vezone ul.genres li a').toString(), '</a>').map(genre => {
const $g = cheerio.load(genre);
return {
name: $g('a').text(),
slug: $g('a').attr('href')?.replace('/genres/', '').replace('/', '')
};
});
} catch (error) {
throw new Error(error.message);
}
}
async getAnimeByGenre(genre, page = 1) {
try {
const $ = await this._fetch(`/genres/${genre}/page/${page}`);
return {
paginationData: this._pagination($.html()),
genre: $('.rvad h1').text().trim().replace('Daftar Genre ', ''),
anime: this._splitElements($('.venser .page .col-anime-con').toString(),
'<div class="col-md-4 col-anime-con genre_2 genre_3 genre_4 genre_9 ">').map(animeEl => {
const $a = cheerio.load(animeEl);
const episodeCount = $a('.col-anime .col-anime-eps').text().replace(/[A-z]/g, '').trim();
return {
title: $a('.col-anime .col-anime-title a').text(),
cover: $a('.col-anime .col-anime-cover img').attr('src'),
rating: $a('.col-anime .col-anime-rating').text() || null,
episode_count: episodeCount || null,
season: $a('.col-anime .col-anime-date').text(),
studio: $a('.col-anime .col-anime-studio').text(),
genres: this._mapGenres($a('.col-anime .col-anime-genre a').toString()),
synopsis: $a('.col-anime .col-synopsis p').text(),
url: $a('.col-anime .col-anime-trailer a').attr('href')
};
})
};
} catch (error) {
throw new Error(error.message);
}
}
async animeLists() {
try {
const $ = await this._fetch('/anime-list/');
return $('.bariskelom').map((_, el) => ({
category: $(el).find('.barispenz').text().trim(),
anime: $(el).find('.penzbar').map((_, item) => {
const name = $(item).find('a.hodebgst').contents().filter(function() {
return this.nodeType === 3;
}).text().trim();
const url = $(item).find('a.hodebgst').attr('href');
return name && url ? { name, url } : null;
}).get().filter(Boolean)
})).get();
} catch (error) {
throw new Error(error.message);
}
}
async getMirror(url) {
try {
const $ = await cheerio.load((await this.inst.get('', { params: { destination: url } })).data);
const mirrorData = { '360p': [], '480p': [], '720p': [] };
const extractMirrors = (sel, quality) => {
$(sel).each((i, el) => {
mirrorData[quality].push({
name: $(el).text().trim(),
dataContent: $(el).attr('data-content'),
isDefault: $(el).attr('data-default') === 'true'
});
});
};
extractMirrors('.mirrorstream .m360p li a', '360p');
extractMirrors('.mirrorstream .m480p li a', '480p');
extractMirrors('.mirrorstream .m720p li a', '720p');
return this.getMirrorUrls(mirrorData);
} catch (error) {
throw new Error(error.message);
}
}
async getMirrorUrls(mirrorData) {
try {
const groupedMirrors = { '360p': [], '480p': [], '720p': [] };
const { data } = await this.inst.post('', new URLSearchParams({
action: 'aa1208d27f29ca340c92c66d1926f13f'
}).toString(), {
params: { destination: 'https://otakudesu.best/wp-admin/admin-ajax.php' },
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
const nonce = data.data;
for (const resolution in mirrorData) {
for (const mirror of mirrorData[resolution]) {
const decodedContent = JSON.parse(Buffer.from(mirror.dataContent, 'base64').toString());
const resp = await this.inst.post('', new URLSearchParams({
...decodedContent, nonce, action: '2a3505c93b0035d3f455df82bf976b84'
}).toString(), {
params: { destination: 'https://otakudesu.best/wp-admin/admin-ajax.php' },
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
const $m = cheerio.load(Buffer.from(resp.data.data, 'base64').toString());
groupedMirrors[resolution].push({
name: mirror.name.trim(),
url: $m('iframe').attr('src'),
isDefault: mirror.isDefault
});
}
}
return groupedMirrors;
} catch (error) {
throw new Error(error.message);
}
}
}
const otaku = new Otakudesu();
otaku.search('oshi no ko').then(console.log);