Fixed Lib-Read chapter list not appearing (#2478)

This commit is contained in:
Vaibhav
2026-08-29 11:29:40 +05:30
committed by GitHub
parent 4f453819dc
commit 1d53c7c974
2 changed files with 137 additions and 5 deletions
+135 -2
View File
@@ -25,6 +25,7 @@ type ReadNovelFullOptions = {
noPages?: string[];
pageAsPath?: boolean;
customJs?: string;
chapterListPaginated?: boolean;
};
export type ReadNovelFullMetadata = {
@@ -143,6 +144,135 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
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(
pageNo: number,
{
@@ -499,7 +629,9 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
parser.write(body);
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;
} else if (novelId !== null) {
const chapterListing =
@@ -525,6 +657,7 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
} else {
chaptersUrl = `${this.site}${chapterListing}?${params.toString()}`;
}
const ajaxResult = await fetchApi(chaptersUrl, fetchOptions);
if (!ajaxResult.ok) {
console.error(`Failed to fetch chapters: ${ajaxResult.status}`);
@@ -637,7 +770,7 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
' ': '&nbsp;',
' ': '&nbsp;',
'\u200C': '', // this is probably a breaking change, report if paragraphs look weird
};
const escapeHtml = (text: string) =>