class NovelBin { constructor(fetchPath, cheerioPath, browser) { this.browser = browser; this.fetch = require(fetchPath); this.cheerio = require(cheerioPath); this.baseUrl = "https://novelbin.me"; this.type = "book-board"; } async fetchSearchResult(query = "", page = 1) { const url = !query || query.trim() === "" ? `${this.baseUrl}/sort/novelbin-hot?page=${page}` : `${this.baseUrl}/search?keyword=${encodeURIComponent(query)}`; 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}` } const idMatch = href.match(/novel-book\/([^/?]+)/); const id = idMatch ? idMatch[1] : null; if (!id) return; const coverUrl = `${this.baseUrl}/media/novel/${id}.jpg`; results.push({ id, title, image: coverUrl, sampleImageUrl: coverUrl, tags: [], type: "book" }); }); const hasNextPage = $(".PagedList-skipToNext a").length > 0; return { results, hasNextPage, page }; } async findChapters(bookId) { const res = await this.fetch(`${this.baseUrl}/novel-book/${bookId}`); 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"; chapters.push({ id: href, title: title.trim(), chapter: chapterNumber, language: "en" }); }); return { chapters: 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 "
Error: Could not find content.
"; 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 }]; } } module.exports = { NovelBin };