fix(readnovelfull): remove multipage and fix pageAsPath (#2299)

* fix(readnovelfull): remove multipage and fix pageAsPath

reverted previous commit, pagination not required for FWN/Libread but
kinda tricky to obtain chapter path (not fully resolved yet) PageAsPath
cannot accept multiple genres TODO, confirm behaviour, reverted for now

Signed-off-by: K1ngfish3r <26593485+K1ngfish3r@users.noreply.github.com>

* chapter response is a json

Signed-off-by: K1ngfish3r <26593485+K1ngfish3r@users.noreply.github.com>

---------

Signed-off-by: K1ngfish3r <26593485+K1ngfish3r@users.noreply.github.com>
This commit is contained in:
K1ngfish3r
2026-07-12 02:15:52 +05:00
committed by GitHub
parent c4545b0780
commit 995825490b
2 changed files with 61 additions and 164 deletions
+6 -4
View File
@@ -55,8 +55,9 @@
"noAjax": true,
"noPages": ["sort/most-popular"],
"pageAsPath": true,
"multiPageChapters": true,
"versionIncrements": 1
"chapterListing": "api/chapterlist.php",
"chapterParam": "aid",
"versionIncrements": 2
}
},
{
@@ -92,8 +93,9 @@
"noAjax": true,
"noPages": ["sort/most-popular"],
"pageAsPath": true,
"multiPageChapters": true,
"versionIncrements": 1,
"chapterListing": "api/chapterlist.php",
"chapterParam": "aid",
"versionIncrements": 2,
"customJs": "$('.txt, #chr-content, #chapter-content').find('*').addBack().contents().filter((_, el) => el.type === 'text').each((_, el) => { el.data = el.data.replace(/(?:𝐟|ᵮ|𝑓|𝒇|𝒻|𝓯|𝔣|𝕗|𝖿|𝗳|𝙛|𝚏|ꬵ|ꞙ|ẝ|𝖋|ⓕ|f|ƒ|ḟ|ʃ|բ|ᶠ|⒡|ſ|ꊰ|ʄ|∱|ᶂ|𝘧|\\bf)(?:𝚛|ꭇ|ᣴ|ℾ|𝚪|𝛤|𝜞|𝝘|𝞒|Ⲅ|Г|Ꮁ|ᒥ|ꭈ|ⲅ|ꮁ|ⓡ|r|ŕ|ṙ|ř|ȑ|ȓ|ṛ|ṝ|ŗ|г|Ր|ɾ|ᥬ|ṟ|ɍ|ʳ|⒭|ɼ|ѓ|ᴦ|ᶉ|𝐫|𝑟|𝒓|𝓇|𝓻|𝔯|𝕣|𝖗|𝗋|𝗿|𝘳|𝙧|ᵲ|ґ|ᵣ|r)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)+(?:𝐰|ꝡ|𝑤|𝒘|𝓌|𝔀|𝔴|𝕨|𝖜|𝗐|𝘄|𝘸|𝙬|𝚠|ա|ẁ|ꮃ|ẃ|ⓦ|⍵|ŵ|ẇ|ẅ|ẘ|ẉ|ⱳ|ὼ|ὠ|ὡ|ὢ|ὣ|ω|ὤ|ὥ|ὦ|ὧ|ῲ|ῳ|ῴ|ῶ|ῷ|Ⱳ|ѡ|ԝ|ᴡ|ώ|ᾠ|ᾡ|ᾢ|ᾣ|ᾤ|ᾥ|ᾦ|ɯ|𝝕|𝟉|𝞏|w)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ꮟ|Ꮟ|𝐛|𝘣|𝒷|𝔟|𝓫|𝖇|𝖻|𝑏|𝙗|𝕓|𝒃|𝗯|𝚋|♭|ᑳ|ᒈ|b|ᖚ|ᕹ|ᕺ|ⓑ|ḃ|ḅ|ҍ|ъ|ḇ|ƃ|ɓ|ƅ|ᖯ|Ƅ|Ь|ᑲ|þ|Ƃ|⒝|Ъ|ᶀ|ᑿ|ᒀ|ᒂ|ᒁ|ᑾ|ь|ƀ|Ҍ|Ѣ|ѣ|ᔎ |b)(?:ո|ռ|ח|𝒏|𝓷|𝙣|𝑛|𝖓|𝔫|𝗇|𝚗|𝗻|ᥒ|ⓝ|ή|n|ǹ|ᴒ|ń|ñ|ᾗ|η|ṅ|ň|ṇ|ɲ|ņ|ṋ|ṉ|ղ|ຖ|Ռ|ƞ|ŋ|⒩|ภ|ก|ɳ|п|ʼn|л|ԉ|Ƞ|ἠ|ἡ|ῃ|դ|ᾐ|ᾑ|ᾒ|ᾓ|ᾔ|ᾕ|ᾖ|ῄ|ῆ|ῇ|ῂ|ἢ|ἣ|ἤ|ἥ|ἦ|ἧ|ὴ|ή|በ|ቡ|ቢ|ባ|ቤ|ብ|ቦ|ȵ|𝛈|𝜂|𝜼|𝝶|𝞰|𝕟|𝘯|𝐧|𝓃|ᶇ|ᵰ|ᥥ|∩|n)(?:ం|ಂ|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝖔|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝛐|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|𝔬|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|𝘰|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|ⓞ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|೦|൦|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:∨|⌄|⋁|ⅴ|𝐯|𝑣|𝒗|𝓋|𝔳|𝕧|𝖛|𝗏|ꮩ|ሀ|ⓥ|v|𝜐|𝝊|ṽ|ṿ|౮|ง|ѵ|ע|ᴠ|ν|ט|ᵥ|ѷ|៴|ᘁ|𝙫|𝚟|𝛎|𝜈|𝝂|𝝼|𝞶|𝘷|𝘃|𝓿|v)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ⓛ|l|ŀ|ĺ|ľ|ḷ|ḹ|ļ|Ӏ|ℓ|ḽ|ḻ|ł|レ|ɭ|ƚ|ɫ|ⱡ|\\||Ɩ|⒧|ʅ|ǀ|ו|ן|Ι|І|||ᶩ|ӏ|𝓘|𝕀|𝖨|𝗜|𝘐|𝐥|𝑙|𝒍|𝓁|𝔩|𝕝|𝖑|𝗅|𝗹|𝘭|𝚕|𝜤|𝝞|ı|𝚤|ɩ|ι|𝛊|𝜄|𝜾|𝞲|I|l)(?:.?(?:🝌|c|ⅽ|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖼|𝗰|𝘤|𝙘|𝚌|ᴄ|ϲ|ⲥ|с|ꮯ|𐐽|ⲥ|𐐽|ꮯ|ĉ|c|ⓒ|ć|č|ċ|ç|ҁ|ƈ|ḉ|ȼ|ↄ|с|ር|ᴄ|ϲ|ҫ|꒝|ς|ɽ|ϛ|𝙲|ᑦ|᧚|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖼|𝗰|𝘤|𝙘|𝚌|₵|🇨|ᥴ|ᒼ|ⅽ|c)(?:ం|ಂ|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝖔|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝛐|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|𝔬|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|𝘰|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|ⓞ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|೦|൦|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:₥|ᵯ|𝖒|𝐦|𝗆|𝔪|𝕞|𝓂|ⓜ|m|ന|ᙢ|൩|ḿ|ṁ|ⅿ|ϻ|ṃ|ጠ|ɱ|៳|ᶆ|𝒎|𝙢|𝓶|𝚖|𝑚|𝗺|᧕|᧗|m))?/g, ''); });"
}
}
+55 -160
View File
@@ -25,7 +25,6 @@ type ReadNovelFullOptions = {
noPages?: string[];
pageAsPath?: boolean;
customJs?: string;
multiPageChapters?: boolean;
};
export type ReadNovelFullMetadata = {
@@ -164,22 +163,13 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
pageAsPath = false,
} = this.options;
const genresValue = (filters?.genres?.value ??
this.filters?.genres?.value) as string[] | string | undefined;
const typeValue = String(
filters?.type?.value ?? this.filters?.type?.value ?? '',
);
const hasGenres = Array.isArray(genresValue)
? genresValue.length > 0
: !!genresValue;
// Skip Pagination for FWN & LR
if (
pageNo !== 1 &&
!showLatestNovels &&
!hasGenres &&
!filters.genres.value.length &&
noPages.length > 0 &&
noPages.includes(typeValue)
noPages.includes(filters.type.value)
) {
return [];
}
@@ -192,14 +182,11 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
if (showLatestNovels) {
params.append(typeParam, latestPage);
} else if (hasGenres) {
} else if (filters.genres.value.length) {
params.append(typeParam, genreParam);
params.append(
genreKey,
Array.isArray(genresValue) ? genresValue.join(',') : genresValue!,
);
params.append(genreKey, filters.genres.value);
} else {
params.append(typeParam, typeValue);
params.append(typeParam, filters.type.value);
}
// Add language parameter if specified
@@ -213,19 +200,13 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
// URL structure with path segments
const basePage = showLatestNovels
? latestPage
: hasGenres
? Array.isArray(genresValue)
? genresValue.join(',')
: genresValue!
: typeValue;
: filters.genres.value.length
? filters.genres.value
: filters.type.value;
if (pageAsPath) {
if (pageNo > 1) {
if (this.options.multiPageChapters) {
url = `${this.site}${basePage}/${pageParam}/${pageNo.toString()}`;
} else {
url = `${this.site}${basePage}/${pageNo.toString()}`;
}
url = `${this.site}${basePage}/${pageNo.toString()}`;
} else {
url = `${this.site}${basePage}`;
}
@@ -258,15 +239,14 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
const authorParts: string[] = [];
const genreArray: string[] = [];
const infoParts: string[] = [];
const chapters: Plugin.ChapterItem[] = [];
let novelId: string | null = null;
let totalChapter: number | null = null;
let novelTitle: string | null = null;
let tempChapter: Partial<Plugin.ChapterItem> = {};
let i = 0;
let depth: number;
let isMultiPageSelect = false;
let multiPageOptionCount = 0;
const chapters: Plugin.ChapterItem[] = [];
const stateStack: ParsingState[] = [ParsingState.Idle];
const currentState = () => stateStack[stateStack.length - 1];
const pushState = (state: ParsingState) => stateStack.push(state);
@@ -329,6 +309,9 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
if (newState) pushState(newState);
}
if (attribs.class?.includes('disqus')) {
novelTitle = attribs['data-disqus-identifier'];
}
break;
case 'br':
if (state === ParsingState.Summary) {
@@ -344,6 +327,9 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
}
break;
case 'a':
if (attribs.class?.includes('set-case')) {
novelId = attribs['data-articleid'];
}
if (state === ParsingState.ChapterList) {
i++;
const href = attribs.href;
@@ -357,18 +343,8 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
novelPath.replace('.html', `/chapter-${i}.html`);
}
break;
case 'select':
if (
this.options.multiPageChapters &&
attribs.id === 'indexselect'
) {
isMultiPageSelect = true;
}
break;
case 'option':
if (isMultiPageSelect && attribs.value) {
multiPageOptionCount++;
}
case 'script':
pushState(ParsingState.Hidden);
break;
}
},
@@ -396,6 +372,12 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
case ParsingState.Status:
statusParts.push(text);
break;
case ParsingState.Hidden:
if (text.includes('window.chapterPagination')) {
totalChapter = Number(text.match(/totalChapters:\s*(\d+)/)![1])!;
} else if (text.includes('sourceid')) {
novelId = text.match(/sourceid=(\d+)/)![1]!;
}
}
},
@@ -446,8 +428,8 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
break;
}
break;
case 'select':
isMultiPageSelect = false;
case 'script':
if (state === ParsingState.Hidden) popState();
break;
default:
return;
@@ -513,131 +495,44 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
parser.write(body);
parser.end();
const multiPageMaxPage = Math.max(1, multiPageOptionCount);
if (this.options.multiPageChapters && multiPageMaxPage > 1) {
// Re-fetch page 1 with max pageSize (200) since default page size is usually 40,
// minimizing total requests required to fetch all chapters.
chapters.length = 0;
const cleanNovelPath = novelPath
.replace(/\.html$/, '')
.replace(/\/$/, '');
const newPageSize = 200;
const fetchAndParse = async (p: number) => {
const ajaxUrl = `${this.site}${cleanNovelPath}?ajax=chapters&page=${p}&pageSize=${newPageSize}`;
try {
const res = await fetchApi(ajaxUrl, {
headers: {
'Referer': this.site + novelPath,
'X-Requested-With': 'XMLHttpRequest',
'Accept': '*/*',
},
});
if (res.ok) {
const data = await res.json();
const html = data.html || '';
const pageChapters: Plugin.ChapterItem[] = [];
let isParsingChapter = false;
let tempAjaxChapter: Partial<Plugin.ChapterItem> = {};
const pageParser = new Parser({
onopentag: (name, attribs) => {
if (name === 'a' && attribs.href) {
isParsingChapter = true;
tempAjaxChapter.name = attribs.title || '';
tempAjaxChapter.path = attribs.href.replace(/^\//, '');
}
},
ontext: data => {
const text = data.trim();
if (isParsingChapter && text) {
tempAjaxChapter.name = tempAjaxChapter.name
? tempAjaxChapter.name + text
: text;
}
},
onclosetag: name => {
if (name === 'a' && isParsingChapter) {
if (tempAjaxChapter.path) {
pageChapters.push({
name: tempAjaxChapter.name?.trim() || `Chapter`,
path: tempAjaxChapter.path,
releaseTime: null,
});
}
tempAjaxChapter = {};
isParsingChapter = false;
}
},
});
pageParser.write(html);
pageParser.end();
return { pageChapters, totalPage: data.totalPage };
} else {
throw new Error(`HTTP Error ${res.status}`);
}
} catch (e) {
console.error(
`Failed to fetch chapters page ${p} for ${novelPath}`,
e,
);
throw e;
}
};
const firstPageData = await fetchAndParse(1);
const allChapters = [...firstPageData.pageChapters];
const newMaxPage = firstPageData.totalPage || 1;
if (newMaxPage > 1) {
for (let i = 2; i <= newMaxPage; i++) {
let chunkSuccess = false;
let retries = 0;
while (!chunkSuccess && retries < 3) {
try {
const result = await fetchAndParse(i);
allChapters.push(...result.pageChapters);
chunkSuccess = true;
} catch (err) {
retries++;
await this.sleep(1000);
}
}
if (i < newMaxPage) {
const randomDelay = Math.floor(Math.random() * 250) + 250; // Random delay between 250ms and 499ms
await this.sleep(randomDelay);
}
}
}
// Assign sequential chapter numbers
for (let i = 0; i < allChapters.length; i++) {
allChapters[i].chapterNumber = i + 1;
if (allChapters[i].name === 'Chapter') {
allChapters[i].name = `Chapter ${i + 1}`;
}
chapters.push(allChapters[i]);
}
novel.chapters = chapters;
} else if (this.options.noAjax && chapters.length > 0) {
if (this.options.noAjax && chapters.length > 0 && !totalChapter) {
novel.chapters = chapters;
} else if (novelId !== null) {
const chapterListing =
this.options.chapterListing || 'ajax/chapter-archive';
const ajaxParam = this.options.chapterParam || 'novelId';
const params = new URLSearchParams({ [ajaxParam]: novelId });
const chaptersUrl = `${this.site}${chapterListing}?${params.toString()}`;
const ajaxResult = await fetchApi(chaptersUrl);
let chaptersUrl: string;
let fetchOptions: FetchInit | undefined;
if (totalChapter) {
chaptersUrl = `${this.site}${chapterListing}`;
params.set('acode', novelTitle || novelPath.split('/').pop()!);
params.set('cid', String(Math.floor(Math.random() * totalChapter)));
fetchOptions = {
method: 'POST',
body: params.toString(),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
};
} else {
chaptersUrl = `${this.site}${chapterListing}?${params.toString()}`;
}
const ajaxResult = await fetchApi(chaptersUrl, fetchOptions);
if (!ajaxResult.ok) {
console.error(`Failed to fetch chapters: ${ajaxResult.status}`);
novel.chapters = [];
} else {
const ajaxBody = await ajaxResult.text();
let ajaxHtml = ajaxBody;
try {
const json = JSON.parse(ajaxBody);
if (typeof json.html === 'string') {
ajaxHtml = json.html;
}
} catch {
// eslint
}
const ajaxChapters: Plugin.ChapterItem[] = [];
let tempAjaxChapter: Partial<Plugin.ChapterItem> = {};
@@ -692,7 +587,7 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase {
},
});
ajaxParser.write(ajaxBody);
ajaxParser.write(ajaxHtml);
ajaxParser.end();
novel.chapters = ajaxChapters;
}