all extensions to new format :P
This commit is contained in:
179
AnimeAV1.js
Normal file
179
AnimeAV1.js
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
class AnimeAV1 {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.type = "anime-board"; // Required for scanner
|
||||||
|
this.api = "https://animeav1.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
getSettings() {
|
||||||
|
return {
|
||||||
|
episodeServers: ["HLS", "HLS-DUB"],
|
||||||
|
supportsDub: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query) {
|
||||||
|
const res = await fetch(`${this.api}/api/search`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ query: query.query }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) return [];
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
return data.map(anime => ({
|
||||||
|
id: anime.title.toLowerCase().replace(/\s+/g, '-'),
|
||||||
|
title: anime.title,
|
||||||
|
url: `${this.api}/anime/${anime.slug}`,
|
||||||
|
subOrDub: "both",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetadata(id) {
|
||||||
|
const html = await fetch(`${this.api}/media/${id}`).then(r => r.text());
|
||||||
|
const parsed = this.parseSvelteData(html);
|
||||||
|
const media = parsed.find(x => x?.data?.media)?.data.media ?? {};
|
||||||
|
|
||||||
|
// IMAGE
|
||||||
|
const imageMatch = html.match(/<img[^>]*class="aspect-poster[^"]*"[^>]*src="([^"]+)"/i);
|
||||||
|
const image = imageMatch ? imageMatch[1] : null;
|
||||||
|
|
||||||
|
// BLOCK INFO (STATUS, SEASON, YEAR)
|
||||||
|
const infoBlockMatch = html.match(
|
||||||
|
/<div class="flex flex-wrap items-center gap-2 text-sm">([\s\S]*?)<\/div>/
|
||||||
|
);
|
||||||
|
|
||||||
|
let status = media.status ?? "Unknown";
|
||||||
|
let season = media.seasons ?? null;
|
||||||
|
let year = media.startDate ? Number(media.startDate.slice(0, 4)) : null;
|
||||||
|
|
||||||
|
if (infoBlockMatch) {
|
||||||
|
const raw = infoBlockMatch[1];
|
||||||
|
|
||||||
|
// Extraer spans internos
|
||||||
|
const spans = [...raw.matchAll(/<span[^>]*>([^<]+)<\/span>/g)].map(m => m[1].trim());
|
||||||
|
|
||||||
|
// EJEMPLO:
|
||||||
|
// ["TV Anime", "•", "2025", "•", "Temporada Otoño", "•", "En emisión"]
|
||||||
|
|
||||||
|
const clean = spans.filter(x => x !== "•");
|
||||||
|
|
||||||
|
// YEAR
|
||||||
|
const yearMatch = clean.find(x => /^\d{4}$/.test(x));
|
||||||
|
if (yearMatch) year = Number(yearMatch);
|
||||||
|
|
||||||
|
// SEASON (el que contiene "Temporada")
|
||||||
|
const seasonMatch = clean.find(x => x.toLowerCase().includes("temporada"));
|
||||||
|
if (seasonMatch) season = seasonMatch;
|
||||||
|
|
||||||
|
// STATUS (normalmente "En emisión", "Finalizado", etc)
|
||||||
|
const statusMatch = clean.find(x =>
|
||||||
|
/emisión|finalizado|completado|pausa|cancelado/i.test(x)
|
||||||
|
);
|
||||||
|
if (statusMatch) status = statusMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: media.title ?? "Unknown",
|
||||||
|
summary: media.synopsis ?? "No summary available",
|
||||||
|
episodes: media.episodesCount ?? 0,
|
||||||
|
characters: [],
|
||||||
|
season,
|
||||||
|
status,
|
||||||
|
studio: "Unknown",
|
||||||
|
score: media.score ?? 0,
|
||||||
|
year,
|
||||||
|
genres: media.genres?.map(g => g.name) ?? [],
|
||||||
|
image
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async findEpisodes(id) {
|
||||||
|
const html = await fetch(`${this.api}/media/${id}`).then(r => r.text());
|
||||||
|
const parsed = this.parseSvelteData(html);
|
||||||
|
|
||||||
|
const media = parsed.find(x => x?.data?.media)?.data?.media;
|
||||||
|
if (!media?.episodes) throw new Error("No se encontró media.episodes");
|
||||||
|
|
||||||
|
return media.episodes.map((ep, i) => ({
|
||||||
|
id: `${media.slug}$${ep.number ?? i + 1}`,
|
||||||
|
number: ep.number ?? i + 1,
|
||||||
|
title: ep.title ?? `Episode ${ep.number ?? i + 1}`,
|
||||||
|
url: `${this.api}/media/${media.slug}/${ep.number ?? i + 1}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async findEpisodeServer(episodeOrId, _server) {
|
||||||
|
const ep = typeof episodeOrId === "string"
|
||||||
|
? (() => { try { return JSON.parse(episodeOrId); } catch { return { id: episodeOrId }; } })()
|
||||||
|
: episodeOrId;
|
||||||
|
|
||||||
|
const pageUrl = ep.url ?? (
|
||||||
|
typeof ep.id === "string" && ep.id.includes("$")
|
||||||
|
? `${this.api}/media/${ep.id.split("$")[0]}/${ep.number ?? ep.id.split("$")[1]}`
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!pageUrl) throw new Error("No se pudo determinar la URL del episodio.");
|
||||||
|
|
||||||
|
const html = await fetch(pageUrl, {
|
||||||
|
headers: { Cookie: "__ddg1_=;__ddg2_=;" },
|
||||||
|
}).then(r => r.text());
|
||||||
|
|
||||||
|
const parsedData = this.parseSvelteData(html);
|
||||||
|
const entry = parsedData.find(x => x?.data?.embeds) || parsedData[3];
|
||||||
|
const embeds = entry?.data?.embeds;
|
||||||
|
if (!embeds) throw new Error("No se encontraron 'embeds' en los datos del episodio.");
|
||||||
|
|
||||||
|
const selectedEmbeds =
|
||||||
|
_server === "HLS"
|
||||||
|
? embeds.SUB ?? []
|
||||||
|
: _server === "HLS-DUB"
|
||||||
|
? embeds.DUB ?? []
|
||||||
|
: (() => { throw new Error(`Servidor desconocido: ${_server}`); })();
|
||||||
|
|
||||||
|
if (!selectedEmbeds.length)
|
||||||
|
throw new Error(`No hay mirrors disponibles para ${_server === "HLS" ? "SUB" : "DUB"}.`);
|
||||||
|
|
||||||
|
const match = selectedEmbeds.find(m =>
|
||||||
|
(m.url || "").includes("zilla-networks.com/play/")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!match)
|
||||||
|
throw new Error(`No se encontró ningún embed de ZillaNetworks en ${_server}.`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
server: _server,
|
||||||
|
headers: { Referer: 'null' },
|
||||||
|
videoSources: [
|
||||||
|
{
|
||||||
|
url: match.url.replace("/play/", "/m3u8/"),
|
||||||
|
type: "m3u8",
|
||||||
|
quality: "auto",
|
||||||
|
subtitles: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
parseSvelteData(html) {
|
||||||
|
const scriptMatch = html.match(/<script[^>]*>\s*({[^<]*__sveltekit_[\s\S]*?)<\/script>/i);
|
||||||
|
if (!scriptMatch) throw new Error("No se encontró bloque SvelteKit en el HTML.");
|
||||||
|
|
||||||
|
const dataMatch = scriptMatch[1].match(/data:\s*(\[[\s\S]*?\])\s*,\s*form:/);
|
||||||
|
if (!dataMatch) throw new Error("No se encontró el bloque 'data' en el script SvelteKit.");
|
||||||
|
|
||||||
|
const jsArray = dataMatch[1];
|
||||||
|
try {
|
||||||
|
return new Function(`"use strict"; return (${jsArray});`)();
|
||||||
|
} catch {
|
||||||
|
const cleaned = jsArray.replace(/\bvoid 0\b/g, "null").replace(/undefined/g, "null");
|
||||||
|
return new Function(`"use strict"; return (${cleaned});`)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AnimeAV1;
|
||||||
235
HiAnime.js
Normal file
235
HiAnime.js
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
class HiAnime {
|
||||||
|
constructor() {
|
||||||
|
this.type = "anime-board";
|
||||||
|
this.baseUrl = "https://hianime.to";
|
||||||
|
}
|
||||||
|
|
||||||
|
getSettings() {
|
||||||
|
return {
|
||||||
|
episodeServers: ["HD-1", "HD-2", "HD-3", "HD-4"],
|
||||||
|
supportsDub: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query) {
|
||||||
|
const normalize = (str) => this.safeString(str).toLowerCase().replace(/[^a-z0-9]+/g, "");
|
||||||
|
|
||||||
|
const start = query.media.startDate;
|
||||||
|
const fetchMatches = async (url) => {
|
||||||
|
const html = await fetch(url).then(res => res.text());
|
||||||
|
|
||||||
|
const regex = /<a href="\/watch\/([^"]+)"[^>]+title="([^"]+)"[^>]+data-id="(\d+)"/g;
|
||||||
|
|
||||||
|
return [...html.matchAll(regex)].map(m => {
|
||||||
|
const id = m[3];
|
||||||
|
const pageUrl = m[1];
|
||||||
|
const title = m[2];
|
||||||
|
|
||||||
|
const jnameRegex = new RegExp(
|
||||||
|
`<h3 class="film-name">[\\s\\S]*?<a[^>]+href="\\/${pageUrl}[^"]*"[^>]+data-jname="([^"]+)"`,
|
||||||
|
"i"
|
||||||
|
);
|
||||||
|
const jnameMatch = html.match(jnameRegex);
|
||||||
|
const jname = jnameMatch ? jnameMatch[1] : null;
|
||||||
|
|
||||||
|
const imageRegex = new RegExp(
|
||||||
|
`<a href="/watch/${pageUrl.replace(/\//g, "\\/")}"[\\s\\S]*?<img[^>]+data-src="([^"]+)"`,
|
||||||
|
"i"
|
||||||
|
);
|
||||||
|
const imageMatch = html.match(imageRegex);
|
||||||
|
const image = imageMatch ? imageMatch[1] : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
pageUrl,
|
||||||
|
title,
|
||||||
|
image,
|
||||||
|
normTitleJP: normalize(this.normalizeSeasonParts(jname)),
|
||||||
|
normTitle: normalize(this.normalizeSeasonParts(title)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = `${this.baseUrl}/search?keyword=${encodeURIComponent(query.query)}&sy=${start.year}&sm=${start.month}&sort=default`;
|
||||||
|
let matches = await fetchMatches(url);
|
||||||
|
|
||||||
|
if (matches.length === 0) return [];
|
||||||
|
|
||||||
|
return matches.map(m => ({
|
||||||
|
id: `${m.id}/${query.dub ? "dub" : "sub"}`,
|
||||||
|
title: m.title,
|
||||||
|
image: m.image,
|
||||||
|
url: `${this.baseUrl}/${m.pageUrl}`,
|
||||||
|
subOrDub: query.dub ? "dub" : "sub"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async findEpisodes(animeId) {
|
||||||
|
const [id, subOrDub] = animeId.split("/");
|
||||||
|
const res = await fetch(`${this.baseUrl}/ajax/v2/episode/list/${id}`, {
|
||||||
|
headers: { "X-Requested-With": "XMLHttpRequest" }
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
const html = json.html;
|
||||||
|
console.log(html)
|
||||||
|
|
||||||
|
|
||||||
|
const episodes = [];
|
||||||
|
const regex = /<a[^>]*class="[^"]*\bep-item\b[^"]*"[^>]*data-number="(\d+)"[^>]*data-id="(\d+)"[^>]*href="([^"]+)"[\s\S]*?<div class="ep-name[^"]*"[^>]*title="([^"]+)"/g;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(html)) !== null) {
|
||||||
|
episodes.push({
|
||||||
|
id: `${match[2]}/${subOrDub}`,
|
||||||
|
number: parseInt(match[1], 10),
|
||||||
|
url: this.baseUrl + match[3],
|
||||||
|
title: match[4],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return episodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findEpisodeServer(episode, _server) {
|
||||||
|
const [id, subOrDub] = episode.id.split("/");
|
||||||
|
let serverName = _server !== "default" ? _server : "HD-1";
|
||||||
|
|
||||||
|
if (_server === "HD-1" || _server === "HD-2" || _server === "HD-3") {
|
||||||
|
const serverJson = await fetch(`${this.baseUrl}/ajax/v2/episode/servers?episodeId=${id}`, {
|
||||||
|
headers: { "X-Requested-With": "XMLHttpRequest" }
|
||||||
|
}).then(res => res.json());
|
||||||
|
|
||||||
|
const serverHtml = serverJson.html;
|
||||||
|
const regex = new RegExp(
|
||||||
|
`<div[^>]*class="item server-item"[^>]*data-type="${subOrDub}"[^>]*data-id="(\\d+)"[^>]*>\\s*<a[^>]*>\\s*${serverName}\\s*</a>`,
|
||||||
|
"i"
|
||||||
|
);
|
||||||
|
|
||||||
|
const match = regex.exec(serverHtml);
|
||||||
|
if (!match) throw new Error(`Server "${serverName}" (${subOrDub}) not found`);
|
||||||
|
|
||||||
|
const serverId = match[1];
|
||||||
|
|
||||||
|
const sourcesJson = await fetch(`${this.baseUrl}/ajax/v2/episode/sources?id=${serverId}`, {
|
||||||
|
headers: { "X-Requested-With": "XMLHttpRequest" }
|
||||||
|
}).then(res => res.json());
|
||||||
|
|
||||||
|
let decryptData = null;
|
||||||
|
let requiredHeaders = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Pass true to get headers back
|
||||||
|
decryptData = await this.extractMegaCloud(sourcesJson.link, true);
|
||||||
|
if (decryptData && decryptData.headersProvided) {
|
||||||
|
requiredHeaders = decryptData.headersProvided;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Primary decrypter failed:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decryptData) {
|
||||||
|
console.warn("Primary decrypter failed — trying ShadeOfChaos fallback...");
|
||||||
|
const fallbackRes = await fetch(
|
||||||
|
`https://ac-api.ofchaos.com/api/anime/embed/convert/v2?embedUrl=${encodeURIComponent(sourcesJson.link)}`
|
||||||
|
);
|
||||||
|
decryptData = await fallbackRes.json();
|
||||||
|
|
||||||
|
// CRITICAL: Fallback headers must mimic the browser behavior expected by the provider
|
||||||
|
// These MUST be used by a server-side proxy; the browser player cannot set them.
|
||||||
|
requiredHeaders = {
|
||||||
|
"Referer": "https://megacloud.club/",
|
||||||
|
"Origin": "https://megacloud.club",
|
||||||
|
"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",
|
||||||
|
"X-Requested-With": "XMLHttpRequest"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamSource =
|
||||||
|
decryptData.sources.find((s) => s.type === "hls") ||
|
||||||
|
decryptData.sources.find((s) => s.type === "mp4");
|
||||||
|
|
||||||
|
if (!streamSource?.file) throw new Error("No valid stream file found");
|
||||||
|
|
||||||
|
const subtitles = (decryptData.tracks || [])
|
||||||
|
.filter((t) => t.kind === "captions")
|
||||||
|
.map((track, index) => ({
|
||||||
|
id: `sub-${index}`,
|
||||||
|
language: track.label || "Unknown",
|
||||||
|
url: track.file,
|
||||||
|
isDefault: !!track.default,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
server: serverName,
|
||||||
|
headers: requiredHeaders,
|
||||||
|
videoSources: [{
|
||||||
|
url: streamSource.file,
|
||||||
|
type: streamSource.type === "hls" ? "m3u8" : "mp4",
|
||||||
|
quality: "auto",
|
||||||
|
subtitles
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (_server === "HD-4") {
|
||||||
|
// Implementation for HD-4 if needed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
safeString(str) {
|
||||||
|
return (typeof str === "string" ? str : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeSeasonParts(title) {
|
||||||
|
const s = this.safeString(title);
|
||||||
|
return s.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "")
|
||||||
|
.replace(/\d+(st|nd|rd|th)/g, (m) => m.replace(/st|nd|rd|th/, ""))
|
||||||
|
.replace(/season|cour|part/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
async extractMegaCloud(embedUrl, returnHeaders = false) {
|
||||||
|
const url = new URL(embedUrl);
|
||||||
|
const baseDomain = `${url.protocol}//${url.host}/`;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Accept": "*/*",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"Referer": baseDomain,
|
||||||
|
"Origin": `${url.protocol}//${url.host}`,
|
||||||
|
"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 fetch(embedUrl, { headers }).then((r) => r.text());
|
||||||
|
const fileIdMatch = html.match(/<title>\s*File\s+#([a-zA-Z0-9]+)\s*-/i);
|
||||||
|
if (!fileIdMatch) throw new Error("file_id not found in embed page");
|
||||||
|
const fileId = fileIdMatch[1];
|
||||||
|
|
||||||
|
let nonce = null;
|
||||||
|
const match48 = html.match(/\b[a-zA-Z0-9]{48}\b/);
|
||||||
|
if (match48) nonce = match48[0];
|
||||||
|
else {
|
||||||
|
const match3x16 = [...html.matchAll(/["']([A-Za-z0-9]{16})["']/g)];
|
||||||
|
if (match3x16.length >= 3) {
|
||||||
|
nonce = match3x16[0][1] + match3x16[1][1] + match3x16[2][1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!nonce) throw new Error("nonce not found");
|
||||||
|
|
||||||
|
const sourcesJson = await fetch(
|
||||||
|
`${baseDomain}embed-2/v3/e-1/getSources?id=${fileId}&_k=${nonce}`,
|
||||||
|
{ headers }
|
||||||
|
).then((r) => r.json());
|
||||||
|
|
||||||
|
return {
|
||||||
|
sources: sourcesJson.sources,
|
||||||
|
tracks: sourcesJson.tracks || [],
|
||||||
|
intro: sourcesJson.intro || null,
|
||||||
|
outro: sourcesJson.outro || null,
|
||||||
|
server: sourcesJson.server || null,
|
||||||
|
headersProvided: returnHeaders ? headers : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = HiAnime;
|
||||||
152
ZeroChan.js
152
ZeroChan.js
@@ -1,113 +1,119 @@
|
|||||||
class ZeroChan {
|
class ZeroChan {
|
||||||
baseUrl = "https://zerochan.net";
|
baseUrl = "https://zerochan.net";
|
||||||
|
|
||||||
constructor(fetchPath, cheerioPath, browser) {
|
constructor() {
|
||||||
this.browser = browser;
|
|
||||||
this.type = "image-board";
|
this.type = "image-board";
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSearchResult(query = "hello", 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 data = await this.browser.scrape(
|
const { result } = await this.scrape(
|
||||||
url,
|
url,
|
||||||
() => {
|
async (page) => {
|
||||||
const list = document.querySelectorAll("#thumbs2 li");
|
return page.evaluate(() => {
|
||||||
if (list.length === 0) {
|
const list = document.querySelectorAll("#thumbs2 li");
|
||||||
return { results: [], hasNextPage: false };
|
if (list.length === 0) {
|
||||||
}
|
return {results: [], hasNextPage: false};
|
||||||
const results = [];
|
}
|
||||||
|
|
||||||
list.forEach(li => {
|
const results = [];
|
||||||
const id = li.getAttribute("data-id");
|
|
||||||
if (!id) return;
|
|
||||||
|
|
||||||
const img = li.querySelector("img");
|
list.forEach(li => {
|
||||||
const imgUrl =
|
const id = li.getAttribute("data-id");
|
||||||
img?.getAttribute("data-src") ||
|
if (!id) return;
|
||||||
img?.getAttribute("src") ||
|
|
||||||
null;
|
|
||||||
|
|
||||||
if (!imgUrl) return;
|
const img = li.querySelector("img");
|
||||||
|
const imgUrl =
|
||||||
|
img?.getAttribute("data-src") ||
|
||||||
|
img?.getAttribute("src") ||
|
||||||
|
null;
|
||||||
|
|
||||||
const tagLinks = li.querySelectorAll("p a");
|
if (!imgUrl) return;
|
||||||
const tags = [...tagLinks]
|
|
||||||
.map(a => a.textContent.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
results.push({
|
const tagLinks = li.querySelectorAll("p a");
|
||||||
id,
|
const tags = [...tagLinks]
|
||||||
image: imgUrl,
|
.map(a => a.textContent.trim())
|
||||||
sampleImageUrl: imgUrl,
|
.filter(Boolean);
|
||||||
tags,
|
|
||||||
type: "preview"
|
results.push({
|
||||||
|
id,
|
||||||
|
image: imgUrl,
|
||||||
|
tags,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hasNextPage =
|
||||||
|
document.querySelector('nav.pagination a[rel="next"]') !== null;
|
||||||
|
|
||||||
|
return {results, hasNextPage};
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasNextPage = document.querySelector('nav.pagination a[rel="next"]') !== null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
results,
|
|
||||||
hasNextPage
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
{ waitSelector: "#thumbs2 li", timeout: 15000, renderWaitTime: 3000, loadImages: true }
|
{
|
||||||
|
waitSelector: "#thumbs2 li",
|
||||||
|
timeout: 15000,
|
||||||
|
renderWaitTime: 3000,
|
||||||
|
loadImages: true
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(data)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results: data.results,
|
results: result.results.map(r => ({
|
||||||
hasNextPage: data.hasNextPage,
|
id: r.id,
|
||||||
|
image: r.image,
|
||||||
|
tags: r.tags
|
||||||
|
})),
|
||||||
|
hasNextPage: result.hasNextPage,
|
||||||
page
|
page
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchInfo(id) {
|
async getInfo(id) {
|
||||||
const url = `${this.baseUrl}/${id}`;
|
const url = `${this.baseUrl}/${id}`;
|
||||||
|
|
||||||
const data = await this.browser.scrape(
|
const { result } = await this.scrape(
|
||||||
url,
|
url,
|
||||||
() => {
|
async (page) => {
|
||||||
const preview = document.querySelector("a.preview");
|
return page.evaluate(() => {
|
||||||
if (!preview) {
|
const preview = document.querySelector("a.preview");
|
||||||
|
if (!preview) {
|
||||||
|
return {
|
||||||
|
fullImage: null,
|
||||||
|
tags: [],
|
||||||
|
createdAt: Date.now()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullImage = preview.getAttribute("href") || null;
|
||||||
|
const img = preview.querySelector("img");
|
||||||
|
const alt = img?.getAttribute("alt") || "";
|
||||||
|
|
||||||
|
let tags = [];
|
||||||
|
if (alt.startsWith("Tags:")) {
|
||||||
|
tags = alt
|
||||||
|
.replace("Tags:", "")
|
||||||
|
.split(",")
|
||||||
|
.map(t => t.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fullImage: null,
|
fullImage,
|
||||||
tags: [],
|
tags,
|
||||||
createdAt: Date.now()
|
createdAt: Date.now()
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
|
|
||||||
const fullImage = preview.getAttribute("href") || null;
|
|
||||||
const img = preview.querySelector("img");
|
|
||||||
const alt = img?.getAttribute("alt") || "";
|
|
||||||
|
|
||||||
let tags = [];
|
|
||||||
if (alt.startsWith("Tags:")) {
|
|
||||||
tags = alt
|
|
||||||
.replace("Tags:", "")
|
|
||||||
.split(",")
|
|
||||||
.map(t => t.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
fullImage,
|
|
||||||
tags,
|
|
||||||
createdAt: Date.now()
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
{ waitSelector: "a.preview img", timeout: 15000 }
|
{ waitSelector: "a.preview img", timeout: 15000 }
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
fullImage: data.fullImage,
|
image: result.fullImage,
|
||||||
tags: data.tags,
|
tags: result.tags
|
||||||
createdAt: data.createdAt,
|
|
||||||
rating: "Unknown"
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { ZeroChan };
|
module.exports = ZeroChan;
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
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 };
|
|
||||||
94
animepictures.js
Normal file
94
animepictures.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
class Animepictures {
|
||||||
|
baseUrl = "https://anime-pictures.net";
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.type = "image-board";
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query = "thighs", page = 1, perPage = 42) {
|
||||||
|
const url = `${this.baseUrl}/posts?page=${page - 1}&search_tag=${query}&order_by=date&lang=en`;
|
||||||
|
|
||||||
|
const { result } = await this.scrape(
|
||||||
|
url,
|
||||||
|
async (page) => {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
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;
|
||||||
|
|
||||||
|
const href = link.getAttribute('href') || "";
|
||||||
|
const idMatch = href.match(/\/posts\/(\d+)/);
|
||||||
|
const id = idMatch ? idMatch[1] : null;
|
||||||
|
|
||||||
|
const imgUrl = img.getAttribute('src');
|
||||||
|
const tagsRaw = img.getAttribute('alt') || "";
|
||||||
|
const tags = tagsRaw.trim().split(/\s+/).filter(Boolean);
|
||||||
|
|
||||||
|
if (id && imgUrl) {
|
||||||
|
results.push({
|
||||||
|
id: id,
|
||||||
|
//full res image: imgUrl.replace("opreviews", "oimages").replace("_cp.avif", ".jpeg"),
|
||||||
|
image: imgUrl,
|
||||||
|
tags: tags,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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: result.results,
|
||||||
|
hasNextPage: result.hasNextPage,
|
||||||
|
page,
|
||||||
|
//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",
|
||||||
|
// "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
|
||||||
|
// "Accept-Language": "en-US,en;q=0.9",
|
||||||
|
// "Referer": "https://anime-pictures.net/",
|
||||||
|
// "Sec-Fetch-Dest": "document",
|
||||||
|
// "Sec-Fetch-Mode": "navigate",
|
||||||
|
// "Sec-Fetch-Site": "none",
|
||||||
|
// "Sec-Fetch-User": "?1"
|
||||||
|
//}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInfo(id) {
|
||||||
|
const url = `${this.baseUrl}/posts/${id}?lang=en`;
|
||||||
|
|
||||||
|
const { result } = await this.scrape(
|
||||||
|
url,
|
||||||
|
async (page) => {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const img = document.querySelector('#big_preview');
|
||||||
|
const image = img ? img.src : null;
|
||||||
|
|
||||||
|
const tagLinks = document.querySelectorAll('.tags li a');
|
||||||
|
const tags = [...tagLinks].map(a => a.textContent.trim());
|
||||||
|
|
||||||
|
return {image, tags};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ waitSelector: '#big_preview', timeout: 15000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
image: result.image,
|
||||||
|
tags: result.tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Animepictures;
|
||||||
143
asmhentai.js
143
asmhentai.js
@@ -1,125 +1,104 @@
|
|||||||
class asmhentai {
|
class asmhentai {
|
||||||
constructor(fetchPath, cheerioPath, browser) {
|
constructor() {
|
||||||
this.baseUrl = "https://asmhentai.com/";
|
this.baseUrl = "https://asmhentai.com";
|
||||||
this.fetch = require(fetchPath);
|
|
||||||
this.cheerio = require(cheerioPath);
|
|
||||||
this.browser = browser;
|
|
||||||
this.type = "book-board";
|
this.type = "book-board";
|
||||||
|
this.mediaType = "manga";
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSearchResult(query = "", page = 1) {
|
async search(queryObj) {
|
||||||
const q = query.trim().replace(/\s+/g, "+");
|
const q = (queryObj.query || "").trim().replace(/\s+/g, "+");
|
||||||
const url = q ? `${this.baseUrl}/search/?q=${q}&page=${page}` : `${this.baseUrl}/?q=&page=${page}`;
|
const html = await fetch(`${this.baseUrl}/search/?q=${q}&page=1`).then(r => r.text());
|
||||||
|
|
||||||
const res = await this.fetch(url);
|
|
||||||
const html = await res.text();
|
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
const items = $(".ov_item .preview_item");
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
items.each((_, el) => {
|
$(".preview_item").each((_, el) => {
|
||||||
const $el = $(el);
|
const href = $(el).find(".image a").attr("href");
|
||||||
|
const id = href?.match(/\/g\/(\d+)\//)?.[1];
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
const href = $el.find(".image a").attr("href") || "";
|
let img = $(el).find(".image img").attr("data-src") || $(el).find(".image img").attr("src") || "";
|
||||||
const id = href.match(/\d+/)?.[0] || null;
|
if (img.startsWith("//")) img = "https:" + img;
|
||||||
|
|
||||||
const img = $el.find(".image img");
|
const image = img.replace("thumb.jpg", "1.jpg");
|
||||||
const raw = img.attr("data-src") || img.attr("src") || "";
|
const title = $(el).find("h2.caption").text().trim();
|
||||||
let image = raw.startsWith("//") ? "https:" + raw : raw;
|
|
||||||
const sampleImageUrl = image.replace("thumb", "cover");
|
|
||||||
|
|
||||||
image = image.replace(/[^\/]+$/, "1.jpg");
|
|
||||||
|
|
||||||
const title = ($el.find(".cpt h2.caption").text() || "").trim();
|
|
||||||
|
|
||||||
const tagsRaw = $el.attr("data-tags") || "";
|
|
||||||
const tags = tagsRaw.split(" ").filter(Boolean);
|
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
id,
|
id,
|
||||||
image,
|
image,
|
||||||
sampleImageUrl,
|
|
||||||
title,
|
title,
|
||||||
tags,
|
rating: null,
|
||||||
type: "book"
|
type: "book"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasNextPage = $('ul.pagination a.page-link').filter((_, el) => $(el).text().trim().toLowerCase() === "next").length > 0;
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetadata(id) {
|
||||||
|
const html = await fetch(`${this.baseUrl}/g/${id}/`).then(r => r.text());
|
||||||
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
|
let image =
|
||||||
|
$('a[href^="/gallery/"] img').attr("data-src") ||
|
||||||
|
$('a[href^="/gallery/"] img').attr("src") ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
if (image.startsWith("//")) image = "https:" + image;
|
||||||
|
|
||||||
|
const genres = $(".tags .tag_list .badge.tag")
|
||||||
|
.map((_, el) => $(el).clone().children().remove().end().text().trim())
|
||||||
|
.get()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results,
|
id,
|
||||||
hasNextPage,
|
title: $("h1").first().text().trim(),
|
||||||
page
|
format: "MANGA",
|
||||||
|
score: 0,
|
||||||
|
genres,
|
||||||
|
status: "unknown",
|
||||||
|
published: "???",
|
||||||
|
summary: "",
|
||||||
|
chapters: 1,
|
||||||
|
image
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapters(mangaId) {
|
async findChapters(mangaId) {
|
||||||
const res = await this.fetch(`${this.baseUrl}/g/${mangaId}/`);
|
const html = await fetch(`${this.baseUrl}/g/${mangaId}/`).then(r => r.text());
|
||||||
const html = await res.text();
|
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
const title = $(".right .info h1").first().text().trim() || "";
|
const title = $("h1").first().text().trim() || "Chapter 1";
|
||||||
|
|
||||||
let cover = $(".cover img").attr("data-src") || $(".cover img").attr("src") || "";
|
let thumb = $(".gallery img").first().attr("data-src") || "";
|
||||||
if (cover.startsWith("//")) cover = "https:" + cover;
|
if (thumb.startsWith("//")) thumb = "https:" + thumb;
|
||||||
|
|
||||||
const firstThumb = $('.gallery a img').first();
|
const base = thumb.match(/https:\/\/[^\/]+\/\d+\/\d+\//)?.[0];
|
||||||
let t = firstThumb.attr("data-src") || "";
|
const pages = parseInt($(".pages").text().match(/\d+/)?.[0] || "0");
|
||||||
|
const ext = thumb.match(/\.(jpg|png|jpeg|gif)/i)?.[1] || "jpg";
|
||||||
|
|
||||||
if (t.startsWith("//")) t = "https:" + t;
|
const chapterId = Buffer.from(JSON.stringify({ base, pages, ext })).toString("base64");
|
||||||
|
|
||||||
// ex: https://images.asmhentai.com/017/598614/8t.jpg
|
return [{
|
||||||
const baseMatch = t.match(/https:\/\/[^\/]+\/\d+\/\d+\//);
|
id: chapterId,
|
||||||
const basePath = baseMatch ? baseMatch[0] : null;
|
title,
|
||||||
|
number: 1,
|
||||||
const pagesText = $(".pages h3").text(); // "Pages: 39"
|
releaseDate: null,
|
||||||
const pagesMatch = pagesText.match(/(\d+)/);
|
index: 0
|
||||||
const pages = pagesMatch ? parseInt(pagesMatch[1]) : 0;
|
}];
|
||||||
|
|
||||||
let ext = "jpg";
|
|
||||||
const extMatch = t.match(/\.(jpg|png|jpeg|gif)/i);
|
|
||||||
if (extMatch) ext = extMatch[1];
|
|
||||||
|
|
||||||
let language = "unknown";
|
|
||||||
const langFlag = $('.info a[href^="/language/"] img').attr("src") || "";
|
|
||||||
if (langFlag.includes("en")) language = "english";
|
|
||||||
if (langFlag.includes("jp")) language = "japanese";
|
|
||||||
|
|
||||||
const encodedChapterId = Buffer.from(
|
|
||||||
JSON.stringify({
|
|
||||||
base: basePath,
|
|
||||||
pages,
|
|
||||||
ext
|
|
||||||
})
|
|
||||||
).toString("base64");
|
|
||||||
|
|
||||||
return {
|
|
||||||
chapters: [
|
|
||||||
{
|
|
||||||
id: encodedChapterId,
|
|
||||||
title,
|
|
||||||
chapter: 1,
|
|
||||||
index: 0,
|
|
||||||
language
|
|
||||||
}
|
|
||||||
],
|
|
||||||
cover
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapterPages(chapterId) {
|
async findChapterPages(chapterId) {
|
||||||
const decoded = JSON.parse(
|
const { base, pages, ext } = JSON.parse(
|
||||||
Buffer.from(chapterId, "base64").toString("utf8")
|
Buffer.from(chapterId, "base64").toString("utf8")
|
||||||
);
|
);
|
||||||
const { base, pages, ext } = decoded;
|
|
||||||
|
|
||||||
return Array.from({ length: pages }, (_, i) => ({
|
return Array.from({ length: pages }, (_, i) => ({
|
||||||
url: `${base}${i + 1}.${ext}`,
|
url: `${base}${i + 1}.${ext}`,
|
||||||
index: i,
|
index: i
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { asmhentai };
|
module.exports = asmhentai;
|
||||||
164
gelbooru.js
164
gelbooru.js
@@ -1,139 +1,83 @@
|
|||||||
class Gelbooru {
|
class Gelbooru {
|
||||||
baseUrl = "https://gelbooru.com";
|
baseUrl = "https://gelbooru.com";
|
||||||
|
|
||||||
constructor(fetchPath, cheerioPath) {
|
constructor() {
|
||||||
this.fetch = require(fetchPath);
|
|
||||||
this.load = require(cheerioPath).load;
|
|
||||||
this.type = "image-board";
|
this.type = "image-board";
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSearchResult(query, page = 1, perPage = 42) {
|
async search(query = "thighs", page = 1, perPage = 42) {
|
||||||
if (!query) query = "original";
|
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=${query}&pid=${(page - 1) * perPage}`;
|
const html = await fetch(url, {
|
||||||
|
headers: { "User-Agent": "Mozilla/5.0" }
|
||||||
const response = await this.fetch(url, {
|
}).then(r => r.text());
|
||||||
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 $ = this.cheerio.load(html);
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
$('.thumbnail-container a').each((i, e) => {
|
$("article.thumbnail-preview > a[id^='p']").each((_, el) => {
|
||||||
const $e = $(e);
|
const id = $(el).attr("id")?.slice(1); // p13123834 → 13123834
|
||||||
const href = $e.attr('href');
|
if (!id) return;
|
||||||
|
|
||||||
const idMatch = href.match(/id=(\d+)/);
|
const img = $(el).find("img");
|
||||||
const id = idMatch ? idMatch[1] : null;
|
const image = img.attr("src");
|
||||||
|
|
||||||
const image = $e.find('img').attr('src');
|
const tags = img.attr("alt")
|
||||||
|
?.replace(/^Rule 34 \|\s*/, "")
|
||||||
|
?.split(",")
|
||||||
|
?.map(t => t.trim())
|
||||||
|
?.filter(Boolean) || [];
|
||||||
|
|
||||||
const tags = $e.find('img').attr('alt')?.trim()?.split(' ').filter(tag => tag !== "");
|
results.push({ id, image, tags });
|
||||||
|
|
||||||
if (id && image) {
|
|
||||||
results.push({
|
|
||||||
id: id,
|
|
||||||
image: image,
|
|
||||||
tags: tags,
|
|
||||||
type: 'preview'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const pagination = $('.pagination a');
|
// pagination
|
||||||
|
const totalPages = Math.max(
|
||||||
let totalPages = 1;
|
page,
|
||||||
pagination.each((i, e) => {
|
...$("a[href*='pid=']")
|
||||||
const href = $(e).attr('href');
|
.map((_, el) =>
|
||||||
if (href && href.includes('pid=')) {
|
Math.floor(
|
||||||
const pidMatch = href.match(/pid=(\d+)/);
|
parseInt($(el).attr("href")?.match(/pid=(\d+)/)?.[1] || 0) / perPage
|
||||||
if (pidMatch) {
|
) + 1
|
||||||
const pid = parseInt(pidMatch[1], 10);
|
)
|
||||||
totalPages = Math.max(totalPages, Math.floor(pid / perPage) + 1);
|
.get()
|
||||||
}
|
);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentPage = page;
|
|
||||||
const nextPage = currentPage < totalPages ? currentPage + 1 : null;
|
|
||||||
const previousPage = currentPage > 1 ? currentPage - 1 : null;
|
|
||||||
const hasNextPage = nextPage !== null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: totalPages * perPage,
|
results,
|
||||||
next: nextPage !== null ? nextPage : 0,
|
page,
|
||||||
previous: previousPage !== null ? previousPage : 0,
|
hasNextPage: page < totalPages
|
||||||
pages: totalPages,
|
|
||||||
page: currentPage,
|
|
||||||
hasNextPage,
|
|
||||||
results
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchInfo(id) {
|
async getInfo(id) {
|
||||||
const url = `${this.baseUrl}/index.php?page=post&s=view&id=${id}`;
|
const html = await fetch(
|
||||||
|
`${this.baseUrl}/index.php?page=post&s=view&id=${id}`,
|
||||||
|
{ headers: { "User-Agent": "Mozilla/5.0" } }
|
||||||
|
).then(r => r.text());
|
||||||
|
|
||||||
const response = await this.fetch(url, {
|
const $ = this.cheerio.load(html);
|
||||||
headers: {
|
const container = $("section.image-container");
|
||||||
'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 image =
|
||||||
|
container.find("#image").attr("src") ||
|
||||||
|
container.attr("data-file-url") ||
|
||||||
|
container.attr("data-large-file-url") ||
|
||||||
|
null;
|
||||||
|
|
||||||
let fullImage;
|
// tags
|
||||||
|
const tags = container
|
||||||
fullImage = $('#gelcom_img').attr('src') || $('#gelcom_mp4').attr('src');
|
.attr("data-tags")
|
||||||
|
?.trim()
|
||||||
if (!fullImage) {
|
?.split(/\s+/)
|
||||||
fullImage = $('#right-col a[href*="/images/"]').attr('href') || $('#right-col a[href*="/videos/"]').attr('href');
|
?.filter(Boolean) || [];
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
id,
|
id,
|
||||||
fullImage,
|
image,
|
||||||
resizedImageUrl: fullImage,
|
tags
|
||||||
tags,
|
};
|
||||||
createdAt,
|
|
||||||
publishedBy,
|
|
||||||
rating,
|
|
||||||
|
|
||||||
comments
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { Gelbooru };
|
module.exports = Gelbooru;
|
||||||
71
giphy.js
71
giphy.js
@@ -1,24 +1,22 @@
|
|||||||
class Giphy {
|
class Giphy {
|
||||||
baseUrl = "https://giphy.com";
|
baseUrl = "https://giphy.com";
|
||||||
|
|
||||||
constructor(fetchPath, cheerioPath, browser) {
|
constructor() {
|
||||||
this.browser = browser;
|
|
||||||
this.type = "image-board";
|
this.type = "image-board";
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSearchResult(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.browser.scrape(
|
const data = await this.scrape(
|
||||||
url,
|
url,
|
||||||
() => {
|
(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');
|
||||||
|
|
||||||
// solo coger sources válidos
|
|
||||||
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');
|
||||||
|
|
||||||
@@ -27,20 +25,14 @@ class Giphy {
|
|||||||
srcImg?.src ||
|
srcImg?.src ||
|
||||||
null;
|
null;
|
||||||
|
|
||||||
// ignorar 1x1 base64
|
|
||||||
if (!rawSrc || rawSrc.startsWith("data:")) return;
|
if (!rawSrc || rawSrc.startsWith("data:")) return;
|
||||||
|
|
||||||
const imgUrl = rawSrc;
|
|
||||||
|
|
||||||
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: imgUrl,
|
image: rawSrc,
|
||||||
sampleImageUrl: imgUrl,
|
|
||||||
tags,
|
|
||||||
type: "preview"
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,24 +40,61 @@ class Giphy {
|
|||||||
results,
|
results,
|
||||||
hasNextPage: false
|
hasNextPage: false
|
||||||
};
|
};
|
||||||
},
|
}),
|
||||||
{ waitSelector: 'picture img, a[data-giphy-id] img', scrollToBottom: true, timeout: 15000}
|
{
|
||||||
|
waitSelector: 'picture img, a[data-giphy-id] img',
|
||||||
|
scrollToBottom: true,
|
||||||
|
timeout: 15000
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results: data.results,
|
results: data.result.results.map(r => ({
|
||||||
hasNextPage: data.hasNextPage,
|
id: r.id,
|
||||||
|
image: r.image
|
||||||
|
})),
|
||||||
|
hasNextPage: data.result.hasNextPage,
|
||||||
page
|
page
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchInfo(id) {
|
async getInfo(id) {
|
||||||
|
const url = `https://giphy.com/gifs/${id}`;
|
||||||
|
|
||||||
|
const data = await this.scrape(
|
||||||
|
url,
|
||||||
|
(page) => page.evaluate(() => {
|
||||||
|
const scripts = document.querySelectorAll(
|
||||||
|
'script[type="application/ld+json"]'
|
||||||
|
);
|
||||||
|
|
||||||
|
let imgsrc = null;
|
||||||
|
|
||||||
|
scripts.forEach(script => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(script.textContent);
|
||||||
|
|
||||||
|
if (json?.["@type"] === "Article" && json?.image?.url) {
|
||||||
|
imgsrc = json.image.url;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
image: imgsrc
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
waitSelector: 'script[type="application/ld+json"]',
|
||||||
|
timeout: 15000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
createdAt: Date.now(),
|
image: data.result.image
|
||||||
rating: "Unknown"
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { Giphy };
|
module.exports = Giphy;
|
||||||
@@ -1,156 +1,188 @@
|
|||||||
class ligntnovelworld {
|
class lightnovelworld {
|
||||||
constructor(fetchPath, cheerioPath, browser) {
|
constructor() {
|
||||||
this.browser = browser;
|
|
||||||
this.fetch = require(fetchPath);
|
|
||||||
this.cheerio = require(cheerioPath);
|
|
||||||
this.baseUrl = "https://lightnovelworld.org/api";
|
this.baseUrl = "https://lightnovelworld.org/api";
|
||||||
|
|
||||||
this.type = "book-board";
|
this.type = "book-board";
|
||||||
|
this.mediaType = "ln";
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSearchResult(query = "", page = 1) {
|
async search(queryObj) {
|
||||||
if (query.trim() !== "") {
|
const query = queryObj.query?.trim() || "";
|
||||||
const res = await this.fetch(`${this.baseUrl}/search/?q=${encodeURIComponent(query)}&search_type=title`);
|
|
||||||
|
if (query !== "") {
|
||||||
|
const res = await fetch(
|
||||||
|
`${this.baseUrl}/search/?q=${encodeURIComponent(query)}&search_type=title`
|
||||||
|
);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
const results = data.novels.map(n => ({
|
if (!data.novels) return [];
|
||||||
|
|
||||||
|
return data.novels.map(n => ({
|
||||||
id: n.slug,
|
id: n.slug,
|
||||||
title: n.title,
|
title: n.title,
|
||||||
image: `https://lightnovelworld.org/${n.cover_path}`,
|
image: `https://lightnovelworld.org/${n.cover_path}`,
|
||||||
sampleImageUrl: `https://lightnovelworld.org/${n.cover_path}`,
|
rating: `Rank ${n.rank}`,
|
||||||
tags: [],
|
format: "Light Novel"
|
||||||
type: "book"
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
|
||||||
results,
|
|
||||||
hasNextPage: false,
|
|
||||||
page
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await this.fetch("https://lightnovelworld.org/");
|
const res = await fetch("https://lightnovelworld.org/");
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
const $ = this.cheerio.load(html);
|
|
||||||
|
|
||||||
const cards = $(".recommendations-grid .recommendation-card");
|
const cards = html.split('class="recommendation-card"').slice(1);
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
cards.each((_, el) => {
|
for (const block of cards) {
|
||||||
const card = $(el);
|
const link = block.match(/href="([^"]+)"/)?.[1] || "";
|
||||||
const link = card.find("a.card-cover-link").attr("href") || "";
|
|
||||||
const id = link.replace(/^\/novel\//, "").replace(/\/$/, "");
|
const id = link.replace(/^\/novel\//, "").replace(/\/$/, "");
|
||||||
const title = card.find(".card-title").text().trim();
|
|
||||||
|
|
||||||
const img = card.find(".card-cover img").attr("src") || "";
|
const title = block.match(/class="card-title"[^>]*>([^<]+)/)?.[1]?.trim() || null;
|
||||||
const imageUrl = img.startsWith("http") ? img : `https://lightnovelworld.org${img}`;
|
|
||||||
|
|
||||||
const tags = card.find(".card-genres .genre-tag").map((_, t) => $(t).text().trim()).get();
|
let img = block.match(/<img[^>]+src="([^"]+)"/)?.[1] || "";
|
||||||
|
if (img && !img.startsWith("http"))
|
||||||
|
img = `https://lightnovelworld.org${img}`;
|
||||||
|
|
||||||
results.push({
|
if (id && title) {
|
||||||
id,
|
results.push({
|
||||||
title,
|
id,
|
||||||
image: imageUrl,
|
title,
|
||||||
sampleImageUrl: imageUrl,
|
image: img,
|
||||||
tags,
|
rating: null,
|
||||||
type: "book"
|
format: "Light Novel"
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetadata(id){
|
||||||
|
const res = await fetch(`https://lightnovelworld.org/novel/${id}`);
|
||||||
|
const html = await res.text();
|
||||||
|
|
||||||
|
const match = html.match(
|
||||||
|
/<script type="application\/ld\+json">([\s\S]*?)<\/script>/
|
||||||
|
);
|
||||||
|
|
||||||
|
let data = {};
|
||||||
|
if(match){
|
||||||
|
try{
|
||||||
|
data = JSON.parse(match[1]);
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawScore = Number(data.aggregateRating?.ratingValue || 1);
|
||||||
|
const score100 = Math.round((rawScore / 5) * 100);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results,
|
id: id,
|
||||||
hasNextPage: false,
|
title: data.name || "",
|
||||||
page
|
format: "Light Novel",
|
||||||
|
score: score100,
|
||||||
|
genres: Array.isArray(data.genre) ? data.genre : [],
|
||||||
|
status: data.status || "",
|
||||||
|
published: "???",
|
||||||
|
summary: data.description || "",
|
||||||
|
chapters: data.numberOfPages ? Number(data.numberOfPages) : 1,
|
||||||
|
image: data.image
|
||||||
|
? (data.image.startsWith("http")
|
||||||
|
? data.image
|
||||||
|
: `https://lightnovelworld.org${data.image}`)
|
||||||
|
: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapters(bookId) {
|
async findChapters(bookId) {
|
||||||
|
const chapters = [];
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
const limit = 500;
|
const limit = 500;
|
||||||
const chapters = [];
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const res = await this.fetch(`https://lightnovelworld.org/api/novel/${bookId}/chapters/?offset=${offset}&limit=${limit}`);
|
const res = await fetch(
|
||||||
|
`https://lightnovelworld.org/api/novel/${bookId}/chapters/?offset=${offset}&limit=${limit}`
|
||||||
|
);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!data.chapters) break;
|
||||||
|
|
||||||
chapters.push(
|
chapters.push(
|
||||||
...data.chapters.map(c => ({
|
...data.chapters.map((c, i) => ({
|
||||||
id: `https://lightnovelworld.org/novel/${bookId}/chapter/${c.number}/`,
|
id: `https://lightnovelworld.org/novel/${bookId}/chapter/${c.number}/`,
|
||||||
title: c.title,
|
title: c.title,
|
||||||
chapter: c.number,
|
number: Number(c.number),
|
||||||
language: 'en'
|
releaseDate: null,
|
||||||
|
index: offset + i
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data.has_more) break;
|
if (!data.has_more) break;
|
||||||
offset += limit;
|
offset += limit;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { chapters: chapters};
|
return chapters;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapterPages(chapterId) {
|
async findChapterPages(chapterId) {
|
||||||
const res = await this.fetch(chapterId, {
|
const data = await this.scrape(
|
||||||
headers: {
|
chapterId,
|
||||||
'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',
|
(page) => page.evaluate(() => document.documentElement.outerHTML),
|
||||||
'referer': chapterId.replace(/\/\d+\/$/, ''),
|
{
|
||||||
'sec-ch-ua': '"Chromium";v="139", "Not;A=Brand";v="99"',
|
waitUntil: "domcontentloaded",
|
||||||
'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'
|
timeout: 15000
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
const html = await res.text();
|
const html = data.result;
|
||||||
const $ = this.cheerio.load(html);
|
if (!html) return '<p>Error loading chapter</p>';
|
||||||
|
|
||||||
const contentDiv = $('#chapterText');
|
const cutPoints = [
|
||||||
|
'<div class="bottom-nav"',
|
||||||
|
'<div class="comments-section"',
|
||||||
|
'<div class="settings-panel"',
|
||||||
|
'©'
|
||||||
|
];
|
||||||
|
|
||||||
if (!contentDiv || contentDiv.length === 0) {
|
let cutIndex = html.length;
|
||||||
return [{
|
for (const marker of cutPoints) {
|
||||||
type: 'text',
|
const pos = html.indexOf(marker);
|
||||||
content: '<p>Error: content not found</p>',
|
if (pos !== -1 && pos < cutIndex) cutIndex = pos;
|
||||||
index: 0
|
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
contentDiv.find('script').remove();
|
const chapterHtml = html.substring(0, cutIndex);
|
||||||
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');
|
const pMatches = [...chapterHtml.matchAll(/<p[^>]*>([\s\S]*?)<\/p>/gi)];
|
||||||
let cleanHtml = '';
|
let cleanHtml = '';
|
||||||
|
|
||||||
paragraphs.each((_, el) => {
|
for (const match of pMatches) {
|
||||||
const p = $(el);
|
let text = match[1]
|
||||||
let text = p.text() || '';
|
.replace(/△▼△▼△▼△/g, '')
|
||||||
|
.replace(/[※▲▼■◆]/g, '')
|
||||||
|
.replace(/ /gi, ' ')
|
||||||
|
.replace(/\s{2,}/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
text = text.replace(/△▼△▼△▼△/g, '').replace(/[※]+/g, '').replace(/\s{2,}/g, ' ').trim();
|
text = text
|
||||||
const htmlP = p.html()?.trim() || '';
|
.replace(/"/g, '"')
|
||||||
const isEmpty = htmlP === '' || htmlP === ' ' || text.trim() === '';
|
.replace(/'/g, "'")
|
||||||
const isAd = text.includes('Remove Ads') || text.includes('Buy no ads') || text.includes('novelfire');
|
.replace(/'/g, "'")
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/“/g, '“')
|
||||||
|
.replace(/”/g, '”')
|
||||||
|
.replace(/‘/g, '‘')
|
||||||
|
.replace(/’/g, '’')
|
||||||
|
.replace(/—/g, '—')
|
||||||
|
.replace(/–/g, '–');
|
||||||
|
|
||||||
if (!isEmpty && !isAd) {
|
if (!text || text.length < 3) continue;
|
||||||
if (p.text() !== text) p.text(text);
|
if (/svg|button|modal|comment|loading|default|dyslexic|roboto|lora|line spacing/i.test(text)) continue;
|
||||||
cleanHtml += $.html(p);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!cleanHtml.trim()) { cleanHtml = contentDiv.html() || ''; }
|
cleanHtml += `<p>${text}</p>\n`;
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return cleanHtml.trim() || '<p>Empty chapter</p>';
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
content: cleanHtml.trim(),
|
|
||||||
index: 0
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { novelupdates: ligntnovelworld };
|
module.exports = lightnovelworld;
|
||||||
|
|||||||
146
mangadex.js
146
mangadex.js
@@ -1,10 +1,9 @@
|
|||||||
class MangaDex {
|
class MangaDex {
|
||||||
constructor(fetchPath, cheerioPath, browser) {
|
constructor() {
|
||||||
this.fetchPath = fetchPath;
|
|
||||||
this.browser = browser;
|
|
||||||
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";
|
||||||
}
|
}
|
||||||
|
|
||||||
getHeaders() {
|
getHeaders() {
|
||||||
@@ -14,38 +13,26 @@ class MangaDex {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async _fetch(url, options = {}) {
|
async search(queryObj) {
|
||||||
if (typeof fetch === 'function') {
|
const query = queryObj.query?.trim() || "";
|
||||||
return fetch(url, options);
|
|
||||||
}
|
|
||||||
const nodeFetch = require(this.fetchPath);
|
|
||||||
return nodeFetch(url, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchSearchResult(query = "", page = 1) {
|
|
||||||
const limit = 25;
|
const limit = 25;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (1 - 1) * limit;
|
||||||
|
|
||||||
let url;
|
const url = `${this.apiUrl}/manga?title=${encodeURIComponent(query)}&limit=${limit}&offset=${offset}&includes[]=cover_art&contentRating[]=safe&contentRating[]=suggestive&availableTranslatedLanguage[]=en`;
|
||||||
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 {
|
try {
|
||||||
const response = await this._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 { results: [], hasNextPage: false, page };
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
if (!json || !Array.isArray(json.data)) {
|
if (!json || !Array.isArray(json.data)) {
|
||||||
return { results: [], hasNextPage: false, page };
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = 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';
|
||||||
@@ -57,36 +44,91 @@ class MangaDex {
|
|||||||
? `https://uploads.mangadex.org/covers/${manga.id}/${coverFileName}.256.jpg`
|
? `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 {
|
return {
|
||||||
id: manga.id,
|
id: manga.id,
|
||||||
image: coverUrl,
|
image: coverUrl,
|
||||||
sampleImageUrl: fullCoverUrl,
|
|
||||||
title: title,
|
title: title,
|
||||||
tags: tags,
|
rating: null,
|
||||||
type: 'book'
|
type: 'book'
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const total = json.total || 0;
|
} catch (e) {
|
||||||
const hasNextPage = (offset + limit) < total;
|
console.error("Error during MangaDex search:", e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetadata(id) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://api.mangadex.org/manga/${id}?includes[]=cover_art`);
|
||||||
|
if (!res.ok) throw new Error("MangaDex API error");
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
const manga = json.data;
|
||||||
|
const attr = manga.attributes;
|
||||||
|
|
||||||
|
const title =
|
||||||
|
attr.title?.en ||
|
||||||
|
Object.values(attr.title || {})[0] ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
const summary =
|
||||||
|
attr.description?.en ||
|
||||||
|
Object.values(attr.description || {})[0] ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
const genres = manga.relationships
|
||||||
|
?.filter(r => r.type === "tag")
|
||||||
|
?.map(r =>
|
||||||
|
r.attributes?.name?.en ||
|
||||||
|
Object.values(r.attributes?.name || {})[0]
|
||||||
|
)
|
||||||
|
?.filter(Boolean) || [];
|
||||||
|
|
||||||
|
const coverRel = manga.relationships.find(r => r.type === "cover_art");
|
||||||
|
const coverFile = coverRel?.attributes?.fileName;
|
||||||
|
const image = coverFile
|
||||||
|
? `https://uploads.mangadex.org/covers/${id}/${coverFile}.512.jpg`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const score100 = 0;
|
||||||
|
|
||||||
|
const statusMap = {
|
||||||
|
ongoing: "Ongoing",
|
||||||
|
completed: "Completed",
|
||||||
|
hiatus: "Hiatus",
|
||||||
|
cancelled: "Cancelled"
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results,
|
id,
|
||||||
hasNextPage,
|
title,
|
||||||
page
|
format: "Manga",
|
||||||
|
score: score100,
|
||||||
|
genres,
|
||||||
|
status: statusMap[attr.status] || "",
|
||||||
|
published: attr.year ? String(attr.year) : "???",
|
||||||
|
summary,
|
||||||
|
chapters: attr.lastChapter ? Number(attr.lastChapter) || 0 : 0,
|
||||||
|
image
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error during MangaDex search:", e);
|
console.error("MangaDex getMetadata error:", e);
|
||||||
return { results: [], hasNextPage: false, error: e.message };
|
return {
|
||||||
|
id,
|
||||||
|
title: "",
|
||||||
|
format: "Manga",
|
||||||
|
score: 0,
|
||||||
|
genres: [],
|
||||||
|
status: "",
|
||||||
|
published: "???",
|
||||||
|
summary: "",
|
||||||
|
chapters: 0,
|
||||||
|
image: ""
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +138,7 @@ class MangaDex {
|
|||||||
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 this._fetch(url, { headers: this.getHeaders() });
|
const response = await fetch(url, { headers: this.getHeaders() });
|
||||||
let chapters = [];
|
let chapters = [];
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -107,7 +149,7 @@ class MangaDex {
|
|||||||
.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}`,
|
||||||
chapter: ch.attributes.chapter,
|
number: ch.attributes.chapter,
|
||||||
index: index,
|
index: index,
|
||||||
language: ch.attributes.translatedLanguage
|
language: ch.attributes.translatedLanguage
|
||||||
}));
|
}));
|
||||||
@@ -124,23 +166,7 @@ class MangaDex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let highResCover = null;
|
return chapters;
|
||||||
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) {
|
} catch (e) {
|
||||||
console.error("Error finding MangaDex chapters:", e);
|
console.error("Error finding MangaDex chapters:", e);
|
||||||
return { chapters: [], cover: null };
|
return { chapters: [], cover: null };
|
||||||
@@ -153,7 +179,7 @@ class MangaDex {
|
|||||||
const url = `${this.apiUrl}/at-home/server/${chapterId}`;
|
const url = `${this.apiUrl}/at-home/server/${chapterId}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this._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();
|
||||||
@@ -177,4 +203,4 @@ class MangaDex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { MangaDex };
|
module.exports = MangaDex;
|
||||||
139
mangapark.js
139
mangapark.js
@@ -1,25 +1,20 @@
|
|||||||
class mangapark {
|
class mangapark {
|
||||||
constructor(fetchPath, cheerioPath, browser) {
|
constructor() {
|
||||||
this.baseUrl = "https://mangapark.net/apo";
|
this.baseUrl = "https://mangapark.net/apo";
|
||||||
this.fetch = require(fetchPath)
|
|
||||||
this.browser = browser;
|
|
||||||
this.type = "book-board";
|
this.type = "book-board";
|
||||||
|
this.mediaType = "manga";
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSearchResult(query = "", page = 1) {
|
async search(queryObj) {
|
||||||
const res = await this.fetch(`${this.baseUrl}/`, {
|
const query = queryObj.query;
|
||||||
|
|
||||||
|
const res = await fetch(`${this.baseUrl}/`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"accept": "*/*",
|
"accept": "*/*",
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"x-apollo-operation-name": "get_searchComic",
|
"x-apollo-operation-name": "get_searchComic",
|
||||||
"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/120.0.0.0 Safari/537.36"
|
||||||
"sec-ch-ua-mobile": "?0",
|
|
||||||
"sec-ch-ua-platform": "\"Windows\"",
|
|
||||||
"sec-fetch-dest": "empty",
|
|
||||||
"sec-fetch-mode": "cors",
|
|
||||||
"sec-fetch-site": "same-origin",
|
|
||||||
"Referer": `https://mangapark.net/search?word=${encodeURIComponent(query)}`
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
query: "query get_searchComic($select: SearchComic_Select) { get_searchComic(select: $select) { reqPage reqSize reqSort reqWord newPage paging { total pages page init size skip limit prev next } items { id data { id dbStatus name origLang tranLang urlPath urlCover600 urlCoverOri genres altNames authors artists is_hot is_new sfw_result score_val follows reviews comments_total max_chapterNode { id data { id dateCreate dbStatus isFinal sfw_result dname urlPath is_new userId userNode { id data { id name uniq avatarUrl urlPath } } } } } sser_follow sser_lastReadChap { date chapterNode { id data { id dbStatus isFinal sfw_result dname urlPath is_new userId userNode { id data { id name uniq avatarUrl urlPath } } } } } } } }",
|
query: "query get_searchComic($select: SearchComic_Select) { get_searchComic(select: $select) { reqPage reqSize reqSort reqWord newPage paging { total pages page init size skip limit prev next } items { id data { id dbStatus name origLang tranLang urlPath urlCover600 urlCoverOri genres altNames authors artists is_hot is_new sfw_result score_val follows reviews comments_total max_chapterNode { id data { id dateCreate dbStatus isFinal sfw_result dname urlPath is_new userId userNode { id data { id name uniq avatarUrl urlPath } } } } } sser_follow sser_lastReadChap { date chapterNode { id data { id dbStatus isFinal sfw_result dname urlPath is_new userId userNode { id data { id name uniq avatarUrl urlPath } } } } } } } }",
|
||||||
@@ -30,38 +25,96 @@ class mangapark {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const results = data.data.get_searchComic.items.map(m => ({
|
|
||||||
id: m.data.urlPath,
|
if (!data.data || !data.data.get_searchComic || !data.data.get_searchComic.items) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.data.get_searchComic.items.map(m => ({
|
||||||
|
id: m.data.urlPath.split('/title/')[1]?.split('-')[0] || mangaId.split('/comic/')[1]?.split('-')[0], // This identifies the book
|
||||||
title: m.data.name,
|
title: m.data.name,
|
||||||
image: `https://mangapark.net/${m.data.urlCoverOri}`,
|
image: `https://mangapark.net/${m.data.urlCoverOri}`,
|
||||||
sampleImageUrl: `https://mangapark.net/${m.data.urlCoverOri}`,
|
rating: m.data.score_val ? Math.round(m.data.score_val * 10) : null,
|
||||||
tags: m.data.genres,
|
type: "book",
|
||||||
type: "book"
|
headers: {
|
||||||
|
referer: "https://mangapark.net"
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetadata(id) {
|
||||||
|
const res = await fetch(`https://mangapark.net/title/${id}`);
|
||||||
|
const html = await res.text();
|
||||||
|
|
||||||
|
const match = html.match(
|
||||||
|
/<script type="qwik\/json">([\s\S]*?)<\/script>/
|
||||||
|
);
|
||||||
|
if (!match) throw new Error("qwik json not found");
|
||||||
|
|
||||||
|
function decodeQwik(obj) {
|
||||||
|
const refs = obj.refs || {};
|
||||||
|
function walk(v) {
|
||||||
|
if (typeof v === "string" && refs[v] !== undefined) {
|
||||||
|
return walk(refs[v]);
|
||||||
|
}
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
return v.map(walk);
|
||||||
|
}
|
||||||
|
if (v && typeof v === "object") {
|
||||||
|
const out = {};
|
||||||
|
for (const k in v) out[k] = walk(v[k]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
return walk(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = JSON.parse(match[1]);
|
||||||
|
const data = decodeQwik(raw);
|
||||||
|
|
||||||
|
const comic =
|
||||||
|
data?.objs?.find(o => o && o.name && o.summary) ||
|
||||||
|
data?.state?.comic;
|
||||||
|
|
||||||
|
if (!comic) throw new Error("comic not found");
|
||||||
|
|
||||||
|
const score100 = comic.score_avg
|
||||||
|
? Math.round((Number(comic.score_avg) / 10) * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results: results,
|
id,
|
||||||
hasNextPage: false,
|
title: comic.name || "",
|
||||||
page
|
format: "Manga",
|
||||||
|
score: score100,
|
||||||
|
genres: comic.genres || [],
|
||||||
|
status: comic.originalStatus || comic.status || "",
|
||||||
|
published: comic.originalPubFrom
|
||||||
|
? String(comic.originalPubFrom)
|
||||||
|
: "???",
|
||||||
|
summary: comic.summary || "",
|
||||||
|
chapters: comic.chaps_normal || comic.chapters_count || 0,
|
||||||
|
image: comic.urlCoverOri
|
||||||
|
? `https://mangapark.net${comic.urlCoverOri}`
|
||||||
|
: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapters(mangaId) {
|
async findChapters(mangaId) {
|
||||||
const comicId = mangaId.split('/title/')[1]?.split('-')[0];
|
const comicId = mangaId
|
||||||
if (!comicId) throw new Error('comicId inválido en mangaId');
|
if (!comicId) {
|
||||||
const res = await this.fetch(this.baseUrl + "/", {
|
console.error("[MangaPark] Invalid ID format:", mangaId);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(this.baseUrl + "/", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"accept": "*/*",
|
"accept": "*/*",
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"x-apollo-operation-name": "get_comicChapterList",
|
"x-apollo-operation-name": "get_comicChapterList",
|
||||||
"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/120.0.0.0 Safari/537.36"
|
||||||
"sec-ch-ua-mobile": "?0",
|
|
||||||
"sec-ch-ua-platform": "\"Windows\"",
|
|
||||||
"sec-fetch-dest": "empty",
|
|
||||||
"sec-fetch-mode": "cors",
|
|
||||||
"sec-fetch-site": "same-origin",
|
|
||||||
"Referer": `https://mangapark.net${mangaId}`
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
query: "query get_comicChapterList($comicId: ID!) { get_comicChapterList(comicId: $comicId){ id data { id comicId isFinal volume serial dname title urlPath sfw_result } } }\n",
|
query: "query get_comicChapterList($comicId: ID!) { get_comicChapterList(comicId: $comicId){ id data { id comicId isFinal volume serial dname title urlPath sfw_result } } }\n",
|
||||||
@@ -71,25 +124,27 @@ class mangapark {
|
|||||||
|
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (!json.data || !json.data.get_comicChapterList) return [];
|
||||||
|
|
||||||
let list = json.data.get_comicChapterList;
|
let list = json.data.get_comicChapterList;
|
||||||
|
|
||||||
list.sort((a, b) => a.data.serial - b.data.serial);
|
list.sort((a, b) => a.data.serial - b.data.serial);
|
||||||
|
|
||||||
let chapters = list.map((c, i) => ({
|
return list.map((c, i) => ({
|
||||||
id: `https://mangapark.net${c.data.urlPath}`,
|
id: `https://mangapark.net${c.data.urlPath}`,
|
||||||
title: c.data.dname || c.data.title || `Chapter ${c.data.serial}`,
|
title: c.data.dname || c.data.title || `Chapter ${c.data.serial}`,
|
||||||
chapter: Number(c.data.serial),
|
number: Number(c.data.serial),
|
||||||
index: i,
|
releaseDate: null,
|
||||||
language: "en"
|
index: i
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
|
||||||
chapters: chapters,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapterPages(chapterUrl) {
|
async findChapterPages(chapterUrl) {
|
||||||
const res = await this.fetch(chapterUrl);
|
const res = await fetch(chapterUrl, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||||
|
}
|
||||||
|
});
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
|
|
||||||
const scripts = html.match(/<script\b[^>]*>[\s\S]*?<\/script>/gi) || [];
|
const scripts = html.match(/<script\b[^>]*>[\s\S]*?<\/script>/gi) || [];
|
||||||
@@ -110,10 +165,10 @@ class mangapark {
|
|||||||
|
|
||||||
return clean.map((url, index) => ({
|
return clean.map((url, index) => ({
|
||||||
url,
|
url,
|
||||||
index
|
index,
|
||||||
|
headers: { referer: 'https://mangapark.net' }
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { mangapark };
|
module.exports = mangapark;
|
||||||
|
|||||||
253
nhentai.js
253
nhentai.js
@@ -1,130 +1,181 @@
|
|||||||
class nhentai {
|
class nhentai {
|
||||||
constructor(fetchPath, cheerioPath, browser) {
|
constructor() {
|
||||||
this.baseUrl = "https://nhentai.net";
|
this.baseUrl = "https://nhentai.net";
|
||||||
this.browser = browser;
|
this.type = "book-board";
|
||||||
this.type = "book-board";
|
this.mediaType = "manga";
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSearchResult(query = "", page = 1) {
|
async search(queryObj) {
|
||||||
const q = query.trim().replace(/\s+/g, "+");
|
const q = queryObj.query.trim().replace(/\s+/g, "+");
|
||||||
const url = q ? `${this.baseUrl}/search/?q=${q}&page=${page}` : `${this.baseUrl}/?q=&page=${page}`;
|
const url = q
|
||||||
|
? `${this.baseUrl}/search/?q=${q}`
|
||||||
|
: `${this.baseUrl}/?q=`;
|
||||||
|
|
||||||
const data = await this.browser.scrape(
|
const { result: data } = await this.scrape(
|
||||||
url,
|
url,
|
||||||
() => {
|
async (page) => {
|
||||||
const container = document.querySelector('.container.index-container');
|
return page.evaluate(() => {
|
||||||
if (!container) return { results: [], hasNextPage: false };
|
const container = document.querySelector('.container.index-container');
|
||||||
|
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() || "";
|
||||||
|
|
||||||
const tagsRaw = el.getAttribute('data-tags') || "";
|
results.push({
|
||||||
const tags = tagsRaw.split(" ").filter(Boolean);
|
id,
|
||||||
|
title,
|
||||||
|
image: coverUrl,
|
||||||
|
rating: null,
|
||||||
|
type: "book"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
results.push({
|
const hasNextPage = !!document.querySelector('section.pagination a.next');
|
||||||
id,
|
return {results, hasNextPage};
|
||||||
image: thumb,
|
});
|
||||||
sampleImageUrl: coverUrl,
|
},
|
||||||
title,
|
{
|
||||||
tags,
|
waitSelector: '.container.index-container',
|
||||||
type: "book"
|
timeout: 55000
|
||||||
});
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
const hasNextPage = !!document.querySelector('section.pagination a.next');
|
return data?.results || [];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
async getMetadata(id) {
|
||||||
results,
|
const { result: data } = await this.scrape(
|
||||||
hasNextPage
|
`${this.baseUrl}/g/${id}/`,
|
||||||
};
|
async (page) => {
|
||||||
},{ waitSelector: '.container.index-container', timeout: 5000}
|
return page.evaluate(() => {
|
||||||
);
|
const title = document.querySelector('h1.title .pretty')?.textContent?.trim() || "";
|
||||||
|
|
||||||
return {
|
const img = document.querySelector('#cover img');
|
||||||
results: data.results,
|
const image =
|
||||||
hasNextPage: data.hasNextPage,
|
img?.dataset?.src ? "https:" + img.dataset.src :
|
||||||
page
|
img?.src?.startsWith("//") ? "https:" + img.src :
|
||||||
};
|
img?.src || "";
|
||||||
}
|
|
||||||
|
|
||||||
|
const tagBlock = document.querySelector('.tag-container.field-name');
|
||||||
|
const genres = tagBlock
|
||||||
|
? [...tagBlock.querySelectorAll('.tags .name')].map(x => x.textContent.trim())
|
||||||
|
: [];
|
||||||
|
|
||||||
async findChapters(mangaId) {
|
const timeEl = document.querySelector('.tag-container.field-name time');
|
||||||
const data = await this.browser.scrape(
|
const published =
|
||||||
`https://nhentai.net/g/${mangaId}/`,
|
timeEl?.getAttribute("datetime") ||
|
||||||
() => {
|
timeEl?.textContent?.trim() ||
|
||||||
const title = document.querySelector('#info > h1 .pretty')?.textContent?.trim() || "";
|
"???";
|
||||||
|
|
||||||
const img = document.querySelector('#cover img');
|
return {title, image, genres, published};
|
||||||
const cover = img?.dataset?.src ? "https:" + img.dataset.src : img?.src?.startsWith("//") ? "https:" + img.src : img?.src || "";
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
waitSelector: "#bigcontainer",
|
||||||
|
timeout: 55000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const hash = cover.match(/galleries\/(\d+)\//)?.[1] || null;
|
if (!data) throw new Error(`Fallo al obtener metadatos para ID ${id}`);
|
||||||
|
|
||||||
const thumbs = document.querySelectorAll('.thumbs img');
|
const formattedDate = data.published
|
||||||
const pages = thumbs.length;
|
? new Date(data.published).toLocaleDateString("es-ES")
|
||||||
|
: "???";
|
||||||
|
|
||||||
const first = thumbs[0];
|
return {
|
||||||
const s = first?.dataset?.src || first?.src || "";
|
id,
|
||||||
const ext = s.match(/t\.(\w+)/)?.[1] || "jpg";
|
title: data.title || "",
|
||||||
|
format: "Manga",
|
||||||
|
score: 0,
|
||||||
|
genres: Array.isArray(data.genres) ? data.genres : [],
|
||||||
|
status: "Finished",
|
||||||
|
published: formattedDate,
|
||||||
|
summary: "",
|
||||||
|
chapters: 1,
|
||||||
|
image: data.image || ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const langTag = [...document.querySelectorAll('#tags .tag-container')].find(x => x.textContent.includes("Languages:"));
|
async findChapters(mangaId) {
|
||||||
const language = langTag?.querySelector('.tags .name')?.textContent?.trim() || "";
|
const { result: data } = await this.scrape(
|
||||||
|
`${this.baseUrl}/g/${mangaId}/`,
|
||||||
|
async (page) => {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const title = document.querySelector('#info > h1 .pretty')?.textContent?.trim() || "";
|
||||||
|
|
||||||
return { title, cover, hash, pages, ext, language };
|
const img = document.querySelector('#cover img');
|
||||||
}, { waitSelector: '#bigcontainer', timeout: 4000 }
|
const cover =
|
||||||
);
|
img?.dataset?.src ? "https:" + img.dataset.src :
|
||||||
|
img?.src?.startsWith("//") ? "https:" + img.src :
|
||||||
|
img?.src || "";
|
||||||
|
|
||||||
const encodedChapterId = Buffer.from(JSON.stringify({
|
const hash = cover.match(/galleries\/(\d+)\//)?.[1] || null;
|
||||||
hash: data.hash,
|
|
||||||
pages: data.pages,
|
|
||||||
ext: data.ext
|
|
||||||
})).toString("base64");
|
|
||||||
|
|
||||||
return {
|
const thumbs = document.querySelectorAll('.thumbs img');
|
||||||
chapters: [
|
const pages = thumbs.length;
|
||||||
{
|
|
||||||
id: encodedChapterId,
|
|
||||||
title: data.title,
|
|
||||||
chapter: 1,
|
|
||||||
index: 0,
|
|
||||||
language: data.language
|
|
||||||
}
|
|
||||||
],
|
|
||||||
cover: data.cover
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
const first = thumbs[0];
|
||||||
|
const s = first?.dataset?.src || first?.src || "";
|
||||||
|
const ext = s.match(/t\.(\w+)/)?.[1] || "jpg";
|
||||||
|
|
||||||
async findChapterPages(chapterId) {
|
const langTag = [...document.querySelectorAll('#tags .tag-container')]
|
||||||
const decoded = JSON.parse(
|
.find(x => x.textContent.includes("Languages:"));
|
||||||
Buffer.from(chapterId, "base64").toString("utf8")
|
|
||||||
);
|
|
||||||
|
|
||||||
const { hash, pages, ext } = decoded;
|
const language = langTag?.querySelector('.tags .name')?.textContent?.trim() || "";
|
||||||
const baseUrl = "https://i.nhentai.net/galleries";
|
|
||||||
|
|
||||||
return Array.from({ length: pages }, (_, i) => ({
|
return {title, cover, hash, pages, ext, language};
|
||||||
url: `${baseUrl}/${hash}/${i + 1}.${ext}`,
|
});
|
||||||
index: i,
|
},
|
||||||
headers: {
|
{
|
||||||
Referer: `https://nhentai.net/g/${hash}/`
|
waitSelector: '#bigcontainer',
|
||||||
}
|
timeout: 55000
|
||||||
}));
|
}
|
||||||
}
|
);
|
||||||
|
|
||||||
|
if (!data?.hash) throw new Error(`Fallo al obtener hash para ID ${mangaId}`);
|
||||||
|
|
||||||
|
const encodedChapterId = Buffer.from(JSON.stringify({
|
||||||
|
hash: data.hash,
|
||||||
|
pages: data.pages,
|
||||||
|
ext: data.ext
|
||||||
|
})).toString("base64");
|
||||||
|
|
||||||
|
return [{
|
||||||
|
id: encodedChapterId,
|
||||||
|
title: data.title,
|
||||||
|
number: 1,
|
||||||
|
releaseDate: null,
|
||||||
|
index: 0,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
module.exports = nhentai;
|
||||||
166
novelbin.js
166
novelbin.js
@@ -1,129 +1,121 @@
|
|||||||
class NovelBin {
|
class NovelBin {
|
||||||
constructor(fetchPath, cheerioPath, browser) {
|
constructor() {
|
||||||
this.browser = browser;
|
|
||||||
this.fetch = require(fetchPath);
|
|
||||||
this.cheerio = require(cheerioPath);
|
|
||||||
this.baseUrl = "https://novelbin.me";
|
this.baseUrl = "https://novelbin.me";
|
||||||
this.type = "book-board";
|
this.type = "book-board";
|
||||||
|
this.mediaType = "ln";
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSearchResult(query = "", page = 1) {
|
async search(queryObj) {
|
||||||
const url = !query || query.trim() === ""
|
const query = queryObj.query || "";
|
||||||
? `${this.baseUrl}/sort/novelbin-hot?page=${page}`
|
const url = `${this.baseUrl}/search?keyword=${encodeURIComponent(query)}`;
|
||||||
: `${this.baseUrl}/search?keyword=${encodeURIComponent(query)}`;
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
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",
|
||||||
|
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
"referer": this.baseUrl + "/"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const res = await this.fetch(url);
|
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
$(".list-novel .row, .col-novel-main .list-novel .row").each((i, el) => {
|
$('h3.novel-title a').each((i, el) => {
|
||||||
const titleEl = $(el).find("h3.novel-title a");
|
const href = $(el).attr('href');
|
||||||
if (!titleEl.length) return;
|
const title = $(el).text().trim();
|
||||||
|
|
||||||
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 idMatch = href.match(/novel-book\/([^/?]+)/);
|
||||||
const id = idMatch ? idMatch[1] : null;
|
const id = idMatch ? idMatch[1] : null;
|
||||||
if (!id) return;
|
|
||||||
const coverUrl = `${this.baseUrl}/media/novel/${id}.jpg`;
|
const img = `${this.baseUrl}/media/novel/${id}.jpg`;
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
image: coverUrl,
|
image: img,
|
||||||
sampleImageUrl: coverUrl,
|
rating: null,
|
||||||
tags: [],
|
|
||||||
type: "book"
|
type: "book"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasNextPage = $(".PagedList-skipToNext a").length > 0;
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetadata(id) {
|
||||||
|
const res = await fetch(`${this.baseUrl}/novel-book/${id}`);
|
||||||
|
const html = await res.text();
|
||||||
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
|
const getMeta = (property) => $(`meta[property='${property}']`).attr('content') || "";
|
||||||
|
|
||||||
|
const title = getMeta("og:novel:novel_name") || $('title').text() || "";
|
||||||
|
const summary = $('meta[name="description"]').attr('content') || "";
|
||||||
|
const genresRaw = getMeta("og:novel:genre");
|
||||||
|
const genres = genresRaw ? genresRaw.split(',').map(g => g.trim()) : [];
|
||||||
|
const status = getMeta("og:novel:status") || "";
|
||||||
|
const image = getMeta("og:image");
|
||||||
|
|
||||||
|
const lastChapterName = getMeta("og:novel:lastest_chapter_name");
|
||||||
|
const chaptersMatch = lastChapterName.match(/Chapter\s+(\d+)/i);
|
||||||
|
const chapters = chaptersMatch ? Number(chaptersMatch[1]) : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results,
|
id,
|
||||||
hasNextPage,
|
title,
|
||||||
page
|
format: "Light Novel",
|
||||||
|
score: 0,
|
||||||
|
genres,
|
||||||
|
status,
|
||||||
|
published: "???",
|
||||||
|
summary,
|
||||||
|
chapters,
|
||||||
|
image
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapters(bookId) {
|
async findChapters(bookId) {
|
||||||
const res = await this.fetch(`${this.baseUrl}/novel-book/${bookId}`);
|
const res = await fetch(`${this.baseUrl}/ajax/chapter-archive?novelId=${bookId}`, {
|
||||||
|
headers: {
|
||||||
|
"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 = [];
|
||||||
|
|
||||||
$("#chapter-archive ul.list-chapter li a").each((i, el) => {
|
$('a[title]').each((i, el) => {
|
||||||
const a = $(el);
|
const fullUrl = $(el).attr('href');
|
||||||
const title = a.attr("title") || a.text().trim();
|
const title = $(el).attr('title').trim();
|
||||||
let href = a.attr("href");
|
const numMatch = title.match(/chapter\s+(\d+(?:\.\d+)?)/i);
|
||||||
|
|
||||||
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({
|
chapters.push({
|
||||||
id: href,
|
id: fullUrl,
|
||||||
title: title.trim(),
|
title,
|
||||||
chapter: chapterNumber,
|
number: numMatch ? numMatch[1] : "0",
|
||||||
language: "en"
|
releaseDate: null,
|
||||||
|
index: i
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return { chapters: chapters };
|
return chapters;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapterPages(chapterId) {
|
async findChapterPages(chapterUrl) {
|
||||||
const url = chapterId.startsWith('http') ? chapterId : `${this.baseUrl}${chapterId}`;
|
const {result} = await this.scrape(chapterUrl, async (page) => {
|
||||||
|
return page.evaluate(() => {
|
||||||
const content = await this.browser.scrape(
|
document.querySelectorAll('div[id^="pf-"]').forEach(e => e.remove());
|
||||||
url,
|
const ps = Array.from(document.querySelectorAll("p")).map(p => p.outerHTML.trim()).filter(p => p.length > 7);
|
||||||
() => {
|
return ps.join("\n");
|
||||||
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());
|
waitUntil: "domcontentloaded",
|
||||||
|
renderWaitTime: 300
|
||||||
const paragraphs = contentDiv.querySelectorAll('p');
|
});
|
||||||
let cleanHtml = '';
|
return result || "<p>Error: chapter text not found</p>";
|
||||||
|
|
||||||
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;
|
||||||
module.exports = { NovelBin };
|
|
||||||
248
novelfire.js
248
novelfire.js
@@ -1,155 +1,137 @@
|
|||||||
class novelfire {
|
class NovelFire {
|
||||||
constructor(fetchPath, cheerioPath, browser) {
|
constructor() {
|
||||||
this.browser = browser;
|
|
||||||
this.fetch = require(fetchPath);
|
|
||||||
this.cheerio = require(cheerioPath);
|
|
||||||
this.baseUrl = "https://novelfire.net";
|
this.baseUrl = "https://novelfire.net";
|
||||||
this.type = "book-board";
|
this.type = "book-board";
|
||||||
|
this.mediaType = "ln";
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSearchResult(query = "", page = 1) {
|
async search(queryObj) {
|
||||||
let html;
|
const query = queryObj.query;
|
||||||
|
|
||||||
if (query.trim() === "") {
|
const res = await fetch(
|
||||||
const res = await this.fetch(`${this.baseUrl}/home`);
|
`${this.baseUrl}/ajax/searchLive?inputContent=${encodeURIComponent(query)}`,
|
||||||
html = await res.text();
|
{ headers: { "accept": "application/json" } }
|
||||||
} else {
|
);
|
||||||
const res = await this.fetch(
|
|
||||||
`${this.baseUrl}/ajax/searchLive?inputContent=${encodeURIComponent(query)}`
|
const data = await res.json();
|
||||||
);
|
if (!data.data) return [];
|
||||||
const data = await res.json();
|
|
||||||
html = data.html;
|
return data.data.map(item => ({
|
||||||
|
id: item.slug,
|
||||||
|
title: item.title,
|
||||||
|
image: `https://novelfire.net/${item.image}`,
|
||||||
|
rating: item.rank ?? null,
|
||||||
|
type: "book"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetadata(id) {
|
||||||
|
const url = `https://novelfire.net/book/${id}`;
|
||||||
|
const html = await (await fetch(url)).text();
|
||||||
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
|
const title = $('h1[itemprop="name"]').first().text().trim() || null;
|
||||||
|
const summary = $('meta[itemprop="description"]').attr('content') || null;
|
||||||
|
const image =
|
||||||
|
$('figure.cover img').attr('src') ||
|
||||||
|
$('img.cover').attr('src') ||
|
||||||
|
$('img[src*="server-"]').attr('src') ||
|
||||||
|
null;
|
||||||
|
|
||||||
|
const genres = $('.categories a.property-item')
|
||||||
|
.map((_, el) => $(el).attr('title') || $(el).text().trim())
|
||||||
|
.get();
|
||||||
|
|
||||||
|
let chapters = null;
|
||||||
|
const latest = $('.chapter-latest-container .latest').text();
|
||||||
|
if (latest) {
|
||||||
|
const m = latest.match(/Chapter\s+(\d+)/i);
|
||||||
|
if (m) chapters = Number(m[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $ = this.cheerio.load(html);
|
let status = 'unknown';
|
||||||
const results = [];
|
const statusClass = $('strong.ongoing, strong.completed').attr('class');
|
||||||
|
if (statusClass) {
|
||||||
$(".novel-item").each((_, el) => {
|
status = statusClass.toLowerCase();
|
||||||
const a = $(el).find("a");
|
}
|
||||||
const href = a.attr("href") || "";
|
|
||||||
const title = $(el).find(".novel-title").text().trim();
|
|
||||||
|
|
||||||
const img = $(el).find("img");
|
|
||||||
const image = img.attr("data-src") || img.attr("src") || "";
|
|
||||||
|
|
||||||
const id = href.replace("https://novelfire.net/book/", "").replace(/\/$/, "");
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
image,
|
|
||||||
sampleImageUrl: image,
|
|
||||||
tags: [],
|
|
||||||
type: "book"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results,
|
id,
|
||||||
hasNextPage: false,
|
title,
|
||||||
page
|
format: 'Light Novel',
|
||||||
|
score: 0,
|
||||||
|
genres,
|
||||||
|
status,
|
||||||
|
published: '???',
|
||||||
|
summary,
|
||||||
|
chapters,
|
||||||
|
image
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapters(bookId) {
|
async findChapters(bookId) {
|
||||||
const url = `https://novelfire.net/book/${bookId}/chapter-1`;
|
const url = `https://novelfire.net/book/${bookId}/chapters`;
|
||||||
|
const html = await (await fetch(url)).text();
|
||||||
const options = await this.browser.scrape(
|
|
||||||
url,
|
|
||||||
async () => {
|
|
||||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
||||||
|
|
||||||
const select = document.querySelector('.chapindex');
|
|
||||||
if (!select) return [];
|
|
||||||
|
|
||||||
select.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
|
||||||
select.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
||||||
|
|
||||||
for (let i = 0; i < 20; i++) {
|
|
||||||
if (document.querySelectorAll('.select2-results__option').length > 0) break;
|
|
||||||
await sleep(300);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...select.querySelectorAll('option')].map(opt => ({
|
|
||||||
id: opt.value,
|
|
||||||
title: opt.textContent.trim(),
|
|
||||||
chapter: Number(opt.dataset.n_sort || 0),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
{
|
|
||||||
waitSelector: '.chapindex',
|
|
||||||
timeout: 10000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
chapters: options.map(o => ({
|
|
||||||
id: `https://novelfire.net/book/${bookId}/chapter-${o.chapter}`,
|
|
||||||
title: o.title,
|
|
||||||
chapter: o.chapter,
|
|
||||||
language: "en"
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async findChapterPages(chapterId) {
|
|
||||||
const res = await this.fetch(chapterId);
|
|
||||||
const html = await res.text();
|
|
||||||
|
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
const contentDiv = $("#content");
|
let postId;
|
||||||
|
|
||||||
if (!contentDiv || contentDiv.length === 0) {
|
$("script").each((_, el) => {
|
||||||
return [{
|
const txt = $(el).html() || "";
|
||||||
type: "text",
|
const m = txt.match(/listChapterDataAjax\?post_id=(\d+)/);
|
||||||
content: "<p>Error: content not found</p>",
|
if (m) postId = m[1];
|
||||||
index: 0
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
contentDiv.find("script").remove();
|
|
||||||
contentDiv.find("ins").remove();
|
|
||||||
contentDiv.find("[id^='pf-']").remove();
|
|
||||||
contentDiv.find(".ads").remove();
|
|
||||||
contentDiv.find(".adsbygoogle").remove();
|
|
||||||
contentDiv.find("div[style*='text-align:center']").remove();
|
|
||||||
contentDiv.find("div[align='center']").remove();
|
|
||||||
contentDiv.find(".nf-ads").remove();
|
|
||||||
contentDiv.find("nfne597").remove();
|
|
||||||
|
|
||||||
const paragraphs = contentDiv.find("p");
|
|
||||||
let cleanHtml = "";
|
|
||||||
|
|
||||||
paragraphs.each((_, el) => {
|
|
||||||
const p = $(el);
|
|
||||||
|
|
||||||
let text = p.text() || "";
|
|
||||||
|
|
||||||
text = text.replace(/△▼△▼△▼△/g, "");
|
|
||||||
text = text.replace(/[※]+/g, "");
|
|
||||||
text = text.replace(/\s{2,}/g, " ");
|
|
||||||
|
|
||||||
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(); }
|
if (!postId) throw new Error("post_id not found");
|
||||||
|
|
||||||
return [
|
const params = new URLSearchParams({
|
||||||
{
|
post_id: postId,
|
||||||
type: "text",
|
draw: 1,
|
||||||
content: cleanHtml.trim(),
|
"columns[0][data]": "title",
|
||||||
index: 0
|
"columns[0][orderable]": "false",
|
||||||
}
|
"columns[1][data]": "created_at",
|
||||||
];
|
"columns[1][orderable]": "true",
|
||||||
|
"order[0][column]": 1,
|
||||||
|
"order[0][dir]": "asc",
|
||||||
|
start: 0,
|
||||||
|
length: 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`https://novelfire.net/listChapterDataAjax?${params}`,
|
||||||
|
{ headers: { "x-requested-with": "XMLHttpRequest" } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
if (!json?.data) throw new Error("Invalid response");
|
||||||
|
|
||||||
|
return json.data.map((c, i) => ({
|
||||||
|
id: `https://novelfire.net/book/${bookId}/chapter-${c.n_sort}`,
|
||||||
|
title: c.title,
|
||||||
|
number: Number(c.n_sort),
|
||||||
|
release_date: c.created_at ?? null,
|
||||||
|
index: i,
|
||||||
|
language: "en"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async findChapterPages(url) {
|
||||||
|
const html = await (await fetch(url)).text();
|
||||||
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
|
const $content = $("#content").clone();
|
||||||
|
|
||||||
|
$content.find("script, ins, .nf-ads, img, nfn2a74").remove();
|
||||||
|
|
||||||
|
$content.find("*").each((_, el) => {
|
||||||
|
$(el).removeAttr("id").removeAttr("class").removeAttr("style");
|
||||||
|
});
|
||||||
|
|
||||||
|
return $content.html()
|
||||||
|
.replace(/adsbygoogle/gi, "")
|
||||||
|
.replace(/novelfire/gi, "")
|
||||||
|
.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { novelupdates: novelfire };
|
module.exports = NovelFire;
|
||||||
186
realbooru.js
186
realbooru.js
@@ -1,170 +1,84 @@
|
|||||||
class Realbooru {
|
class Realbooru {
|
||||||
|
|
||||||
baseUrl = "https://realbooru.com";
|
baseUrl = "https://realbooru.com";
|
||||||
|
|
||||||
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"
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(fetchPath, cheerioPath) {
|
constructor() {
|
||||||
this.fetch = require(fetchPath);
|
|
||||||
this.cheerio = require(cheerioPath);
|
|
||||||
this.type = "image-board";
|
this.type = "image-board";
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadDoc(body) {
|
async search(query = "original", page = 1, perPage = 42) {
|
||||||
return this.cheerio.load(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchSearchResult(query, page = 1, perPage = 42) {
|
|
||||||
if (!query) query = "original";
|
|
||||||
|
|
||||||
const offset = (page - 1) * perPage;
|
const offset = (page - 1) * perPage;
|
||||||
const url = `${this.baseUrl}/index.php?page=post&s=list&tags=${query}&pid=${offset}`;
|
|
||||||
|
|
||||||
try {
|
const tags = query
|
||||||
const response = await this.fetch(url, { headers: this.headers });
|
.trim()
|
||||||
const data = await response.text();
|
.split(/\s+/)
|
||||||
|
.join("+") + "+";
|
||||||
|
|
||||||
const $ = this.cheerio.load(data);
|
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 $ = this.cheerio.load(html);
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
$('#post-list a[id^="p"], #post-list a[href*="&s=view"], .thumb a').each((i, e) => {
|
$('div.col.thumb').each((_, el) => {
|
||||||
const $a = $(e);
|
const id = ($(el).attr('id') || "").replace('s', '');
|
||||||
|
const img = $(el).find('img');
|
||||||
|
|
||||||
const href = $a.attr('href');
|
let image = img.attr('src');
|
||||||
let id = null;
|
if (image && !image.startsWith('http')) image = 'https:' + image;
|
||||||
if (href) {
|
|
||||||
const idMatch = href.match(/&id=(\d+)/);
|
|
||||||
if (idMatch) {
|
|
||||||
id = idMatch[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!id) {
|
const title = img.attr('title') || '';
|
||||||
id = $a.closest('span, div').attr('id')?.replace('s', '').replace('post_', '');
|
const tags = title
|
||||||
}
|
.split(',')
|
||||||
|
.map(t => t.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
const imageElement = $a.find('img').first();
|
if (id && image) {
|
||||||
let image = imageElement.attr('src');
|
results.push({ id, image, tags });
|
||||||
|
|
||||||
if (image && !image.startsWith('http')) {
|
|
||||||
image = `https:${image}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tags = imageElement.attr('alt')?.trim()?.split(' ').filter(tag => tag !== "");
|
|
||||||
if (!tags || tags.length === 0) {
|
|
||||||
tags = $a.attr('title')?.trim()?.split(' ').filter(tag => tag !== "");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id && image) {
|
|
||||||
results.push({
|
|
||||||
id: id,
|
|
||||||
image: image,
|
|
||||||
tags: tags,
|
|
||||||
type: 'preview'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const pagination = $('#paginator .pagination');
|
|
||||||
const lastPageLink = pagination.find('a[alt="last page"]');
|
|
||||||
let totalPages = 1;
|
|
||||||
|
|
||||||
if (lastPageLink.length > 0) {
|
|
||||||
const pid = lastPageLink.attr('href')?.split('pid=')[1];
|
|
||||||
totalPages = Math.ceil(parseInt(pid || "0", 10) / perPage) + 1;
|
|
||||||
} else {
|
|
||||||
const pageLinks = pagination.find('a');
|
|
||||||
if (pageLinks.length > 0) {
|
|
||||||
const lastLinkText = pageLinks.eq(-2).text();
|
|
||||||
totalPages = parseInt(lastLinkText, 10) || 1;
|
|
||||||
} else if (results.length > 0) {
|
|
||||||
totalPages = 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const currentPage = page;
|
let totalPages = page;
|
||||||
const hasNextPage = currentPage < totalPages;
|
const lastPid = $('a[alt="last page"]').attr('href')?.match(/pid=(\d+)/);
|
||||||
const next = hasNextPage ? (currentPage + 1) : 0;
|
if (lastPid) {
|
||||||
const previous = currentPage > 1 ? (currentPage - 1) : 0;
|
totalPages = Math.floor(parseInt(lastPid[1], 10) / perPage) + 1;
|
||||||
|
|
||||||
const total = totalPages * perPage;
|
|
||||||
|
|
||||||
return { total, next, previous, pages: totalPages, page: currentPage, hasNextPage, results };
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error during Realbooru search:", e);
|
|
||||||
|
|
||||||
return { total: 0, next: 0, previous: 0, pages: 1, page: 1, hasNextPage: false, results: [] };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
results,
|
||||||
|
page,
|
||||||
|
hasNextPage: page < totalPages
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchInfo(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 $ = this.cheerio.load(html);
|
||||||
|
|
||||||
const fetchHeaders = { ...this.headers };
|
let image =
|
||||||
|
$('video source').attr('src') ||
|
||||||
|
$('#image').attr('src') ||
|
||||||
|
null;
|
||||||
|
|
||||||
const response = await this.fetch(url, { headers: fetchHeaders });
|
if (image && !image.startsWith('http')) {
|
||||||
const original = await response.text();
|
image = this.baseUrl + image;
|
||||||
|
|
||||||
const $ = this.cheerio.load(original);
|
|
||||||
|
|
||||||
let fullImage = $('#image').attr('src') || $('video').attr('src');
|
|
||||||
|
|
||||||
const originalLink = $('div.link-list a:contains("Original image")').attr('href');
|
|
||||||
if (originalLink) {
|
|
||||||
fullImage = originalLink;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullImage && !fullImage.startsWith('http')) {
|
const tags = [];
|
||||||
fullImage = `https:${fullImage}`;
|
$('#tagLink a').each((_, el) => {
|
||||||
}
|
tags.push($(el).text().trim());
|
||||||
|
});
|
||||||
let resizedImageUrl = $('#image-holder img').attr('src');
|
|
||||||
if (resizedImageUrl && !resizedImageUrl.startsWith('http')) {
|
|
||||||
resizedImageUrl = `https:${resizedImageUrl}`;
|
|
||||||
} else if (!resizedImageUrl) {
|
|
||||||
|
|
||||||
resizedImageUrl = fullImage;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = $('.tag-list a.tag-link').map((i, el) => $(el).text().trim()).get();
|
|
||||||
|
|
||||||
const stats = $('#stats ul');
|
|
||||||
|
|
||||||
const postedData = stats.find("li:contains('Posted:')").text().trim();
|
|
||||||
const postedDateMatch = postedData.match(/Posted: (.*?) by/);
|
|
||||||
const createdAt = postedDateMatch ? new Date(postedDateMatch[1]).getTime() : undefined;
|
|
||||||
|
|
||||||
const publishedByMatch = postedData.match(/by\s*(.*)/);
|
|
||||||
const publishedBy = publishedByMatch ? publishedByMatch[1].trim() : undefined;
|
|
||||||
|
|
||||||
const rating = stats.find("li:contains('Rating:')").text().trim().split("Rating: ")[1] || undefined;
|
|
||||||
|
|
||||||
const comments = $('#comment-list div').map((i, el) => {
|
|
||||||
const $el = $(el);
|
|
||||||
const id = $el.attr('id')?.replace('c', '');
|
|
||||||
const user = $el.find('.col1').text().trim().split("\n")[0];
|
|
||||||
const comment = $el.find('.col2').text().trim();
|
|
||||||
if (id && user && comment) {
|
|
||||||
return { id, user, comment };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}).get().filter(Boolean);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
fullImage,
|
image,
|
||||||
resizedImageUrl,
|
tags
|
||||||
tags,
|
|
||||||
createdAt,
|
|
||||||
publishedBy,
|
|
||||||
rating,
|
|
||||||
comments
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { Realbooru };
|
module.exports = Realbooru;
|
||||||
102
rule34.js
102
rule34.js
@@ -5,79 +5,61 @@ class Rule34 {
|
|||||||
'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(fetchPath, cheerioPath) {
|
constructor() {
|
||||||
this.fetch = require(fetchPath);
|
|
||||||
this.cheerio = require(cheerioPath);
|
|
||||||
this.type = "image-board";
|
this.type = "image-board";
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSearchResult(query, page = 1, perPage = 42) {
|
async search(query = "alisa_mikhailovna_kujou", page = 1, perPage = 42) {
|
||||||
if (!query) query = "alisa_mikhailovna_kujou";
|
|
||||||
|
|
||||||
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 this.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((i, 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').attr('alt')?.trim()?.split(' ').filter(tag => tag !== "");
|
const tags = $e.find('img')
|
||||||
|
.attr('alt')
|
||||||
|
?.trim()
|
||||||
|
.split(' ')
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
if (id && image) {
|
if (id && image) {
|
||||||
results.push({
|
results.push({
|
||||||
id: id,
|
id,
|
||||||
image: image,
|
image,
|
||||||
tags: tags,
|
tags
|
||||||
type: 'preview'
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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 > 0) {
|
const pid = Number(lastPageLink.attr('href')?.split('pid=')[1] ?? 0);
|
||||||
|
totalPages = Math.ceil(pid / perPage) + 1;
|
||||||
const pid = lastPageLink.attr('href')?.split('pid=')[1];
|
|
||||||
totalPages = Math.ceil(parseInt(pid || "0", 10) / perPage) + 1;
|
|
||||||
} else {
|
|
||||||
|
|
||||||
const pageLinks = pagination.find('a');
|
|
||||||
if (pageLinks.length > 0) {
|
|
||||||
|
|
||||||
const lastLinkText = pageLinks.eq(-2).text();
|
|
||||||
totalPages = parseInt(lastLinkText, 10) || 1;
|
|
||||||
} else if (results.length > 0) {
|
|
||||||
totalPages = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPage = page;
|
return {
|
||||||
const hasNextPage = currentPage < totalPages;
|
page,
|
||||||
const next = hasNextPage ? (currentPage + 1) : 0;
|
hasNextPage: page < totalPages,
|
||||||
const previous = currentPage > 1 ? (currentPage - 1) : 0;
|
results
|
||||||
|
};
|
||||||
const total = totalPages * perPage;
|
|
||||||
|
|
||||||
return { total, next, previous, pages: totalPages, page: currentPage, hasNextPage, results };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchInfo(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 = {
|
||||||
@@ -91,55 +73,27 @@ class Rule34 {
|
|||||||
const resizeHeaders = { ...this.headers, 'cookie': cookieString };
|
const resizeHeaders = { ...this.headers, 'cookie': cookieString };
|
||||||
|
|
||||||
const [resizedResponse, nonResizedResponse] = await Promise.all([
|
const [resizedResponse, nonResizedResponse] = await Promise.all([
|
||||||
this.fetch(url, { headers: resizeHeaders }),
|
fetch(url, { headers: resizeHeaders }),
|
||||||
this.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 $resized = this.cheerio.load(resized);
|
|
||||||
const $ = this.cheerio.load(original);
|
const $ = this.cheerio.load(original);
|
||||||
|
|
||||||
let resizedImageUrl = $resized('#image').attr('src');
|
|
||||||
if (resizedImageUrl && !resizedImageUrl.startsWith('http')) {
|
|
||||||
resizedImageUrl = `https:${resizedImageUrl}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 !== "");
|
||||||
const stats = $('#stats ul');
|
|
||||||
|
|
||||||
const postedData = stats.find('li:nth-child(2)').text().trim();
|
|
||||||
const createdAt = new Date(postedData.split("Posted: ")[1].split("by")[0]).getTime();
|
|
||||||
const publishedBy = postedData.split("by")[1].trim();
|
|
||||||
const rating = stats.find("li:contains('Rating:')").text().trim().split("Rating: ")[1];
|
|
||||||
|
|
||||||
const comments = $('#comment-list div').map((i, el) => {
|
|
||||||
const $el = $(el);
|
|
||||||
const id = $el.attr('id')?.replace('c', '');
|
|
||||||
const user = $el.find('.col1').text().trim().split("\n")[0];
|
|
||||||
const comment = $el.find('.col2').text().trim();
|
|
||||||
if (id && user && comment) {
|
|
||||||
return { id, user, comment };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}).get().filter(Boolean);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
fullImage,
|
image: fullImage,
|
||||||
resizedImageUrl,
|
tags
|
||||||
tags,
|
|
||||||
createdAt,
|
|
||||||
publishedBy,
|
|
||||||
rating,
|
|
||||||
comments
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { Rule34 };
|
module.exports = Rule34;
|
||||||
138
tenor.js
138
tenor.js
@@ -1,61 +1,65 @@
|
|||||||
class Tenor {
|
class Tenor {
|
||||||
baseUrl = "https://tenor.com";
|
baseUrl = "https://tenor.com";
|
||||||
|
|
||||||
constructor(fetchPath, cheerioPath, browser) {
|
constructor() {
|
||||||
this.browser = browser;
|
|
||||||
this.type = "image-board";
|
this.type = "image-board";
|
||||||
|
|
||||||
this.lastQuery = null;
|
this.lastQuery = null;
|
||||||
this.seenIds = new Set();
|
this.seenIds = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSearchResult(query = "hello", page = 1, perPage = 48) {
|
async search(query, page = 1, perPage = 48) {
|
||||||
|
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.replace(" ", "-")}-gifs`;
|
const url = `${this.baseUrl}/search/${query.replaceAll(" ", "-")}-gifs`;
|
||||||
|
|
||||||
const data = await this.browser.scrape(
|
const { result } = await this.scrape(
|
||||||
url,
|
url,
|
||||||
() => {
|
async (page) => {
|
||||||
// Fallback selectors: try specific class first, then generic figure
|
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 id = idMatch ? idMatch[1] : null;
|
||||||
|
|
||||||
let idMatch = href.match(/-(\d+)(?:$|\/?$)/);
|
const imgUrl =
|
||||||
const id = idMatch ? idMatch[1] : null;
|
img.getAttribute('src') ||
|
||||||
|
img.getAttribute('data-src');
|
||||||
|
|
||||||
// Tenor lazy loads images, so we check 'src' AND 'data-src'
|
const tagsRaw = img.getAttribute('alt') || "";
|
||||||
const imgUrl = img.getAttribute('src') || img.getAttribute('data-src');
|
const tags = tagsRaw.trim().split(/\s+/).filter(Boolean);
|
||||||
const tagsRaw = img.getAttribute('alt') || "";
|
|
||||||
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(
|
||||||
|
new Map(results.map(r => [r.id, r])).values()
|
||||||
|
);
|
||||||
|
|
||||||
|
return {results: uniqueResults, hasNextPage: true};
|
||||||
});
|
});
|
||||||
|
|
||||||
const uniqueResults = Array.from(new Map(results.map(item => [item.id, item])).values());
|
|
||||||
|
|
||||||
return { results: uniqueResults, hasNextPage: true };
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
waitSelector: 'figure',
|
waitSelector: "figure",
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
scrollToBottom: true,
|
scrollToBottom: true,
|
||||||
renderWaitTime: 3000,
|
renderWaitTime: 3000,
|
||||||
@@ -63,61 +67,49 @@ class Tenor {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const newResults = data.results.filter(item => !this.seenIds.has(item.id));
|
const newResults = result.results.filter(r => !this.seenIds.has(r.id));
|
||||||
|
newResults.forEach(r => this.seenIds.add(r.id));
|
||||||
newResults.forEach(item => this.seenIds.add(item.id));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results: newResults,
|
results: newResults.map(r => ({
|
||||||
hasNextPage: data.hasNextPage,
|
id: r.id,
|
||||||
|
image: r.image,
|
||||||
|
tags: r.tags,
|
||||||
|
})),
|
||||||
|
hasNextPage: result.hasNextPage,
|
||||||
page
|
page
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchInfo(id) {
|
async getInfo(id) {
|
||||||
const url = `${this.baseUrl}/view/gif-${id}`;
|
const url = `${this.baseUrl}/view/gif-${id}`;
|
||||||
|
|
||||||
const data = await this.browser.scrape(
|
const { result } = await this.scrape(
|
||||||
url,
|
url,
|
||||||
() => {
|
async (page) => {
|
||||||
const img = document.querySelector('img[alt]');
|
return page.evaluate(() => {
|
||||||
const fullImage = img?.src || null;
|
const img = document.querySelector(".Gif img");
|
||||||
|
const fullImage = img?.src || null;
|
||||||
|
|
||||||
const tags = [...document.querySelectorAll('.tag-list li a .RelatedTag')]
|
const tags = [...document.querySelectorAll(".tag-list li a .RelatedTag")]
|
||||||
.map(tag => tag.textContent.trim())
|
.map(t => t.textContent.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
let createdAt = Date.now();
|
return { fullImage, tags };
|
||||||
|
});
|
||||||
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 }
|
{ waitSelector: ".Gif img", timeout: 15000 }
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
fullImage: data.fullImage,
|
image: result.fullImage,
|
||||||
tags: data.tags,
|
tags: result.tags,
|
||||||
createdAt: data.createdAt,
|
title: result.tags?.join(" ") || `Tenor GIF ${id}`,
|
||||||
rating: "Unknown"
|
headers: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { Tenor };
|
module.exports = Tenor;
|
||||||
26
waifupics.js
26
waifupics.js
@@ -8,12 +8,11 @@ class WaifuPics {
|
|||||||
'happy', 'wink', 'poke', 'dance', 'cringe'
|
'happy', 'wink', 'poke', 'dance', 'cringe'
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(fetchPath, cheerioPath) {
|
constructor() {
|
||||||
this.fetch = require(fetchPath);
|
|
||||||
this.type = "image-board";
|
this.type = "image-board";
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSearchResult(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];
|
||||||
@@ -34,7 +33,7 @@ class WaifuPics {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const response = await this.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: [] }),
|
||||||
@@ -49,13 +48,12 @@ class WaifuPics {
|
|||||||
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 = `${page}-${id}`;
|
const uniqueId = `${id}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: uniqueId,
|
id: uniqueId,
|
||||||
image: url,
|
image: url,
|
||||||
tags: [category],
|
tags: [category],
|
||||||
type: 'preview'
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,19 +81,13 @@ class WaifuPics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchInfo(id) {
|
async getInfo(id) {
|
||||||
console.log(`[WaifuPics] fetchInfo called for ${id}, but this API only provides direct URLs.`);
|
|
||||||
return {
|
return {
|
||||||
id: id,
|
id,
|
||||||
fullImage: `https://i.waifu.pics/${id}`,
|
image: `https://i.waifu.pics/${id}`,
|
||||||
resizedImageUrl: `https://i.waifu.pics/${id}`,
|
tags: []
|
||||||
tags: [],
|
|
||||||
createdAt: null,
|
|
||||||
publishedBy: 'Waifu.pics',
|
|
||||||
rating: 'sfw',
|
|
||||||
comments: []
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { WaifuPics };
|
module.exports = WaifuPics;
|
||||||
216
wattpad.js
216
wattpad.js
@@ -1,59 +1,23 @@
|
|||||||
class wattpad {
|
class wattpad {
|
||||||
constructor(fetchPath, cheerioPath, browser) {
|
constructor() {
|
||||||
this.browser = browser;
|
|
||||||
this.fetch = require(fetchPath);
|
|
||||||
this.cheerio = require(cheerioPath);
|
|
||||||
this.baseUrl = "https://wattpad.com";
|
this.baseUrl = "https://wattpad.com";
|
||||||
this.type = "book-board";
|
this.type = "book-board";
|
||||||
|
this.mediaType = "ln";
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSearchResult(query = "", page = 1) {
|
async search(queryObj) {
|
||||||
if (!query || query.trim() === "") {
|
const query = queryObj.query?.trim() || "";
|
||||||
const res = await this.fetch("https://www.wattpad.com/");
|
|
||||||
const html = await res.text();
|
|
||||||
const $ = this.cheerio.load(html);
|
|
||||||
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
$("li.splide__slide").each((_, el) => {
|
|
||||||
const li = $(el);
|
|
||||||
|
|
||||||
const link = li.find("a[data-testid='coverLink']").attr("href") || "";
|
|
||||||
const img = li.find("img[data-testid='image']").attr("src") || "";
|
|
||||||
const title = li.find("img[data-testid='image']").attr("alt") || "";
|
|
||||||
|
|
||||||
if (link && img) {
|
|
||||||
const id = link.split("/story/")[1]?.split(/[^0-9]/)[0] || null;
|
|
||||||
|
|
||||||
if (id) {
|
|
||||||
results.push({
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
image: img,
|
|
||||||
sampleImageUrl: img,
|
|
||||||
tags: [],
|
|
||||||
type: "book"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
results,
|
|
||||||
hasNextPage: false,
|
|
||||||
page: 1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const limit = 15;
|
const limit = 15;
|
||||||
const offset = (page - 1) * limit;
|
const offset = 0;
|
||||||
|
|
||||||
const url = `${this.baseUrl}/v4/search/stories?query=${query}&fields=stories%28id%2Ctitle%2CvoteCount%2CreadCount%2CcommentCount%2Cdescription%2Ccompleted%2Cmature%2Ccover%2Curl%2CisPaywalled%2CpaidModel%2Clength%2Clanguage%28id%29%2Cuser%28name%29%2CnumParts%2ClastPublishedPart%28createDate%29%2Cpromoted%2Csponsor%28name%2Cavatar%29%2Ctags%2Ctracking%28clickUrl%2CimpressionUrl%2CthirdParty%28impressionUrls%2CclickUrls%29%29%2Ccontest%28endDate%2CctaLabel%2CctaURL%29%29%2Ctotal%2Ctags%2Cnexturl&limit=${limit}&mature=false&offset=${offset}`;
|
const url =
|
||||||
|
`${this.baseUrl}/v4/search/stories?` +
|
||||||
|
`query=${encodeURIComponent(query)}` +
|
||||||
|
`&limit=${limit}&offset=${offset}&mature=false`;
|
||||||
|
|
||||||
const res = await this.fetch(url);
|
const json = await fetch(url).then(r => r.json());
|
||||||
const json = await res.json();
|
|
||||||
|
|
||||||
const results = json.stories.map(n => ({
|
return json.stories.map(n => ({
|
||||||
id: n.id,
|
id: n.id,
|
||||||
title: n.title,
|
title: n.title,
|
||||||
image: n.cover,
|
image: n.cover,
|
||||||
@@ -61,104 +25,118 @@ class wattpad {
|
|||||||
tags: n.tags,
|
tags: n.tags,
|
||||||
type: "book"
|
type: "book"
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const totalPages = Math.ceil(json.total / limit);
|
async getMetadata(id) {
|
||||||
const hasNextPage = page < totalPages;
|
const html = await fetch(`${this.baseUrl}/story/${id}`).then(r => r.text());
|
||||||
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
|
const script = $('script')
|
||||||
|
.map((_, el) => $(el).html())
|
||||||
|
.get()
|
||||||
|
.find(t => t?.includes('window.__remixContext'));
|
||||||
|
|
||||||
|
if (!script) return null;
|
||||||
|
|
||||||
|
const jsonText = script.match(/window\.__remixContext\s*=\s*({[\s\S]*?});/)?.[1];
|
||||||
|
if (!jsonText) return null;
|
||||||
|
|
||||||
|
let ctx;
|
||||||
|
try {
|
||||||
|
ctx = JSON.parse(jsonText);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = ctx?.state?.loaderData?.["routes/story.$storyid"];
|
||||||
|
const story = route?.story;
|
||||||
|
const meta = route?.meta;
|
||||||
|
|
||||||
|
if (!story) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results,
|
id: story.id,
|
||||||
hasNextPage,
|
title: story.title,
|
||||||
page
|
format: "Novel",
|
||||||
|
score: story.voteCount ?? null,
|
||||||
|
genres: story.tags || [],
|
||||||
|
status: story.completed ? "Completed" : "Ongoing",
|
||||||
|
published: story.createDate?.split("T")[0] || "???",
|
||||||
|
summary: story.description || meta?.description || "",
|
||||||
|
chapters: story.numParts || story.parts?.length || 1,
|
||||||
|
image: story.cover || meta?.image || "",
|
||||||
|
language: story.language?.name?.toLowerCase() || "unknown",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapters(bookId) {
|
async findChapters(bookId) {
|
||||||
const res = await this.fetch(`${this.baseUrl}/story/${bookId}`);
|
const html = await fetch(`${this.baseUrl}/story/${bookId}`).then(r => r.text());
|
||||||
const html = await res.text();
|
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
const chapters = [];
|
const script = $('script')
|
||||||
|
.map((_, el) => $(el).html())
|
||||||
|
.get()
|
||||||
|
.find(t => t?.includes('window.__remixContext'));
|
||||||
|
|
||||||
$('div.Y26Ib ul[aria-label="story-parts"] li a').each((i, el) => {
|
if (!script) return [];
|
||||||
const href = $(el).attr("href") || "";
|
|
||||||
const match = href.match(/wattpad\.com\/(\d+)/);
|
|
||||||
const id = match ? match[1] : null;
|
|
||||||
|
|
||||||
const titleText = $(el).find('div.wpYp-').text().trim();
|
const jsonText = script.match(/window\.__remixContext\s*=\s*({[\s\S]*?});/)?.[1];
|
||||||
|
if (!jsonText) return [];
|
||||||
|
|
||||||
let chapterNumber = i + 1;
|
let ctx;
|
||||||
let title = titleText;
|
try {
|
||||||
|
ctx = JSON.parse(jsonText);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const match2 = titleText.match(/^(\d+)\s*-\s*(.*)$/);
|
const story = ctx?.state?.loaderData?.["routes/story.$storyid"]?.story;
|
||||||
if (match2) {
|
if (!story?.parts) return [];
|
||||||
chapterNumber = parseInt(match[1], 10);
|
|
||||||
title = match2[2].trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
chapters.push({
|
return story.parts.map((p, i) => ({
|
||||||
id: id,
|
id: String(p.id),
|
||||||
title,
|
title: p.title || `Chapter ${i + 1}`,
|
||||||
chapter: chapterNumber,
|
number: i + 1,
|
||||||
language: "en"
|
language: story.language?.name?.toLowerCase() || "en",
|
||||||
});
|
index: i
|
||||||
});
|
}));
|
||||||
|
|
||||||
return { chapters };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapterPages(chapterId) {
|
async findChapterPages(chapterId) {
|
||||||
const ampUrl = `https://www.wattpad.com/amp/${chapterId}`;
|
const html = await fetch(`https://www.wattpad.com/amp/${chapterId}`).then(r => r.text());
|
||||||
const res = await this.fetch(ampUrl);
|
|
||||||
const html = await res.text();
|
|
||||||
|
|
||||||
const $ = this.cheerio.load(html);
|
const $ = this.cheerio.load(html);
|
||||||
|
|
||||||
const title = $("#amp-reading h2").first().text().trim();
|
const title = $('h2').first().text().trim();
|
||||||
const titleHtml = title ? `<h1>${title}</h1>\n\n` : "";
|
|
||||||
|
|
||||||
const paragraphsHtml = [];
|
const container = $('.story-body-type');
|
||||||
|
if (!container.length) return "";
|
||||||
|
|
||||||
$(".story-body-type p").each((i, el) => {
|
container.find('[data-media-type="image"]').remove();
|
||||||
const p = $(el);
|
|
||||||
|
|
||||||
if (p.attr("data-media-type") !== "image") {
|
const parts = [];
|
||||||
let h = p.html() || "";
|
|
||||||
h = h
|
|
||||||
.replace(/<br\s*\/?>/gi, "<br>")
|
|
||||||
.replace(/\u00A0/g, " ")
|
|
||||||
.replace(/[ \t]+/g, " ")
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
if (h.length > 0) {
|
container.find('p').each((_, el) => {
|
||||||
paragraphsHtml.push(`<p>${h}</p>`);
|
const text = $(el)
|
||||||
}
|
.html()
|
||||||
return;
|
.replace(/\u00A0/g, " ")
|
||||||
}
|
.replace(/[ \t]+/g, " ")
|
||||||
|
.trim();
|
||||||
|
|
||||||
const ampImg = p.find("amp-img").first();
|
if (text) parts.push(`<p>${text}</p>`);
|
||||||
|
|
||||||
if (ampImg.length) {
|
|
||||||
const src = ampImg.attr("src") || "";
|
|
||||||
const width = ampImg.attr("width") || "";
|
|
||||||
const height = ampImg.attr("height") || "";
|
|
||||||
|
|
||||||
if (src) {
|
|
||||||
paragraphsHtml.push(
|
|
||||||
`<img src="${src}" width="${width}" height="${height}">`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
const cleanHTML = titleHtml + paragraphsHtml.join("\n\n");
|
|
||||||
|
|
||||||
return [
|
container.find('amp-img').each((_, el) => {
|
||||||
{
|
const src = $(el).attr('src');
|
||||||
type: "text",
|
const w = $(el).attr('width');
|
||||||
content: cleanHTML.trim(),
|
const h = $(el).attr('height');
|
||||||
index: 0
|
if (src) parts.push(`<img src="${src}" width="${w}" height="${h}">`);
|
||||||
}
|
});
|
||||||
];
|
|
||||||
|
return (
|
||||||
|
(title ? `<h1>${title}</h1>\n\n` : "") +
|
||||||
|
parts.join("\n\n")
|
||||||
|
).trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { novelupdates: wattpad };
|
module.exports = wattpad;
|
||||||
Reference in New Issue
Block a user