class nhentai { constructor(fetchPath, cheerioPath, browser) { this.baseUrl = "https://nhentai.net"; this.browser = browser; this.type = "book-board"; } 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 data = await this.browser.scrape( url, () => { 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() || ""; const tagsRaw = el.getAttribute('data-tags') || ""; const tags = tagsRaw.split(" ").filter(Boolean); 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: 5000} ); return { results: data.results, hasNextPage: data.hasNextPage, page }; } async findChapters(mangaId) { const data = await this.browser.scrape( `https://nhentai.net/g/${mangaId}/`, () => { 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: 4000 } ); const encodedChapterId = Buffer.from(JSON.stringify({ hash: data.hash, pages: data.pages, ext: data.ext })).toString("base64"); return { chapters: [ { id: encodedChapterId, title: data.title, chapter: 1, index: 0, language: data.language } ], cover: data.cover }; } 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 };