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);
}
}
}
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);