diff --git a/plugins/multisrc/readnovelfull/sources.json b/plugins/multisrc/readnovelfull/sources.json index dbdc72344..28558971c 100644 --- a/plugins/multisrc/readnovelfull/sources.json +++ b/plugins/multisrc/readnovelfull/sources.json @@ -54,7 +54,9 @@ "postSearch": true, "noAjax": true, "noPages": ["sort/most-popular"], - "pageAsPath": true + "pageAsPath": true, + "multiPageChapters": true, + "versionIncrements": 1 } }, { @@ -90,6 +92,8 @@ "noAjax": true, "noPages": ["sort/most-popular"], "pageAsPath": true, + "multiPageChapters": true, + "versionIncrements": 1, "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, ''); });" } } diff --git a/plugins/multisrc/readnovelfull/template.ts b/plugins/multisrc/readnovelfull/template.ts index 3554ac12a..e13a357cb 100644 --- a/plugins/multisrc/readnovelfull/template.ts +++ b/plugins/multisrc/readnovelfull/template.ts @@ -206,7 +206,11 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase { if (pageAsPath) { if (pageNo > 1) { - url = `${this.site}${basePage}/${pageNo.toString()}`; + if (this.options.multiPageChapters) { + url = `${this.site}${basePage}/${pageParam}/${pageNo.toString()}`; + } else { + url = `${this.site}${basePage}/${pageNo.toString()}`; + } } else { url = `${this.site}${basePage}`; } @@ -239,12 +243,15 @@ 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 tempChapter: Partial = {}; let i = 0; let depth: number; + let isMultiPageSelect = false; + let multiPageOptionCount = 0; + let chapters: Plugin.ChapterItem[] = []; + const stateStack: ParsingState[] = [ParsingState.Idle]; const currentState = () => stateStack[stateStack.length - 1]; const pushState = (state: ParsingState) => stateStack.push(state); @@ -335,6 +342,19 @@ 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++; + } + break; } }, @@ -411,6 +431,9 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase { break; } break; + case 'select': + isMultiPageSelect = false; + break; default: return; } @@ -475,7 +498,135 @@ export class ReadNovelFullPlugin implements Plugin.PluginBase { parser.write(body); parser.end(); - if (this.options.noAjax && chapters.length > 0) { + let multiPageMaxPage = 1; + if (multiPageOptionCount > 1) { + multiPageMaxPage = multiPageOptionCount; + } + + if (this.options.multiPageChapters && multiPageMaxPage > 1) { + 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); + if (res.ok) { + const data = await res.json(); + const html = data.html || ''; + const pageChapters: Plugin.ChapterItem[] = []; + let isParsingChapter = false; + let tempAjaxChapter: Partial = {}; + + const pageParser = new Parser({ + onopentag: (name, attribs) => { + if (name === 'a' && attribs.href) { + isParsingChapter = true; + tempAjaxChapter.name = attribs.title || ''; + tempAjaxChapter.path = attribs.href.startsWith('/') + ? attribs.href.substring(1) + : attribs.href; + } + }, + 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`, // Number will be assigned later + 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; // Rethrow to trigger the retry logic + } + }; + + // Fetch page 1 first to determine true max page + const firstPageData = await fetchAndParse(1); + const allChapters = [...firstPageData.pageChapters]; + const newMaxPage = firstPageData.totalPage || 1; + + // Fetch remaining pages concurrently in smaller batches with a delay to prevent Cloudflare bans + if (newMaxPage > 1) { + const batchSize = 3; // Process 3 pages concurrently per chunk + for (let i = 2; i <= newMaxPage; i += batchSize) { + let chunkSuccess = false; + let retries = 0; + + while (!chunkSuccess && retries < 3) { + try { + const promises = []; + for (let p = i; p < i + batchSize && p <= newMaxPage; p++) { + promises.push(fetchAndParse(p)); + } + + const results = await Promise.all(promises); + for (const result of results) { + allChapters.push(...result.pageChapters); + } + chunkSuccess = true; + } catch (err) { + retries++; + console.warn( + `Rate limit triggered on batch ${i}. Retrying ${retries}/3...`, + ); + if (retries >= 3) { + console.error( + `Failed to fetch batch ${i} after 3 retries. Aborting.`, + ); + throw new Error( + 'Cloudflare Rate Limit (HTTP 429/503) triggered. Failed to fetch all chapters.', + ); + } + await new Promise(resolve => setTimeout(resolve, 2000)); + } + } + + // Proactive delay between batches to keep Cloudflare happy + if (i + batchSize <= newMaxPage) { + await new Promise(resolve => setTimeout(resolve, 500)); + } + } + } + + // 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) { novel.chapters = chapters; } else if (novelId !== null) { const chapterListing =