rynn-k / gists
otakudesu.js javascript
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('&nbsp', '')).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);
        }
    }
}

// Usage:
const otaku = new Otakudesu();
otaku.search('oshi no ko').then(console.log);
14231 bytes ยท Updated Feb 27, 2026