Upload files to "/"

This commit is contained in:
2025-11-23 23:25:11 +01:00
parent c59ba1d556
commit e66c8e255b
5 changed files with 464 additions and 31 deletions

84
anime_pictures.js Normal file
View File

@@ -0,0 +1,84 @@
class Anime_pictures {
baseUrl = "https://anime-pictures.net";
constructor(fetchPath, cheerioPath, browser) {
this.browser = browser;
this.type = "image-board";
}
async fetchSearchResult(query = "thighs", page = 1, perPage = 48) {
const url = `${this.baseUrl}/posts?page=${page - 1}&search_tag=${query}&order_by=date&lang=en`;
const data = await this.browser.scrape(
url,
() => {
const items = document.querySelectorAll('.img-block.img-block-big');
const results = [];
items.forEach(div => {
const link = div.querySelector('a');
const img = div.querySelector('img');
if (!link || !img) return;
let href = link.getAttribute('href') || "";
let idMatch = href.match(/\/posts\/(\d+)/);
let id = idMatch ? idMatch[1] : null;
let imgUrl = img.getAttribute('src');
let tagsRaw = img.getAttribute('alt') || "";
let tags = tagsRaw.trim().split(/\s+/).filter(Boolean);
if (id && imgUrl) {
results.push({
id,
image: imgUrl,
sampleImageUrl: imgUrl,
tags,
type: "preview"
});
}
});
const nextPageBtn = document.querySelector('.numeric_pages a.desktop_only');
const hasNextPage = !!nextPageBtn;
return { results, hasNextPage };
},
{ waitSelector: '.img-block.img-block-big', timeout: 15000 }
);
return {
results: data.results,
hasNextPage: data.hasNextPage,
page
};
}
async fetchInfo(id) {
const url = `${this.baseUrl}/posts/${id}?lang=en`;
const data = await this.browser.scrape(
url,
() => {
const img = document.querySelector('#big_preview');
const fullImage = img ? img.src : null;
const tagLinks = document.querySelectorAll('.tags li a');
const tags = [...tagLinks].map(a => a.textContent.trim());
return { fullImage, tags };
},
{ waitSelector: '#big_preview', timeout: 15000 }
);
return {
id,
fullImage: data.fullImage,
tags: data.tags,
createdAt: Date.now(),
rating: "Unknown"
};
}
}
module.exports = { Anime_pictures };

71
giphy.js Normal file
View File

@@ -0,0 +1,71 @@
class Giphy {
baseUrl = "https://giphy.com";
constructor(fetchPath, cheerioPath, browser) {
this.browser = browser;
this.type = "image-board";
}
async fetchSearchResult(query = "hello", page = 1, perPage = 48) {
const url = `${this.baseUrl}/search/${query.trim().replace(/\s+/g, "-")}`;
const data = await this.browser.scrape(
url,
() => {
const items = document.querySelectorAll('a[data-giphy-id]');
const results = [];
items.forEach(el => {
const id = el.getAttribute('data-giphy-id');
// solo coger sources válidos
const srcWebp = el.querySelector('source[type="image/webp"][srcset^="http"]');
const srcImg = el.querySelector('img');
let rawSrc =
srcWebp?.getAttribute("srcset")?.split(" ")[0] ||
srcImg?.src ||
null;
// ignorar 1x1 base64
if (!rawSrc || rawSrc.startsWith("data:")) return;
const imgUrl = rawSrc;
const alt = srcImg?.getAttribute("alt") || "";
const tags = alt.trim().split(/\s+/).filter(Boolean);
results.push({
id,
image: imgUrl,
sampleImageUrl: imgUrl,
tags,
type: "preview"
});
});
return {
results,
hasNextPage: false
};
},
{ waitSelector: 'picture img, a[data-giphy-id] img', scrollToBottom: true, timeout: 15000}
);
return {
results: data.results,
hasNextPage: data.hasNextPage,
page
};
}
async fetchInfo(id) {
return {
id,
createdAt: Date.now(),
rating: "Unknown"
};
}
}
module.exports = { Giphy };

156
lightnovelworld.js Normal file
View File

@@ -0,0 +1,156 @@
class ligntnovelworld {
constructor(fetchPath, cheerioPath, browser) {
this.browser = browser;
this.fetch = require(fetchPath);
this.cheerio = require(cheerioPath);
this.baseUrl = "https://lightnovelworld.org/api";
this.type = "book-board";
}
async fetchSearchResult(query = "", page = 1) {
if (query.trim() !== "") {
const res = await this.fetch(`${this.baseUrl}/search/?q=${encodeURIComponent(query)}&search_type=title`);
const data = await res.json();
const results = data.novels.map(n => ({
id: n.slug,
title: n.title,
image: `https://lightnovelworld.org/${n.cover_path}`,
sampleImageUrl: `https://lightnovelworld.org/${n.cover_path}`,
tags: [],
type: "book"
}));
return {
results,
hasNextPage: false,
page
};
}
const res = await this.fetch("https://lightnovelworld.org/");
const html = await res.text();
const $ = this.cheerio.load(html);
const cards = $(".recommendations-grid .recommendation-card");
const results = [];
cards.each((_, el) => {
const card = $(el);
const link = card.find("a.card-cover-link").attr("href") || "";
const id = link.replace(/^\/novel\//, "").replace(/\/$/, "");
const title = card.find(".card-title").text().trim();
const img = card.find(".card-cover img").attr("src") || "";
const imageUrl = img.startsWith("http") ? img : `https://lightnovelworld.org${img}`;
const tags = card.find(".card-genres .genre-tag").map((_, t) => $(t).text().trim()).get();
results.push({
id,
title,
image: imageUrl,
sampleImageUrl: imageUrl,
tags,
type: "book"
});
});
return {
results,
hasNextPage: false,
page
};
}
async findChapters(bookId) {
let offset = 0;
const limit = 500;
const chapters = [];
while (true) {
const res = await this.fetch(`https://lightnovelworld.org/api/novel/${bookId}/chapters/?offset=${offset}&limit=${limit}`);
const data = await res.json();
chapters.push(
...data.chapters.map(c => ({
id: `https://lightnovelworld.org/novel/${bookId}/chapter/${c.number}/`,
title: c.title,
chapter: c.number,
language: 'en'
}))
);
if (!data.has_more) break;
offset += limit;
}
return chapters;
}
async findChapterPages(chapterId) {
const res = await this.fetch(chapterId, {
headers: {
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'referer': chapterId.replace(/\/\d+\/$/, ''),
'sec-ch-ua': '"Chromium";v="139", "Not;A=Brand";v="99"',
'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'
}
});
const html = await res.text();
const $ = this.cheerio.load(html);
const contentDiv = $('#chapterText');
if (!contentDiv || contentDiv.length === 0) {
return [{
type: 'text',
content: '<p>Error: content not found</p>',
index: 0
}];
}
contentDiv.find('script').remove();
contentDiv.find('style').remove();
contentDiv.find('ins').remove();
contentDiv.find("[id^='pf-']").remove();
contentDiv.find('.chapter-ad-container').remove();
contentDiv.find('.ad-unit').remove();
contentDiv.find('.ads').remove();
contentDiv.find('.adsbygoogle').remove();
contentDiv.find('.nf-ads').remove();
contentDiv.find('div[align="center"]').remove();
contentDiv.find("div[style*='text-align:center']").remove();
const paragraphs = contentDiv.find('p');
let cleanHtml = '';
paragraphs.each((_, el) => {
const p = $(el);
let text = p.text() || '';
text = text.replace(/△▼△▼△▼△/g, '').replace(/[※]+/g, '').replace(/\s{2,}/g, ' ').trim();
const htmlP = p.html()?.trim() || '';
const isEmpty = htmlP === '' || htmlP === '&nbsp;' || text.trim() === '';
const isAd = text.includes('Remove Ads') || text.includes('Buy no ads') || text.includes('novelfire');
if (!isEmpty && !isAd) {
if (p.text() !== text) p.text(text);
cleanHtml += $.html(p);
}
});
if (!cleanHtml.trim()) { cleanHtml = contentDiv.html() || ''; }
return [
{
type: 'text',
content: cleanHtml.trim(),
index: 0
}
];
}
}
module.exports = { novelupdates: ligntnovelworld };

View File

@@ -52,43 +52,42 @@ class novelfire {
}
async findChapters(bookId) {
const base = `https://novelfire.net/book/${bookId}/chapters`;
const chapters = [];
const url = `https://novelfire.net/book/${bookId}/chapter-1`;
const firstRes = await this.fetch(base);
const firstHtml = await firstRes.text();
let $ = this.cheerio.load(firstHtml);
const options = await this.browser.scrape(
url,
async () => {
const sleep = ms => new Promise(r => setTimeout(r, ms));
let totalPages = 1;
const pageLinks = $(".pagination a.page-link");
pageLinks.each((_, el) => {
const num = parseInt($(el).text().trim(), 10);
if (!isNaN(num) && num > totalPages) totalPages = num;
});
const select = document.querySelector('.chapindex');
if (!select) return [];
for (let page = 1; page <= totalPages; page++) {
const url = page === 1 ? base : `${base}?page=${page}`;
select.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
select.dispatchEvent(new MouseEvent('click', { bubbles: true }));
const res = await this.fetch(url);
const html = await res.text();
$ = this.cheerio.load(html);
for (let i = 0; i < 20; i++) {
if (document.querySelectorAll('.select2-results__option').length > 0) break;
await sleep(300);
}
$(".chapter-list li").each((_, el) => {
const a = $(el).find("a");
const href = a.attr("href");
const title = a.find(".chapter-title").text().trim();
const chapterNumber = parseInt(a.find(".chapter-no").text().trim(), 10);
return [...select.querySelectorAll('option')].map(opt => ({
id: opt.value,
title: opt.textContent.trim(),
chapter: Number(opt.dataset.n_sort || 0),
}));
},
{
waitSelector: '.chapindex',
timeout: 10000
}
);
chapters.push({
id: href,
title,
chapter: chapterNumber,
language: "en"
});
});
}
return chapters;
return options.map(o => ({
id: `https://novelfire.net/book/${bookId}/chapter-${o.chapter}`,
title: o.title,
chapter: o.chapter,
language: "en"
}));
}
async findChapterPages(chapterId) {

123
tenor.js Normal file
View File

@@ -0,0 +1,123 @@
class Tenor {
baseUrl = "https://tenor.com";
constructor(fetchPath, cheerioPath, browser) {
this.browser = browser;
this.type = "image-board";
this.lastQuery = null;
this.seenIds = new Set();
}
async fetchSearchResult(query = "hello", page = 1, perPage = 48) {
if (query !== this.lastQuery) {
this.lastQuery = query;
this.seenIds.clear();
}
const url = `${this.baseUrl}/search/${query.replace(" ", "-")}-gifs`;
const data = await this.browser.scrape(
url,
() => {
// Fallback selectors: try specific class first, then generic figure
const items = document.querySelectorAll('div.GifList figure, figure');
const results = [];
items.forEach(fig => {
const link = fig.querySelector('a');
const img = fig.querySelector('img');
if (!link || !img) return;
const href = link.getAttribute('href') || "";
let idMatch = href.match(/-(\d+)(?:$|\/?$)/);
const id = idMatch ? idMatch[1] : null;
// Tenor lazy loads images, so we check 'src' AND 'data-src'
const imgUrl = img.getAttribute('src') || img.getAttribute('data-src');
const tagsRaw = img.getAttribute('alt') || "";
const tags = tagsRaw.trim().split(/\s+/).filter(Boolean);
if (id && imgUrl && !imgUrl.includes('placeholder')) {
results.push({
id,
image: imgUrl,
sampleImageUrl: imgUrl,
tags,
type: "preview"
});
}
});
const uniqueResults = Array.from(new Map(results.map(item => [item.id, item])).values());
return { results: uniqueResults, hasNextPage: true };
},
{
waitSelector: 'figure',
timeout: 30000,
scrollToBottom: true,
renderWaitTime: 3000,
loadImages: true
}
);
const newResults = data.results.filter(item => !this.seenIds.has(item.id));
newResults.forEach(item => this.seenIds.add(item.id));
return {
results: newResults,
hasNextPage: data.hasNextPage,
page
};
}
async fetchInfo(id) {
const url = `${this.baseUrl}/view/gif-${id}`;
const data = await this.browser.scrape(
url,
() => {
const img = document.querySelector('img[alt]');
const fullImage = img?.src || null;
const tags = [...document.querySelectorAll('.tag-list li a .RelatedTag')]
.map(tag => tag.textContent.trim())
.filter(Boolean);
let createdAt = Date.now();
const detailNodes = [...document.querySelectorAll('.gif-details dd')];
const createdNode = detailNodes.find(n => n.textContent.includes("Created:"));
if (createdNode) {
const raw = createdNode.textContent.replace("Created:", "").trim();
const parts = raw.split(/[\/,: ]+/);
if (parts.length >= 6) {
let [dd, mm, yyyy, hh, min, ss] = parts.map(p => parseInt(p, 10));
createdAt = new Date(yyyy, mm - 1, dd, hh, min, ss).getTime();
}
}
return {
fullImage,
tags,
createdAt
};
},
{ waitSelector: 'img[alt]', timeout: 15000 }
);
return {
id,
fullImage: data.fullImage,
tags: data.tags,
createdAt: data.createdAt,
rating: "Unknown"
};
}
}
module.exports = { Tenor };