@@ -4,6 +4,7 @@ import { Plugin } from '@/types/plugin';
44import { NovelStatus } from '@libs/novelStatus' ;
55import { Filters , FilterTypes } from '@libs/filterInputs' ;
66import { defaultCover } from '@/types/constants' ;
7+ import { storage } from '@libs/storage' ;
78
89class NovelFire implements Plugin . PluginBase {
910 id = 'novelfire' ;
@@ -12,6 +13,16 @@ class NovelFire implements Plugin.PluginBase {
1213 icon = 'src/en/novelfire/icon.png' ;
1314 site = 'https://novelfire.net/' ;
1415
16+ singlePage = storage . get ( 'singlePage' ) ;
17+ pluginSettings = {
18+ singlePage : {
19+ value : '' ,
20+ label :
21+ 'Force load all chapters on a single page (Slower & use more data)' ,
22+ type : 'Switch' ,
23+ } ,
24+ } ;
25+
1526 async getCheerio ( url : string , search : boolean ) : Promise < CheerioAPI > {
1627 const r = await fetchApi ( url ) ;
1728 if ( ! r . ok && search != true )
@@ -127,6 +138,69 @@ class NovelFire implements Plugin.PluginBase {
127138 return sortedChapters ;
128139 }
129140
141+ async getAllChaptersForce (
142+ novelPath : string ,
143+ pages : number ,
144+ ) : Promise < Plugin . ChapterItem [ ] > {
145+ const pagesArray = Array . from ( { length : pages } , ( _ , i ) => i + 1 ) ;
146+ const allChapters : Plugin . ChapterItem [ ] = [ ] ;
147+
148+ // When pages > ~30, we get rate limited. To mitigate, split into chunks and retry chunk on rate limit with delay.
149+ const chunkSize = 5 ; // 5 pages per chunk was tested to be a good balance between speed and rate limiting.
150+ const retryCount = 10 ;
151+ const sleepTime = 3.5 ; // Rate limit seems to be around ~10s, so usually 3 retries should be enough for another ~30 pages.
152+
153+ const chaptersArray : Plugin . ChapterItem [ ] [ ] = [ ] ;
154+
155+ for ( let i = 0 ; i < pagesArray . length ; i += chunkSize ) {
156+ const pagesArrayChunk = pagesArray . slice ( i , i + chunkSize ) ;
157+
158+ const firstPage = pagesArrayChunk [ 0 ] ;
159+ const lastPage = pagesArrayChunk [ pagesArrayChunk . length - 1 ] ;
160+
161+ let attempt = 0 ;
162+
163+ while ( attempt < retryCount ) {
164+ try {
165+ // Parse all pages in chunk in parallel
166+ const chaptersArrayChunk = await Promise . all (
167+ pagesArrayChunk . map ( page =>
168+ this . parsePage ( novelPath , page . toString ( ) ) ,
169+ ) ,
170+ ) ;
171+
172+ chaptersArray . push ( ...chaptersArrayChunk ) ;
173+ break ;
174+ } catch ( err ) {
175+ if ( err instanceof NovelFireThrottlingError ) {
176+ attempt += 1 ;
177+ console . warn (
178+ `[pages=${ firstPage } -${ lastPage } ] Novel Fire is rate limiting requests. Retry attempt ${ attempt + 1 } in ${ sleepTime } seconds...` ,
179+ ) ;
180+ if ( attempt === retryCount ) {
181+ throw err ;
182+ }
183+
184+ // Sleep for X second before retrying
185+ await new Promise ( resolve => setTimeout ( resolve , sleepTime * 1000 ) ) ;
186+ } else {
187+ throw err ;
188+ }
189+ }
190+ }
191+ }
192+
193+ // Merge all chapters into a single array
194+ for ( let chapters of chaptersArray ) {
195+ // For some reason it's formatted this way, this fixes it.
196+ chapters = chapters . chapters ;
197+ for ( let i = 0 ; i < Object . keys ( chapters ) . length ; i ++ ) {
198+ allChapters . push ( chapters [ i ] ) ;
199+ }
200+ }
201+ return allChapters ;
202+ }
203+
130204 async parseNovel (
131205 novelPathRaw : string ,
132206 ) : Promise < Plugin . SourceNovel & { totalPages : number } > {
@@ -196,6 +270,15 @@ class NovelFire implements Plugin.PluginBase {
196270 . text ( )
197271 . trim ( ) ;
198272 novel . totalPages = Math . ceil ( parseInt ( totalChapters ) / 100 ) ;
273+ if ( this . singlePage ) {
274+ novel . chapters = await this . getAllChaptersForce (
275+ novelPath ,
276+ novel . totalPages ,
277+ ) ;
278+ if ( novel . totalPages > 1 && novel . chapters . length > 100 ) {
279+ novel . totalPages = 1 ;
280+ }
281+ }
199282 }
200283
201284 return novel as Plugin . SourceNovel & { totalPages : number } ;
0 commit comments