diff --git a/AnimeAV1.js b/anime/AnimeAV1.js similarity index 100% rename from AnimeAV1.js rename to anime/AnimeAV1.js diff --git a/HiAnime.js b/anime/HiAnime.js similarity index 100% rename from HiAnime.js rename to anime/HiAnime.js diff --git a/asmhentai.js b/book/asmhentai.js similarity index 100% rename from asmhentai.js rename to book/asmhentai.js diff --git a/lightnovelworld.js b/book/lightnovelworld.js similarity index 100% rename from lightnovelworld.js rename to book/lightnovelworld.js diff --git a/mangadex.js b/book/mangadex.js similarity index 97% rename from mangadex.js rename to book/mangadex.js index 4315d0d..69016ab 100644 --- a/mangadex.js +++ b/book/mangadex.js @@ -1,206 +1,206 @@ -class MangaDex { - constructor() { - this.baseUrl = "https://mangadex.org"; - this.apiUrl = "https://api.mangadex.org"; - this.type = "book-board"; - this.mediaType = "manga"; - } - - getHeaders() { - return { - 'User-Agent': 'MangaDex-Client-Adapter/1.0', - 'Content-Type': 'application/json' - }; - } - - async search(queryObj) { - const query = queryObj.query?.trim() || ""; - const limit = 25; - const offset = (1 - 1) * limit; - - 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 fetch(url, { headers: this.getHeaders() }); - if (!response.ok) { - console.error(`MangaDex API Error: ${response.statusText}`); - return []; - } - - const json = await response.json(); - if (!json || !Array.isArray(json.data)) { - return []; - } - - 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` - : ''; - - - return { - id: manga.id, - image: coverUrl, - title: title, - rating: null, - type: 'book' - }; - }); - - } 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 { - 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("MangaDex getMetadata error:", e); - return { - id, - title: "", - format: "Manga", - score: 0, - genres: [], - status: "", - published: "???", - summary: "", - chapters: 0, - image: "" - }; - } - } - - async findChapters(mangaId) { - if (!mangaId) return []; - - const url = `${this.apiUrl}/manga/${mangaId}/feed?translatedLanguage[]=en&order[chapter]=asc&limit=500&includes[]=scanlation_group`; - - try { - const response = await fetch(url, { headers: this.getHeaders() }); - let chapters = []; - - if (response.ok) { - const json = await response.json(); - if (json && Array.isArray(json.data)) { - const allChapters = json.data - .filter(ch => ch.attributes.chapter && !ch.attributes.externalUrl) - .map((ch, index) => ({ - id: ch.id, - title: ch.attributes.title || `Chapter ${ch.attributes.chapter}`, - number: ch.attributes.chapter, - index: index, - language: ch.attributes.translatedLanguage - })); - - const seenChapters = new Set(); - allChapters.forEach(ch => { - if (!seenChapters.has(ch.chapter)) { - seenChapters.add(ch.chapter); - chapters.push(ch); - } - }); - - chapters.sort((a, b) => parseFloat(a.chapter) - parseFloat(b.chapter)); - } - } - - return chapters; - } catch (e) { - console.error("Error finding MangaDex chapters:", e); - return { chapters: [], cover: null }; - } - } - - - async findChapterPages(chapterId) { - if (!chapterId) return []; - const url = `${this.apiUrl}/at-home/server/${chapterId}`; - - try { - const response = await fetch(url, { headers: this.getHeaders() }); - if (!response.ok) throw new Error(`Failed to fetch pages: ${response.statusText}`); - - const json = await response.json(); - if (!json || !json.baseUrl || !json.chapter) return []; - - const baseUrl = json.baseUrl; - const chapterHash = json.chapter.hash; - const imageFilenames = json.chapter.data; - - return imageFilenames.map((filename, index) => ({ - url: `${baseUrl}/data/${chapterHash}/${filename}`, - index: index, - headers: { - 'Referer': `https://mangadex.org/chapter/${chapterId}` - } - })); - } catch (e) { - console.error("Error finding MangaDex pages:", e); - return []; - } - } -} - +class MangaDex { + constructor() { + this.baseUrl = "https://mangadex.org"; + this.apiUrl = "https://api.mangadex.org"; + this.type = "book-board"; + this.mediaType = "manga"; + } + + getHeaders() { + return { + 'User-Agent': 'MangaDex-Client-Adapter/1.0', + 'Content-Type': 'application/json' + }; + } + + async search(queryObj) { + const query = queryObj.query?.trim() || ""; + const limit = 25; + const offset = (1 - 1) * limit; + + 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 fetch(url, { headers: this.getHeaders() }); + if (!response.ok) { + console.error(`MangaDex API Error: ${response.statusText}`); + return []; + } + + const json = await response.json(); + if (!json || !Array.isArray(json.data)) { + return []; + } + + 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` + : ''; + + + return { + id: manga.id, + image: coverUrl, + title: title, + rating: null, + type: 'book' + }; + }); + + } 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 { + 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("MangaDex getMetadata error:", e); + return { + id, + title: "", + format: "Manga", + score: 0, + genres: [], + status: "", + published: "???", + summary: "", + chapters: 0, + image: "" + }; + } + } + + async findChapters(mangaId) { + if (!mangaId) return []; + + const url = `${this.apiUrl}/manga/${mangaId}/feed?translatedLanguage[]=en&order[chapter]=asc&limit=500&includes[]=scanlation_group`; + + try { + const response = await fetch(url, { headers: this.getHeaders() }); + let chapters = []; + + if (response.ok) { + const json = await response.json(); + if (json && Array.isArray(json.data)) { + const allChapters = json.data + .filter(ch => ch.attributes.chapter && !ch.attributes.externalUrl) + .map((ch, index) => ({ + id: ch.id, + title: ch.attributes.title || `Chapter ${ch.attributes.chapter}`, + number: ch.attributes.chapter, + index: index, + language: ch.attributes.translatedLanguage + })); + + const seenChapters = new Set(); + allChapters.forEach(ch => { + if (!seenChapters.has(ch.chapter)) { + seenChapters.add(ch.chapter); + chapters.push(ch); + } + }); + + chapters.sort((a, b) => parseFloat(a.chapter) - parseFloat(b.chapter)); + } + } + + return chapters; + } catch (e) { + console.error("Error finding MangaDex chapters:", e); + return { chapters: [], cover: null }; + } + } + + + async findChapterPages(chapterId) { + if (!chapterId) return []; + const url = `${this.apiUrl}/at-home/server/${chapterId}`; + + try { + const response = await fetch(url, { headers: this.getHeaders() }); + if (!response.ok) throw new Error(`Failed to fetch pages: ${response.statusText}`); + + const json = await response.json(); + if (!json || !json.baseUrl || !json.chapter) return []; + + const baseUrl = json.baseUrl; + const chapterHash = json.chapter.hash; + const imageFilenames = json.chapter.data; + + return imageFilenames.map((filename, index) => ({ + url: `${baseUrl}/data/${chapterHash}/${filename}`, + index: index, + headers: { + 'Referer': `https://mangadex.org/chapter/${chapterId}` + } + })); + } catch (e) { + console.error("Error finding MangaDex pages:", e); + return []; + } + } +} + module.exports = MangaDex; \ No newline at end of file diff --git a/mangapark.js b/book/mangapark.js similarity index 100% rename from mangapark.js rename to book/mangapark.js diff --git a/nhentai.js b/book/nhentai.js similarity index 96% rename from nhentai.js rename to book/nhentai.js index fbe7d62..afa013c 100644 --- a/nhentai.js +++ b/book/nhentai.js @@ -1,181 +1,181 @@ -class nhentai { - constructor() { - this.baseUrl = "https://nhentai.net"; - this.type = "book-board"; - this.mediaType = "manga"; - } - - async search(queryObj) { - const q = queryObj.query.trim().replace(/\s+/g, "+"); - const url = q - ? `${this.baseUrl}/search/?q=${q}` - : `${this.baseUrl}/?q=`; - - 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 = []; - - galleryEls.forEach(el => { - const a = el.querySelector('a.cover'); - if (!a) return; - - const href = a.getAttribute('href'); - const id = href.match(/\d+/)?.[0] || null; - - const img = a.querySelector('img.lazyload'); - const thumbRaw = img?.dataset?.src || img?.src || ""; - const thumb = thumbRaw.startsWith("//") ? "https:" + thumbRaw : thumbRaw; - const coverUrl = thumb.replace("thumb", "cover"); - - const caption = a.querySelector('.caption'); - const title = caption?.textContent.trim() || ""; - - results.push({ - id, - title, - image: coverUrl, - rating: null, - type: "book" - }); - }); - - const hasNextPage = !!document.querySelector('section.pagination a.next'); - return {results, hasNextPage}; - }); - }, - { - waitSelector: '.container.index-container', - timeout: 55000 - } - ); - - return data?.results || []; - } - - 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() || ""; - - 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()) - : []; - - const timeEl = document.querySelector('.tag-container.field-name time'); - const published = - timeEl?.getAttribute("datetime") || - timeEl?.textContent?.trim() || - "???"; - - return {title, image, genres, published}; - }); - }, - { - waitSelector: "#bigcontainer", - timeout: 55000 - } - ); - - if (!data) throw new Error(`Fallo al obtener metadatos para ID ${id}`); - - const formattedDate = data.published - ? new Date(data.published).toLocaleDateString("es-ES") - : "???"; - - 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 || "" - }; - } - - 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() || ""; - - const img = document.querySelector('#cover img'); - const cover = - img?.dataset?.src ? "https:" + img.dataset.src : - img?.src?.startsWith("//") ? "https:" + img.src : - img?.src || ""; - - const hash = cover.match(/galleries\/(\d+)\//)?.[1] || null; - - const thumbs = document.querySelectorAll('.thumbs img'); - const pages = thumbs.length; - - const first = thumbs[0]; - const s = first?.dataset?.src || first?.src || ""; - const ext = s.match(/t\.(\w+)/)?.[1] || "jpg"; - - const langTag = [...document.querySelectorAll('#tags .tag-container')] - .find(x => x.textContent.includes("Languages:")); - - const language = langTag?.querySelector('.tags .name')?.textContent?.trim() || ""; - - return {title, cover, hash, pages, ext, language}; - }); - }, - { - waitSelector: '#bigcontainer', - timeout: 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}/` } - })); - } -} - +class nhentai { + constructor() { + this.baseUrl = "https://nhentai.net"; + this.type = "book-board"; + this.mediaType = "manga"; + } + + async search(queryObj) { + const q = queryObj.query.trim().replace(/\s+/g, "+"); + const url = q + ? `${this.baseUrl}/search/?q=${q}` + : `${this.baseUrl}/?q=`; + + 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 = []; + + galleryEls.forEach(el => { + const a = el.querySelector('a.cover'); + if (!a) return; + + const href = a.getAttribute('href'); + const id = href.match(/\d+/)?.[0] || null; + + const img = a.querySelector('img.lazyload'); + const thumbRaw = img?.dataset?.src || img?.src || ""; + const thumb = thumbRaw.startsWith("//") ? "https:" + thumbRaw : thumbRaw; + const coverUrl = thumb.replace("thumb", "cover"); + + const caption = a.querySelector('.caption'); + const title = caption?.textContent.trim() || ""; + + results.push({ + id, + title, + image: coverUrl, + rating: null, + type: "book" + }); + }); + + const hasNextPage = !!document.querySelector('section.pagination a.next'); + return {results, hasNextPage}; + }); + }, + { + waitSelector: '.container.index-container', + timeout: 55000 + } + ); + + return data?.results || []; + } + + 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() || ""; + + 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()) + : []; + + const timeEl = document.querySelector('.tag-container.field-name time'); + const published = + timeEl?.getAttribute("datetime") || + timeEl?.textContent?.trim() || + "???"; + + return {title, image, genres, published}; + }); + }, + { + waitSelector: "#bigcontainer", + timeout: 55000 + } + ); + + if (!data) throw new Error(`Fallo al obtener metadatos para ID ${id}`); + + const formattedDate = data.published + ? new Date(data.published).toLocaleDateString("es-ES") + : "???"; + + 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 || "" + }; + } + + 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() || ""; + + const img = document.querySelector('#cover img'); + const cover = + img?.dataset?.src ? "https:" + img.dataset.src : + img?.src?.startsWith("//") ? "https:" + img.src : + img?.src || ""; + + const hash = cover.match(/galleries\/(\d+)\//)?.[1] || null; + + const thumbs = document.querySelectorAll('.thumbs img'); + const pages = thumbs.length; + + const first = thumbs[0]; + const s = first?.dataset?.src || first?.src || ""; + const ext = s.match(/t\.(\w+)/)?.[1] || "jpg"; + + const langTag = [...document.querySelectorAll('#tags .tag-container')] + .find(x => x.textContent.includes("Languages:")); + + const language = langTag?.querySelector('.tags .name')?.textContent?.trim() || ""; + + return {title, cover, hash, pages, ext, language}; + }); + }, + { + waitSelector: '#bigcontainer', + timeout: 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 diff --git a/novelbin.js b/book/novelbin.js similarity index 97% rename from novelbin.js rename to book/novelbin.js index 1a029e2..4219c6f 100644 --- a/novelbin.js +++ b/book/novelbin.js @@ -1,121 +1,121 @@ -class NovelBin { - constructor() { - this.baseUrl = "https://novelbin.me"; - this.type = "book-board"; - this.mediaType = "ln"; - } - - 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 html = await res.text(); - const $ = this.cheerio.load(html); - - const results = []; - - $('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; - - const img = `${this.baseUrl}/media/novel/${id}.jpg`; - - results.push({ - id, - title, - image: img, - rating: null, - type: "book" - }); - }); - - 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 { - id, - title, - format: "Light Novel", - score: 0, - genres, - status, - published: "???", - summary, - chapters, - image - }; - } - - async findChapters(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 = []; - - $('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: fullUrl, - title, - number: numMatch ? numMatch[1] : "0", - releaseDate: null, - index: i - }); - }); - - return chapters; - } - - 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 || "
Error: chapter text not found
"; - } -} +class NovelBin { + constructor() { + this.baseUrl = "https://novelbin.me"; + this.type = "book-board"; + this.mediaType = "ln"; + } + + 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 html = await res.text(); + const $ = this.cheerio.load(html); + + const results = []; + + $('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; + + const img = `${this.baseUrl}/media/novel/${id}.jpg`; + + results.push({ + id, + title, + image: img, + rating: null, + type: "book" + }); + }); + + 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 { + id, + title, + format: "Light Novel", + score: 0, + genres, + status, + published: "???", + summary, + chapters, + image + }; + } + + async findChapters(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 = []; + + $('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: fullUrl, + title, + number: numMatch ? numMatch[1] : "0", + releaseDate: null, + index: i + }); + }); + + return chapters; + } + + 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 || "Error: chapter text not found
"; + } +} module.exports = NovelBin; \ No newline at end of file diff --git a/novelfire.js b/book/novelfire.js similarity index 96% rename from novelfire.js rename to book/novelfire.js index 5d0ac62..c85bd9a 100644 --- a/novelfire.js +++ b/book/novelfire.js @@ -1,137 +1,137 @@ -class NovelFire { - constructor() { - this.baseUrl = "https://novelfire.net"; - this.type = "book-board"; - this.mediaType = "ln"; - } - - async search(queryObj) { - const query = queryObj.query; - - 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]); - } - - let status = 'unknown'; - const statusClass = $('strong.ongoing, strong.completed').attr('class'); - if (statusClass) { - status = statusClass.toLowerCase(); - } - - return { - id, - title, - format: 'Light Novel', - score: 0, - genres, - status, - published: '???', - summary, - chapters, - image - }; - } - - async findChapters(bookId) { - const url = `https://novelfire.net/book/${bookId}/chapters`; - const html = await (await fetch(url)).text(); - - const $ = this.cheerio.load(html); - let postId; - - $("script").each((_, el) => { - const txt = $(el).html() || ""; - const m = txt.match(/listChapterDataAjax\?post_id=(\d+)/); - if (m) postId = m[1]; - }); - - if (!postId) throw new Error("post_id not found"); - - 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(); - } -} - +class NovelFire { + constructor() { + this.baseUrl = "https://novelfire.net"; + this.type = "book-board"; + this.mediaType = "ln"; + } + + async search(queryObj) { + const query = queryObj.query; + + 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]); + } + + let status = 'unknown'; + const statusClass = $('strong.ongoing, strong.completed').attr('class'); + if (statusClass) { + status = statusClass.toLowerCase(); + } + + return { + id, + title, + format: 'Light Novel', + score: 0, + genres, + status, + published: '???', + summary, + chapters, + image + }; + } + + async findChapters(bookId) { + const url = `https://novelfire.net/book/${bookId}/chapters`; + const html = await (await fetch(url)).text(); + + const $ = this.cheerio.load(html); + let postId; + + $("script").each((_, el) => { + const txt = $(el).html() || ""; + const m = txt.match(/listChapterDataAjax\?post_id=(\d+)/); + if (m) postId = m[1]; + }); + + if (!postId) throw new Error("post_id not found"); + + 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 = NovelFire; \ No newline at end of file diff --git a/wattpad.js b/book/wattpad.js similarity index 100% rename from wattpad.js rename to book/wattpad.js diff --git a/ZeroChan.js b/image/ZeroChan.js similarity index 96% rename from ZeroChan.js rename to image/ZeroChan.js index aa56c49..00d61b0 100644 --- a/ZeroChan.js +++ b/image/ZeroChan.js @@ -1,119 +1,119 @@ -class ZeroChan { - baseUrl = "https://zerochan.net"; - - constructor() { - this.type = "image-board"; - } - - async search(query = "thighs", page = 1, perPage = 48) { - const url = `${this.baseUrl}/${query.trim().replace(/\s+/g, "+")}?p=${page}`; - - const { result } = await this.scrape( - url, - async (page) => { - return page.evaluate(() => { - const list = document.querySelectorAll("#thumbs2 li"); - if (list.length === 0) { - return {results: [], hasNextPage: false}; - } - - const results = []; - - list.forEach(li => { - const id = li.getAttribute("data-id"); - if (!id) return; - - const img = li.querySelector("img"); - const imgUrl = - img?.getAttribute("data-src") || - img?.getAttribute("src") || - null; - - if (!imgUrl) return; - - 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}; - }); - }, - { - waitSelector: "#thumbs2 li", - timeout: 15000, - renderWaitTime: 3000, - loadImages: true - } - ); - - return { - results: result.results.map(r => ({ - id: r.id, - image: r.image, - tags: r.tags - })), - hasNextPage: result.hasNextPage, - page - }; - - } - - async getInfo(id) { - const url = `${this.baseUrl}/${id}`; - - const { result } = await this.scrape( - url, - 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, - tags, - createdAt: Date.now() - }; - }); - }, - { waitSelector: "a.preview img", timeout: 15000 } - ); - - return { - id, - image: result.fullImage, - tags: result.tags - }; - } -} - +class ZeroChan { + baseUrl = "https://zerochan.net"; + + constructor() { + this.type = "image-board"; + } + + async search(query = "thighs", page = 1, perPage = 48) { + const url = `${this.baseUrl}/${query.trim().replace(/\s+/g, "+")}?p=${page}`; + + const { result } = await this.scrape( + url, + async (page) => { + return page.evaluate(() => { + const list = document.querySelectorAll("#thumbs2 li"); + if (list.length === 0) { + return {results: [], hasNextPage: false}; + } + + const results = []; + + list.forEach(li => { + const id = li.getAttribute("data-id"); + if (!id) return; + + const img = li.querySelector("img"); + const imgUrl = + img?.getAttribute("data-src") || + img?.getAttribute("src") || + null; + + if (!imgUrl) return; + + 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}; + }); + }, + { + waitSelector: "#thumbs2 li", + timeout: 15000, + renderWaitTime: 3000, + loadImages: true + } + ); + + return { + results: result.results.map(r => ({ + id: r.id, + image: r.image, + tags: r.tags + })), + hasNextPage: result.hasNextPage, + page + }; + + } + + async getInfo(id) { + const url = `${this.baseUrl}/${id}`; + + const { result } = await this.scrape( + url, + 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, + tags, + createdAt: Date.now() + }; + }); + }, + { waitSelector: "a.preview img", timeout: 15000 } + ); + + return { + id, + image: result.fullImage, + tags: result.tags + }; + } +} + module.exports = ZeroChan; \ No newline at end of file diff --git a/animepictures.js b/image/animepictures.js similarity index 100% rename from animepictures.js rename to image/animepictures.js diff --git a/gelbooru.js b/image/gelbooru.js similarity index 96% rename from gelbooru.js rename to image/gelbooru.js index c0f5c9e..70ae057 100644 --- a/gelbooru.js +++ b/image/gelbooru.js @@ -1,83 +1,83 @@ -class Gelbooru { - baseUrl = "https://gelbooru.com"; - - constructor() { - this.type = "image-board"; - } - - 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 html = await fetch(url, { - headers: { "User-Agent": "Mozilla/5.0" } - }).then(r => r.text()); - - const $ = this.cheerio.load(html); - const results = []; - - $("article.thumbnail-preview > a[id^='p']").each((_, el) => { - const id = $(el).attr("id")?.slice(1); // p13123834 → 13123834 - if (!id) return; - - const img = $(el).find("img"); - const image = img.attr("src"); - - const tags = img.attr("alt") - ?.replace(/^Rule 34 \|\s*/, "") - ?.split(",") - ?.map(t => t.trim()) - ?.filter(Boolean) || []; - - results.push({ id, image, tags }); - }); - - // 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() - ); - - return { - results, - page, - hasNextPage: page < totalPages - }; - } - - 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 $ = this.cheerio.load(html); - const container = $("section.image-container"); - - let image = - container.find("#image").attr("src") || - container.attr("data-file-url") || - container.attr("data-large-file-url") || - null; - - // tags - const tags = container - .attr("data-tags") - ?.trim() - ?.split(/\s+/) - ?.filter(Boolean) || []; - - return { - id, - image, - tags - }; - } -} - +class Gelbooru { + baseUrl = "https://gelbooru.com"; + + constructor() { + this.type = "image-board"; + } + + 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 html = await fetch(url, { + headers: { "User-Agent": "Mozilla/5.0" } + }).then(r => r.text()); + + const $ = this.cheerio.load(html); + const results = []; + + $("article.thumbnail-preview > a[id^='p']").each((_, el) => { + const id = $(el).attr("id")?.slice(1); // p13123834 → 13123834 + if (!id) return; + + const img = $(el).find("img"); + const image = img.attr("src"); + + const tags = img.attr("alt") + ?.replace(/^Rule 34 \|\s*/, "") + ?.split(",") + ?.map(t => t.trim()) + ?.filter(Boolean) || []; + + results.push({ id, image, tags }); + }); + + // 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() + ); + + return { + results, + page, + hasNextPage: page < totalPages + }; + } + + 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 $ = this.cheerio.load(html); + const container = $("section.image-container"); + + let image = + container.find("#image").attr("src") || + container.attr("data-file-url") || + container.attr("data-large-file-url") || + null; + + // tags + const tags = container + .attr("data-tags") + ?.trim() + ?.split(/\s+/) + ?.filter(Boolean) || []; + + return { + id, + image, + tags + }; + } +} + module.exports = Gelbooru; \ No newline at end of file diff --git a/giphy.js b/image/giphy.js similarity index 96% rename from giphy.js rename to image/giphy.js index e51b9af..08fda51 100644 --- a/giphy.js +++ b/image/giphy.js @@ -1,100 +1,100 @@ -class Giphy { - baseUrl = "https://giphy.com"; - - constructor() { - this.type = "image-board"; - } - - async search(query = "hello", page = 1, perPage = 48) { - const url = `${this.baseUrl}/search/${query.trim().replace(/\s+/g, "-")}`; - - 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'); - - const srcWebp = el.querySelector('source[type="image/webp"][srcset^="http"]'); - const srcImg = el.querySelector('img'); - - let rawSrc = - srcWebp?.getAttribute("srcset")?.split(" ")[0] || - srcImg?.src || - null; - - if (!rawSrc || rawSrc.startsWith("data:")) return; - - const alt = srcImg?.getAttribute("alt") || ""; - const tags = alt.trim().split(/\s+/).filter(Boolean); - - results.push({ - id, - image: rawSrc, - }); - }); - - return { - results, - hasNextPage: false - }; - }), - { - waitSelector: 'picture img, a[data-giphy-id] img', - scrollToBottom: true, - timeout: 15000 - } - ); - - return { - results: data.result.results.map(r => ({ - id: r.id, - image: r.image - })), - hasNextPage: data.result.hasNextPage, - page - }; - } - - 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, - image: data.result.image - }; - } -} - +class Giphy { + baseUrl = "https://giphy.com"; + + constructor() { + this.type = "image-board"; + } + + async search(query = "hello", page = 1, perPage = 48) { + const url = `${this.baseUrl}/search/${query.trim().replace(/\s+/g, "-")}`; + + 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'); + + const srcWebp = el.querySelector('source[type="image/webp"][srcset^="http"]'); + const srcImg = el.querySelector('img'); + + let rawSrc = + srcWebp?.getAttribute("srcset")?.split(" ")[0] || + srcImg?.src || + null; + + if (!rawSrc || rawSrc.startsWith("data:")) return; + + const alt = srcImg?.getAttribute("alt") || ""; + const tags = alt.trim().split(/\s+/).filter(Boolean); + + results.push({ + id, + image: rawSrc, + }); + }); + + return { + results, + hasNextPage: false + }; + }), + { + waitSelector: 'picture img, a[data-giphy-id] img', + scrollToBottom: true, + timeout: 15000 + } + ); + + return { + results: data.result.results.map(r => ({ + id: r.id, + image: r.image + })), + hasNextPage: data.result.hasNextPage, + page + }; + } + + 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, + image: data.result.image + }; + } +} + module.exports = Giphy; \ No newline at end of file diff --git a/realbooru.js b/image/realbooru.js similarity index 96% rename from realbooru.js rename to image/realbooru.js index 2aa647d..584c83a 100644 --- a/realbooru.js +++ b/image/realbooru.js @@ -1,84 +1,84 @@ -class Realbooru { - baseUrl = "https://realbooru.com"; - - headers = { - "User-Agent": "Mozilla/5.0" - }; - - constructor() { - this.type = "image-board"; - } - - async search(query = "original", page = 1, perPage = 42) { - const offset = (page - 1) * perPage; - - const tags = query - .trim() - .split(/\s+/) - .join("+") + "+"; - - 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 = []; - - $('div.col.thumb').each((_, el) => { - const id = ($(el).attr('id') || "").replace('s', ''); - const img = $(el).find('img'); - - let image = img.attr('src'); - if (image && !image.startsWith('http')) image = 'https:' + image; - - const title = img.attr('title') || ''; - const tags = title - .split(',') - .map(t => t.trim()) - .filter(Boolean); - - if (id && image) { - results.push({ id, image, tags }); - } - }); - - 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 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); - - let image = - $('video source').attr('src') || - $('#image').attr('src') || - null; - - if (image && !image.startsWith('http')) { - image = this.baseUrl + image; - } - - const tags = []; - $('#tagLink a').each((_, el) => { - tags.push($(el).text().trim()); - }); - - return { - id, - image, - tags - }; - } -} - +class Realbooru { + baseUrl = "https://realbooru.com"; + + headers = { + "User-Agent": "Mozilla/5.0" + }; + + constructor() { + this.type = "image-board"; + } + + async search(query = "original", page = 1, perPage = 42) { + const offset = (page - 1) * perPage; + + const tags = query + .trim() + .split(/\s+/) + .join("+") + "+"; + + 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 = []; + + $('div.col.thumb').each((_, el) => { + const id = ($(el).attr('id') || "").replace('s', ''); + const img = $(el).find('img'); + + let image = img.attr('src'); + if (image && !image.startsWith('http')) image = 'https:' + image; + + const title = img.attr('title') || ''; + const tags = title + .split(',') + .map(t => t.trim()) + .filter(Boolean); + + if (id && image) { + results.push({ id, image, tags }); + } + }); + + 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 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); + + let image = + $('video source').attr('src') || + $('#image').attr('src') || + null; + + if (image && !image.startsWith('http')) { + image = this.baseUrl + image; + } + + const tags = []; + $('#tagLink a').each((_, el) => { + tags.push($(el).text().trim()); + }); + + return { + id, + image, + tags + }; + } +} + module.exports = Realbooru; \ No newline at end of file diff --git a/rule34.js b/image/rule34.js similarity index 96% rename from rule34.js rename to image/rule34.js index ded2fdd..e759d6c 100644 --- a/rule34.js +++ b/image/rule34.js @@ -1,99 +1,99 @@ -class Rule34 { - baseUrl = "https://rule34.xxx"; - - 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' - }; - - constructor() { - this.type = "image-board"; - } - - 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 fetch(url, { headers: this.headers }); - const data = await response.text(); - const $ = this.cheerio.load(data); - - const results = []; - - $('.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(Boolean); - - if (id && image) { - results.push({ - id, - image, - tags - }); - } - }); - - const pagination = $('#paginator .pagination'); - const lastPageLink = pagination.find('a[alt="last page"]'); - - let totalPages = 1; - if (lastPageLink.length) { - const pid = Number(lastPageLink.attr('href')?.split('pid=')[1] ?? 0); - totalPages = Math.ceil(pid / perPage) + 1; - } - - return { - page, - hasNextPage: page < totalPages, - results - }; - } - - async getInfo(id) { - const url = `${this.baseUrl}/index.php?page=post&s=view&id=${id}`; - - const resizeCookies = { - 'resize-notification': 1, - 'resize-original': 1 - }; - - const cookieString = Object.entries(resizeCookies).map(([key, value]) => `${key}=${value}`).join('; '); - - const fetchHeaders = { ...this.headers }; - const resizeHeaders = { ...this.headers, 'cookie': cookieString }; - - const [resizedResponse, nonResizedResponse] = await Promise.all([ - fetch(url, { headers: resizeHeaders }), - fetch(url, { headers: fetchHeaders }) - ]); - - const [resized, original] = await Promise.all([resizedResponse.text(), nonResizedResponse.text()]); - - const $ = this.cheerio.load(original); - - let fullImage = $('#image').attr('src'); - if (fullImage && !fullImage.startsWith('http')) { - fullImage = `https:${fullImage}`; - } - - const tags = $('#image').attr('alt')?.trim()?.split(' ').filter(tag => tag !== ""); - - return { - id, - image: fullImage, - tags - }; - } -} - +class Rule34 { + baseUrl = "https://rule34.xxx"; + + 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' + }; + + constructor() { + this.type = "image-board"; + } + + 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 fetch(url, { headers: this.headers }); + const data = await response.text(); + const $ = this.cheerio.load(data); + + const results = []; + + $('.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(Boolean); + + if (id && image) { + results.push({ + id, + image, + tags + }); + } + }); + + const pagination = $('#paginator .pagination'); + const lastPageLink = pagination.find('a[alt="last page"]'); + + let totalPages = 1; + if (lastPageLink.length) { + const pid = Number(lastPageLink.attr('href')?.split('pid=')[1] ?? 0); + totalPages = Math.ceil(pid / perPage) + 1; + } + + return { + page, + hasNextPage: page < totalPages, + results + }; + } + + async getInfo(id) { + const url = `${this.baseUrl}/index.php?page=post&s=view&id=${id}`; + + const resizeCookies = { + 'resize-notification': 1, + 'resize-original': 1 + }; + + const cookieString = Object.entries(resizeCookies).map(([key, value]) => `${key}=${value}`).join('; '); + + const fetchHeaders = { ...this.headers }; + const resizeHeaders = { ...this.headers, 'cookie': cookieString }; + + const [resizedResponse, nonResizedResponse] = await Promise.all([ + fetch(url, { headers: resizeHeaders }), + fetch(url, { headers: fetchHeaders }) + ]); + + const [resized, original] = await Promise.all([resizedResponse.text(), nonResizedResponse.text()]); + + const $ = this.cheerio.load(original); + + let fullImage = $('#image').attr('src'); + if (fullImage && !fullImage.startsWith('http')) { + fullImage = `https:${fullImage}`; + } + + const tags = $('#image').attr('alt')?.trim()?.split(' ').filter(tag => tag !== ""); + + return { + id, + image: fullImage, + tags + }; + } +} + module.exports = Rule34; \ No newline at end of file diff --git a/tenor.js b/image/tenor.js similarity index 97% rename from tenor.js rename to image/tenor.js index 1571452..36b0325 100644 --- a/tenor.js +++ b/image/tenor.js @@ -1,115 +1,115 @@ -class Tenor { - baseUrl = "https://tenor.com"; - - constructor() { - this.type = "image-board"; - this.lastQuery = null; - this.seenIds = new Set(); - } - - 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.replaceAll(" ", "-")}-gifs`; - - const { result } = await this.scrape( - url, - 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; - - const href = link.getAttribute('href') || ""; - const idMatch = href.match(/-(\d+)(?:$|\/?$)/); - const id = idMatch ? idMatch[1] : null; - - const imgUrl = - img.getAttribute('src') || - img.getAttribute('data-src'); - - const tagsRaw = img.getAttribute('alt') || ""; - const tags = tagsRaw.trim().split(/\s+/).filter(Boolean); - - if (id && imgUrl && !imgUrl.includes("placeholder")) { - results.push({ - id, - image: imgUrl, - sampleImageUrl: imgUrl, - tags, - type: "preview" - }); - } - }); - - const uniqueResults = Array.from( - new Map(results.map(r => [r.id, r])).values() - ); - - return {results: uniqueResults, hasNextPage: true}; - }); - }, - { - waitSelector: "figure", - timeout: 30000, - scrollToBottom: true, - renderWaitTime: 3000, - loadImages: true - } - ); - - const newResults = result.results.filter(r => !this.seenIds.has(r.id)); - newResults.forEach(r => this.seenIds.add(r.id)); - - return { - results: newResults.map(r => ({ - id: r.id, - image: r.image, - tags: r.tags, - })), - hasNextPage: result.hasNextPage, - page - }; - - } - - async getInfo(id) { - const url = `${this.baseUrl}/view/gif-${id}`; - - const { result } = await this.scrape( - url, - 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(t => t.textContent.trim()) - .filter(Boolean); - - return { fullImage, tags }; - }); - }, - { waitSelector: ".Gif img", timeout: 15000 } - ); - - return { - id, - image: result.fullImage, - tags: result.tags, - title: result.tags?.join(" ") || `Tenor GIF ${id}`, - headers: "" - }; - } -} - +class Tenor { + baseUrl = "https://tenor.com"; + + constructor() { + this.type = "image-board"; + this.lastQuery = null; + this.seenIds = new Set(); + } + + 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.replaceAll(" ", "-")}-gifs`; + + const { result } = await this.scrape( + url, + 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; + + const href = link.getAttribute('href') || ""; + const idMatch = href.match(/-(\d+)(?:$|\/?$)/); + const id = idMatch ? idMatch[1] : null; + + const imgUrl = + img.getAttribute('src') || + img.getAttribute('data-src'); + + const tagsRaw = img.getAttribute('alt') || ""; + const tags = tagsRaw.trim().split(/\s+/).filter(Boolean); + + if (id && imgUrl && !imgUrl.includes("placeholder")) { + results.push({ + id, + image: imgUrl, + sampleImageUrl: imgUrl, + tags, + type: "preview" + }); + } + }); + + const uniqueResults = Array.from( + new Map(results.map(r => [r.id, r])).values() + ); + + return {results: uniqueResults, hasNextPage: true}; + }); + }, + { + waitSelector: "figure", + timeout: 30000, + scrollToBottom: true, + renderWaitTime: 3000, + loadImages: true + } + ); + + const newResults = result.results.filter(r => !this.seenIds.has(r.id)); + newResults.forEach(r => this.seenIds.add(r.id)); + + return { + results: newResults.map(r => ({ + id: r.id, + image: r.image, + tags: r.tags, + })), + hasNextPage: result.hasNextPage, + page + }; + + } + + async getInfo(id) { + const url = `${this.baseUrl}/view/gif-${id}`; + + const { result } = await this.scrape( + url, + 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(t => t.textContent.trim()) + .filter(Boolean); + + return { fullImage, tags }; + }); + }, + { waitSelector: ".Gif img", timeout: 15000 } + ); + + return { + id, + image: result.fullImage, + tags: result.tags, + title: result.tags?.join(" ") || `Tenor GIF ${id}`, + headers: "" + }; + } +} + module.exports = Tenor; \ No newline at end of file diff --git a/waifupics.js b/image/waifupics.js similarity index 96% rename from waifupics.js rename to image/waifupics.js index 1747326..ab7b94d 100644 --- a/waifupics.js +++ b/image/waifupics.js @@ -1,93 +1,93 @@ -class WaifuPics { - baseUrl = "https://api.waifu.pics"; - - SFW_CATEGORIES = [ - 'waifu', 'neko', 'shinobu', 'megumin', 'bully', 'cuddle', 'cry', 'hug', 'awoo', - 'kiss', 'lick', 'pat', 'smug', 'bonk', 'yeet', 'blush', 'smile', 'wave', - 'highfive', 'handhold', 'nom', 'bite', 'glomp', 'slap', 'kill', 'kick', - 'happy', 'wink', 'poke', 'dance', 'cringe' - ]; - - constructor() { - this.type = "image-board"; - } - - async search(query, page = 1, perPage = 42) { - if (!query) query = "waifu"; - - const category = query.trim().split(' ')[0]; - - if (!this.SFW_CATEGORIES.includes(category)) { - console.warn(`[WaifuPics] Category '${category}' not supported.`); - - return { - total: 0, - next: 0, - previous: 0, - pages: 1, - page: 1, - hasNextPage: false, - results: [] - }; - } - - try { - - const response = await fetch(`${this.baseUrl}/many/sfw/${category}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ exclude: [] }), - }); - - if (!response.ok) { - throw new Error(`API returned ${response.status}: ${await response.text()}`); - } - - const data = await response.json(); - - const results = data.files.map((url, index) => { - - const id = url.substring(url.lastIndexOf('/') + 1) || `${category}-${index}`; - const uniqueId = `${id}`; - - return { - id: uniqueId, - image: url, - tags: [category], - }; - }); - - return { - total: 30, - next: page + 1, - previous: page > 1 ? page - 1 : 0, - pages: page + 1, - page: page, - hasNextPage: true, - results: results - }; - - } catch (error) { - console.error(`[WaifuPics] Error fetching images:`, error); - return { - total: 0, - next: 0, - previous: 0, - pages: 1, - page: 1, - hasNextPage: false, - results: [] - }; - } - } - - async getInfo(id) { - return { - id, - image: `https://i.waifu.pics/${id}`, - tags: [] - }; - } -} - +class WaifuPics { + baseUrl = "https://api.waifu.pics"; + + SFW_CATEGORIES = [ + 'waifu', 'neko', 'shinobu', 'megumin', 'bully', 'cuddle', 'cry', 'hug', 'awoo', + 'kiss', 'lick', 'pat', 'smug', 'bonk', 'yeet', 'blush', 'smile', 'wave', + 'highfive', 'handhold', 'nom', 'bite', 'glomp', 'slap', 'kill', 'kick', + 'happy', 'wink', 'poke', 'dance', 'cringe' + ]; + + constructor() { + this.type = "image-board"; + } + + async search(query, page = 1, perPage = 42) { + if (!query) query = "waifu"; + + const category = query.trim().split(' ')[0]; + + if (!this.SFW_CATEGORIES.includes(category)) { + console.warn(`[WaifuPics] Category '${category}' not supported.`); + + return { + total: 0, + next: 0, + previous: 0, + pages: 1, + page: 1, + hasNextPage: false, + results: [] + }; + } + + try { + + const response = await fetch(`${this.baseUrl}/many/sfw/${category}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ exclude: [] }), + }); + + if (!response.ok) { + throw new Error(`API returned ${response.status}: ${await response.text()}`); + } + + const data = await response.json(); + + const results = data.files.map((url, index) => { + + const id = url.substring(url.lastIndexOf('/') + 1) || `${category}-${index}`; + const uniqueId = `${id}`; + + return { + id: uniqueId, + image: url, + tags: [category], + }; + }); + + return { + total: 30, + next: page + 1, + previous: page > 1 ? page - 1 : 0, + pages: page + 1, + page: page, + hasNextPage: true, + results: results + }; + + } catch (error) { + console.error(`[WaifuPics] Error fetching images:`, error); + return { + total: 0, + next: 0, + previous: 0, + pages: 1, + page: 1, + hasNextPage: false, + results: [] + }; + } + } + + async getInfo(id) { + return { + id, + image: `https://i.waifu.pics/${id}`, + tags: [] + }; + } +} + module.exports = WaifuPics; \ No newline at end of file diff --git a/marketplace.json b/marketplace.json new file mode 100644 index 0000000..aa273eb --- /dev/null +++ b/marketplace.json @@ -0,0 +1,155 @@ +{ + "extensions": { + "AnimeAV1": { + "name": "AnimeAV1", + "type": "anime-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/AnimeAV1.js", + "domain": "" + }, + "animepictures": { + "name": "Anime Pictures", + "type": "book-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/animepictures.js", + "domain": "", + "nsfw": true + }, + "asmhentai": { + "name": "ASM Hentai", + "type": "book-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/asmhentai.js", + "domain": "", + "nsfw": true + }, + "gelbooru": { + "name": "Gelbooru", + "type": "image-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/gelbooru.js", + "domain": "", + "nsfw": true + }, + "giphy": { + "name": "Giphy", + "type": "image-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/giphy.js", + "domain": "" + }, + "HiAnime": { + "name": "HiAnime", + "type": "anime-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/anime/HiAnime.js", + "domain": "" + }, + "lightnovelworld": { + "name": "Lightnovelworld", + "type": "book-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/lightnovelworld.js", + "domain": "", + "broken": true + }, + "mangadex": { + "name": "Mangadex", + "type": "book-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/mangadex.js", + "domain": "" + }, + "mangapark": { + "name": "Mangapark", + "type": "book-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/mangapark.js", + "domain": "" + }, + "nhentai": { + "name": "nhentai", + "type": "book-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/nhentai.js", + "domain": "", + "nsfw": true + }, + "novelbin": { + "name": "Novelbin", + "type": "book-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/novelbin.js", + "domain": "" + }, + "novelfire": { + "name": "NovelFire", + "type": "book-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/novelfire.js", + "domain": "" + }, + "realbooru": { + "name": "Realbooru", + "type": "image-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/realbooru.js", + "domain": "", + "nsfw": true + }, + "rule34": { + "name": "Rule34", + "type": "image-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/rule34.js", + "domain": "", + "nsfw": true + }, + "tenor": { + "name": "Tenor", + "type": "image-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/tenor.js", + "domain": "" + }, + "waifupics": { + "name": "Waifupics", + "type": "image-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/waifupics.js", + "domain": "" + }, + "wattpad": { + "name": "Wattpad", + "type": "book-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/book/wattpad.js", + "domain": "" + }, + "ZeroChan": { + "name": "ZeroChan", + "type": "image-board", + "description": ".", + "author": "lenafx", + "entry": "https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/image/ZeroChan.js", + "domain": "" + } + } +} \ No newline at end of file