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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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