rynn-k / gists
crotpedia.js javascript
const axios = require('axios');
const cheerio = require('cheerio');

class Crotpedia {
    constructor(guestEmail = 'crotpedia-guest@gmail.com', guestPassword = 'crotpedia-guest') {
        this.client = axios.create({
            baseURL: 'https://cloudflare-cors-anywhere.supershadowcube.workers.dev/?url=https://crotpedia.net',
            headers: {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36'
            }
        });
        
        this.GUEST_EMAIL = guestEmail;
        this.GUEST_PASSWORD = guestPassword;

        this.client.interceptors.response.use(response => {
            const setCookie = response.headers['set-cookie'];
            if (setCookie) {
                const incoming = {};
                const headers = Array.isArray(setCookie) ? setCookie : [setCookie];
                for (const header of headers) {
                    const [pair] = header.split(';');
                    const [k, ...v] = pair.split('=');
                    incoming[k.trim()] = v.join('=').trim();
                }
                const current = {};
                const existing = this.client.defaults.headers.common['cookie'];
                if (existing) {
                    for (const part of existing.split(';')) {
                        const [k, ...v] = part.split('=');
                        current[k.trim()] = v.join('=').trim();
                    }
                }
                this.client.defaults.headers.common['cookie'] = Object.entries({ ...current, ...incoming }).map(([k, v]) => `${k}=${v}`).join('; ');
            }
            return response;
        });
    }
    
    search = async function (query, page = 1) {
        try {
            if (!query) throw new Error('Query is required.');
            
            const url = page > 1 ? `/search/${encodeURIComponent(query)}/page/${page}/` : `/?s=${encodeURIComponent(query)}`;
            
            const { data: htmlContent } = await this.client.get(url);
            const $ = cheerio.load(htmlContent);
            const results = [];
            
            $('.flexbox2-item').each((_, el) => {
                const element = $(el);
                const title = element.find('.flexbox2-title span').first().text().trim();
                const link = element.find('a').attr('href');
                const image = element.find('img').attr('src');
                const studio = element.find('.flexbox2-title .studio').text().trim();
                const scoreText = element.find('.score').text().replace('★', '').trim();
                const score = parseFloat(scoreText) || null;
                const chapterInfo = element.find('.season').text().trim();
                const genres = [];
                
                element.find('.genres a').each((_, genreEl) => {
                    genres.push($(genreEl).text().trim());
                });
                
                if (title && link) {
                    results.push({
                        title,
                        studio: studio || null,
                        score,
                        latestChapter: chapterInfo || null,
                        genres,
                        cover: image || null,
                        url: link
                    });
                }
            });
            
            const paginationItems = $('.pagination .page-numbers');
            const currentPage = parseInt($('.pagination .page-numbers.current').text().trim()) || page;
            const pages = [];
            paginationItems.each((_, el) => {
                const n = parseInt($(el).text().trim());
                if (!isNaN(n)) pages.push(n);
            });
            const totalPages = pages.length ? Math.max(...pages) : currentPage;
            
            if (page > totalPages) throw new Error(`Page ${page} does not exist. Total pages: ${totalPages}.`);
            
            return {
                page: currentPage,
                totalPages,
                results
            };
        } catch (error) {
            throw new Error(error.message);
        }
    }
    
    detail = async function (seriesUrl) {
        try {
            if (!seriesUrl.includes('https://crotpedia.net/baca/series')) throw new Error('Invalid url.');
            
            const { data: htmlContent } = await this.client.get(seriesUrl.replace('https://crotpedia.net', ''));
            const $ = cheerio.load(htmlContent);
            const seriesFlexLeft = $('.series-flexleft');
            
            if (seriesFlexLeft.length === 0) throw new Error('Series page structure not recognized.');
            
            const title = seriesFlexLeft.find('.series-titlex h2').text().trim();
            const originalTitle = seriesFlexLeft.find('.series-titlex span').text().trim() || null;
            let coverImageUrl = $('.series-cover .series-bg').css('background-image');
            
            if (coverImageUrl) {
                coverImageUrl = coverImageUrl.replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
            } else {
                coverImageUrl = seriesFlexLeft.find('.series-thumb img').attr('src') || null;
            }
            
            const type = seriesFlexLeft.find('.series-infoz.block .type').text().trim() || null;
            const status = seriesFlexLeft.find('.series-infoz.block .status').text().trim() || null;
            const scoreText = seriesFlexLeft.find('.series-infoz.score span').text().trim();
            const score = parseFloat(scoreText) || null;
            const seriesInfoListItems = {};
            
            seriesFlexLeft.find('ul.series-infolist li').each((index, element) => {
                const labelElement = $(element).find('b');
                const label = labelElement.text().trim().replace(':', '').toLowerCase().replace(/\s+/g, '_');
                let valueNode = labelElement.siblings('span');
                let value = valueNode.text().trim();
                
                if (!value && valueNode.find('a').length) {
                    value = { text: valueNode.find('a').text().trim(), url: valueNode.find('a').attr('href') };
                } else if (!value && labelElement[0] && labelElement[0].nextSibling) {
                    value = labelElement[0].nextSibling.nodeValue ? labelElement[0].nextSibling.nodeValue.trim() : null;
                }
                
                if (label && value !== undefined && value !== null && String(value).trim() !== '') {
                    seriesInfoListItems[label] = value;
                }
            });
            
            const chapters = [];
            $('.series-chapterlist li').each((index, element) => {
                const chapterLi = $(element);
                const infoLinkElement = chapterLi.find('.flexch-infoz a');
                const chapterTitle = infoLinkElement.find('span').first().text().trim();
                const chapterUrl = infoLinkElement.attr('href');
                const chapterDate = infoLinkElement.find('span.date').text().trim() || null;
                
                if (chapterTitle && chapterUrl) {
                    chapters.push({ title: chapterTitle, date: chapterDate, url: chapterUrl });
                }
            });
            
            return { title, originalTitle, cover: coverImageUrl, type, status, score, details: seriesInfoListItems, chapters };
        } catch (error) {
            throw new Error(error.message);
        }
    }
    
    chapter = async function (chapterUrl) {
        try {
            if (!chapterUrl.includes('https://crotpedia.net/baca')) throw new Error('Invalid url.');
            
            let finalHtml;
            const initialResponse = await this.client.get(chapterUrl.replace('https://crotpedia.net', ''));
            let $ = cheerio.load(initialResponse.data);
            const loginForm = $('form#koi_login_form');
            
            if (loginForm.length > 0) {
                const nonce = loginForm.find('input[name="koi_login_nonce"]').val();
                if (!nonce) throw new Error('Login nonce not found.');
                
                const loginData = new URLSearchParams();
                loginData.append('koi_user_login', this.GUEST_EMAIL);
                loginData.append('koi_user_pass', this.GUEST_PASSWORD);
                loginData.append('koi_login_nonce', nonce);
                
                const loginPostResponse = await this.client.post(chapterUrl, loginData.toString(), {
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded',
                        Referer: chapterUrl
                    }
                });
                
                if (cheerio.load(loginPostResponse.data)('form#koi_login_form').length > 0) throw new Error('Login failed after POST.');
                
                const finalGetResponse = await this.client.get(chapterUrl);
                finalHtml = finalGetResponse.data;
                
                if (cheerio.load(finalHtml)('form#koi_login_form').length > 0) throw new Error('Login failed after final GET.');
            } else {
                finalHtml = initialResponse.data;
            }
            
            const $page = cheerio.load(finalHtml);
            const navigationData = {};
            const images = [];
            const chapNavElement = $page('#chapnav .content');
            
            if (chapNavElement.length > 0) {
                navigationData.seriesTitle = chapNavElement.find('.infox .title a').text().trim() || null;
                navigationData.seriesCover = chapNavElement.find('.thumb img').attr('src') || null;
                navigationData.chapterName = chapNavElement.find('.infox .chapter').text().trim() || null;
                
                let chapterDateRaw = chapNavElement.find('.infox .date').text().trim();
                navigationData.chapterDate = chapterDateRaw.startsWith('•') ? chapterDateRaw.substring(1).trim() : chapterDateRaw || null;
                
                const prevLink = chapNavElement.find('.navigation .leftnav a');
                navigationData.previousChapterUrl = prevLink.length ? prevLink.attr('href') : null;
                
                const nextLink = chapNavElement.find('.navigation .rightnav a');
                navigationData.nextChapterUrl = nextLink.length ? nextLink.attr('href') : null;
            }
            
            $page('.reader-area p img').each((_, element) => {
                const imageUrl = $page(element).attr('src');
                if (imageUrl) images.push(imageUrl);
            });
            
            return {
                ...(Object.keys(navigationData).length > 0 ? { ...navigationData } : {}),
                images
            };
        } catch (error) {
            throw new Error(error.message);
        }
    }
}

// Usage:
const crt = new Crotpedia();
crt.search('yuri').then(console.log);
11097 bytes · Updated Mar 5, 2026