const axios = require('axios');
const crypto = require('crypto');
class Kemono {
constructor() {
this.SERVICES = ['patreon', 'fanbox', 'discord', 'fantia', 'afdian', 'boosty', 'dlsite', 'gumroad', 'subscribestar'];
}
getHeaders(referer = '') {
const ip = [10, crypto.randomInt(256), crypto.randomInt(256), crypto.randomInt(256)].join('.');
return {
'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',
...(referer && { referer, accept: 'text/css' }),
'x-forwarded-for': ip,
'x-originating-ip': ip,
'x-remote-ip': ip,
'x-remote-addr': ip,
'x-forwarded-host': ip,
'x-connecting-ip': ip,
'client-ip': ip,
'x-client-ip': ip,
'x-real-ip': ip,
'x-forwarded-for-original': ip,
'x-forwarded': ip,
'x-cluster-client-ip': ip,
'x-original-forwarded-for': ip
};
}
popular = async function (page = 1) {
try {
if (isNaN(page)) throw new Error('Page must be a number.');
const { data } = await axios.get(`https://kemono.cr/api/v1/posts/popular?${page > 1 ? `o=${page * 50}&` : ''}period=recent`, {
headers: this.getHeaders()
});
const totalPages = Math.floor(data.props.count / 50);
if (parseInt(page) > totalPages) throw new Error(`Page ${page} exceeds total pages (${totalPages}).`);
return {
page: parseInt(page),
total_pages: totalPages,
posts: data.posts.filter(p => p.attachments.length > 0).map(p => ({
id: p.id,
title: p.title,
user_id: p.user,
service: p.service,
published: p.published,
attachments: p.attachments.map(a => 'https://n2.kemono.cr/data' + a.path)
}))
};
} catch (error) {
throw new Error(error.message);
}
}
search = async function (query, page = 1) {
try {
if (!query) throw new Error('Query is required.');
if (isNaN(page)) throw new Error('Page must be a number.');
const { data } = await axios.get(`https://kemono.cr/api/v1/posts?${page > 1 ? `o=${page * 50}&` : ''}q=${encodeURIComponent(query)}`, {
headers: this.getHeaders()
});
const totalPages = Math.floor(data.count / 50);
if (parseInt(page) > totalPages) throw new Error(`Page ${page} exceeds total pages (${totalPages}).`);
return {
page: parseInt(page),
total_pages: totalPages,
posts: data.posts.filter(p => p.attachments.length > 0).map(p => ({
id: p.id,
title: p.title,
user_id: p.user,
service: p.service,
published: p.published,
attachments: p.attachments.map(a => 'https://n2.kemono.cr/data' + a.path)
}))
};
} catch (error) {
throw new Error(error.message);
}
}
profile = async function (id, service = 'patreon') {
try {
if (isNaN(id)) throw new Error('Id must be a number.');
if (!this.SERVICES.includes(service)) throw new Error(`Invalid service. Available: ${this.SERVICES.join(', ')}.`);
const base = `https://kemono.cr/api/v1/${service}/user/${id}`;
const ref = `https://kemono.cr/${service}/user/${id}`;
const [{ data: profile }, { data: posts }] = await Promise.all([
axios.get(`${base}/profile`, { headers: this.getHeaders(ref) }),
axios.get(`${base}/posts`, { headers: this.getHeaders(ref) })
]);
return {
id: profile.id,
name: profile.name,
service: profile.service,
public_id: profile.public_id,
post_count: profile.post_count,
updated: profile.updated,
posts: posts.filter(p => p.attachments.length > 0).map(p => ({
id: p.id,
title: p.title,
published: p.published,
attachments: p.attachments.map(a => 'https://n2.kemono.cr/data' + a.path)
}))
};
} catch (error) {
throw new Error(error.message);
}
}
}
const k = new Kemono();
k.search('loli').then(console.log);