fix: Quanben Plugin (#2174)

* Quanben missing chapters fix

* Renaming chapter list with a mirror

* Version number
This commit is contained in:
Straekz
2026-05-18 00:48:48 -04:00
committed by GitHub
parent d7ab8385ea
commit d161170582
+47 -41
View File
@@ -18,7 +18,7 @@ const getStandardNovelPath = (url?: string): string | undefined => {
const parsedUrl = parseUrl(url); const parsedUrl = parseUrl(url);
if (!parsedUrl) return undefined; if (!parsedUrl) return undefined;
const match = parsedUrl.pathname.match(/^(\/amp)?(\/n\/[^\/]+\/)/); const match = parsedUrl.pathname.match(/^(\/amp)?(\/n\/[^\/]+\/)/);
return match?.[2]; return match?.[2]?.replace(/^\//, '');
}; };
const getChapterFileName = (url?: string): string | undefined => { const getChapterFileName = (url?: string): string | undefined => {
@@ -47,7 +47,7 @@ class QuanbenPlugin implements Plugin.PluginBase {
id = 'quanben'; id = 'quanben';
name = 'Quanben'; name = 'Quanben';
site = 'https://www.quanben.io/'; site = 'https://www.quanben.io/';
version = '1.0.1'; version = '1.1.0';
icon = 'src/cn/quanben/icon.png'; icon = 'src/cn/quanben/icon.png';
defaultCover = defaultCover; defaultCover = defaultCover;
@@ -134,80 +134,86 @@ class QuanbenPlugin implements Plugin.PluginBase {
// novel details and metadata // novel details and metadata
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const standardPath = novelPath.replace(/^\/amp/, ''); const standardPath = novelPath.replace(/^\/amp/, '').replace(/^\//, '');
if (!standardPath.startsWith('/n/') || !standardPath.endsWith('/')) if (!standardPath.startsWith('n/') || !standardPath.endsWith('/'))
throw new Error(`[Quanben parseNovel] Invalid path: ${novelPath}`); throw new Error(`[Quanben parseNovel] Invalid path: ${novelPath}`);
const fullUrl = makeAbsolute(standardPath, this.site); const fullUrl = this.site + standardPath;
if (!fullUrl)
throw new Error(`[Quanben parseNovel] Could not construct full URL`);
const res = await fetchApi(fullUrl); const res = await fetchApi(fullUrl);
if (!res.ok) if (!res.ok)
throw new Error(`[Quanben parseNovel] Failed to fetch: ${fullUrl}`); throw new Error(`[Quanben parseNovel] Failed to fetch: ${fullUrl}`);
const $ = parseHTML(await res.text()); const $ = parseHTML(await res.text());
// Helper to read Open Graph / novel meta tags, falling back to empty string
const getMeta = (prop: string) =>
$(`meta[property="${prop}"]`).attr('content')?.trim() || '';
const $info = $('div.list2').first(); const $info = $('div.list2').first();
const $desc = $('div.description').first(); const $desc = $('div.description').first();
const statusText = getMeta('og:novel:status');
const novel: Plugin.SourceNovel = { const novel: Plugin.SourceNovel = {
path: standardPath, path: standardPath,
name: $info.find('h3').text().trim() || 'Unknown Novel', name:
getMeta('og:novel:book_name') ||
$info.find('h3').text().trim() ||
'Unknown Novel',
cover: cover:
getMeta('og:image') ||
makeAbsolute($info.find('img').attr('src'), this.site) || makeAbsolute($info.find('img').attr('src'), this.site) ||
this.defaultCover, this.defaultCover,
summary: summary:
$desc.find('p').text().trim() || $desc.text().trim() || undefined, getMeta('og:description') ||
author: $info.find("p:contains('作者:') span").text().trim() || undefined, $desc.find('p').text().trim() ||
status: NovelStatus.Unknown, $desc.text().trim() ||
genres: $info.find("p:contains('类别:') span").text().trim() || undefined, undefined,
author:
getMeta('og:novel:author') ||
$info.find("p:contains('作者:') span").text().trim() ||
undefined,
status: statusText
? statusText.includes('完结')
? NovelStatus.Completed
: NovelStatus.Ongoing
: NovelStatus.Unknown,
genres:
getMeta('og:novel:category') ||
$info.find("p:contains('类别:') span").text().trim() ||
undefined,
chapters: await this.parseChapterList(standardPath), chapters: await this.parseChapterList(standardPath),
}; };
novel.status = NovelStatus.Completed;
return novel; return novel;
} }
async parseChapterList(novelPath: string): Promise<Plugin.ChapterItem[]> { async parseChapterList(novelPath: string): Promise<Plugin.ChapterItem[]> {
if (!novelPath.startsWith('/n/') || !novelPath.endsWith('/')) return []; if (!novelPath.startsWith('n/') || !novelPath.endsWith('/')) return [];
const url = makeAbsolute(novelPath + 'list.html', this.site); const novelSlug = novelPath.match(/^n\/([^\/]+)\//)?.[1];
if (!url) return []; if (!novelSlug) return [];
const res = await fetchApi(url); const mirrorUrl = `https://quanben5.com/n/${novelSlug}/xiaoshuo.html`;
const res = await fetchApi(mirrorUrl);
if (!res.ok) return []; if (!res.ok) return [];
const $ = parseHTML(await res.text()); const $ = parseHTML(await res.text());
const chapters: Plugin.ChapterItem[] = []; const chapters: Plugin.ChapterItem[] = [];
const novelName = novelPath.match(/\/n\/([^\/]+)\//)?.[1];
if (!novelName) return [];
$('ul.list3 li a').each((_i, el) => {
const $el = $(el);
const name = $el.text().trim();
const href = $el.attr('href');
if (!name || !href) return;
const fileName = getChapterFileName(makeAbsolute(href, this.site));
if (!fileName) return;
$('ul li a').each((_, el) => {
const name = $(el).text().trim();
if (!name) return;
const i = chapters.length + 1;
chapters.push({ chapters.push({
name, name,
path: `${novelName}/${fileName}`, path: `${novelSlug}/${i}.html`,
chapterNumber: i,
}); });
}); });
const uniqueChapters = Array.from( return chapters;
new Map(chapters.map(c => [c.path, c])).values(),
);
uniqueChapters.sort((a, b) => {
const numA = parseInt(a.path.match(/(\d+)\.html$/)?.[1] || '0', 10);
const numB = parseInt(b.path.match(/(\d+)\.html$/)?.[1] || '0', 10);
return numA - numB;
});
return uniqueChapters.map((c, idx) => ({ ...c, chapterNumber: idx + 1 }));
} }
async parseChapter(chapterPath: string): Promise<string> { async parseChapter(chapterPath: string): Promise<string> {
@@ -263,7 +269,7 @@ class QuanbenPlugin implements Plugin.PluginBase {
if (path) if (path)
novels.push({ novels.push({
name, name,
path: '/amp' + path, path,
cover: cover || this.defaultCover, cover: cover || this.defaultCover,
}); });
} }