new format and marketplace

This commit is contained in:
2025-12-19 22:35:35 +01:00
parent 9fe48f93fe
commit 9aea0f1551
19 changed files with 1482 additions and 1327 deletions

188
book/lightnovelworld.js Normal file
View File

@@ -0,0 +1,188 @@
class lightnovelworld {
constructor() {
this.baseUrl = "https://lightnovelworld.org/api";
this.type = "book-board";
this.mediaType = "ln";
}
async search(queryObj) {
const query = queryObj.query?.trim() || "";
if (query !== "") {
const res = await fetch(
`${this.baseUrl}/search/?q=${encodeURIComponent(query)}&search_type=title`
);
const data = await res.json();
if (!data.novels) return [];
return data.novels.map(n => ({
id: n.slug,
title: n.title,
image: `https://lightnovelworld.org/${n.cover_path}`,
rating: `Rank ${n.rank}`,
format: "Light Novel"
}));
}
const res = await fetch("https://lightnovelworld.org/");
const html = await res.text();
const cards = html.split('class="recommendation-card"').slice(1);
const results = [];
for (const block of cards) {
const link = block.match(/href="([^"]+)"/)?.[1] || "";
const id = link.replace(/^\/novel\//, "").replace(/\/$/, "");
const title = block.match(/class="card-title"[^>]*>([^<]+)/)?.[1]?.trim() || null;
let img = block.match(/<img[^>]+src="([^"]+)"/)?.[1] || "";
if (img && !img.startsWith("http"))
img = `https://lightnovelworld.org${img}`;
if (id && title) {
results.push({
id,
title,
image: img,
rating: null,
format: "Light Novel"
});
}
}
return results;
}
async getMetadata(id){
const res = await fetch(`https://lightnovelworld.org/novel/${id}`);
const html = await res.text();
const match = html.match(
/<script type="application\/ld\+json">([\s\S]*?)<\/script>/
);
let data = {};
if(match){
try{
data = JSON.parse(match[1]);
}catch(e){}
}
const rawScore = Number(data.aggregateRating?.ratingValue || 1);
const score100 = Math.round((rawScore / 5) * 100);
return {
id: id,
title: data.name || "",
format: "Light Novel",
score: score100,
genres: Array.isArray(data.genre) ? data.genre : [],
status: data.status || "",
published: "???",
summary: data.description || "",
chapters: data.numberOfPages ? Number(data.numberOfPages) : 1,
image: data.image
? (data.image.startsWith("http")
? data.image
: `https://lightnovelworld.org${data.image}`)
: ""
};
}
async findChapters(bookId) {
const chapters = [];
let offset = 0;
const limit = 500;
while (true) {
const res = await fetch(
`https://lightnovelworld.org/api/novel/${bookId}/chapters/?offset=${offset}&limit=${limit}`
);
const data = await res.json();
if (!data.chapters) break;
chapters.push(
...data.chapters.map((c, i) => ({
id: `https://lightnovelworld.org/novel/${bookId}/chapter/${c.number}/`,
title: c.title,
number: Number(c.number),
releaseDate: null,
index: offset + i
}))
);
if (!data.has_more) break;
offset += limit;
}
return chapters;
}
async findChapterPages(chapterId) {
const data = await this.scrape(
chapterId,
(page) => page.evaluate(() => document.documentElement.outerHTML),
{
waitUntil: "domcontentloaded",
timeout: 15000
}
);
const html = data.result;
if (!html) return '<p>Error loading chapter</p>';
const cutPoints = [
'<div class="bottom-nav"',
'<div class="comments-section"',
'<div class="settings-panel"',
'&copy;'
];
let cutIndex = html.length;
for (const marker of cutPoints) {
const pos = html.indexOf(marker);
if (pos !== -1 && pos < cutIndex) cutIndex = pos;
}
const chapterHtml = html.substring(0, cutIndex);
const pMatches = [...chapterHtml.matchAll(/<p[^>]*>([\s\S]*?)<\/p>/gi)];
let cleanHtml = '';
for (const match of pMatches) {
let text = match[1]
.replace(/△▼△▼△▼△/g, '')
.replace(/[※▲▼■◆]/g, '')
.replace(/&nbsp;/gi, ' ')
.replace(/\s{2,}/g, ' ')
.trim();
text = text
.replace(/&quot;/g, '"')
.replace(/&#x27;/g, "'")
.replace(/&#39;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/&ldquo;/g, '“')
.replace(/&rdquo;/g, '”')
.replace(/&lsquo;/g, '')
.replace(/&rsquo;/g, '')
.replace(/&mdash;/g, '—')
.replace(/&ndash;/g, '');
if (!text || text.length < 3) continue;
if (/svg|button|modal|comment|loading|default|dyslexic|roboto|lora|line spacing/i.test(text)) continue;
cleanHtml += `<p>${text}</p>\n`;
}
return cleanHtml.trim() || '<p>Empty chapter</p>';
}
}
module.exports = lightnovelworld;