Upload files to "/"

This commit is contained in:
2025-11-23 23:23:26 +01:00
commit 591b664375
4 changed files with 578 additions and 0 deletions

139
gelbooru.js Normal file
View File

@@ -0,0 +1,139 @@
class Gelbooru {
baseUrl = "https://gelbooru.com";
constructor(fetchPath, cheerioPath) {
this.fetch = require(fetchPath);
this.load = require(cheerioPath).load;
this.type = "image-board";
}
async fetchSearchResult(query, page = 1, perPage = 42) {
if (!query) query = "original";
const url = `${this.baseUrl}/index.php?page=post&s=list&tags=${query}&pid=${(page - 1) * perPage}`;
const response = await this.fetch(url, {
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'
}
});
const data = await response.text();
const $ = this.load(data);
const results = [];
$('.thumbnail-container a').each((i, e) => {
const $e = $(e);
const href = $e.attr('href');
const idMatch = href.match(/id=(\d+)/);
const id = idMatch ? idMatch[1] : null;
const image = $e.find('img').attr('src');
const tags = $e.find('img').attr('alt')?.trim()?.split(' ').filter(tag => tag !== "");
if (id && image) {
results.push({
id: id,
image: image,
tags: tags,
type: 'preview'
});
}
});
const pagination = $('.pagination a');
let totalPages = 1;
pagination.each((i, e) => {
const href = $(e).attr('href');
if (href && href.includes('pid=')) {
const pidMatch = href.match(/pid=(\d+)/);
if (pidMatch) {
const pid = parseInt(pidMatch[1], 10);
totalPages = Math.max(totalPages, Math.floor(pid / perPage) + 1);
}
}
});
const currentPage = page;
const nextPage = currentPage < totalPages ? currentPage + 1 : null;
const previousPage = currentPage > 1 ? currentPage - 1 : null;
const hasNextPage = nextPage !== null;
return {
total: totalPages * perPage,
next: nextPage !== null ? nextPage : 0,
previous: previousPage !== null ? previousPage : 0,
pages: totalPages,
page: currentPage,
hasNextPage,
results
};
}
async fetchInfo(id) {
const url = `${this.baseUrl}/index.php?page=post&s=view&id=${id}`;
const response = await this.fetch(url, {
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'
}
});
const original = await response.text();
const $ = this.load(original);
let fullImage;
fullImage = $('#gelcom_img').attr('src') || $('#gelcom_mp4').attr('src');
if (!fullImage) {
fullImage = $('#right-col a[href*="/images/"]').attr('href') || $('#right-col a[href*="/videos/"]').attr('href');
}
if (fullImage && fullImage.startsWith('/')) {
fullImage = new URL(fullImage, this.baseUrl).href;
}
const tagsList = $('#tag-list a');
const tags = tagsList.map((i, el) => $(el).text().trim()).get();
const stats = $('#post-view-image-container + br + br + br + br + ul, #stats');
const postedData = stats.find("li:contains('Posted:')").text().trim();
const createdAt = new Date(postedData.split("Posted: ")[1]).getTime();
const publishedBy = stats.find("li:contains('User:') a").text().trim() || null;
const rating = stats.find("li:contains('Rating:')").text().trim().split("Rating: ")[1];
const comments = $('#comment-list .comment').map((i, el) => {
const $e = $(el);
const id = $e.attr('id')?.replace('c', '');
const user = $e.find('.comment-user a').text().trim();
const comment = $e.find('.comment-body').text().trim();
return {
id,
user,
comment,
}
}).get().filter(Boolean).filter((comment) => comment.comment !== '');
return {
id,
fullImage,
resizedImageUrl: fullImage,
tags,
createdAt,
publishedBy,
rating,
comments
}
}
}
module.exports = { Gelbooru };

180
mangadex.js Normal file
View File

@@ -0,0 +1,180 @@
class MangaDex {
constructor(fetchPath, cheerioPath, browser) {
this.fetchPath = fetchPath;
this.browser = browser;
this.baseUrl = "https://mangadex.org";
this.apiUrl = "https://api.mangadex.org";
this.type = "book-board";
}
getHeaders() {
return {
'User-Agent': 'MangaDex-Client-Adapter/1.0',
'Content-Type': 'application/json'
};
}
async _fetch(url, options = {}) {
if (typeof fetch === 'function') {
return fetch(url, options);
}
const nodeFetch = require(this.fetchPath);
return nodeFetch(url, options);
}
async fetchSearchResult(query = "", page = 1) {
const limit = 25;
const offset = (page - 1) * limit;
let url;
if (!query || query.trim() === "") {
url = `${this.apiUrl}/manga?limit=${limit}&offset=${offset}&includes[]=cover_art&contentRating[]=safe&contentRating[]=suggestive&availableTranslatedLanguage[]=en&order[followedCount]=desc`;
} else {
url = `${this.apiUrl}/manga?title=${encodeURIComponent(query)}&limit=${limit}&offset=${offset}&includes[]=cover_art&contentRating[]=safe&contentRating[]=suggestive&availableTranslatedLanguage[]=en`;
}
try {
const response = await this._fetch(url, { headers: this.getHeaders() });
if (!response.ok) {
console.error(`MangaDex API Error: ${response.statusText}`);
return { results: [], hasNextPage: false, page };
}
const json = await response.json();
if (!json || !Array.isArray(json.data)) {
return { results: [], hasNextPage: false, page };
}
const results = 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`
: '';
const fullCoverUrl = coverFileName
? `https://uploads.mangadex.org/covers/${manga.id}/${coverFileName}`
: '';
const tags = attributes.tags
? attributes.tags.map(t => t.attributes.name.en)
: [];
return {
id: manga.id,
image: coverUrl,
sampleImageUrl: fullCoverUrl,
title: title,
tags: tags,
type: 'book'
};
});
const total = json.total || 0;
const hasNextPage = (offset + limit) < total;
return {
results,
hasNextPage,
page
};
} catch (e) {
console.error("Error during MangaDex search:", e);
return { results: [], hasNextPage: false, error: e.message };
}
}
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 this._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}`,
chapter: 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));
}
}
let highResCover = null;
try {
const mangaRes = await this._fetch(`${this.apiUrl}/manga/${mangaId}?includes[]=cover_art`);
if (mangaRes.ok) {
const mangaJson = await mangaRes.json();
const coverRel = mangaJson.data.relationships.find(r => r.type === 'cover_art');
if(coverRel && coverRel.attributes && coverRel.attributes.fileName) {
highResCover = `https://uploads.mangadex.org/covers/${mangaId}/${coverRel.attributes.fileName}`;
}
}
} catch(e) { }
return {
chapters: chapters,
cover: highResCover
};
} 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 this._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 };

130
nhentai.js Normal file
View File

@@ -0,0 +1,130 @@
class nhentai {
constructor(fetchPath, cheerioPath, browser) {
this.baseUrl = "https://nhentai.net";
this.browser = browser;
this.type = "book-board";
}
async fetchSearchResult(query = "", page = 1) {
const q = query.trim().replace(/\s+/g, "+");
const url = q ? `${this.baseUrl}/search/?q=${q}&page=${page}` : `${this.baseUrl}/?q=&page=${page}`;
const data = await this.browser.scrape(
url,
() => {
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() || "";
const tagsRaw = el.getAttribute('data-tags') || "";
const tags = tagsRaw.split(" ").filter(Boolean);
results.push({
id,
image: thumb,
sampleImageUrl: coverUrl,
title,
tags,
type: "book"
});
});
const hasNextPage = !!document.querySelector('section.pagination a.next');
return {
results,
hasNextPage
};
},{ waitSelector: '.container.index-container', timeout: 5000}
);
return {
results: data.results,
hasNextPage: data.hasNextPage,
page
};
}
async findChapters(mangaId) {
const data = await this.browser.scrape(
`https://nhentai.net/g/${mangaId}/`,
() => {
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: 4000 }
);
const encodedChapterId = Buffer.from(JSON.stringify({
hash: data.hash,
pages: data.pages,
ext: data.ext
})).toString("base64");
return {
chapters: [
{
id: encodedChapterId,
title: data.title,
chapter: 1,
index: 0,
language: data.language
}
],
cover: data.cover
};
}
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 };

129
novelbin.js Normal file
View File

@@ -0,0 +1,129 @@
class NovelBin {
constructor(fetchPath, cheerioPath, browser) {
this.browser = browser;
this.fetch = require(fetchPath);
this.cheerio = require(cheerioPath);
this.baseUrl = "https://novelbin.me";
this.type = "book-board";
}
async fetchSearchResult(query = "", page = 1) {
const url = !query || query.trim() === ""
? `${this.baseUrl}/sort/novelbin-hot?page=${page}`
: `${this.baseUrl}/search?keyword=${encodeURIComponent(query)}`;
const res = await this.fetch(url);
const html = await res.text();
const $ = this.cheerio.load(html);
const results = [];
$(".list-novel .row, .col-novel-main .list-novel .row").each((i, el) => {
const titleEl = $(el).find("h3.novel-title a");
if (!titleEl.length) return;
const title = titleEl.text().trim();
let href = titleEl.attr("href");
if (!href) return;
if (!href.startsWith("http")) { href = `${this.baseUrl}${href}` }
const idMatch = href.match(/novel-book\/([^/?]+)/);
const id = idMatch ? idMatch[1] : null;
if (!id) return;
const coverUrl = `${this.baseUrl}/media/novel/${id}.jpg`;
results.push({
id,
title,
image: coverUrl,
sampleImageUrl: coverUrl,
tags: [],
type: "book"
});
});
const hasNextPage = $(".PagedList-skipToNext a").length > 0;
return {
results,
hasNextPage,
page
};
}
async findChapters(bookId) {
const res = await this.fetch(`${this.baseUrl}/novel-book/${bookId}`);
const html = await res.text();
const $ = this.cheerio.load(html);
const chapters = [];
$("#chapter-archive ul.list-chapter li a").each((i, el) => {
const a = $(el);
const title = a.attr("title") || a.text().trim();
let href = a.attr("href");
if (!href) return;
if (href.startsWith("https://novelbin.me")) { href = href.replace("https://novelbin.me", "") }
const match = title.match(/chapter\s*([\d.]+)/i);
const chapterNumber = match ? match[1] : "0";
chapters.push({
id: href,
title: title.trim(),
chapter: chapterNumber,
language: "en"
});
});
return chapters;
}
async findChapterPages(chapterId) {
const url = chapterId.startsWith('http') ? chapterId : `${this.baseUrl}${chapterId}`;
const content = await this.browser.scrape(
url,
() => {
const contentDiv = document.querySelector('#chr-content, .chr-c');
if (!contentDiv) return "<p>Error: Could not find content.</p>";
contentDiv.querySelectorAll('script, div[id^="pf-"], div[style*="text-align:center"], ins, div[align="center"], .ads, .adsbygoogle').forEach(el => el.remove());
const paragraphs = contentDiv.querySelectorAll('p');
let cleanHtml = '';
paragraphs.forEach(p => {
let text = p.textContent || '';
text = text.replace(/△▼△▼△▼△/g, '');
text = text.replace(/[※\s]{2,}/g, '');
const html = p.innerHTML.trim();
const isAd = text.includes('Remove Ads From') || text.includes('Buy no ads experience');
const isEmpty = html === '' || html === '&nbsp;' || text.trim() === '';
if (!isAd && !isEmpty) {
if (p.textContent !== text) {
p.textContent = text;
}
cleanHtml += p.outerHTML;
}
});
return cleanHtml || contentDiv.innerHTML;
},
{ waitSelector: '#chr-content', timeout: 2000 }
);
return [{
type: 'text',
content: content,
index: 0
}];
}
}
module.exports = { NovelBin };