189 lines
5.7 KiB
JavaScript
189 lines
5.7 KiB
JavaScript
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"',
|
||
'©'
|
||
];
|
||
|
||
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(/ /gi, ' ')
|
||
.replace(/\s{2,}/g, ' ')
|
||
.trim();
|
||
|
||
text = text
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, "'")
|
||
.replace(/'/g, "'")
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/&/g, '&')
|
||
.replace(/“/g, '“')
|
||
.replace(/”/g, '”')
|
||
.replace(/‘/g, '‘')
|
||
.replace(/’/g, '’')
|
||
.replace(/—/g, '—')
|
||
.replace(/–/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;
|