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);
}
}
}
const crt = new Crotpedia();
crt.search('yuri').then(console.log);