all extensions to new format :P

This commit is contained in:
2025-12-15 19:40:07 +01:00
parent 9986b64ace
commit 9fe48f93fe
19 changed files with 1725 additions and 1375 deletions

View File

@@ -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 === '&nbsp;' || 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 };
module.exports = NovelBin;