@@ -22,59 +22,63 @@ class LnorisPlugin implements Plugin.PluginBase {
2222 } ,
2323 } ;
2424
25- private async fetchPage ( url : string ) : Promise < string > {
25+ private libraryCache : CachedNovel [ ] | null = null ;
26+
27+ private async fetchPage (
28+ url : string ,
29+ ttl = 8 * 60 * 60 * 1000 ,
30+ ) : Promise < string > {
2631 if ( storage . get ( 'clearCache' ) ) {
2732 storage . clearAll ( ) ;
33+ this . libraryCache = null ;
2834 storage . set ( 'clearCache' , false ) ;
2935 }
3036 const cached = storage . get < string > ( url ) ;
3137 if ( cached ) return cached ;
3238 const body = await ( await fetchApi ( url ) ) . text ( ) ;
33- // cache for 8 hours
34- storage . set ( url , body , 8 * 60 * 60 * 1000 ) ;
39+ storage . set ( url , body , ttl ) ;
3540 return body ;
3641 }
3742
3843 private extractAppData ( html : string ) : Record < string , unknown > | null {
39- const match = html . match (
40- / < s c r i p t [ ^ > ] * i d = " a p p - d a t a " [ ^ > ] * > ( [ \s \S ] * ?) < \/ s c r i p t > / ,
41- ) ;
42- if ( ! match ?. [ 1 ] ) return null ;
44+ const scriptContent : string [ ] = [ ] ;
45+ let inScript = false ;
46+
47+ const parser = new Parser ( {
48+ onopentag : ( name , attribs ) => {
49+ if ( name === 'script' && attribs . id === 'app-data' ) {
50+ inScript = true ;
51+ }
52+ } ,
53+ ontext : text => {
54+ if ( inScript ) {
55+ scriptContent . push ( text ) ;
56+ }
57+ } ,
58+ onclosetag : name => {
59+ if ( name === 'script' ) {
60+ inScript = false ;
61+ }
62+ } ,
63+ } ) ;
64+
65+ parser . write ( html ) ;
66+ parser . end ( ) ;
67+
68+ if ( ! scriptContent . length ) return null ;
4369 try {
44- return JSON . parse ( match [ 1 ] ) ;
70+ return JSON . parse ( scriptContent . join ( '' ) ) ;
4571 } catch {
4672 return null ;
4773 }
4874 }
4975
50- /**
51- * Run async tasks with concurrency limit. Avoids flooding mobile network
52- * with 30+ parallel requests that compete for bandwidth.
53- */
54- private async asyncPool < T , R > (
55- items : T [ ] ,
56- processor : ( item : T ) => Promise < R > ,
57- limit = 5 ,
58- ) : Promise < R [ ] > {
59- const results : R [ ] = new Array ( items . length ) ;
60- let nextIndex = 0 ;
61-
62- const worker = async ( ) : Promise < void > => {
63- while ( nextIndex < items . length ) {
64- const idx = nextIndex ++ ;
65- results [ idx ] = await processor ( items [ idx ] ) ;
66- }
67- } ;
68-
69- await Promise . all (
70- Array . from ( { length : Math . min ( limit , items . length ) } , ( ) => worker ( ) ) ,
71- ) ;
72- return results ;
73- }
74-
7576 private async getLibraryNovels ( ) : Promise < CachedNovel [ ] > {
77+ if ( this . libraryCache ) return this . libraryCache ;
78+
7679 const url = this . site + 'library' ;
77- const body = await this . fetchPage ( url ) ;
80+ // library gets 1 hour cache
81+ const body = await this . fetchPage ( url , 1 * 60 * 60 * 1000 ) ;
7882 let tempNovel : Partial < Plugin . NovelItem > & {
7983 author ?: string ;
8084 tags ?: string ;
@@ -141,11 +145,12 @@ class LnorisPlugin implements Plugin.PluginBase {
141145 parser . write ( body ) ;
142146 parser . end ( ) ;
143147
148+ this . libraryCache = novels ;
144149 return novels ;
145150 }
146151
147152 async popularNovels (
148- _pageNo : number ,
153+ pageNo : number ,
149154 { filters } : Plugin . PopularNovelsOptions < typeof this . filters > ,
150155 ) : Promise < Plugin . NovelItem [ ] > {
151156 const parsedList = await this . getLibraryNovels ( ) ;
@@ -188,7 +193,10 @@ class LnorisPlugin implements Plugin.PluginBase {
188193 break ;
189194 }
190195
191- return filtered . map ( item => item . novel ) ;
196+ const novels = filtered . map ( item => item . novel ) ;
197+ if ( filters . reverse . value ) novels . reverse ( ) ;
198+ const start = ( pageNo - 1 ) * 36 ;
199+ return novels . slice ( start , start + 36 ) ;
192200 }
193201
194202 async parseNovel ( novelPath : string ) : Promise < Plugin . SourceNovel > {
@@ -237,7 +245,6 @@ class LnorisPlugin implements Plugin.PluginBase {
237245 break ;
238246 case 'img' :
239247 if ( state === ParsingState . HeroCard ) {
240- novel . name = attribs . alt ;
241248 novel . cover = attribs . src ;
242249 }
243250 break ;
@@ -275,6 +282,11 @@ class LnorisPlugin implements Plugin.PluginBase {
275282 pushState ( ParsingState . VolCardMeta ) ;
276283 }
277284 break ;
285+ case 'button' :
286+ if ( state === ParsingState . HeroCard ) {
287+ novel . name = attribs [ 'data-series-title' ] ;
288+ }
289+ break ;
278290 }
279291 } ,
280292
@@ -369,6 +381,9 @@ class LnorisPlugin implements Plugin.PluginBase {
369381 }
370382 }
371383 }
384+ for ( const key of Object . keys ( volumeMap ) )
385+ if ( novel . name && volumeMap [ key ] . includes ( novel . name ) )
386+ volumeMap [ key ] = volumeMap [ key ] . replace ( novel . name , '-' ) ;
372387 } ,
373388 } ) ;
374389
@@ -382,12 +397,9 @@ class LnorisPlugin implements Plugin.PluginBase {
382397
383398 const volumeUrls = Object . keys ( volumeMap ) ;
384399
385- // Process volumes with concurrency limit (5)
386- const chapters2D = await this . asyncPool (
387- volumeUrls ,
388- async volUrl => {
389- const fullVolUrl = this . site + volUrl ;
390- const volHtml = await this . fetchPage ( fullVolUrl ) ;
400+ const chapters2D = await Promise . all (
401+ volumeUrls . map ( async volUrl => {
402+ const volHtml = await this . fetchPage ( this . site + volUrl ) ;
391403 const volTitle = getVolumeName ( volUrl , volumeMap [ volUrl ] ) ;
392404 const volChapters : Plugin . ChapterItem [ ] = [ ] ;
393405
@@ -409,7 +421,7 @@ class LnorisPlugin implements Plugin.PluginBase {
409421 return volChapters ;
410422 }
411423
412- // Fallback: htmlparser2 for #toc-list
424+ // Fallback #toc-list
413425 let inTocList = false ;
414426 const tocParser = new Parser ( {
415427 onopentag : ( name , attribs ) => {
@@ -432,8 +444,7 @@ class LnorisPlugin implements Plugin.PluginBase {
432444 tocParser . write ( volHtml ) ;
433445 tocParser . end ( ) ;
434446 return volChapters ;
435- } ,
436- 5 ,
447+ } ) ,
437448 ) ;
438449 const chapters = chapters2D . flat ( ) ;
439450
@@ -469,19 +480,23 @@ class LnorisPlugin implements Plugin.PluginBase {
469480 . join ( '<hr>' ) ;
470481 }
471482
472- async searchNovels ( searchTerm : string ) : Promise < Plugin . NovelItem [ ] > {
473- const parsedList = await this . getLibraryNovels ( ) ;
474-
483+ async searchNovels (
484+ searchTerm : string ,
485+ pageNo : number ,
486+ ) : Promise < Plugin . NovelItem [ ] > {
475487 const term = searchTerm . toLowerCase ( ) ;
476- const filteredList = parsedList . filter ( item => {
477- return (
478- item . novel . name . toLowerCase ( ) . includes ( term ) ||
479- item . author . toLowerCase ( ) . includes ( term ) ||
480- item . tags . some ( t => t . includes ( term ) )
481- ) ;
482- } ) ;
483-
484- return filteredList . map ( item => item . novel ) ;
488+ const parsedList = await this . getLibraryNovels ( ) ;
489+ const filtered = parsedList
490+ . filter (
491+ item =>
492+ item . novel . name . toLowerCase ( ) . includes ( term ) ||
493+ item . author . toLowerCase ( ) . includes ( term ) ||
494+ item . tags . some ( t => t . includes ( term ) ) ,
495+ )
496+ . map ( item => item . novel ) ;
497+
498+ const start = ( pageNo - 1 ) * 36 ;
499+ return filtered . slice ( start , start + 36 ) ;
485500 }
486501
487502 // resolveUrl = (path: string, _isNovel?: boolean) => {
@@ -500,6 +515,11 @@ class LnorisPlugin implements Plugin.PluginBase {
500515 ] ,
501516 type : FilterTypes . Picker ,
502517 } ,
518+ reverse : {
519+ label : 'Reverse Results' ,
520+ value : false ,
521+ type : FilterTypes . Switch ,
522+ } ,
503523 genre : {
504524 label : 'Genre' ,
505525 value : {
0 commit comments