Upload files to "/"
This commit is contained in:
139
gelbooru.js
Normal file
139
gelbooru.js
Normal 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
180
mangadex.js
Normal 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
130
nhentai.js
Normal 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
129
novelbin.js
Normal 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 === ' ' || 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 };
|
||||
Reference in New Issue
Block a user