Files
WaifuBoard-Extensions/book/lightnovelworld.js
2025-12-19 22:35:35 +01:00

189 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;