Fixed Lib-Read chapter list not appearing (#2478)
This commit is contained in:
@@ -56,9 +56,8 @@
|
|||||||
"noAjax": true,
|
"noAjax": true,
|
||||||
"noPages": ["sort/most-popular"],
|
"noPages": ["sort/most-popular"],
|
||||||
"pageAsPath": true,
|
"pageAsPath": true,
|
||||||
"chapterListing": "api/chapterlist.php",
|
"chapterListPaginated": true,
|
||||||
"chapterParam": "aid",
|
"versionIncrements": 3
|
||||||
"versionIncrements": 2
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type ReadNovelFullOptions = {
|
|||||||
noPages?: string[];
|
noPages?: string[];
|
||||||
pageAsPath?: boolean;
|
pageAsPath?: boolean;
|
||||||
customJs?: string;
|
customJs?: string;
|
||||||
|
chapterListPaginated?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReadNovelFullMetadata = {
|
export type ReadNovelFullMetadata = {
|
||||||
@@ -143,6 +144,135 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
|
|||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// HELPERS (chapter-list pagination)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
parseChapterListFragment(
|
||||||
|
html: string,
|
||||||
|
startIndex: number,
|
||||||
|
): Plugin.ChapterItem[] {
|
||||||
|
const chapters: Plugin.ChapterItem[] = [];
|
||||||
|
let tempChapter: Partial<Plugin.ChapterItem> = {};
|
||||||
|
let i = startIndex;
|
||||||
|
let inChapter = false;
|
||||||
|
|
||||||
|
const parser = new Parser({
|
||||||
|
onopentag: (name, attribs) => {
|
||||||
|
if (name === 'a' && attribs.href) {
|
||||||
|
i++;
|
||||||
|
inChapter = true;
|
||||||
|
tempChapter.name = attribs.title || `Chapter ${i}`;
|
||||||
|
tempChapter.releaseTime = null;
|
||||||
|
tempChapter.chapterNumber = i;
|
||||||
|
tempChapter.path = attribs.href.startsWith('/')
|
||||||
|
? attribs.href.substring(1)
|
||||||
|
: attribs.href;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclosetag: name => {
|
||||||
|
if (name === 'a' && inChapter) {
|
||||||
|
if (tempChapter.name && tempChapter.path) {
|
||||||
|
chapters.push({ ...tempChapter } as Plugin.ChapterItem);
|
||||||
|
}
|
||||||
|
tempChapter = {};
|
||||||
|
inChapter = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.write(html);
|
||||||
|
parser.end();
|
||||||
|
return chapters;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchAllChaptersViaJsonAjax(
|
||||||
|
novelPath: string,
|
||||||
|
): Promise<Plugin.ChapterItem[]> {
|
||||||
|
const pageSize = 40;
|
||||||
|
const rateLimitState = { backoffUntil: 0 };
|
||||||
|
|
||||||
|
const first = await this.fetchChapterPageWithRetry(
|
||||||
|
novelPath,
|
||||||
|
1,
|
||||||
|
pageSize,
|
||||||
|
rateLimitState,
|
||||||
|
);
|
||||||
|
if (!first) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const allChapters: Plugin.ChapterItem[] = [...first.chapters];
|
||||||
|
let fetchedPages = 1;
|
||||||
|
|
||||||
|
if (first.totalPages > 1) {
|
||||||
|
for (let page = 2; page <= first.totalPages; page++) {
|
||||||
|
const result = await this.fetchChapterPageWithRetry(
|
||||||
|
novelPath,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
rateLimitState,
|
||||||
|
);
|
||||||
|
if (result) {
|
||||||
|
allChapters.push(...result.chapters);
|
||||||
|
fetchedPages++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allChapters;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchChapterPageWithRetry(
|
||||||
|
novelPath: string,
|
||||||
|
page: number,
|
||||||
|
pageSize: number,
|
||||||
|
rateLimitState: { backoffUntil: number },
|
||||||
|
): Promise<{ totalPages: number; chapters: Plugin.ChapterItem[] } | null> {
|
||||||
|
const url = `${this.site}${novelPath}?ajax=chapters&page=${page}&pageSize=${pageSize}`;
|
||||||
|
const maxAttempts = 3;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||||
|
// Wait out any rate-limit cooldown before sending.
|
||||||
|
const waitMs = rateLimitState.backoffUntil - Date.now();
|
||||||
|
if (waitMs > 0) {
|
||||||
|
await this.sleep(waitMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await fetchApi(url);
|
||||||
|
if (result.ok) {
|
||||||
|
const json = await result.json();
|
||||||
|
const chapters = this.parseChapterListFragment(
|
||||||
|
json.html || '',
|
||||||
|
(page - 1) * pageSize,
|
||||||
|
);
|
||||||
|
await this.sleep(150); // pacing delay — confirmed safe (zero 429s) across multiple runs/novels
|
||||||
|
return { totalPages: json.totalPage || 1, chapters };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status === 429) {
|
||||||
|
const retryAfterHeader = result.headers?.get?.('Retry-After');
|
||||||
|
const retryAfterMs = retryAfterHeader
|
||||||
|
? Number(retryAfterHeader) * 1000
|
||||||
|
: 3000 * (attempt + 1); // fallback: 3s, 6s, 9s
|
||||||
|
const cooldownUntil = Date.now() + retryAfterMs;
|
||||||
|
// Only extend the cooldown, never shorten it
|
||||||
|
if (cooldownUntil > rateLimitState.backoffUntil) {
|
||||||
|
rateLimitState.backoffUntil = cooldownUntil;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.sleep(800 * (attempt + 1));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
await this.sleep(800 * (attempt + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// ============================================================================================
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
pageNo: number,
|
pageNo: number,
|
||||||
{
|
{
|
||||||
@@ -499,7 +629,9 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
|
|||||||
parser.write(body);
|
parser.write(body);
|
||||||
parser.end();
|
parser.end();
|
||||||
|
|
||||||
if (this.options.noAjax && chapters.length > 0 && !totalChapter) {
|
if (this.options.chapterListPaginated) {
|
||||||
|
novel.chapters = await this.fetchAllChaptersViaJsonAjax(novelPath);
|
||||||
|
} else if (this.options.noAjax && chapters.length > 0 && !totalChapter) {
|
||||||
novel.chapters = chapters;
|
novel.chapters = chapters;
|
||||||
} else if (novelId !== null) {
|
} else if (novelId !== null) {
|
||||||
const chapterListing =
|
const chapterListing =
|
||||||
@@ -525,6 +657,7 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
|
|||||||
} else {
|
} else {
|
||||||
chaptersUrl = `${this.site}${chapterListing}?${params.toString()}`;
|
chaptersUrl = `${this.site}${chapterListing}?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ajaxResult = await fetchApi(chaptersUrl, fetchOptions);
|
const ajaxResult = await fetchApi(chaptersUrl, fetchOptions);
|
||||||
if (!ajaxResult.ok) {
|
if (!ajaxResult.ok) {
|
||||||
console.error(`Failed to fetch chapters: ${ajaxResult.status}`);
|
console.error(`Failed to fetch chapters: ${ajaxResult.status}`);
|
||||||
@@ -637,7 +770,7 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
|
|||||||
'>': '>',
|
'>': '>',
|
||||||
'"': '"',
|
'"': '"',
|
||||||
"'": ''',
|
"'": ''',
|
||||||
' ': ' ',
|
' ': ' ',
|
||||||
'\u200C': '', // this is probably a breaking change, report if paragraphs look weird
|
'\u200C': '', // this is probably a breaking change, report if paragraphs look weird
|
||||||
};
|
};
|
||||||
const escapeHtml = (text: string) =>
|
const escapeHtml = (text: string) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user