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

class SakuraNovel {
    getHTML = async function (url, options = {}) {
        try {
            const { method = 'GET', data = null, headers = {} } = options;
            
            const config = {
                method: method.toLowerCase(),
                url: `https://cloudflare-cors-anywhere.supershadowcube.workers.dev/?url=${url}`,
                headers: headers
            };
            
            if (method.toUpperCase() === 'POST' && data) {
                config.data = data;
            }
            
            const { data: html } = await axios(config);
            return cheerio.load(html);
        } catch (error) {
            throw new Error(error.message);
        }
    }
    
    search = async function (query) {
        try {
            if (!query) throw new Error('Query is required.');
            
            const $ = await this.getHTML('https://sakuranovel.id/wp-admin/admin-ajax.php', {
                method: 'POST',
                data: new URLSearchParams({
                    action: 'data_fetch',
                    keyword: query
                }).toString(),
                headers: {
                    'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
                    origin: 'https://sakuranovel.id',
                    referer: 'https://sakuranovel.id/',
                    'user-agent': 'Mozilla/5.0 (Linux; Android 15; SM-F958 Build/AP3A.240905.015) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.86 Mobile Safari/537.36',
                    'x-requested-with': 'XMLHttpRequest'
                }
            });
            
            const results = $('.searchbox').map((_, el) => {
                const title = $(el).find('.searchbox-title').text().trim() || null;
                const cover = $(el).find('.searchbox-thumb img').attr('src')?.replace('i0.wp.com/', '')?.split('?')?.[0] || null;
                const type = $(el).find('.type').text().trim() || null;
                const status = $(el).find('.status').text().trim() || null;
                const url = $(el).find('a').attr('href') || null;
                return {
                    title,
                    cover,
                    type,
                    status,
                    url
                };
            }).get();
            
            if (!results) throw new Error('No result found.');
            return results;
        } catch (error) {
            throw new Error(error.message);
        }
    }
    
    detail = async function (url) {
        try {
            if (!/^https:\/\/sakuranovel\.id\/series\/[\w-]+\/?$/.test(url)) throw new Error('Invalid detail URL format. Expected format: https://sakuranovel.id/series/[series-slug]/');
            
            const $ = await this.getHTML(url);
            if ($('.series-titlex h2').length === 0) throw new Error('Invalid series page or series not found.');
            
            const title = $('.series-titlex h2').text().trim();
            const alternativeTitle = $('.series-titlex span').text().trim();
            const cover = $('.series-thumb img').attr('src') || null;
            const type = $('.series-infoz.block .type').text().trim() || 'N/A';
            const status = $('.series-infoz.block .status').text().trim() || 'N/A';
            const rating = $('.series-infoz.score span[itemprop="ratingValue"]').text().trim() || 'N/A';
            const bookmarks = parseInt($('.favcount span').text().trim(), 10) || 0;
            const getDetail = (label) => {
                const element = $(`.series-infolist li:contains("${label}")`);
                element.find('b').remove(); 
                return element.text().trim();
            }
            const genres = $('.series-genres a').map((_, el) => $(el).text().trim()).get();
            const tags = $('.series-infolist li:contains("Tags") a').map((_, el) => $(el).text().trim()).get();
            const synopsis = $('.series-synops p').map((_, el) => $(el).text().trim()).get().join('\n\n');
            const chapters = $('.series-chapterlists li').map((_, el) => {
                const linkElement = $(el).find('.flexch-infoz a');
                const chapterTitle = linkElement.find('span').first().text().replace(/\s\s+/g, ' ').replace(/ Bahasa Indonesia$/i, '').trim();
                const chapterUrl = linkElement.attr('href');
                const releaseDate = linkElement.find('.date').text().trim();
                return { 
                    title: chapterTitle,
                    url: chapterUrl,
                    releaseDate
                };
            }).get();
            
            return {
                title,
                alternative_title: alternativeTitle,
                cover,
                type,
                status,
                rating,
                bookmarks,
                country: getDetail('Country'),
                published: getDetail('Published'),
                author: getDetail('Author'),
                genres,
                tags,
                synopsis,
                chapters
            };
        } catch (error) {
            throw new Error(error.message);
        }
    }
    
    chapter = async function (url) {
        try {
            if (!/^https:\/\/sakuranovel\.id\/(?!series\/)[\w-]+\d+[\w-]*\/?$/.test(url)) throw new Error('Invalid chapter URL format. Expected format: https://sakuranovel.id/[series-slug]-ch-[number]-[chapter-title]/');
            
            const $ = await this.getHTML(url);
            if ($('h2.title-chapter').length === 0) throw new Error('Invalid chapter page or chapter not found.');
            
            const fullTitle = $('h2.title-chapter').text().trim();
            const chapterInfo = fullTitle.replace(/ Bahasa Indonesia$/i, '').trim();
            
            const contentContainer = $('.tldariinggrissendiribrojangancopy');
            const images = contentContainer.find('img').map((_, el) => {
                let src = $(el).attr('src') || $(el).attr('data-src') || $(el).attr('srcset');
                if (src) {
                    src = src.split(' ')[0].split(',')[0];
                    src = src.split('?')[0];
                    return src;
                }
                return null;
            }).get().filter(Boolean);
            
            const textContent = contentContainer.find('p').map((_, el) => {
                const text = $(el).text().trim();
                if (text && 
                    !text.includes('—Baca novel lain di sakuranovel—') &&
                    !text.includes('Baca novel lain di sakuranovel')) {
                    return text;
                }
                return null;
            }).get().filter(Boolean).join('\n\n');
            
            const navigation = {
                previousChapter: $('.entry-pagination .pagi-prev a').attr('href') || null,
                tableOfContents: $('.entry-pagination .pagi-toc a').attr('href') || null,
                nextChapter: $('.entry-pagination .pagi-next a').attr('href') || null,
            };
            
            return {
                chapter_info: chapterInfo,
                content: textContent || null,
                images: images.length > 0 ? images : null,
                navigation,
            };
        } catch (error) {
            throw new Error(error.message);
        }
    }
}

// Usage:
const s = new SakuraNovel();
s.detail('https://sakuranovel.id/series/osananajimi-ga-hikikomori-bishoujo-nano-de-houkago-wa-kanojo-no-heya-de-sugoshiteiru-ga-koibito-dewa-nai/').then(console.log);
7697 bytes · Updated Mar 24, 2026