new format and marketplace

This commit is contained in:
2025-12-19 22:35:35 +01:00
parent 9fe48f93fe
commit 9aea0f1551
19 changed files with 1482 additions and 1327 deletions

119
image/ZeroChan.js Normal file
View File

@@ -0,0 +1,119 @@
class ZeroChan {
baseUrl = "https://zerochan.net";
constructor() {
this.type = "image-board";
}
async search(query = "thighs", page = 1, perPage = 48) {
const url = `${this.baseUrl}/${query.trim().replace(/\s+/g, "+")}?p=${page}`;
const { result } = await this.scrape(
url,
async (page) => {
return page.evaluate(() => {
const list = document.querySelectorAll("#thumbs2 li");
if (list.length === 0) {
return {results: [], hasNextPage: false};
}
const results = [];
list.forEach(li => {
const id = li.getAttribute("data-id");
if (!id) return;
const img = li.querySelector("img");
const imgUrl =
img?.getAttribute("data-src") ||
img?.getAttribute("src") ||
null;
if (!imgUrl) return;
const tagLinks = li.querySelectorAll("p a");
const tags = [...tagLinks]
.map(a => a.textContent.trim())
.filter(Boolean);
results.push({
id,
image: imgUrl,
tags,
});
});
const hasNextPage =
document.querySelector('nav.pagination a[rel="next"]') !== null;
return {results, hasNextPage};
});
},
{
waitSelector: "#thumbs2 li",
timeout: 15000,
renderWaitTime: 3000,
loadImages: true
}
);
return {
results: result.results.map(r => ({
id: r.id,
image: r.image,
tags: r.tags
})),
hasNextPage: result.hasNextPage,
page
};
}
async getInfo(id) {
const url = `${this.baseUrl}/${id}`;
const { result } = await this.scrape(
url,
async (page) => {
return page.evaluate(() => {
const preview = document.querySelector("a.preview");
if (!preview) {
return {
fullImage: null,
tags: [],
createdAt: Date.now()
};
}
const fullImage = preview.getAttribute("href") || null;
const img = preview.querySelector("img");
const alt = img?.getAttribute("alt") || "";
let tags = [];
if (alt.startsWith("Tags:")) {
tags = alt
.replace("Tags:", "")
.split(",")
.map(t => t.trim())
.filter(Boolean);
}
return {
fullImage,
tags,
createdAt: Date.now()
};
});
},
{ waitSelector: "a.preview img", timeout: 15000 }
);
return {
id,
image: result.fullImage,
tags: result.tags
};
}
}
module.exports = ZeroChan;

94
image/animepictures.js Normal file
View File

@@ -0,0 +1,94 @@
class Animepictures {
baseUrl = "https://anime-pictures.net";
constructor() {
this.type = "image-board";
}
async search(query = "thighs", page = 1, perPage = 42) {
const url = `${this.baseUrl}/posts?page=${page - 1}&search_tag=${query}&order_by=date&lang=en`;
const { result } = await this.scrape(
url,
async (page) => {
return page.evaluate(() => {
const items = document.querySelectorAll('.img-block.img-block-big');
const results = [];
items.forEach(div => {
const link = div.querySelector('a');
const img = div.querySelector('img');
if (!link || !img) return;
const href = link.getAttribute('href') || "";
const idMatch = href.match(/\/posts\/(\d+)/);
const id = idMatch ? idMatch[1] : null;
const imgUrl = img.getAttribute('src');
const tagsRaw = img.getAttribute('alt') || "";
const tags = tagsRaw.trim().split(/\s+/).filter(Boolean);
if (id && imgUrl) {
results.push({
id: id,
//full res image: imgUrl.replace("opreviews", "oimages").replace("_cp.avif", ".jpeg"),
image: imgUrl,
tags: tags,
});
}
});
const nextPageBtn = document.querySelector('.numeric_pages a.desktop_only');
const hasNextPage = !!nextPageBtn;
return {results, hasNextPage};
});
},
{ waitSelector: '.img-block.img-block-big', timeout: 15000 }
);
return {
results: result.results,
hasNextPage: result.hasNextPage,
page,
//headers: {
// "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
// "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
// "Accept-Language": "en-US,en;q=0.9",
// "Referer": "https://anime-pictures.net/",
// "Sec-Fetch-Dest": "document",
// "Sec-Fetch-Mode": "navigate",
// "Sec-Fetch-Site": "none",
// "Sec-Fetch-User": "?1"
//}
};
}
async getInfo(id) {
const url = `${this.baseUrl}/posts/${id}?lang=en`;
const { result } = await this.scrape(
url,
async (page) => {
return page.evaluate(() => {
const img = document.querySelector('#big_preview');
const image = img ? img.src : null;
const tagLinks = document.querySelectorAll('.tags li a');
const tags = [...tagLinks].map(a => a.textContent.trim());
return {image, tags};
});
},
{ waitSelector: '#big_preview', timeout: 15000 }
);
return {
id,
image: result.image,
tags: result.tags,
};
}
}
module.exports = Animepictures;

83
image/gelbooru.js Normal file
View File

@@ -0,0 +1,83 @@
class Gelbooru {
baseUrl = "https://gelbooru.com";
constructor() {
this.type = "image-board";
}
async search(query = "thighs", page = 1, perPage = 42) {
const url = `${this.baseUrl}/index.php?page=post&s=list&tags=${encodeURIComponent(query)}&pid=${(page - 1) * perPage}`;
const html = await fetch(url, {
headers: { "User-Agent": "Mozilla/5.0" }
}).then(r => r.text());
const $ = this.cheerio.load(html);
const results = [];
$("article.thumbnail-preview > a[id^='p']").each((_, el) => {
const id = $(el).attr("id")?.slice(1); // p13123834 → 13123834
if (!id) return;
const img = $(el).find("img");
const image = img.attr("src");
const tags = img.attr("alt")
?.replace(/^Rule 34 \|\s*/, "")
?.split(",")
?.map(t => t.trim())
?.filter(Boolean) || [];
results.push({ id, image, tags });
});
// pagination
const totalPages = Math.max(
page,
...$("a[href*='pid=']")
.map((_, el) =>
Math.floor(
parseInt($(el).attr("href")?.match(/pid=(\d+)/)?.[1] || 0) / perPage
) + 1
)
.get()
);
return {
results,
page,
hasNextPage: page < totalPages
};
}
async getInfo(id) {
const html = await fetch(
`${this.baseUrl}/index.php?page=post&s=view&id=${id}`,
{ headers: { "User-Agent": "Mozilla/5.0" } }
).then(r => r.text());
const $ = this.cheerio.load(html);
const container = $("section.image-container");
let image =
container.find("#image").attr("src") ||
container.attr("data-file-url") ||
container.attr("data-large-file-url") ||
null;
// tags
const tags = container
.attr("data-tags")
?.trim()
?.split(/\s+/)
?.filter(Boolean) || [];
return {
id,
image,
tags
};
}
}
module.exports = Gelbooru;

100
image/giphy.js Normal file
View File

@@ -0,0 +1,100 @@
class Giphy {
baseUrl = "https://giphy.com";
constructor() {
this.type = "image-board";
}
async search(query = "hello", page = 1, perPage = 48) {
const url = `${this.baseUrl}/search/${query.trim().replace(/\s+/g, "-")}`;
const data = await this.scrape(
url,
(page) => page.evaluate(() => {
const items = document.querySelectorAll('a[data-giphy-id]');
const results = [];
items.forEach(el => {
const id = el.getAttribute('data-giphy-id');
const srcWebp = el.querySelector('source[type="image/webp"][srcset^="http"]');
const srcImg = el.querySelector('img');
let rawSrc =
srcWebp?.getAttribute("srcset")?.split(" ")[0] ||
srcImg?.src ||
null;
if (!rawSrc || rawSrc.startsWith("data:")) return;
const alt = srcImg?.getAttribute("alt") || "";
const tags = alt.trim().split(/\s+/).filter(Boolean);
results.push({
id,
image: rawSrc,
});
});
return {
results,
hasNextPage: false
};
}),
{
waitSelector: 'picture img, a[data-giphy-id] img',
scrollToBottom: true,
timeout: 15000
}
);
return {
results: data.result.results.map(r => ({
id: r.id,
image: r.image
})),
hasNextPage: data.result.hasNextPage,
page
};
}
async getInfo(id) {
const url = `https://giphy.com/gifs/${id}`;
const data = await this.scrape(
url,
(page) => page.evaluate(() => {
const scripts = document.querySelectorAll(
'script[type="application/ld+json"]'
);
let imgsrc = null;
scripts.forEach(script => {
try {
const json = JSON.parse(script.textContent);
if (json?.["@type"] === "Article" && json?.image?.url) {
imgsrc = json.image.url;
}
} catch {}
});
return {
image: imgsrc
};
}),
{
waitSelector: 'script[type="application/ld+json"]',
timeout: 15000
}
);
return {
id,
image: data.result.image
};
}
}
module.exports = Giphy;

84
image/realbooru.js Normal file
View File

@@ -0,0 +1,84 @@
class Realbooru {
baseUrl = "https://realbooru.com";
headers = {
"User-Agent": "Mozilla/5.0"
};
constructor() {
this.type = "image-board";
}
async search(query = "original", page = 1, perPage = 42) {
const offset = (page - 1) * perPage;
const tags = query
.trim()
.split(/\s+/)
.join("+") + "+";
const url = `${this.baseUrl}/index.php?page=post&s=list&tags=${tags}&pid=${offset}`;
const html = await fetch(url, { headers: this.headers }).then(r => r.text());
const $ = this.cheerio.load(html);
const results = [];
$('div.col.thumb').each((_, el) => {
const id = ($(el).attr('id') || "").replace('s', '');
const img = $(el).find('img');
let image = img.attr('src');
if (image && !image.startsWith('http')) image = 'https:' + image;
const title = img.attr('title') || '';
const tags = title
.split(',')
.map(t => t.trim())
.filter(Boolean);
if (id && image) {
results.push({ id, image, tags });
}
});
let totalPages = page;
const lastPid = $('a[alt="last page"]').attr('href')?.match(/pid=(\d+)/);
if (lastPid) {
totalPages = Math.floor(parseInt(lastPid[1], 10) / perPage) + 1;
}
return {
results,
page,
hasNextPage: page < totalPages
};
}
async getInfo(id) {
const url = `${this.baseUrl}/index.php?page=post&s=view&id=${id}`;
const html = await fetch(url, { headers: this.headers }).then(r => r.text());
const $ = this.cheerio.load(html);
let image =
$('video source').attr('src') ||
$('#image').attr('src') ||
null;
if (image && !image.startsWith('http')) {
image = this.baseUrl + image;
}
const tags = [];
$('#tagLink a').each((_, el) => {
tags.push($(el).text().trim());
});
return {
id,
image,
tags
};
}
}
module.exports = Realbooru;

99
image/rule34.js Normal file
View File

@@ -0,0 +1,99 @@
class Rule34 {
baseUrl = "https://rule34.xxx";
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
};
constructor() {
this.type = "image-board";
}
async search(query = "alisa_mikhailovna_kujou", page = 1, perPage = 42) {
const offset = (page - 1) * perPage;
const url = `${this.baseUrl}/index.php?page=post&s=list&tags=${query}&pid=${offset}`;
const response = await fetch(url, { headers: this.headers });
const data = await response.text();
const $ = this.cheerio.load(data);
const results = [];
$('.image-list span').each((_, e) => {
const $e = $(e);
const id = $e.attr('id')?.replace('s', '');
let image = $e.find('img').attr('src');
if (image && !image.startsWith('http')) {
image = `https:${image}`;
}
const tags = $e.find('img')
.attr('alt')
?.trim()
.split(' ')
.filter(Boolean);
if (id && image) {
results.push({
id,
image,
tags
});
}
});
const pagination = $('#paginator .pagination');
const lastPageLink = pagination.find('a[alt="last page"]');
let totalPages = 1;
if (lastPageLink.length) {
const pid = Number(lastPageLink.attr('href')?.split('pid=')[1] ?? 0);
totalPages = Math.ceil(pid / perPage) + 1;
}
return {
page,
hasNextPage: page < totalPages,
results
};
}
async getInfo(id) {
const url = `${this.baseUrl}/index.php?page=post&s=view&id=${id}`;
const resizeCookies = {
'resize-notification': 1,
'resize-original': 1
};
const cookieString = Object.entries(resizeCookies).map(([key, value]) => `${key}=${value}`).join('; ');
const fetchHeaders = { ...this.headers };
const resizeHeaders = { ...this.headers, 'cookie': cookieString };
const [resizedResponse, nonResizedResponse] = await Promise.all([
fetch(url, { headers: resizeHeaders }),
fetch(url, { headers: fetchHeaders })
]);
const [resized, original] = await Promise.all([resizedResponse.text(), nonResizedResponse.text()]);
const $ = this.cheerio.load(original);
let fullImage = $('#image').attr('src');
if (fullImage && !fullImage.startsWith('http')) {
fullImage = `https:${fullImage}`;
}
const tags = $('#image').attr('alt')?.trim()?.split(' ').filter(tag => tag !== "");
return {
id,
image: fullImage,
tags
};
}
}
module.exports = Rule34;

115
image/tenor.js Normal file
View File

@@ -0,0 +1,115 @@
class Tenor {
baseUrl = "https://tenor.com";
constructor() {
this.type = "image-board";
this.lastQuery = null;
this.seenIds = new Set();
}
async search(query, page = 1, perPage = 48) {
query = query?.trim() || "thighs";
if (query !== this.lastQuery) {
this.lastQuery = query;
this.seenIds.clear();
}
const url = `${this.baseUrl}/search/${query.replaceAll(" ", "-")}-gifs`;
const { result } = await this.scrape(
url,
async (page) => {
return page.evaluate(() => {
const items = document.querySelectorAll('div.GifList figure, figure');
const results = [];
items.forEach(fig => {
const link = fig.querySelector('a');
const img = fig.querySelector('img');
if (!link || !img) return;
const href = link.getAttribute('href') || "";
const idMatch = href.match(/-(\d+)(?:$|\/?$)/);
const id = idMatch ? idMatch[1] : null;
const imgUrl =
img.getAttribute('src') ||
img.getAttribute('data-src');
const tagsRaw = img.getAttribute('alt') || "";
const tags = tagsRaw.trim().split(/\s+/).filter(Boolean);
if (id && imgUrl && !imgUrl.includes("placeholder")) {
results.push({
id,
image: imgUrl,
sampleImageUrl: imgUrl,
tags,
type: "preview"
});
}
});
const uniqueResults = Array.from(
new Map(results.map(r => [r.id, r])).values()
);
return {results: uniqueResults, hasNextPage: true};
});
},
{
waitSelector: "figure",
timeout: 30000,
scrollToBottom: true,
renderWaitTime: 3000,
loadImages: true
}
);
const newResults = result.results.filter(r => !this.seenIds.has(r.id));
newResults.forEach(r => this.seenIds.add(r.id));
return {
results: newResults.map(r => ({
id: r.id,
image: r.image,
tags: r.tags,
})),
hasNextPage: result.hasNextPage,
page
};
}
async getInfo(id) {
const url = `${this.baseUrl}/view/gif-${id}`;
const { result } = await this.scrape(
url,
async (page) => {
return page.evaluate(() => {
const img = document.querySelector(".Gif img");
const fullImage = img?.src || null;
const tags = [...document.querySelectorAll(".tag-list li a .RelatedTag")]
.map(t => t.textContent.trim())
.filter(Boolean);
return { fullImage, tags };
});
},
{ waitSelector: ".Gif img", timeout: 15000 }
);
return {
id,
image: result.fullImage,
tags: result.tags,
title: result.tags?.join(" ") || `Tenor GIF ${id}`,
headers: ""
};
}
}
module.exports = Tenor;

93
image/waifupics.js Normal file
View File

@@ -0,0 +1,93 @@
class WaifuPics {
baseUrl = "https://api.waifu.pics";
SFW_CATEGORIES = [
'waifu', 'neko', 'shinobu', 'megumin', 'bully', 'cuddle', 'cry', 'hug', 'awoo',
'kiss', 'lick', 'pat', 'smug', 'bonk', 'yeet', 'blush', 'smile', 'wave',
'highfive', 'handhold', 'nom', 'bite', 'glomp', 'slap', 'kill', 'kick',
'happy', 'wink', 'poke', 'dance', 'cringe'
];
constructor() {
this.type = "image-board";
}
async search(query, page = 1, perPage = 42) {
if (!query) query = "waifu";
const category = query.trim().split(' ')[0];
if (!this.SFW_CATEGORIES.includes(category)) {
console.warn(`[WaifuPics] Category '${category}' not supported.`);
return {
total: 0,
next: 0,
previous: 0,
pages: 1,
page: 1,
hasNextPage: false,
results: []
};
}
try {
const response = await fetch(`${this.baseUrl}/many/sfw/${category}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ exclude: [] }),
});
if (!response.ok) {
throw new Error(`API returned ${response.status}: ${await response.text()}`);
}
const data = await response.json();
const results = data.files.map((url, index) => {
const id = url.substring(url.lastIndexOf('/') + 1) || `${category}-${index}`;
const uniqueId = `${id}`;
return {
id: uniqueId,
image: url,
tags: [category],
};
});
return {
total: 30,
next: page + 1,
previous: page > 1 ? page - 1 : 0,
pages: page + 1,
page: page,
hasNextPage: true,
results: results
};
} catch (error) {
console.error(`[WaifuPics] Error fetching images:`, error);
return {
total: 0,
next: 0,
previous: 0,
pages: 1,
page: 1,
hasNextPage: false,
results: []
};
}
}
async getInfo(id) {
return {
id,
image: `https://i.waifu.pics/${id}`,
tags: []
};
}
}
module.exports = WaifuPics;