diff --git a/AnimeAV1.js b/AnimeAV1.js new file mode 100644 index 0000000..3e5e53f --- /dev/null +++ b/AnimeAV1.js @@ -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(/]*class="aspect-poster[^"]*"[^>]*src="([^"]+)"/i); + const image = imageMatch ? imageMatch[1] : null; + + // BLOCK INFO (STATUS, SEASON, YEAR) + const infoBlockMatch = html.match( + /
([\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>/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(/]*>\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; \ No newline at end of file diff --git a/HiAnime.js b/HiAnime.js new file mode 100644 index 0000000..29fa091 --- /dev/null +++ b/HiAnime.js @@ -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 = /]+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( + `

[\\s\\S]*?]+href="\\/${pageUrl}[^"]*"[^>]+data-jname="([^"]+)"`, + "i" + ); + const jnameMatch = html.match(jnameRegex); + const jname = jnameMatch ? jnameMatch[1] : null; + + const imageRegex = new RegExp( + `]+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 = /]*class="[^"]*\bep-item\b[^"]*"[^>]*data-number="(\d+)"[^>]*data-id="(\d+)"[^>]*href="([^"]+)"[\s\S]*?
]*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( + `]*class="item server-item"[^>]*data-type="${subOrDub}"[^>]*data-id="(\\d+)"[^>]*>\\s*]*>\\s*${serverName}\\s*`, + "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(/\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; \ No newline at end of file diff --git a/ZeroChan.js b/ZeroChan.js index 32d480e..aa56c49 100644 --- a/ZeroChan.js +++ b/ZeroChan.js @@ -1,113 +1,119 @@ class ZeroChan { baseUrl = "https://zerochan.net"; - constructor(fetchPath, cheerioPath, browser) { - this.browser = browser; + constructor() { 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 data = await this.browser.scrape( + const { result } = await this.scrape( url, - () => { - const list = document.querySelectorAll("#thumbs2 li"); - if (list.length === 0) { - return { results: [], hasNextPage: false }; - } - const results = []; + async (page) => { + return page.evaluate(() => { + const list = document.querySelectorAll("#thumbs2 li"); + if (list.length === 0) { + return {results: [], hasNextPage: false}; + } - list.forEach(li => { - const id = li.getAttribute("data-id"); - if (!id) return; + const results = []; - const img = li.querySelector("img"); - const imgUrl = - img?.getAttribute("data-src") || - img?.getAttribute("src") || - null; + list.forEach(li => { + const id = li.getAttribute("data-id"); + if (!id) return; - if (!imgUrl) return; + const img = li.querySelector("img"); + const imgUrl = + img?.getAttribute("data-src") || + img?.getAttribute("src") || + null; - const tagLinks = li.querySelectorAll("p a"); - const tags = [...tagLinks] - .map(a => a.textContent.trim()) - .filter(Boolean); + if (!imgUrl) return; - results.push({ - id, - image: imgUrl, - sampleImageUrl: imgUrl, - tags, - type: "preview" + const tagLinks = li.querySelectorAll("p a"); + const tags = [...tagLinks] + .map(a => a.textContent.trim()) + .filter(Boolean); + + 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 { - results: data.results, - hasNextPage: data.hasNextPage, + results: result.results.map(r => ({ + id: r.id, + image: r.image, + tags: r.tags + })), + hasNextPage: result.hasNextPage, page }; + } - async fetchInfo(id) { + async getInfo(id) { const url = `${this.baseUrl}/${id}`; - const data = await this.browser.scrape( + const { result } = await this.scrape( url, - () => { - const preview = document.querySelector("a.preview"); - if (!preview) { + async (page) => { + return page.evaluate(() => { + 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 { - fullImage: null, - tags: [], + fullImage, + 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 { - fullImage, - tags, - createdAt: Date.now() - }; + }); }, { waitSelector: "a.preview img", timeout: 15000 } ); return { id, - fullImage: data.fullImage, - tags: data.tags, - createdAt: data.createdAt, - rating: "Unknown" + image: result.fullImage, + tags: result.tags }; } } -module.exports = { ZeroChan }; \ No newline at end of file +module.exports = ZeroChan; \ No newline at end of file diff --git a/anime_pictures.js b/anime_pictures.js deleted file mode 100644 index c17df2a..0000000 --- a/anime_pictures.js +++ /dev/null @@ -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 }; \ No newline at end of file diff --git a/animepictures.js b/animepictures.js new file mode 100644 index 0000000..2b55e38 --- /dev/null +++ b/animepictures.js @@ -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; \ No newline at end of file diff --git a/asmhentai.js b/asmhentai.js index 97db9b4..e6eefec 100644 --- a/asmhentai.js +++ b/asmhentai.js @@ -1,125 +1,104 @@ class asmhentai { - constructor(fetchPath, cheerioPath, browser) { - this.baseUrl = "https://asmhentai.com/"; - this.fetch = require(fetchPath); - this.cheerio = require(cheerioPath); - this.browser = browser; + constructor() { + this.baseUrl = "https://asmhentai.com"; this.type = "book-board"; + this.mediaType = "manga"; } - async fetchSearchResult(query = "", page = 1) { - const q = query.trim().replace(/\s+/g, "+"); - const url = q ? `${this.baseUrl}/search/?q=${q}&page=${page}` : `${this.baseUrl}/?q=&page=${page}`; - - const res = await this.fetch(url); - const html = await res.text(); + async search(queryObj) { + const q = (queryObj.query || "").trim().replace(/\s+/g, "+"); + const html = await fetch(`${this.baseUrl}/search/?q=${q}&page=1`).then(r => r.text()); const $ = this.cheerio.load(html); - const items = $(".ov_item .preview_item"); const results = []; - items.each((_, el) => { - const $el = $(el); + $(".preview_item").each((_, 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") || ""; - const id = href.match(/\d+/)?.[0] || null; + let img = $(el).find(".image img").attr("data-src") || $(el).find(".image img").attr("src") || ""; + if (img.startsWith("//")) img = "https:" + img; - const img = $el.find(".image img"); - const raw = img.attr("data-src") || img.attr("src") || ""; - 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); + const image = img.replace("thumb.jpg", "1.jpg"); + const title = $(el).find("h2.caption").text().trim(); results.push({ id, image, - sampleImageUrl, title, - tags, + rating: null, 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 { - results, - hasNextPage, - page + id, + title: $("h1").first().text().trim(), + format: "MANGA", + score: 0, + genres, + status: "unknown", + published: "???", + summary: "", + chapters: 1, + image }; } async findChapters(mangaId) { - const res = await this.fetch(`${this.baseUrl}/g/${mangaId}/`); - const html = await res.text(); + const html = await fetch(`${this.baseUrl}/g/${mangaId}/`).then(r => r.text()); 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") || ""; - if (cover.startsWith("//")) cover = "https:" + cover; + let thumb = $(".gallery img").first().attr("data-src") || ""; + if (thumb.startsWith("//")) thumb = "https:" + thumb; - const firstThumb = $('.gallery a img').first(); - let t = firstThumb.attr("data-src") || ""; + const base = thumb.match(/https:\/\/[^\/]+\/\d+\/\d+\//)?.[0]; + 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 - const baseMatch = t.match(/https:\/\/[^\/]+\/\d+\/\d+\//); - const basePath = baseMatch ? baseMatch[0] : null; - - const pagesText = $(".pages h3").text(); // "Pages: 39" - const pagesMatch = pagesText.match(/(\d+)/); - 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 - }; + return [{ + id: chapterId, + title, + number: 1, + releaseDate: null, + index: 0 + }]; } async findChapterPages(chapterId) { - const decoded = JSON.parse( + const { base, pages, ext } = JSON.parse( Buffer.from(chapterId, "base64").toString("utf8") ); - const { base, pages, ext } = decoded; return Array.from({ length: pages }, (_, i) => ({ url: `${base}${i + 1}.${ext}`, - index: i, + index: i })); } } -module.exports = { asmhentai }; \ No newline at end of file +module.exports = asmhentai; \ No newline at end of file diff --git a/gelbooru.js b/gelbooru.js index 5081c3d..c0f5c9e 100644 --- a/gelbooru.js +++ b/gelbooru.js @@ -1,139 +1,83 @@ class Gelbooru { baseUrl = "https://gelbooru.com"; - constructor(fetchPath, cheerioPath) { - this.fetch = require(fetchPath); - this.load = require(cheerioPath).load; + constructor() { this.type = "image-board"; } - async fetchSearchResult(query, page = 1, perPage = 42) { - if (!query) query = "original"; + async search(query = "thighs", page = 1, perPage = 42) { + 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 response = await this.fetch(url, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' - } - }); - const data = await response.text(); - - const $ = this.load(data); + const html = await fetch(url, { + headers: { "User-Agent": "Mozilla/5.0" } + }).then(r => r.text()); + const $ = this.cheerio.load(html); const results = []; - $('.thumbnail-container a').each((i, e) => { - const $e = $(e); - const href = $e.attr('href'); + $("article.thumbnail-preview > a[id^='p']").each((_, el) => { + const id = $(el).attr("id")?.slice(1); // p13123834 → 13123834 + if (!id) return; - const idMatch = href.match(/id=(\d+)/); - const id = idMatch ? idMatch[1] : null; + const img = $(el).find("img"); + 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 !== ""); - - if (id && image) { - results.push({ - id: id, - image: image, - tags: tags, - type: 'preview' - }); - } + results.push({ id, image, tags }); }); - const pagination = $('.pagination a'); + // pagination + const totalPages = Math.max( + page, + ...$("a[href*='pid=']") + .map((_, el) => + Math.floor( + parseInt($(el).attr("href")?.match(/pid=(\d+)/)?.[1] || 0) / perPage + ) + 1 + ) + .get() + ); - let totalPages = 1; - pagination.each((i, e) => { - const href = $(e).attr('href'); - if (href && href.includes('pid=')) { - const pidMatch = href.match(/pid=(\d+)/); - if (pidMatch) { - const pid = parseInt(pidMatch[1], 10); - totalPages = Math.max(totalPages, Math.floor(pid / perPage) + 1); - } - } - }); - - const currentPage = page; - const nextPage = currentPage < totalPages ? currentPage + 1 : null; - const previousPage = currentPage > 1 ? currentPage - 1 : null; - const hasNextPage = nextPage !== null; - - return { - total: totalPages * perPage, - next: nextPage !== null ? nextPage : 0, - previous: previousPage !== null ? previousPage : 0, - pages: totalPages, - page: currentPage, - hasNextPage, - results + return { + results, + page, + hasNextPage: page < totalPages }; } - async fetchInfo(id) { - const url = `${this.baseUrl}/index.php?page=post&s=view&id=${id}`; + async getInfo(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, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' - } - }); - const original = await response.text(); + const $ = this.cheerio.load(html); + const container = $("section.image-container"); - 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; - - fullImage = $('#gelcom_img').attr('src') || $('#gelcom_mp4').attr('src'); - - if (!fullImage) { - fullImage = $('#right-col a[href*="/images/"]').attr('href') || $('#right-col a[href*="/videos/"]').attr('href'); - } - - if (fullImage && fullImage.startsWith('/')) { - fullImage = new URL(fullImage, this.baseUrl).href; - } - - const tagsList = $('#tag-list a'); - const tags = tagsList.map((i, el) => $(el).text().trim()).get(); - - const stats = $('#post-view-image-container + br + br + br + br + ul, #stats'); - - const postedData = stats.find("li:contains('Posted:')").text().trim(); - const createdAt = new Date(postedData.split("Posted: ")[1]).getTime(); - - const publishedBy = stats.find("li:contains('User:') a").text().trim() || null; - - const rating = stats.find("li:contains('Rating:')").text().trim().split("Rating: ")[1]; - - const comments = $('#comment-list .comment').map((i, el) => { - const $e = $(el); - const id = $e.attr('id')?.replace('c', ''); - const user = $e.find('.comment-user a').text().trim(); - const comment = $e.find('.comment-body').text().trim(); - return { - id, - user, - comment, - } - }).get().filter(Boolean).filter((comment) => comment.comment !== ''); + // tags + const tags = container + .attr("data-tags") + ?.trim() + ?.split(/\s+/) + ?.filter(Boolean) || []; return { id, - fullImage, - resizedImageUrl: fullImage, - tags, - createdAt, - publishedBy, - rating, - - comments - } + image, + tags + }; } } -module.exports = { Gelbooru }; \ No newline at end of file +module.exports = Gelbooru; \ No newline at end of file diff --git a/giphy.js b/giphy.js index 01619ea..e51b9af 100644 --- a/giphy.js +++ b/giphy.js @@ -1,24 +1,22 @@ class Giphy { baseUrl = "https://giphy.com"; - constructor(fetchPath, cheerioPath, browser) { - this.browser = browser; + constructor() { 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 data = await this.browser.scrape( + const data = await this.scrape( url, - () => { + (page) => page.evaluate(() => { const items = document.querySelectorAll('a[data-giphy-id]'); const results = []; items.forEach(el => { const id = el.getAttribute('data-giphy-id'); - // solo coger sources válidos const srcWebp = el.querySelector('source[type="image/webp"][srcset^="http"]'); const srcImg = el.querySelector('img'); @@ -27,20 +25,14 @@ class Giphy { srcImg?.src || null; - // ignorar 1x1 base64 if (!rawSrc || rawSrc.startsWith("data:")) return; - const imgUrl = rawSrc; - const alt = srcImg?.getAttribute("alt") || ""; const tags = alt.trim().split(/\s+/).filter(Boolean); results.push({ id, - image: imgUrl, - sampleImageUrl: imgUrl, - tags, - type: "preview" + image: rawSrc, }); }); @@ -48,24 +40,61 @@ class Giphy { results, 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 { - results: data.results, - hasNextPage: data.hasNextPage, + results: data.result.results.map(r => ({ + id: r.id, + image: r.image + })), + hasNextPage: data.result.hasNextPage, 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 { id, - createdAt: Date.now(), - rating: "Unknown" + image: data.result.image }; } } -module.exports = { Giphy }; \ No newline at end of file +module.exports = Giphy; \ No newline at end of file diff --git a/lightnovelworld.js b/lightnovelworld.js index 393c382..1ac23d0 100644 --- a/lightnovelworld.js +++ b/lightnovelworld.js @@ -1,156 +1,188 @@ -class ligntnovelworld { - constructor(fetchPath, cheerioPath, browser) { - this.browser = browser; - this.fetch = require(fetchPath); - this.cheerio = require(cheerioPath); - this.baseUrl = "https://lightnovelworld.org/api"; - this.type = "book-board"; - } - - async fetchSearchResult(query = "", page = 1) { - if (query.trim() !== "") { - const res = await this.fetch(`${this.baseUrl}/search/?q=${encodeURIComponent(query)}&search_type=title`); - const data = await res.json(); - - const results = data.novels.map(n => ({ - id: n.slug, - title: n.title, - image: `https://lightnovelworld.org/${n.cover_path}`, - sampleImageUrl: `https://lightnovelworld.org/${n.cover_path}`, - tags: [], - type: "book" - })); - - return { - results, - hasNextPage: false, - page - }; - } - - const res = await this.fetch("https://lightnovelworld.org/"); - const html = await res.text(); - const $ = this.cheerio.load(html); - - const cards = $(".recommendations-grid .recommendation-card"); - const results = []; - - cards.each((_, el) => { - const card = $(el); - const link = card.find("a.card-cover-link").attr("href") || ""; - const id = link.replace(/^\/novel\//, "").replace(/\/$/, ""); - const title = card.find(".card-title").text().trim(); - - const img = card.find(".card-cover img").attr("src") || ""; - const imageUrl = img.startsWith("http") ? img : `https://lightnovelworld.org${img}`; - - const tags = card.find(".card-genres .genre-tag").map((_, t) => $(t).text().trim()).get(); - - results.push({ - id, - title, - image: imageUrl, - sampleImageUrl: imageUrl, - tags, - type: "book" - }); - }); - - return { - results, - hasNextPage: false, - page - }; - } - - async findChapters(bookId) { - let offset = 0; - const limit = 500; - const chapters = []; - - while (true) { - const res = await this.fetch(`https://lightnovelworld.org/api/novel/${bookId}/chapters/?offset=${offset}&limit=${limit}`); - const data = await res.json(); - - chapters.push( - ...data.chapters.map(c => ({ - id: `https://lightnovelworld.org/novel/${bookId}/chapter/${c.number}/`, - title: c.title, - chapter: c.number, - language: 'en' - })) - ); - if (!data.has_more) break; - offset += limit; - - } - - return { chapters: chapters}; - } - - async findChapterPages(chapterId) { - const res = await this.fetch(chapterId, { - headers: { - 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - 'referer': chapterId.replace(/\/\d+\/$/, ''), - 'sec-ch-ua': '"Chromium";v="139", "Not;A=Brand";v="99"', - 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36' - } - }); - - const html = await res.text(); - const $ = this.cheerio.load(html); - - const contentDiv = $('#chapterText'); - - if (!contentDiv || contentDiv.length === 0) { - return [{ - type: 'text', - content: '<p>Error: content not found</p>', - index: 0 - }]; - } - - contentDiv.find('script').remove(); - contentDiv.find('style').remove(); - contentDiv.find('ins').remove(); - contentDiv.find("[id^='pf-']").remove(); - contentDiv.find('.chapter-ad-container').remove(); - contentDiv.find('.ad-unit').remove(); - contentDiv.find('.ads').remove(); - contentDiv.find('.adsbygoogle').remove(); - contentDiv.find('.nf-ads').remove(); - contentDiv.find('div[align="center"]').remove(); - contentDiv.find("div[style*='text-align:center']").remove(); - - const paragraphs = contentDiv.find('p'); - let cleanHtml = ''; - - paragraphs.each((_, el) => { - const p = $(el); - let text = p.text() || ''; - - text = text.replace(/△▼△▼△▼△/g, '').replace(/[※]+/g, '').replace(/\s{2,}/g, ' ').trim(); - const htmlP = p.html()?.trim() || ''; - const isEmpty = htmlP === '' || htmlP === ' ' || text.trim() === ''; - const isAd = text.includes('Remove Ads') || text.includes('Buy no ads') || text.includes('novelfire'); - - if (!isEmpty && !isAd) { - if (p.text() !== text) p.text(text); - cleanHtml += $.html(p); - } - }); - - if (!cleanHtml.trim()) { cleanHtml = contentDiv.html() || ''; } - - return [ - { - type: 'text', - content: cleanHtml.trim(), - index: 0 - } - ]; - } -} - -module.exports = { novelupdates: ligntnovelworld }; \ No newline at end of file +class lightnovelworld { + constructor() { + this.baseUrl = "https://lightnovelworld.org/api"; + + this.type = "book-board"; + this.mediaType = "ln"; + } + + async search(queryObj) { + const query = queryObj.query?.trim() || ""; + + if (query !== "") { + const res = await fetch( + `${this.baseUrl}/search/?q=${encodeURIComponent(query)}&search_type=title` + ); + const data = await res.json(); + + if (!data.novels) return []; + + return data.novels.map(n => ({ + id: n.slug, + title: n.title, + image: `https://lightnovelworld.org/${n.cover_path}`, + rating: `Rank ${n.rank}`, + format: "Light Novel" + })); + } + + const res = await fetch("https://lightnovelworld.org/"); + const html = await res.text(); + + const cards = html.split('class="recommendation-card"').slice(1); + const results = []; + + for (const block of cards) { + const link = block.match(/href="([^"]+)"/)?.[1] || ""; + const id = link.replace(/^\/novel\//, "").replace(/\/$/, ""); + + const title = block.match(/class="card-title"[^>]*>([^<]+)/)?.[1]?.trim() || null; + + let img = block.match(/<img[^>]+src="([^"]+)"/)?.[1] || ""; + if (img && !img.startsWith("http")) + img = `https://lightnovelworld.org${img}`; + + if (id && title) { + results.push({ + id, + title, + image: img, + rating: null, + 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 { + id: id, + title: data.name || "", + 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) { + const chapters = []; + let offset = 0; + const limit = 500; + + while (true) { + const res = await fetch( + `https://lightnovelworld.org/api/novel/${bookId}/chapters/?offset=${offset}&limit=${limit}` + ); + const data = await res.json(); + + if (!data.chapters) break; + + chapters.push( + ...data.chapters.map((c, i) => ({ + id: `https://lightnovelworld.org/novel/${bookId}/chapter/${c.number}/`, + title: c.title, + number: Number(c.number), + releaseDate: null, + index: offset + i + })) + ); + + if (!data.has_more) break; + offset += limit; + } + + return chapters; + } + + async findChapterPages(chapterId) { + const data = await this.scrape( + chapterId, + (page) => page.evaluate(() => document.documentElement.outerHTML), + { + waitUntil: "domcontentloaded", + timeout: 15000 + } + ); + + const html = data.result; + if (!html) return '<p>Error loading chapter</p>'; + + const cutPoints = [ + '<div class="bottom-nav"', + '<div class="comments-section"', + '<div class="settings-panel"', + '©' + ]; + + let cutIndex = html.length; + for (const marker of cutPoints) { + const pos = html.indexOf(marker); + if (pos !== -1 && pos < cutIndex) cutIndex = pos; + } + + const chapterHtml = html.substring(0, cutIndex); + + const pMatches = [...chapterHtml.matchAll(/<p[^>]*>([\s\S]*?)<\/p>/gi)]; + let cleanHtml = ''; + + for (const match of pMatches) { + let text = match[1] + .replace(/△▼△▼△▼△/g, '') + .replace(/[※▲▼■◆]/g, '') + .replace(/ /gi, ' ') + .replace(/\s{2,}/g, ' ') + .trim(); + + text = text + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/g, "'") + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/“/g, '“') + .replace(/”/g, '”') + .replace(/‘/g, '‘') + .replace(/’/g, '’') + .replace(/—/g, '—') + .replace(/–/g, '–'); + + if (!text || text.length < 3) continue; + if (/svg|button|modal|comment|loading|default|dyslexic|roboto|lora|line spacing/i.test(text)) continue; + + cleanHtml += `<p>${text}</p>\n`; + } + + return cleanHtml.trim() || '<p>Empty chapter</p>'; + } +} + +module.exports = lightnovelworld; diff --git a/mangadex.js b/mangadex.js index c368f2a..4315d0d 100644 --- a/mangadex.js +++ b/mangadex.js @@ -1,10 +1,9 @@ class MangaDex { - constructor(fetchPath, cheerioPath, browser) { - this.fetchPath = fetchPath; - this.browser = browser; + constructor() { this.baseUrl = "https://mangadex.org"; this.apiUrl = "https://api.mangadex.org"; - this.type = "book-board"; + this.type = "book-board"; + this.mediaType = "manga"; } getHeaders() { @@ -14,79 +13,122 @@ class MangaDex { }; } - async _fetch(url, options = {}) { - if (typeof fetch === 'function') { - return fetch(url, options); - } - const nodeFetch = require(this.fetchPath); - return nodeFetch(url, options); - } - - async fetchSearchResult(query = "", page = 1) { + async search(queryObj) { + const query = queryObj.query?.trim() || ""; const limit = 25; - const offset = (page - 1) * limit; + const offset = (1 - 1) * limit; - let url; - if (!query || query.trim() === "") { - url = `${this.apiUrl}/manga?limit=${limit}&offset=${offset}&includes[]=cover_art&contentRating[]=safe&contentRating[]=suggestive&availableTranslatedLanguage[]=en&order[followedCount]=desc`; - } else { - url = `${this.apiUrl}/manga?title=${encodeURIComponent(query)}&limit=${limit}&offset=${offset}&includes[]=cover_art&contentRating[]=safe&contentRating[]=suggestive&availableTranslatedLanguage[]=en`; - } + const url = `${this.apiUrl}/manga?title=${encodeURIComponent(query)}&limit=${limit}&offset=${offset}&includes[]=cover_art&contentRating[]=safe&contentRating[]=suggestive&availableTranslatedLanguage[]=en`; try { - const response = await this._fetch(url, { headers: this.getHeaders() }); + const response = await fetch(url, { headers: this.getHeaders() }); if (!response.ok) { console.error(`MangaDex API Error: ${response.statusText}`); - return { results: [], hasNextPage: false, page }; + return []; } const json = await response.json(); 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 titleObject = attributes.title || {}; const title = titleObject.en || Object.values(titleObject)[0] || 'Unknown Title'; const coverRelationship = manga.relationships?.find(rel => rel.type === 'cover_art'); const coverFileName = coverRelationship?.attributes?.fileName; - - const coverUrl = coverFileName - ? `https://uploads.mangadex.org/covers/${manga.id}/${coverFileName}.256.jpg` - : ''; - - const fullCoverUrl = coverFileName - ? `https://uploads.mangadex.org/covers/${manga.id}/${coverFileName}` + + const coverUrl = coverFileName + ? `https://uploads.mangadex.org/covers/${manga.id}/${coverFileName}.256.jpg` : ''; - const tags = attributes.tags - ? attributes.tags.map(t => t.attributes.name.en) - : []; return { id: manga.id, image: coverUrl, - sampleImageUrl: fullCoverUrl, title: title, - tags: tags, + rating: null, type: 'book' }; }); - const total = json.total || 0; - const hasNextPage = (offset + limit) < total; + } catch (e) { + 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 { - results, - hasNextPage, - page + id, + title, + 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) { - console.error("Error during MangaDex search:", e); - return { results: [], hasNextPage: false, error: e.message }; + console.error("MangaDex getMetadata error:", e); + 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`; try { - const response = await this._fetch(url, { headers: this.getHeaders() }); + const response = await fetch(url, { headers: this.getHeaders() }); let chapters = []; if (response.ok) { @@ -107,7 +149,7 @@ class MangaDex { .map((ch, index) => ({ id: ch.id, title: ch.attributes.title || `Chapter ${ch.attributes.chapter}`, - chapter: ch.attributes.chapter, + number: ch.attributes.chapter, index: index, language: ch.attributes.translatedLanguage })); @@ -124,23 +166,7 @@ class MangaDex { } } - let highResCover = null; - try { - const mangaRes = await this._fetch(`${this.apiUrl}/manga/${mangaId}?includes[]=cover_art`); - if (mangaRes.ok) { - const mangaJson = await mangaRes.json(); - const coverRel = mangaJson.data.relationships.find(r => r.type === 'cover_art'); - if(coverRel && coverRel.attributes && coverRel.attributes.fileName) { - highResCover = `https://uploads.mangadex.org/covers/${mangaId}/${coverRel.attributes.fileName}`; - } - } - } catch(e) { } - - return { - chapters: chapters, - cover: highResCover - }; - + return chapters; } catch (e) { console.error("Error finding MangaDex chapters:", e); return { chapters: [], cover: null }; @@ -153,7 +179,7 @@ class MangaDex { const url = `${this.apiUrl}/at-home/server/${chapterId}`; 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}`); const json = await response.json(); @@ -177,4 +203,4 @@ class MangaDex { } } -module.exports = { MangaDex }; \ No newline at end of file +module.exports = MangaDex; \ No newline at end of file diff --git a/mangapark.js b/mangapark.js index df12bf2..a700583 100644 --- a/mangapark.js +++ b/mangapark.js @@ -1,25 +1,20 @@ class mangapark { - constructor(fetchPath, cheerioPath, browser) { + constructor() { this.baseUrl = "https://mangapark.net/apo"; - this.fetch = require(fetchPath) - this.browser = browser; this.type = "book-board"; + this.mediaType = "manga"; } - async fetchSearchResult(query = "", page = 1) { - const res = await this.fetch(`${this.baseUrl}/`, { + async search(queryObj) { + const query = queryObj.query; + + const res = await fetch(`${this.baseUrl}/`, { method: "POST", headers: { "accept": "*/*", "content-type": "application/json", "x-apollo-operation-name": "get_searchComic", - "sec-ch-ua": "\"Chromium\";v=\"139\", \"Not;A=Brand\";v=\"99\"", - "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)}` + "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" }, 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 } } } } } } } }", @@ -30,38 +25,96 @@ class mangapark { }); 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, image: `https://mangapark.net/${m.data.urlCoverOri}`, - sampleImageUrl: `https://mangapark.net/${m.data.urlCoverOri}`, - tags: m.data.genres, - type: "book" + rating: m.data.score_val ? Math.round(m.data.score_val * 10) : null, + 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 { - results: results, - hasNextPage: false, - page + id, + title: comic.name || "", + 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) { - const comicId = mangaId.split('/title/')[1]?.split('-')[0]; - if (!comicId) throw new Error('comicId inválido en mangaId'); - const res = await this.fetch(this.baseUrl + "/", { + const comicId = mangaId + if (!comicId) { + console.error("[MangaPark] Invalid ID format:", mangaId); + return []; + } + + const res = await fetch(this.baseUrl + "/", { method: "POST", headers: { "accept": "*/*", "content-type": "application/json", "x-apollo-operation-name": "get_comicChapterList", - "sec-ch-ua": "\"Chromium\";v=\"139\", \"Not;A=Brand\";v=\"99\"", - "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}` + "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" }, 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", @@ -71,25 +124,27 @@ class mangapark { const json = await res.json(); + if (!json.data || !json.data.get_comicChapterList) return []; + let list = json.data.get_comicChapterList; 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}`, title: c.data.dname || c.data.title || `Chapter ${c.data.serial}`, - chapter: Number(c.data.serial), - index: i, - language: "en" + number: Number(c.data.serial), + releaseDate: null, + index: i })); - - return { - chapters: chapters, - }; } 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 scripts = html.match(/<script\b[^>]*>[\s\S]*?<\/script>/gi) || []; @@ -110,10 +165,10 @@ class mangapark { return clean.map((url, index) => ({ url, - index + index, + headers: { referer: 'https://mangapark.net' } })); } - } -module.exports = { mangapark }; \ No newline at end of file +module.exports = mangapark; diff --git a/nhentai.js b/nhentai.js index 919c7f1..fbe7d62 100644 --- a/nhentai.js +++ b/nhentai.js @@ -1,130 +1,181 @@ class nhentai { - constructor(fetchPath, cheerioPath, browser) { - this.baseUrl = "https://nhentai.net"; - this.browser = browser; - this.type = "book-board"; - } + constructor() { + this.baseUrl = "https://nhentai.net"; + this.type = "book-board"; + this.mediaType = "manga"; + } - async fetchSearchResult(query = "", page = 1) { - const q = query.trim().replace(/\s+/g, "+"); - const url = q ? `${this.baseUrl}/search/?q=${q}&page=${page}` : `${this.baseUrl}/?q=&page=${page}`; + async search(queryObj) { + const q = queryObj.query.trim().replace(/\s+/g, "+"); + const url = q + ? `${this.baseUrl}/search/?q=${q}` + : `${this.baseUrl}/?q=`; - const data = await this.browser.scrape( - url, - () => { - const container = document.querySelector('.container.index-container'); - if (!container) return { results: [], hasNextPage: false }; + const { result: data } = await this.scrape( + url, + async (page) => { + return page.evaluate(() => { + const container = document.querySelector('.container.index-container'); + if (!container) return {results: [], hasNextPage: false}; - const galleryEls = container.querySelectorAll('.gallery'); - const results = []; + const galleryEls = container.querySelectorAll('.gallery'); + const results = []; - galleryEls.forEach(el => { - const a = el.querySelector('a.cover'); - if (!a) return; + galleryEls.forEach(el => { + const a = el.querySelector('a.cover'); + if (!a) return; - const href = a.getAttribute('href'); - const id = href.match(/\d+/)?.[0] || null; + const href = a.getAttribute('href'); + const id = href.match(/\d+/)?.[0] || null; - const img = a.querySelector('img.lazyload'); - const thumbRaw = img?.dataset?.src || img?.src || ""; - const thumb = thumbRaw.startsWith("//") ? "https:" + thumbRaw : thumbRaw; - const coverUrl = thumb.replace("thumb", "cover"); + const img = a.querySelector('img.lazyload'); + const thumbRaw = img?.dataset?.src || img?.src || ""; + const thumb = thumbRaw.startsWith("//") ? "https:" + thumbRaw : thumbRaw; + const coverUrl = thumb.replace("thumb", "cover"); - const caption = a.querySelector('.caption'); - const title = caption?.textContent.trim() || ""; + const caption = a.querySelector('.caption'); + const title = caption?.textContent.trim() || ""; - const tagsRaw = el.getAttribute('data-tags') || ""; - const tags = tagsRaw.split(" ").filter(Boolean); + results.push({ + id, + title, + image: coverUrl, + rating: null, + type: "book" + }); + }); - results.push({ - id, - image: thumb, - sampleImageUrl: coverUrl, - title, - tags, - type: "book" - }); - }); + const hasNextPage = !!document.querySelector('section.pagination a.next'); + return {results, hasNextPage}; + }); + }, + { + waitSelector: '.container.index-container', + timeout: 55000 + } + ); - const hasNextPage = !!document.querySelector('section.pagination a.next'); + return data?.results || []; + } - return { - results, - hasNextPage - }; - },{ waitSelector: '.container.index-container', timeout: 5000} - ); + async getMetadata(id) { + const { result: data } = await this.scrape( + `${this.baseUrl}/g/${id}/`, + async (page) => { + return page.evaluate(() => { + const title = document.querySelector('h1.title .pretty')?.textContent?.trim() || ""; - return { - results: data.results, - hasNextPage: data.hasNextPage, - page - }; - } + const img = document.querySelector('#cover img'); + const image = + img?.dataset?.src ? "https:" + img.dataset.src : + 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 data = await this.browser.scrape( - `https://nhentai.net/g/${mangaId}/`, - () => { - const title = document.querySelector('#info > h1 .pretty')?.textContent?.trim() || ""; + const timeEl = document.querySelector('.tag-container.field-name time'); + const published = + timeEl?.getAttribute("datetime") || + timeEl?.textContent?.trim() || + "???"; - const img = document.querySelector('#cover img'); - const cover = img?.dataset?.src ? "https:" + img.dataset.src : img?.src?.startsWith("//") ? "https:" + img.src : img?.src || ""; + return {title, image, genres, published}; + }); + }, + { + 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 pages = thumbs.length; + const formattedDate = data.published + ? new Date(data.published).toLocaleDateString("es-ES") + : "???"; - const first = thumbs[0]; - const s = first?.dataset?.src || first?.src || ""; - const ext = s.match(/t\.(\w+)/)?.[1] || "jpg"; + return { + id, + 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:")); - const language = langTag?.querySelector('.tags .name')?.textContent?.trim() || ""; + async findChapters(mangaId) { + 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 }; - }, { waitSelector: '#bigcontainer', timeout: 4000 } - ); + const img = document.querySelector('#cover img'); + const cover = + img?.dataset?.src ? "https:" + img.dataset.src : + img?.src?.startsWith("//") ? "https:" + img.src : + img?.src || ""; - const encodedChapterId = Buffer.from(JSON.stringify({ - hash: data.hash, - pages: data.pages, - ext: data.ext - })).toString("base64"); + const hash = cover.match(/galleries\/(\d+)\//)?.[1] || null; - return { - chapters: [ - { - id: encodedChapterId, - title: data.title, - chapter: 1, - index: 0, - language: data.language - } - ], - cover: data.cover - }; + const thumbs = document.querySelectorAll('.thumbs img'); + const pages = thumbs.length; - } + const first = thumbs[0]; + const s = first?.dataset?.src || first?.src || ""; + const ext = s.match(/t\.(\w+)/)?.[1] || "jpg"; - async findChapterPages(chapterId) { - const decoded = JSON.parse( - Buffer.from(chapterId, "base64").toString("utf8") - ); + const langTag = [...document.querySelectorAll('#tags .tag-container')] + .find(x => x.textContent.includes("Languages:")); - const { hash, pages, ext } = decoded; - const baseUrl = "https://i.nhentai.net/galleries"; + const language = langTag?.querySelector('.tags .name')?.textContent?.trim() || ""; - return Array.from({ length: pages }, (_, i) => ({ - url: `${baseUrl}/${hash}/${i + 1}.${ext}`, - index: i, - headers: { - Referer: `https://nhentai.net/g/${hash}/` - } - })); - } + return {title, cover, hash, pages, ext, language}; + }); + }, + { + 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 }; \ No newline at end of file +module.exports = nhentai; \ No newline at end of file diff --git a/novelbin.js b/novelbin.js index bfc23d9..1a029e2 100644 --- a/novelbin.js +++ b/novelbin.js @@ -1,129 +1,121 @@ class NovelBin { - constructor(fetchPath, cheerioPath, browser) { - this.browser = browser; - this.fetch = require(fetchPath); - this.cheerio = require(cheerioPath); + constructor() { this.baseUrl = "https://novelbin.me"; - this.type = "book-board"; + this.type = "book-board"; + this.mediaType = "ln"; } - async fetchSearchResult(query = "", page = 1) { - const url = !query || query.trim() === "" - ? `${this.baseUrl}/sort/novelbin-hot?page=${page}` - : `${this.baseUrl}/search?keyword=${encodeURIComponent(query)}`; + async search(queryObj) { + const query = queryObj.query || ""; + const url = `${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 $ = this.cheerio.load(html); const results = []; - $(".list-novel .row, .col-novel-main .list-novel .row").each((i, el) => { - const titleEl = $(el).find("h3.novel-title a"); - if (!titleEl.length) return; - - const title = titleEl.text().trim(); - let href = titleEl.attr("href"); - if (!href) return; - - if (!href.startsWith("http")) { href = `${this.baseUrl}${href}` } + $('h3.novel-title a').each((i, el) => { + const href = $(el).attr('href'); + const title = $(el).text().trim(); const idMatch = href.match(/novel-book\/([^/?]+)/); 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({ id, title, - image: coverUrl, - sampleImageUrl: coverUrl, - tags: [], + image: img, + rating: null, 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 { - results, - hasNextPage, - page + id, + title, + format: "Light Novel", + score: 0, + genres, + status, + published: "???", + summary, + chapters, + image }; } 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 $ = this.cheerio.load(html); const chapters = []; - $("#chapter-archive ul.list-chapter li a").each((i, el) => { - const a = $(el); - const title = a.attr("title") || a.text().trim(); - let href = a.attr("href"); - - if (!href) return; - - if (href.startsWith("https://novelbin.me")) { href = href.replace("https://novelbin.me", "") } - const match = title.match(/chapter\s*([\d.]+)/i); - const chapterNumber = match ? match[1] : "0"; + $('a[title]').each((i, el) => { + const fullUrl = $(el).attr('href'); + const title = $(el).attr('title').trim(); + const numMatch = title.match(/chapter\s+(\d+(?:\.\d+)?)/i); chapters.push({ - id: href, - title: title.trim(), - chapter: chapterNumber, - language: "en" + id: fullUrl, + title, + number: numMatch ? numMatch[1] : "0", + releaseDate: null, + index: i }); }); - return { chapters: chapters }; + return chapters; } - async findChapterPages(chapterId) { - const url = chapterId.startsWith('http') ? chapterId : `${this.baseUrl}${chapterId}`; - - const content = await this.browser.scrape( - url, - () => { - const contentDiv = document.querySelector('#chr-content, .chr-c'); - if (!contentDiv) return "<p>Error: Could not find content.</p>"; - contentDiv.querySelectorAll('script, div[id^="pf-"], div[style*="text-align:center"], ins, div[align="center"], .ads, .adsbygoogle').forEach(el => el.remove()); - - const paragraphs = contentDiv.querySelectorAll('p'); - let cleanHtml = ''; - - paragraphs.forEach(p => { - let text = p.textContent || ''; - - text = text.replace(/△▼△▼△▼△/g, ''); - text = text.replace(/[※\s]{2,}/g, ''); - - const html = p.innerHTML.trim(); - - const isAd = text.includes('Remove Ads From') || text.includes('Buy no ads experience'); - const isEmpty = html === '' || html === ' ' || text.trim() === ''; - - if (!isAd && !isEmpty) { - if (p.textContent !== text) { - p.textContent = text; - } - cleanHtml += p.outerHTML; - } - }); - - return cleanHtml || contentDiv.innerHTML; - }, - { waitSelector: '#chr-content', timeout: 2000 } - ); - - return [{ - type: 'text', - content: content, - index: 0 - }]; + async findChapterPages(chapterUrl) { + const {result} = await this.scrape(chapterUrl, async (page) => { + return page.evaluate(() => { + document.querySelectorAll('div[id^="pf-"]').forEach(e => e.remove()); + const ps = Array.from(document.querySelectorAll("p")).map(p => p.outerHTML.trim()).filter(p => p.length > 7); + return ps.join("\n"); + }); + }, { + waitUntil: "domcontentloaded", + renderWaitTime: 300 + }); + return result || "<p>Error: chapter text not found</p>"; } } - -module.exports = { NovelBin }; \ No newline at end of file +module.exports = NovelBin; \ No newline at end of file diff --git a/novelfire.js b/novelfire.js index 031dd71..5d0ac62 100644 --- a/novelfire.js +++ b/novelfire.js @@ -1,155 +1,137 @@ -class novelfire { - constructor(fetchPath, cheerioPath, browser) { - this.browser = browser; - this.fetch = require(fetchPath); - this.cheerio = require(cheerioPath); +class NovelFire { + constructor() { this.baseUrl = "https://novelfire.net"; this.type = "book-board"; + this.mediaType = "ln"; } - async fetchSearchResult(query = "", page = 1) { - let html; + async search(queryObj) { + const query = queryObj.query; - if (query.trim() === "") { - const res = await this.fetch(`${this.baseUrl}/home`); - html = await res.text(); - } else { - const res = await this.fetch( - `${this.baseUrl}/ajax/searchLive?inputContent=${encodeURIComponent(query)}` - ); - const data = await res.json(); - html = data.html; + const res = await fetch( + `${this.baseUrl}/ajax/searchLive?inputContent=${encodeURIComponent(query)}`, + { headers: { "accept": "application/json" } } + ); + + const data = await res.json(); + if (!data.data) return []; + + 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); - const results = []; - - $(".novel-item").each((_, el) => { - 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" - }); - }); + let status = 'unknown'; + const statusClass = $('strong.ongoing, strong.completed').attr('class'); + if (statusClass) { + status = statusClass.toLowerCase(); + } return { - results, - hasNextPage: false, - page + id, + title, + format: 'Light Novel', + score: 0, + genres, + status, + published: '???', + summary, + chapters, + image }; } async findChapters(bookId) { - const url = `https://novelfire.net/book/${bookId}/chapter-1`; - - 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 url = `https://novelfire.net/book/${bookId}/chapters`; + const html = await (await fetch(url)).text(); const $ = this.cheerio.load(html); - const contentDiv = $("#content"); + let postId; - if (!contentDiv || contentDiv.length === 0) { - return [{ - type: "text", - content: "<p>Error: content not found</p>", - 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); - } + $("script").each((_, el) => { + const txt = $(el).html() || ""; + const m = txt.match(/listChapterDataAjax\?post_id=(\d+)/); + if (m) postId = m[1]; }); - if (!cleanHtml.trim()) { cleanHtml = contentDiv.html(); } + if (!postId) throw new Error("post_id not found"); - return [ - { - type: "text", - content: cleanHtml.trim(), - index: 0 - } - ]; + const params = new URLSearchParams({ + post_id: postId, + draw: 1, + "columns[0][data]": "title", + "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 }; \ No newline at end of file +module.exports = NovelFire; \ No newline at end of file diff --git a/realbooru.js b/realbooru.js index dc595ea..2aa647d 100644 --- a/realbooru.js +++ b/realbooru.js @@ -1,170 +1,84 @@ class Realbooru { - - baseUrl = "https://realbooru.com"; + baseUrl = "https://realbooru.com"; 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) { - this.fetch = require(fetchPath); - this.cheerio = require(cheerioPath); + constructor() { this.type = "image-board"; } - LoadDoc(body) { - return this.cheerio.load(body); - } - - async fetchSearchResult(query, page = 1, perPage = 42) { - if (!query) query = "original"; - + async search(query = "original", page = 1, perPage = 42) { const offset = (page - 1) * perPage; - const url = `${this.baseUrl}/index.php?page=post&s=list&tags=${query}&pid=${offset}`; - try { - const response = await this.fetch(url, { headers: this.headers }); - const data = await response.text(); + const tags = query + .trim() + .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) => { - const $a = $(e); + $('div.col.thumb').each((_, el) => { + const id = ($(el).attr('id') || "").replace('s', ''); + const img = $(el).find('img'); - const href = $a.attr('href'); - let id = null; - if (href) { - const idMatch = href.match(/&id=(\d+)/); - if (idMatch) { - id = idMatch[1]; - } - } + let image = img.attr('src'); + if (image && !image.startsWith('http')) image = 'https:' + image; - if (!id) { - id = $a.closest('span, div').attr('id')?.replace('s', '').replace('post_', ''); - } + const title = img.attr('title') || ''; + const tags = title + .split(',') + .map(t => t.trim()) + .filter(Boolean); - const imageElement = $a.find('img').first(); - let image = imageElement.attr('src'); - - 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; - } + if (id && image) { + results.push({ id, image, tags }); } + }); - const currentPage = page; - const hasNextPage = currentPage < totalPages; - const next = hasNextPage ? (currentPage + 1) : 0; - const previous = currentPage > 1 ? (currentPage - 1) : 0; - - 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: [] }; + let totalPages = page; + const lastPid = $('a[alt="last page"]').attr('href')?.match(/pid=(\d+)/); + if (lastPid) { + totalPages = Math.floor(parseInt(lastPid[1], 10) / perPage) + 1; } + + 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 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 }); - const original = await response.text(); - - 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 (image && !image.startsWith('http')) { + image = this.baseUrl + image; } - if (fullImage && !fullImage.startsWith('http')) { - fullImage = `https:${fullImage}`; - } - - 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); + const tags = []; + $('#tagLink a').each((_, el) => { + tags.push($(el).text().trim()); + }); return { id, - fullImage, - resizedImageUrl, - tags, - createdAt, - publishedBy, - rating, - comments + image, + tags }; } } -module.exports = { Realbooru }; \ No newline at end of file +module.exports = Realbooru; \ No newline at end of file diff --git a/rule34.js b/rule34.js index a7c2d22..ded2fdd 100644 --- a/rule34.js +++ b/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' }; - constructor(fetchPath, cheerioPath) { - this.fetch = require(fetchPath); - this.cheerio = require(cheerioPath); + constructor() { this.type = "image-board"; } - async fetchSearchResult(query, page = 1, perPage = 42) { - if (!query) query = "alisa_mikhailovna_kujou"; - + async search(query = "alisa_mikhailovna_kujou", page = 1, perPage = 42) { const offset = (page - 1) * perPage; 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 $ = this.cheerio.load(data); const results = []; - $('.image-list span').each((i, e) => { + $('.image-list span').each((_, e) => { const $e = $(e); const id = $e.attr('id')?.replace('s', ''); - let image = $e.find('img').attr('src'); + if (image && !image.startsWith('http')) { 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) { results.push({ - id: id, - image: image, - tags: tags, - type: 'preview' - + id, + image, + tags }); } }); 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; - } - + if (lastPageLink.length) { + const pid = Number(lastPageLink.attr('href')?.split('pid=')[1] ?? 0); + totalPages = Math.ceil(pid / perPage) + 1; } - const currentPage = page; - const hasNextPage = currentPage < totalPages; - const next = hasNextPage ? (currentPage + 1) : 0; - const previous = currentPage > 1 ? (currentPage - 1) : 0; - - const total = totalPages * perPage; - - return { total, next, previous, pages: totalPages, page: currentPage, hasNextPage, results }; + return { + page, + hasNextPage: page < totalPages, + results + }; } - async fetchInfo(id) { + async getInfo(id) { const url = `${this.baseUrl}/index.php?page=post&s=view&id=${id}`; const resizeCookies = { @@ -91,55 +73,27 @@ class Rule34 { const resizeHeaders = { ...this.headers, 'cookie': cookieString }; const [resizedResponse, nonResizedResponse] = await Promise.all([ - this.fetch(url, { headers: resizeHeaders }), - this.fetch(url, { headers: fetchHeaders }) + fetch(url, { headers: resizeHeaders }), + fetch(url, { headers: fetchHeaders }) ]); const [resized, original] = await Promise.all([resizedResponse.text(), nonResizedResponse.text()]); - const $resized = this.cheerio.load(resized); const $ = this.cheerio.load(original); - let resizedImageUrl = $resized('#image').attr('src'); - if (resizedImageUrl && !resizedImageUrl.startsWith('http')) { - resizedImageUrl = `https:${resizedImageUrl}`; - } - let fullImage = $('#image').attr('src'); if (fullImage && !fullImage.startsWith('http')) { fullImage = `https:${fullImage}`; } 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 { id, - fullImage, - resizedImageUrl, - tags, - createdAt, - publishedBy, - rating, - comments + image: fullImage, + tags }; } } -module.exports = { Rule34 }; \ No newline at end of file +module.exports = Rule34; \ No newline at end of file diff --git a/tenor.js b/tenor.js index 205c562..1571452 100644 --- a/tenor.js +++ b/tenor.js @@ -1,61 +1,65 @@ class Tenor { baseUrl = "https://tenor.com"; - constructor(fetchPath, cheerioPath, browser) { - this.browser = browser; + constructor() { this.type = "image-board"; - this.lastQuery = null; 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) { this.lastQuery = query; 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, - () => { - // Fallback selectors: try specific class first, then generic figure - const items = document.querySelectorAll('div.GifList figure, figure'); - const results = []; + async (page) => { + return page.evaluate(() => { + const items = document.querySelectorAll('div.GifList figure, figure'); + const results = []; - items.forEach(fig => { - const link = fig.querySelector('a'); - const img = fig.querySelector('img'); - if (!link || !img) return; + items.forEach(fig => { + const link = fig.querySelector('a'); + const img = fig.querySelector('img'); + 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 id = idMatch ? idMatch[1] : null; + const imgUrl = + img.getAttribute('src') || + img.getAttribute('data-src'); - // Tenor lazy loads images, so we check 'src' AND 'data-src' - const imgUrl = img.getAttribute('src') || img.getAttribute('data-src'); - const tagsRaw = img.getAttribute('alt') || ""; - const tags = tagsRaw.trim().split(/\s+/).filter(Boolean); + const tagsRaw = img.getAttribute('alt') || ""; + const tags = tagsRaw.trim().split(/\s+/).filter(Boolean); - if (id && imgUrl && !imgUrl.includes('placeholder')) { - results.push({ - id, - image: imgUrl, - sampleImageUrl: imgUrl, - tags, - type: "preview" - }); - } + if (id && imgUrl && !imgUrl.includes("placeholder")) { + results.push({ + id, + image: imgUrl, + sampleImageUrl: imgUrl, + tags, + 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, scrollToBottom: true, renderWaitTime: 3000, @@ -63,61 +67,49 @@ class Tenor { } ); - const newResults = data.results.filter(item => !this.seenIds.has(item.id)); - - newResults.forEach(item => this.seenIds.add(item.id)); + const newResults = result.results.filter(r => !this.seenIds.has(r.id)); + newResults.forEach(r => this.seenIds.add(r.id)); return { - results: newResults, - hasNextPage: data.hasNextPage, + results: newResults.map(r => ({ + id: r.id, + image: r.image, + tags: r.tags, + })), + hasNextPage: result.hasNextPage, page }; + } - async fetchInfo(id) { + async getInfo(id) { const url = `${this.baseUrl}/view/gif-${id}`; - const data = await this.browser.scrape( + const { result } = await this.scrape( url, - () => { - const img = document.querySelector('img[alt]'); - const fullImage = img?.src || null; + async (page) => { + return page.evaluate(() => { + const img = document.querySelector(".Gif img"); + const fullImage = img?.src || null; - const tags = [...document.querySelectorAll('.tag-list li a .RelatedTag')] - .map(tag => tag.textContent.trim()) - .filter(Boolean); + const tags = [...document.querySelectorAll(".tag-list li a .RelatedTag")] + .map(t => t.textContent.trim()) + .filter(Boolean); - let createdAt = Date.now(); - - const detailNodes = [...document.querySelectorAll('.gif-details dd')]; - const createdNode = detailNodes.find(n => n.textContent.includes("Created:")); - - if (createdNode) { - const raw = createdNode.textContent.replace("Created:", "").trim(); - const parts = raw.split(/[\/,: ]+/); - if (parts.length >= 6) { - let [dd, mm, yyyy, hh, min, ss] = parts.map(p => parseInt(p, 10)); - createdAt = new Date(yyyy, mm - 1, dd, hh, min, ss).getTime(); - } - } - - return { - fullImage, - tags, - createdAt - }; + return { fullImage, tags }; + }); }, - { waitSelector: 'img[alt]', timeout: 15000 } + { waitSelector: ".Gif img", timeout: 15000 } ); return { id, - fullImage: data.fullImage, - tags: data.tags, - createdAt: data.createdAt, - rating: "Unknown" + image: result.fullImage, + tags: result.tags, + title: result.tags?.join(" ") || `Tenor GIF ${id}`, + headers: "" }; } } -module.exports = { Tenor }; \ No newline at end of file +module.exports = Tenor; \ No newline at end of file diff --git a/waifupics.js b/waifupics.js index 7f25b56..1747326 100644 --- a/waifupics.js +++ b/waifupics.js @@ -8,12 +8,11 @@ class WaifuPics { 'happy', 'wink', 'poke', 'dance', 'cringe' ]; - constructor(fetchPath, cheerioPath) { - this.fetch = require(fetchPath); + constructor() { this.type = "image-board"; } - async fetchSearchResult(query, page = 1, perPage = 42) { + async search(query, page = 1, perPage = 42) { if (!query) query = "waifu"; const category = query.trim().split(' ')[0]; @@ -34,7 +33,7 @@ class WaifuPics { try { - const response = await this.fetch(`${this.baseUrl}/many/sfw/${category}`, { + const response = await fetch(`${this.baseUrl}/many/sfw/${category}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ exclude: [] }), @@ -49,13 +48,12 @@ class WaifuPics { const results = data.files.map((url, index) => { const id = url.substring(url.lastIndexOf('/') + 1) || `${category}-${index}`; - const uniqueId = `${page}-${id}`; + const uniqueId = `${id}`; return { id: uniqueId, image: url, tags: [category], - type: 'preview' }; }); @@ -83,19 +81,13 @@ class WaifuPics { } } - async fetchInfo(id) { - console.log(`[WaifuPics] fetchInfo called for ${id}, but this API only provides direct URLs.`); + async getInfo(id) { return { - id: id, - fullImage: `https://i.waifu.pics/${id}`, - resizedImageUrl: `https://i.waifu.pics/${id}`, - tags: [], - createdAt: null, - publishedBy: 'Waifu.pics', - rating: 'sfw', - comments: [] + id, + image: `https://i.waifu.pics/${id}`, + tags: [] }; } } -module.exports = { WaifuPics }; \ No newline at end of file +module.exports = WaifuPics; \ No newline at end of file diff --git a/wattpad.js b/wattpad.js index 32bad61..be602fa 100644 --- a/wattpad.js +++ b/wattpad.js @@ -1,59 +1,23 @@ class wattpad { - constructor(fetchPath, cheerioPath, browser) { - this.browser = browser; - this.fetch = require(fetchPath); - this.cheerio = require(cheerioPath); + constructor() { this.baseUrl = "https://wattpad.com"; this.type = "book-board"; + this.mediaType = "ln"; } - async fetchSearchResult(query = "", page = 1) { - if (!query || 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 - }; - } - + async search(queryObj) { + const query = queryObj.query?.trim() || ""; 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 res.json(); + const json = await fetch(url).then(r => r.json()); - const results = json.stories.map(n => ({ + return json.stories.map(n => ({ id: n.id, title: n.title, image: n.cover, @@ -61,104 +25,118 @@ class wattpad { tags: n.tags, type: "book" })); + } - const totalPages = Math.ceil(json.total / limit); - const hasNextPage = page < totalPages; + async getMetadata(id) { + 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 { - results, - hasNextPage, - page + id: story.id, + title: story.title, + 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) { - const res = await this.fetch(`${this.baseUrl}/story/${bookId}`); - const html = await res.text(); + const html = await fetch(`${this.baseUrl}/story/${bookId}`).then(r => r.text()); 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) => { - const href = $(el).attr("href") || ""; - const match = href.match(/wattpad\.com\/(\d+)/); - const id = match ? match[1] : null; + if (!script) return []; - 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 title = titleText; + let ctx; + try { + ctx = JSON.parse(jsonText); + } catch { + return []; + } - const match2 = titleText.match(/^(\d+)\s*-\s*(.*)$/); - if (match2) { - chapterNumber = parseInt(match[1], 10); - title = match2[2].trim(); - } + const story = ctx?.state?.loaderData?.["routes/story.$storyid"]?.story; + if (!story?.parts) return []; - chapters.push({ - id: id, - title, - chapter: chapterNumber, - language: "en" - }); - }); - - return { chapters }; + return story.parts.map((p, i) => ({ + id: String(p.id), + title: p.title || `Chapter ${i + 1}`, + number: i + 1, + language: story.language?.name?.toLowerCase() || "en", + index: i + })); } async findChapterPages(chapterId) { - const ampUrl = `https://www.wattpad.com/amp/${chapterId}`; - const res = await this.fetch(ampUrl); - const html = await res.text(); - + const html = await fetch(`https://www.wattpad.com/amp/${chapterId}`).then(r => r.text()); const $ = this.cheerio.load(html); - const title = $("#amp-reading h2").first().text().trim(); - const titleHtml = title ? `<h1>${title}</h1>\n\n` : ""; + const title = $('h2').first().text().trim(); - const paragraphsHtml = []; + const container = $('.story-body-type'); + if (!container.length) return ""; - $(".story-body-type p").each((i, el) => { - const p = $(el); + container.find('[data-media-type="image"]').remove(); - if (p.attr("data-media-type") !== "image") { - let h = p.html() || ""; - h = h - .replace(/<br\s*\/?>/gi, "<br>") - .replace(/\u00A0/g, " ") - .replace(/[ \t]+/g, " ") - .trim(); + const parts = []; - if (h.length > 0) { - paragraphsHtml.push(`<p>${h}</p>`); - } - return; - } + container.find('p').each((_, el) => { + const text = $(el) + .html() + .replace(/\u00A0/g, " ") + .replace(/[ \t]+/g, " ") + .trim(); - const ampImg = p.find("amp-img").first(); - - 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}">` - ); - } - } + if (text) parts.push(`<p>${text}</p>`); }); - const cleanHTML = titleHtml + paragraphsHtml.join("\n\n"); - return [ - { - type: "text", - content: cleanHTML.trim(), - index: 0 - } - ]; + container.find('amp-img').each((_, el) => { + const src = $(el).attr('src'); + const w = $(el).attr('width'); + const h = $(el).attr('height'); + 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 }; \ No newline at end of file +module.exports = wattpad; \ No newline at end of file