Upload files to "/"
This commit is contained in:
84
anime_pictures.js
Normal file
84
anime_pictures.js
Normal 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
71
giphy.js
Normal 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
156
lightnovelworld.js
Normal 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 === ' ' || 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 };
|
||||
61
novelfire.js
61
novelfire.js
@@ -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
123
tenor.js
Normal 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 };
|
||||
Reference in New Issue
Block a user