class NovelFire { constructor() { this.baseUrl = "https://novelfire.net"; this.type = "book-board"; this.mediaType = "ln"; this.version = "1.0" } 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;