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

View File

@@ -1,206 +1,206 @@
class MangaDex {
constructor() {
this.baseUrl = "https://mangadex.org";
this.apiUrl = "https://api.mangadex.org";
this.type = "book-board";
this.mediaType = "manga";
}
getHeaders() {
return {
'User-Agent': 'MangaDex-Client-Adapter/1.0',
'Content-Type': 'application/json'
};
}
async search(queryObj) {
const query = queryObj.query?.trim() || "";
const limit = 25;
const offset = (1 - 1) * limit;
const url = `${this.apiUrl}/manga?title=${encodeURIComponent(query)}&limit=${limit}&offset=${offset}&includes[]=cover_art&contentRating[]=safe&contentRating[]=suggestive&availableTranslatedLanguage[]=en`;
try {
const response = await fetch(url, { headers: this.getHeaders() });
if (!response.ok) {
console.error(`MangaDex API Error: ${response.statusText}`);
return [];
}
const json = await response.json();
if (!json || !Array.isArray(json.data)) {
return [];
}
return json.data.map(manga => {
const attributes = manga.attributes;
const titleObject = attributes.title || {};
const title = titleObject.en || Object.values(titleObject)[0] || 'Unknown Title';
const coverRelationship = manga.relationships?.find(rel => rel.type === 'cover_art');
const coverFileName = coverRelationship?.attributes?.fileName;
const coverUrl = coverFileName
? `https://uploads.mangadex.org/covers/${manga.id}/${coverFileName}.256.jpg`
: '';
return {
id: manga.id,
image: coverUrl,
title: title,
rating: null,
type: 'book'
};
});
} catch (e) {
console.error("Error during MangaDex search:", e);
return [];
}
}
async getMetadata(id) {
try {
const res = await fetch(`https://api.mangadex.org/manga/${id}?includes[]=cover_art`);
if (!res.ok) throw new Error("MangaDex API error");
const json = await res.json();
const manga = json.data;
const attr = manga.attributes;
const title =
attr.title?.en ||
Object.values(attr.title || {})[0] ||
"";
const summary =
attr.description?.en ||
Object.values(attr.description || {})[0] ||
"";
const genres = manga.relationships
?.filter(r => r.type === "tag")
?.map(r =>
r.attributes?.name?.en ||
Object.values(r.attributes?.name || {})[0]
)
?.filter(Boolean) || [];
const coverRel = manga.relationships.find(r => r.type === "cover_art");
const coverFile = coverRel?.attributes?.fileName;
const image = coverFile
? `https://uploads.mangadex.org/covers/${id}/${coverFile}.512.jpg`
: "";
const score100 = 0;
const statusMap = {
ongoing: "Ongoing",
completed: "Completed",
hiatus: "Hiatus",
cancelled: "Cancelled"
};
return {
id,
title,
format: "Manga",
score: score100,
genres,
status: statusMap[attr.status] || "",
published: attr.year ? String(attr.year) : "???",
summary,
chapters: attr.lastChapter ? Number(attr.lastChapter) || 0 : 0,
image
};
} catch (e) {
console.error("MangaDex getMetadata error:", e);
return {
id,
title: "",
format: "Manga",
score: 0,
genres: [],
status: "",
published: "???",
summary: "",
chapters: 0,
image: ""
};
}
}
async findChapters(mangaId) {
if (!mangaId) return [];
const url = `${this.apiUrl}/manga/${mangaId}/feed?translatedLanguage[]=en&order[chapter]=asc&limit=500&includes[]=scanlation_group`;
try {
const response = await fetch(url, { headers: this.getHeaders() });
let chapters = [];
if (response.ok) {
const json = await response.json();
if (json && Array.isArray(json.data)) {
const allChapters = json.data
.filter(ch => ch.attributes.chapter && !ch.attributes.externalUrl)
.map((ch, index) => ({
id: ch.id,
title: ch.attributes.title || `Chapter ${ch.attributes.chapter}`,
number: ch.attributes.chapter,
index: index,
language: ch.attributes.translatedLanguage
}));
const seenChapters = new Set();
allChapters.forEach(ch => {
if (!seenChapters.has(ch.chapter)) {
seenChapters.add(ch.chapter);
chapters.push(ch);
}
});
chapters.sort((a, b) => parseFloat(a.chapter) - parseFloat(b.chapter));
}
}
return chapters;
} catch (e) {
console.error("Error finding MangaDex chapters:", e);
return { chapters: [], cover: null };
}
}
async findChapterPages(chapterId) {
if (!chapterId) return [];
const url = `${this.apiUrl}/at-home/server/${chapterId}`;
try {
const response = await fetch(url, { headers: this.getHeaders() });
if (!response.ok) throw new Error(`Failed to fetch pages: ${response.statusText}`);
const json = await response.json();
if (!json || !json.baseUrl || !json.chapter) return [];
const baseUrl = json.baseUrl;
const chapterHash = json.chapter.hash;
const imageFilenames = json.chapter.data;
return imageFilenames.map((filename, index) => ({
url: `${baseUrl}/data/${chapterHash}/${filename}`,
index: index,
headers: {
'Referer': `https://mangadex.org/chapter/${chapterId}`
}
}));
} catch (e) {
console.error("Error finding MangaDex pages:", e);
return [];
}
}
}
class MangaDex {
constructor() {
this.baseUrl = "https://mangadex.org";
this.apiUrl = "https://api.mangadex.org";
this.type = "book-board";
this.mediaType = "manga";
}
getHeaders() {
return {
'User-Agent': 'MangaDex-Client-Adapter/1.0',
'Content-Type': 'application/json'
};
}
async search(queryObj) {
const query = queryObj.query?.trim() || "";
const limit = 25;
const offset = (1 - 1) * limit;
const url = `${this.apiUrl}/manga?title=${encodeURIComponent(query)}&limit=${limit}&offset=${offset}&includes[]=cover_art&contentRating[]=safe&contentRating[]=suggestive&availableTranslatedLanguage[]=en`;
try {
const response = await fetch(url, { headers: this.getHeaders() });
if (!response.ok) {
console.error(`MangaDex API Error: ${response.statusText}`);
return [];
}
const json = await response.json();
if (!json || !Array.isArray(json.data)) {
return [];
}
return json.data.map(manga => {
const attributes = manga.attributes;
const titleObject = attributes.title || {};
const title = titleObject.en || Object.values(titleObject)[0] || 'Unknown Title';
const coverRelationship = manga.relationships?.find(rel => rel.type === 'cover_art');
const coverFileName = coverRelationship?.attributes?.fileName;
const coverUrl = coverFileName
? `https://uploads.mangadex.org/covers/${manga.id}/${coverFileName}.256.jpg`
: '';
return {
id: manga.id,
image: coverUrl,
title: title,
rating: null,
type: 'book'
};
});
} catch (e) {
console.error("Error during MangaDex search:", e);
return [];
}
}
async getMetadata(id) {
try {
const res = await fetch(`https://api.mangadex.org/manga/${id}?includes[]=cover_art`);
if (!res.ok) throw new Error("MangaDex API error");
const json = await res.json();
const manga = json.data;
const attr = manga.attributes;
const title =
attr.title?.en ||
Object.values(attr.title || {})[0] ||
"";
const summary =
attr.description?.en ||
Object.values(attr.description || {})[0] ||
"";
const genres = manga.relationships
?.filter(r => r.type === "tag")
?.map(r =>
r.attributes?.name?.en ||
Object.values(r.attributes?.name || {})[0]
)
?.filter(Boolean) || [];
const coverRel = manga.relationships.find(r => r.type === "cover_art");
const coverFile = coverRel?.attributes?.fileName;
const image = coverFile
? `https://uploads.mangadex.org/covers/${id}/${coverFile}.512.jpg`
: "";
const score100 = 0;
const statusMap = {
ongoing: "Ongoing",
completed: "Completed",
hiatus: "Hiatus",
cancelled: "Cancelled"
};
return {
id,
title,
format: "Manga",
score: score100,
genres,
status: statusMap[attr.status] || "",
published: attr.year ? String(attr.year) : "???",
summary,
chapters: attr.lastChapter ? Number(attr.lastChapter) || 0 : 0,
image
};
} catch (e) {
console.error("MangaDex getMetadata error:", e);
return {
id,
title: "",
format: "Manga",
score: 0,
genres: [],
status: "",
published: "???",
summary: "",
chapters: 0,
image: ""
};
}
}
async findChapters(mangaId) {
if (!mangaId) return [];
const url = `${this.apiUrl}/manga/${mangaId}/feed?translatedLanguage[]=en&order[chapter]=asc&limit=500&includes[]=scanlation_group`;
try {
const response = await fetch(url, { headers: this.getHeaders() });
let chapters = [];
if (response.ok) {
const json = await response.json();
if (json && Array.isArray(json.data)) {
const allChapters = json.data
.filter(ch => ch.attributes.chapter && !ch.attributes.externalUrl)
.map((ch, index) => ({
id: ch.id,
title: ch.attributes.title || `Chapter ${ch.attributes.chapter}`,
number: ch.attributes.chapter,
index: index,
language: ch.attributes.translatedLanguage
}));
const seenChapters = new Set();
allChapters.forEach(ch => {
if (!seenChapters.has(ch.chapter)) {
seenChapters.add(ch.chapter);
chapters.push(ch);
}
});
chapters.sort((a, b) => parseFloat(a.chapter) - parseFloat(b.chapter));
}
}
return chapters;
} catch (e) {
console.error("Error finding MangaDex chapters:", e);
return { chapters: [], cover: null };
}
}
async findChapterPages(chapterId) {
if (!chapterId) return [];
const url = `${this.apiUrl}/at-home/server/${chapterId}`;
try {
const response = await fetch(url, { headers: this.getHeaders() });
if (!response.ok) throw new Error(`Failed to fetch pages: ${response.statusText}`);
const json = await response.json();
if (!json || !json.baseUrl || !json.chapter) return [];
const baseUrl = json.baseUrl;
const chapterHash = json.chapter.hash;
const imageFilenames = json.chapter.data;
return imageFilenames.map((filename, index) => ({
url: `${baseUrl}/data/${chapterHash}/${filename}`,
index: index,
headers: {
'Referer': `https://mangadex.org/chapter/${chapterId}`
}
}));
} catch (e) {
console.error("Error finding MangaDex pages:", e);
return [];
}
}
}
module.exports = MangaDex;

View File

@@ -1,181 +1,181 @@
class nhentai {
constructor() {
this.baseUrl = "https://nhentai.net";
this.type = "book-board";
this.mediaType = "manga";
}
async search(queryObj) {
const q = queryObj.query.trim().replace(/\s+/g, "+");
const url = q
? `${this.baseUrl}/search/?q=${q}`
: `${this.baseUrl}/?q=`;
const { result: data } = await this.scrape(
url,
async (page) => {
return page.evaluate(() => {
const container = document.querySelector('.container.index-container');
if (!container) return {results: [], hasNextPage: false};
const galleryEls = container.querySelectorAll('.gallery');
const results = [];
galleryEls.forEach(el => {
const a = el.querySelector('a.cover');
if (!a) return;
const href = a.getAttribute('href');
const id = href.match(/\d+/)?.[0] || null;
const img = a.querySelector('img.lazyload');
const thumbRaw = img?.dataset?.src || img?.src || "";
const thumb = thumbRaw.startsWith("//") ? "https:" + thumbRaw : thumbRaw;
const coverUrl = thumb.replace("thumb", "cover");
const caption = a.querySelector('.caption');
const title = caption?.textContent.trim() || "";
results.push({
id,
title,
image: coverUrl,
rating: null,
type: "book"
});
});
const hasNextPage = !!document.querySelector('section.pagination a.next');
return {results, hasNextPage};
});
},
{
waitSelector: '.container.index-container',
timeout: 55000
}
);
return data?.results || [];
}
async getMetadata(id) {
const { result: data } = await this.scrape(
`${this.baseUrl}/g/${id}/`,
async (page) => {
return page.evaluate(() => {
const title = document.querySelector('h1.title .pretty')?.textContent?.trim() || "";
const img = document.querySelector('#cover img');
const image =
img?.dataset?.src ? "https:" + img.dataset.src :
img?.src?.startsWith("//") ? "https:" + img.src :
img?.src || "";
const tagBlock = document.querySelector('.tag-container.field-name');
const genres = tagBlock
? [...tagBlock.querySelectorAll('.tags .name')].map(x => x.textContent.trim())
: [];
const timeEl = document.querySelector('.tag-container.field-name time');
const published =
timeEl?.getAttribute("datetime") ||
timeEl?.textContent?.trim() ||
"???";
return {title, image, genres, published};
});
},
{
waitSelector: "#bigcontainer",
timeout: 55000
}
);
if (!data) throw new Error(`Fallo al obtener metadatos para ID ${id}`);
const formattedDate = data.published
? new Date(data.published).toLocaleDateString("es-ES")
: "???";
return {
id,
title: data.title || "",
format: "Manga",
score: 0,
genres: Array.isArray(data.genres) ? data.genres : [],
status: "Finished",
published: formattedDate,
summary: "",
chapters: 1,
image: data.image || ""
};
}
async findChapters(mangaId) {
const { result: data } = await this.scrape(
`${this.baseUrl}/g/${mangaId}/`,
async (page) => {
return page.evaluate(() => {
const title = document.querySelector('#info > h1 .pretty')?.textContent?.trim() || "";
const img = document.querySelector('#cover img');
const cover =
img?.dataset?.src ? "https:" + img.dataset.src :
img?.src?.startsWith("//") ? "https:" + img.src :
img?.src || "";
const hash = cover.match(/galleries\/(\d+)\//)?.[1] || null;
const thumbs = document.querySelectorAll('.thumbs img');
const pages = thumbs.length;
const first = thumbs[0];
const s = first?.dataset?.src || first?.src || "";
const ext = s.match(/t\.(\w+)/)?.[1] || "jpg";
const langTag = [...document.querySelectorAll('#tags .tag-container')]
.find(x => x.textContent.includes("Languages:"));
const language = langTag?.querySelector('.tags .name')?.textContent?.trim() || "";
return {title, cover, hash, pages, ext, language};
});
},
{
waitSelector: '#bigcontainer',
timeout: 55000
}
);
if (!data?.hash) throw new Error(`Fallo al obtener hash para ID ${mangaId}`);
const encodedChapterId = Buffer.from(JSON.stringify({
hash: data.hash,
pages: data.pages,
ext: data.ext
})).toString("base64");
return [{
id: encodedChapterId,
title: data.title,
number: 1,
releaseDate: null,
index: 0,
}];
}
async findChapterPages(chapterId) {
const decoded = JSON.parse(Buffer.from(chapterId, "base64").toString("utf8"));
const { hash, pages, ext } = decoded;
const baseUrl = "https://i.nhentai.net/galleries";
return Array.from({ length: pages }, (_, i) => ({
url: `${baseUrl}/${hash}/${i + 1}.${ext}`,
index: i,
headers: { Referer: `https://nhentai.net/g/${hash}/` }
}));
}
}
class nhentai {
constructor() {
this.baseUrl = "https://nhentai.net";
this.type = "book-board";
this.mediaType = "manga";
}
async search(queryObj) {
const q = queryObj.query.trim().replace(/\s+/g, "+");
const url = q
? `${this.baseUrl}/search/?q=${q}`
: `${this.baseUrl}/?q=`;
const { result: data } = await this.scrape(
url,
async (page) => {
return page.evaluate(() => {
const container = document.querySelector('.container.index-container');
if (!container) return {results: [], hasNextPage: false};
const galleryEls = container.querySelectorAll('.gallery');
const results = [];
galleryEls.forEach(el => {
const a = el.querySelector('a.cover');
if (!a) return;
const href = a.getAttribute('href');
const id = href.match(/\d+/)?.[0] || null;
const img = a.querySelector('img.lazyload');
const thumbRaw = img?.dataset?.src || img?.src || "";
const thumb = thumbRaw.startsWith("//") ? "https:" + thumbRaw : thumbRaw;
const coverUrl = thumb.replace("thumb", "cover");
const caption = a.querySelector('.caption');
const title = caption?.textContent.trim() || "";
results.push({
id,
title,
image: coverUrl,
rating: null,
type: "book"
});
});
const hasNextPage = !!document.querySelector('section.pagination a.next');
return {results, hasNextPage};
});
},
{
waitSelector: '.container.index-container',
timeout: 55000
}
);
return data?.results || [];
}
async getMetadata(id) {
const { result: data } = await this.scrape(
`${this.baseUrl}/g/${id}/`,
async (page) => {
return page.evaluate(() => {
const title = document.querySelector('h1.title .pretty')?.textContent?.trim() || "";
const img = document.querySelector('#cover img');
const image =
img?.dataset?.src ? "https:" + img.dataset.src :
img?.src?.startsWith("//") ? "https:" + img.src :
img?.src || "";
const tagBlock = document.querySelector('.tag-container.field-name');
const genres = tagBlock
? [...tagBlock.querySelectorAll('.tags .name')].map(x => x.textContent.trim())
: [];
const timeEl = document.querySelector('.tag-container.field-name time');
const published =
timeEl?.getAttribute("datetime") ||
timeEl?.textContent?.trim() ||
"???";
return {title, image, genres, published};
});
},
{
waitSelector: "#bigcontainer",
timeout: 55000
}
);
if (!data) throw new Error(`Fallo al obtener metadatos para ID ${id}`);
const formattedDate = data.published
? new Date(data.published).toLocaleDateString("es-ES")
: "???";
return {
id,
title: data.title || "",
format: "Manga",
score: 0,
genres: Array.isArray(data.genres) ? data.genres : [],
status: "Finished",
published: formattedDate,
summary: "",
chapters: 1,
image: data.image || ""
};
}
async findChapters(mangaId) {
const { result: data } = await this.scrape(
`${this.baseUrl}/g/${mangaId}/`,
async (page) => {
return page.evaluate(() => {
const title = document.querySelector('#info > h1 .pretty')?.textContent?.trim() || "";
const img = document.querySelector('#cover img');
const cover =
img?.dataset?.src ? "https:" + img.dataset.src :
img?.src?.startsWith("//") ? "https:" + img.src :
img?.src || "";
const hash = cover.match(/galleries\/(\d+)\//)?.[1] || null;
const thumbs = document.querySelectorAll('.thumbs img');
const pages = thumbs.length;
const first = thumbs[0];
const s = first?.dataset?.src || first?.src || "";
const ext = s.match(/t\.(\w+)/)?.[1] || "jpg";
const langTag = [...document.querySelectorAll('#tags .tag-container')]
.find(x => x.textContent.includes("Languages:"));
const language = langTag?.querySelector('.tags .name')?.textContent?.trim() || "";
return {title, cover, hash, pages, ext, language};
});
},
{
waitSelector: '#bigcontainer',
timeout: 55000
}
);
if (!data?.hash) throw new Error(`Fallo al obtener hash para ID ${mangaId}`);
const encodedChapterId = Buffer.from(JSON.stringify({
hash: data.hash,
pages: data.pages,
ext: data.ext
})).toString("base64");
return [{
id: encodedChapterId,
title: data.title,
number: 1,
releaseDate: null,
index: 0,
}];
}
async findChapterPages(chapterId) {
const decoded = JSON.parse(Buffer.from(chapterId, "base64").toString("utf8"));
const { hash, pages, ext } = decoded;
const baseUrl = "https://i.nhentai.net/galleries";
return Array.from({ length: pages }, (_, i) => ({
url: `${baseUrl}/${hash}/${i + 1}.${ext}`,
index: i,
headers: { Referer: `https://nhentai.net/g/${hash}/` }
}));
}
}
module.exports = nhentai;

View File

@@ -1,121 +1,121 @@
class NovelBin {
constructor() {
this.baseUrl = "https://novelbin.me";
this.type = "book-board";
this.mediaType = "ln";
}
async search(queryObj) {
const query = queryObj.query || "";
const url = `${this.baseUrl}/search?keyword=${encodeURIComponent(query)}`;
const res = await fetch(url, {
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": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"referer": this.baseUrl + "/"
}
});
const html = await res.text();
const $ = this.cheerio.load(html);
const results = [];
$('h3.novel-title a').each((i, el) => {
const href = $(el).attr('href');
const title = $(el).text().trim();
const idMatch = href.match(/novel-book\/([^/?]+)/);
const id = idMatch ? idMatch[1] : null;
const img = `${this.baseUrl}/media/novel/${id}.jpg`;
results.push({
id,
title,
image: img,
rating: null,
type: "book"
});
});
return results;
}
async getMetadata(id) {
const res = await fetch(`${this.baseUrl}/novel-book/${id}`);
const html = await res.text();
const $ = this.cheerio.load(html);
const getMeta = (property) => $(`meta[property='${property}']`).attr('content') || "";
const title = getMeta("og:novel:novel_name") || $('title').text() || "";
const summary = $('meta[name="description"]').attr('content') || "";
const genresRaw = getMeta("og:novel:genre");
const genres = genresRaw ? genresRaw.split(',').map(g => g.trim()) : [];
const status = getMeta("og:novel:status") || "";
const image = getMeta("og:image");
const lastChapterName = getMeta("og:novel:lastest_chapter_name");
const chaptersMatch = lastChapterName.match(/Chapter\s+(\d+)/i);
const chapters = chaptersMatch ? Number(chaptersMatch[1]) : 0;
return {
id,
title,
format: "Light Novel",
score: 0,
genres,
status,
published: "???",
summary,
chapters,
image
};
}
async findChapters(bookId) {
const res = await fetch(`${this.baseUrl}/ajax/chapter-archive?novelId=${bookId}`, {
headers: {
"user-agent": "Mozilla/5.0"
}
});
const html = await res.text();
const $ = this.cheerio.load(html);
const chapters = [];
$('a[title]').each((i, el) => {
const fullUrl = $(el).attr('href');
const title = $(el).attr('title').trim();
const numMatch = title.match(/chapter\s+(\d+(?:\.\d+)?)/i);
chapters.push({
id: fullUrl,
title,
number: numMatch ? numMatch[1] : "0",
releaseDate: null,
index: i
});
});
return chapters;
}
async findChapterPages(chapterUrl) {
const {result} = await this.scrape(chapterUrl, async (page) => {
return page.evaluate(() => {
document.querySelectorAll('div[id^="pf-"]').forEach(e => e.remove());
const ps = Array.from(document.querySelectorAll("p")).map(p => p.outerHTML.trim()).filter(p => p.length > 7);
return ps.join("\n");
});
}, {
waitUntil: "domcontentloaded",
renderWaitTime: 300
});
return result || "<p>Error: chapter text not found</p>";
}
}
class NovelBin {
constructor() {
this.baseUrl = "https://novelbin.me";
this.type = "book-board";
this.mediaType = "ln";
}
async search(queryObj) {
const query = queryObj.query || "";
const url = `${this.baseUrl}/search?keyword=${encodeURIComponent(query)}`;
const res = await fetch(url, {
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": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"referer": this.baseUrl + "/"
}
});
const html = await res.text();
const $ = this.cheerio.load(html);
const results = [];
$('h3.novel-title a').each((i, el) => {
const href = $(el).attr('href');
const title = $(el).text().trim();
const idMatch = href.match(/novel-book\/([^/?]+)/);
const id = idMatch ? idMatch[1] : null;
const img = `${this.baseUrl}/media/novel/${id}.jpg`;
results.push({
id,
title,
image: img,
rating: null,
type: "book"
});
});
return results;
}
async getMetadata(id) {
const res = await fetch(`${this.baseUrl}/novel-book/${id}`);
const html = await res.text();
const $ = this.cheerio.load(html);
const getMeta = (property) => $(`meta[property='${property}']`).attr('content') || "";
const title = getMeta("og:novel:novel_name") || $('title').text() || "";
const summary = $('meta[name="description"]').attr('content') || "";
const genresRaw = getMeta("og:novel:genre");
const genres = genresRaw ? genresRaw.split(',').map(g => g.trim()) : [];
const status = getMeta("og:novel:status") || "";
const image = getMeta("og:image");
const lastChapterName = getMeta("og:novel:lastest_chapter_name");
const chaptersMatch = lastChapterName.match(/Chapter\s+(\d+)/i);
const chapters = chaptersMatch ? Number(chaptersMatch[1]) : 0;
return {
id,
title,
format: "Light Novel",
score: 0,
genres,
status,
published: "???",
summary,
chapters,
image
};
}
async findChapters(bookId) {
const res = await fetch(`${this.baseUrl}/ajax/chapter-archive?novelId=${bookId}`, {
headers: {
"user-agent": "Mozilla/5.0"
}
});
const html = await res.text();
const $ = this.cheerio.load(html);
const chapters = [];
$('a[title]').each((i, el) => {
const fullUrl = $(el).attr('href');
const title = $(el).attr('title').trim();
const numMatch = title.match(/chapter\s+(\d+(?:\.\d+)?)/i);
chapters.push({
id: fullUrl,
title,
number: numMatch ? numMatch[1] : "0",
releaseDate: null,
index: i
});
});
return chapters;
}
async findChapterPages(chapterUrl) {
const {result} = await this.scrape(chapterUrl, async (page) => {
return page.evaluate(() => {
document.querySelectorAll('div[id^="pf-"]').forEach(e => e.remove());
const ps = Array.from(document.querySelectorAll("p")).map(p => p.outerHTML.trim()).filter(p => p.length > 7);
return ps.join("\n");
});
}, {
waitUntil: "domcontentloaded",
renderWaitTime: 300
});
return result || "<p>Error: chapter text not found</p>";
}
}
module.exports = NovelBin;

View File

@@ -1,137 +1,137 @@
class NovelFire {
constructor() {
this.baseUrl = "https://novelfire.net";
this.type = "book-board";
this.mediaType = "ln";
}
async search(queryObj) {
const query = queryObj.query;
const res = await fetch(
`${this.baseUrl}/ajax/searchLive?inputContent=${encodeURIComponent(query)}`,
{ headers: { "accept": "application/json" } }
);
const data = await res.json();
if (!data.data) return [];
return data.data.map(item => ({
id: item.slug,
title: item.title,
image: `https://novelfire.net/${item.image}`,
rating: item.rank ?? null,
type: "book"
}));
}
async getMetadata(id) {
const url = `https://novelfire.net/book/${id}`;
const html = await (await fetch(url)).text();
const $ = this.cheerio.load(html);
const title = $('h1[itemprop="name"]').first().text().trim() || null;
const summary = $('meta[itemprop="description"]').attr('content') || null;
const image =
$('figure.cover img').attr('src') ||
$('img.cover').attr('src') ||
$('img[src*="server-"]').attr('src') ||
null;
const genres = $('.categories a.property-item')
.map((_, el) => $(el).attr('title') || $(el).text().trim())
.get();
let chapters = null;
const latest = $('.chapter-latest-container .latest').text();
if (latest) {
const m = latest.match(/Chapter\s+(\d+)/i);
if (m) chapters = Number(m[1]);
}
let status = 'unknown';
const statusClass = $('strong.ongoing, strong.completed').attr('class');
if (statusClass) {
status = statusClass.toLowerCase();
}
return {
id,
title,
format: 'Light Novel',
score: 0,
genres,
status,
published: '???',
summary,
chapters,
image
};
}
async findChapters(bookId) {
const url = `https://novelfire.net/book/${bookId}/chapters`;
const html = await (await fetch(url)).text();
const $ = this.cheerio.load(html);
let postId;
$("script").each((_, el) => {
const txt = $(el).html() || "";
const m = txt.match(/listChapterDataAjax\?post_id=(\d+)/);
if (m) postId = m[1];
});
if (!postId) throw new Error("post_id not found");
const params = new URLSearchParams({
post_id: postId,
draw: 1,
"columns[0][data]": "title",
"columns[0][orderable]": "false",
"columns[1][data]": "created_at",
"columns[1][orderable]": "true",
"order[0][column]": 1,
"order[0][dir]": "asc",
start: 0,
length: 1000
});
const res = await fetch(
`https://novelfire.net/listChapterDataAjax?${params}`,
{ headers: { "x-requested-with": "XMLHttpRequest" } }
);
const json = await res.json();
if (!json?.data) throw new Error("Invalid response");
return json.data.map((c, i) => ({
id: `https://novelfire.net/book/${bookId}/chapter-${c.n_sort}`,
title: c.title,
number: Number(c.n_sort),
release_date: c.created_at ?? null,
index: i,
language: "en"
}));
}
async findChapterPages(url) {
const html = await (await fetch(url)).text();
const $ = this.cheerio.load(html);
const $content = $("#content").clone();
$content.find("script, ins, .nf-ads, img, nfn2a74").remove();
$content.find("*").each((_, el) => {
$(el).removeAttr("id").removeAttr("class").removeAttr("style");
});
return $content.html()
.replace(/adsbygoogle/gi, "")
.replace(/novelfire/gi, "")
.trim();
}
}
class NovelFire {
constructor() {
this.baseUrl = "https://novelfire.net";
this.type = "book-board";
this.mediaType = "ln";
}
async search(queryObj) {
const query = queryObj.query;
const res = await fetch(
`${this.baseUrl}/ajax/searchLive?inputContent=${encodeURIComponent(query)}`,
{ headers: { "accept": "application/json" } }
);
const data = await res.json();
if (!data.data) return [];
return data.data.map(item => ({
id: item.slug,
title: item.title,
image: `https://novelfire.net/${item.image}`,
rating: item.rank ?? null,
type: "book"
}));
}
async getMetadata(id) {
const url = `https://novelfire.net/book/${id}`;
const html = await (await fetch(url)).text();
const $ = this.cheerio.load(html);
const title = $('h1[itemprop="name"]').first().text().trim() || null;
const summary = $('meta[itemprop="description"]').attr('content') || null;
const image =
$('figure.cover img').attr('src') ||
$('img.cover').attr('src') ||
$('img[src*="server-"]').attr('src') ||
null;
const genres = $('.categories a.property-item')
.map((_, el) => $(el).attr('title') || $(el).text().trim())
.get();
let chapters = null;
const latest = $('.chapter-latest-container .latest').text();
if (latest) {
const m = latest.match(/Chapter\s+(\d+)/i);
if (m) chapters = Number(m[1]);
}
let status = 'unknown';
const statusClass = $('strong.ongoing, strong.completed').attr('class');
if (statusClass) {
status = statusClass.toLowerCase();
}
return {
id,
title,
format: 'Light Novel',
score: 0,
genres,
status,
published: '???',
summary,
chapters,
image
};
}
async findChapters(bookId) {
const url = `https://novelfire.net/book/${bookId}/chapters`;
const html = await (await fetch(url)).text();
const $ = this.cheerio.load(html);
let postId;
$("script").each((_, el) => {
const txt = $(el).html() || "";
const m = txt.match(/listChapterDataAjax\?post_id=(\d+)/);
if (m) postId = m[1];
});
if (!postId) throw new Error("post_id not found");
const params = new URLSearchParams({
post_id: postId,
draw: 1,
"columns[0][data]": "title",
"columns[0][orderable]": "false",
"columns[1][data]": "created_at",
"columns[1][orderable]": "true",
"order[0][column]": 1,
"order[0][dir]": "asc",
start: 0,
length: 1000
});
const res = await fetch(
`https://novelfire.net/listChapterDataAjax?${params}`,
{ headers: { "x-requested-with": "XMLHttpRequest" } }
);
const json = await res.json();
if (!json?.data) throw new Error("Invalid response");
return json.data.map((c, i) => ({
id: `https://novelfire.net/book/${bookId}/chapter-${c.n_sort}`,
title: c.title,
number: Number(c.n_sort),
release_date: c.created_at ?? null,
index: i,
language: "en"
}));
}
async findChapterPages(url) {
const html = await (await fetch(url)).text();
const $ = this.cheerio.load(html);
const $content = $("#content").clone();
$content.find("script, ins, .nf-ads, img, nfn2a74").remove();
$content.find("*").each((_, el) => {
$(el).removeAttr("id").removeAttr("class").removeAttr("style");
});
return $content.html()
.replace(/adsbygoogle/gi, "")
.replace(/novelfire/gi, "")
.trim();
}
}
module.exports = NovelFire;

View File

@@ -1,119 +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
};
}
}
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;

View File

@@ -1,83 +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
};
}
}
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;

View File

@@ -1,100 +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
};
}
}
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;

View File

@@ -1,84 +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
};
}
}
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;

View File

@@ -1,99 +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
};
}
}
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;

View File

@@ -1,115 +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: ""
};
}
}
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;

View File

@@ -1,93 +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: []
};
}
}
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;

155
marketplace.json Normal file
View File

@@ -0,0 +1,155 @@
{
"extensions": {
"AnimeAV1": {
"name": "AnimeAV1",
"type": "anime-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/AnimeAV1.js",
"domain": ""
},
"animepictures": {
"name": "Anime Pictures",
"type": "book-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/animepictures.js",
"domain": "",
"nsfw": true
},
"asmhentai": {
"name": "ASM Hentai",
"type": "book-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/asmhentai.js",
"domain": "",
"nsfw": true
},
"gelbooru": {
"name": "Gelbooru",
"type": "image-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/gelbooru.js",
"domain": "",
"nsfw": true
},
"giphy": {
"name": "Giphy",
"type": "image-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/giphy.js",
"domain": ""
},
"HiAnime": {
"name": "HiAnime",
"type": "anime-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/HiAnime.js",
"domain": ""
},
"lightnovelworld": {
"name": "Lightnovelworld",
"type": "book-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/lightnovelworld.js",
"domain": "",
"broken": true
},
"mangadex": {
"name": "Mangadex",
"type": "book-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/mangadex.js",
"domain": ""
},
"mangapark": {
"name": "Mangapark",
"type": "book-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/mangapark.js",
"domain": ""
},
"nhentai": {
"name": "nhentai",
"type": "book-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/nhentai.js",
"domain": "",
"nsfw": true
},
"novelbin": {
"name": "Novelbin",
"type": "book-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/novelbin.js",
"domain": ""
},
"novelfire": {
"name": "NovelFire",
"type": "book-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/novelfire.js",
"domain": ""
},
"realbooru": {
"name": "Realbooru",
"type": "image-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/realbooru.js",
"domain": "",
"nsfw": true
},
"rule34": {
"name": "Rule34",
"type": "image-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/rule34.js",
"domain": "",
"nsfw": true
},
"tenor": {
"name": "Tenor",
"type": "image-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/tenor.js",
"domain": ""
},
"waifupics": {
"name": "Waifupics",
"type": "image-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/waifupics.js",
"domain": ""
},
"wattpad": {
"name": "Wattpad",
"type": "book-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/wattpad.js",
"domain": ""
},
"ZeroChan": {
"name": "ZeroChan",
"type": "image-board",
"description": ".",
"author": "lenafx",
"entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/ZeroChan.js",
"domain": ""
}
}
}