Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 47 additions & 41 deletions plugins/chinese/Quanben.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const getStandardNovelPath = (url?: string): string | undefined => {
const parsedUrl = parseUrl(url);
if (!parsedUrl) return undefined;
const match = parsedUrl.pathname.match(/^(\/amp)?(\/n\/[^\/]+\/)/);
return match?.[2];
return match?.[2]?.replace(/^\//, '');
};

const getChapterFileName = (url?: string): string | undefined => {
Expand Down Expand Up @@ -47,7 +47,7 @@ class QuanbenPlugin implements Plugin.PluginBase {
id = 'quanben';
name = 'Quanben';
site = 'https://www.quanben.io/';
version = '1.0.1';
version = '2.1.3';
icon = 'src/cn/quanben/icon.png';
defaultCover = defaultCover;

Expand Down Expand Up @@ -134,80 +134,86 @@ class QuanbenPlugin implements Plugin.PluginBase {

// novel details and metadata
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const standardPath = novelPath.replace(/^\/amp/, '');
if (!standardPath.startsWith('/n/') || !standardPath.endsWith('/'))
const standardPath = novelPath.replace(/^\/amp/, '').replace(/^\//, '');
if (!standardPath.startsWith('n/') || !standardPath.endsWith('/'))
throw new Error(`[Quanben parseNovel] Invalid path: ${novelPath}`);

const fullUrl = makeAbsolute(standardPath, this.site);
if (!fullUrl)
throw new Error(`[Quanben parseNovel] Could not construct full URL`);
const fullUrl = this.site + standardPath;

const res = await fetchApi(fullUrl);
if (!res.ok)
throw new Error(`[Quanben parseNovel] Failed to fetch: ${fullUrl}`);

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 $desc = $('div.description').first();

const statusText = getMeta('og:novel:status');

const novel: Plugin.SourceNovel = {
path: standardPath,
name: $info.find('h3').text().trim() || 'Unknown Novel',
name:
getMeta('og:novel:book_name') ||
$info.find('h3').text().trim() ||
'Unknown Novel',
cover:
getMeta('og:image') ||
makeAbsolute($info.find('img').attr('src'), this.site) ||
this.defaultCover,
summary:
$desc.find('p').text().trim() || $desc.text().trim() || undefined,
author: $info.find("p:contains('作者:') span").text().trim() || undefined,
status: NovelStatus.Unknown,
genres: $info.find("p:contains('类别:') span").text().trim() || undefined,
getMeta('og:description') ||
$desc.find('p').text().trim() ||
$desc.text().trim() ||
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),
};

novel.status = NovelStatus.Completed;

return novel;
}

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);
if (!url) return [];
const novelSlug = novelPath.match(/^n\/([^\/]+)\//)?.[1];
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 [];

const $ = parseHTML(await res.text());
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({
name,
path: `${novelName}/${fileName}`,
path: `${novelSlug}/${i}.html`,
chapterNumber: i,
});
});

const uniqueChapters = Array.from(
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 }));
return chapters;
}

async parseChapter(chapterPath: string): Promise<string> {
Expand Down Expand Up @@ -263,7 +269,7 @@ class QuanbenPlugin implements Plugin.PluginBase {
if (path)
novels.push({
name,
path: '/amp' + path,
path,
cover: cover || this.defaultCover,
});
}
Expand Down