11import { Plugin } from '@typings/plugin' ;
22import { fetchApi , fetchFile } from '@libs/fetch' ;
33import { NovelStatus } from '@libs/novelStatus' ;
4- import { load as parseHTML } from 'cheerio' ;
4+ import { CheerioAPI , load as parseHTML } from 'cheerio' ;
55import qs from 'qs' ;
6+ import { Filters , FilterTypes } from '@libs/filterInputs' ;
67
78class FreeWebNovel implements Plugin . PluginBase {
89 id = 'FWN.com' ;
910 name = 'Free Web Novel' ;
1011 site = 'https://freewebnovel.com' ;
11- version = '1.0.0 ' ;
12+ version = '1.0.1 ' ;
1213 icon = 'src/en/freewebnovel/icon.png' ;
1314
14- async popularNovels (
15- page : number ,
16- { showLatestNovels } : Plugin . PopularNovelsOptions ,
17- ) : Promise < Plugin . NovelItem [ ] > {
18- const sort = showLatestNovels
19- ? '/latest-release-novels/'
20- : '/completed-novels/' ;
21-
22- const body = await fetchApi ( this . site + sort + page ) . then ( res =>
23- res . text ( ) ,
24- ) ;
25- const loadedCheerio = parseHTML ( body ) ;
15+ async getCheerio ( url : string ) : Promise < CheerioAPI > {
16+ const r = await fetchApi ( url ) ;
17+ if ( ! r . ok )
18+ throw new Error (
19+ `Could not reach site (${ r . status } : ${ r . statusText } ) try to open in webview.` ,
20+ ) ;
21+ return parseHTML ( await r . text ( ) ) ;
22+ }
2623
27- const novels : Plugin . NovelItem [ ] = loadedCheerio ( '.li-row' )
24+ parseNovels ( loadedCheerio : CheerioAPI ) : Plugin . NovelItem [ ] {
25+ return loadedCheerio ( '.li-row' )
2826 . map ( ( index , element ) => ( {
2927 name : loadedCheerio ( element ) . find ( '.tit' ) . text ( ) || '' ,
3028 cover : loadedCheerio ( element ) . find ( 'img' ) . attr ( 'src' ) ,
3129 path : loadedCheerio ( element ) . find ( 'h3 > a' ) . attr ( 'href' ) || '' ,
3230 } ) )
3331 . get ( )
3432 . filter ( novel => novel . name && novel . path ) ;
33+ }
3534
36- return novels ;
35+ async popularNovels (
36+ page : number ,
37+ {
38+ showLatestNovels,
39+ filters,
40+ } : Plugin . PopularNovelsOptions < typeof this . filters > ,
41+ ) : Promise < Plugin . NovelItem [ ] > {
42+ let url = this . site ;
43+ if ( showLatestNovels ) url += '/latest-release-novels/' ;
44+ else {
45+ if ( filters && filters . genres && filters . genres . value !== 'all' )
46+ url += filters . genres . value ;
47+ else {
48+ url += '/most-popular-novels/' ;
49+ if ( page != 1 ) return [ ] ;
50+ page = 0 ;
51+ }
52+ }
53+ url += page ;
54+
55+ const $ = await this . getCheerio ( url ) ;
56+ return this . parseNovels ( $ ) ;
3757 }
3858
3959 async parseNovel ( novelPath : string ) : Promise < Plugin . SourceNovel > {
40- const body = await fetchApi ( this . site + novelPath ) . then ( res => res . text ( ) ) ;
41- const loadedCheerio = parseHTML ( body ) ;
60+ const loadedCheerio = await this . getCheerio ( this . site + novelPath ) ;
4261
4362 const novel : Plugin . SourceNovel = {
4463 path : novelPath ,
@@ -62,6 +81,13 @@ class FreeWebNovel implements Plugin.PluginBase {
6281 . text ( )
6382 . replace ( / [ \t \n ] / g, '' ) ;
6483
84+ novel . genres = loadedCheerio ( '[title=Genre]' )
85+ . next ( )
86+ . text ( )
87+ . trim ( )
88+ . replace ( / [ \t \n ] / g, ',' )
89+ . replace ( / , / g, ',' ) ;
90+
6591 const chapters : Plugin . ChapterItem [ ] = loadedCheerio ( '#idData > li > a' )
6692 . map ( ( chapterIndex , element ) => ( {
6793 name :
@@ -83,40 +109,88 @@ class FreeWebNovel implements Plugin.PluginBase {
83109 }
84110
85111 async parseChapter ( chapterPath : string ) : Promise < string > {
86- const body = await fetchApi ( this . site + chapterPath ) . then ( res =>
87- res . text ( ) ,
88- ) ;
89- const loadedCheerio = parseHTML ( body ) ;
112+ const loadedCheerio = await this . getCheerio ( this . site + chapterPath ) ;
113+
114+ // remove freewebnovel signature
115+ if ( loadedCheerio ( 'style' ) . text ( ) . includes ( 'p:nth-last-child(1)' ) )
116+ loadedCheerio ( 'div.txt' ) . find ( 'p:last-child' ) . remove ( ) ;
90117
91118 const chapterText = loadedCheerio ( 'div.txt' ) . html ( ) || '' ;
92119 return chapterText ;
93120 }
94121
95122 async searchNovels ( searchTerm : string ) : Promise < Plugin . NovelItem [ ] > {
96- const body = await fetchApi ( this . site + '/search/' , {
123+ const r = await fetchApi ( this . site + '/search/' , {
97124 headers : {
98125 'Content-Type' : 'application/x-www-form-urlencoded' ,
99126 Referer : this . site ,
100127 Origin : this . site ,
101128 } ,
102129 method : 'POST' ,
103130 body : qs . stringify ( { searchkey : searchTerm } ) ,
104- } ) . then ( res => res . text ( ) ) ;
105-
106- const loadedCheerio = parseHTML ( body ) ;
107- const novels : Plugin . NovelItem [ ] = loadedCheerio ( '.li-row > .li > .con' )
108- . map ( ( index , element ) => ( {
109- name : loadedCheerio ( element ) . find ( '.tit' ) . text ( ) || '' ,
110- cover : loadedCheerio ( element ) . find ( '.pic > a > img' ) . attr ( 'src' ) ,
111- path : loadedCheerio ( element ) . find ( 'h3 > a' ) . attr ( 'href' ) || '' ,
112- } ) )
113- . get ( )
114- . filter ( novel => novel . name && novel . path ) ;
115-
116- return novels ;
131+ } ) ;
132+ if ( ! r . ok )
133+ throw new Error (
134+ 'Could not reach site (' + r . status + ') try to open in webview.' ,
135+ ) ;
136+
137+ const loadedCheerio = parseHTML ( await r . text ( ) ) ;
138+ const alertText =
139+ loadedCheerio ( 'script' )
140+ . text ( )
141+ . match ( / a l e r t \( ( .* ?) \) / ) ?. [ 1 ] || '' ;
142+ if ( alertText ) throw new Error ( alertText ) ;
143+
144+ return this . parseNovels ( loadedCheerio ) ;
117145 }
118146
119147 fetchImage = fetchFile ;
148+
149+ filters = {
150+ genres : {
151+ type : FilterTypes . Picker ,
152+ label : 'Genre' ,
153+ value : 'all' ,
154+ options : [
155+ { label : 'Action' , value : '/genres/Action/' } ,
156+ { label : 'Adult' , value : '/genres/Adult/' } ,
157+ { label : 'Adventure' , value : '/genres/Adventure/' } ,
158+ { label : 'Comedy' , value : '/genres/Comedy/' } ,
159+ { label : 'Drama' , value : '/genres/Drama/' } ,
160+ { label : 'Eastern' , value : '/genres-novel/Eastern' } ,
161+ { label : 'Ecchi' , value : '/genres/Ecchi/' } ,
162+ { label : 'Fantasy' , value : '/genres/Fantasy/' } ,
163+ { label : 'Gender Bender' , value : '/genres/Gender+Bender/' } ,
164+ { label : 'Harem' , value : '/genres/Harem/' } ,
165+ { label : 'Historical' , value : '/genres/Historical/' } ,
166+ { label : 'Horror' , value : '/genres/Horror/' } ,
167+ { label : 'Josei' , value : '/genres/Josei/' } ,
168+ { label : 'Game' , value : '/genres/Game/' } ,
169+ { label : 'Martial Arts' , value : '/genres/Martial+Arts/' } ,
170+ { label : 'Mature' , value : '/genres/Mature/' } ,
171+ { label : 'Mecha' , value : '/genres/Mecha/' } ,
172+ { label : 'Mystery' , value : '/genres/Mystery/' } ,
173+ { label : 'Psychological' , value : '/genres/Psychological/' } ,
174+ { label : 'Reincarnation' , value : '/genres-novel/Reincarnation' } ,
175+ { label : 'Romance' , value : '/genres/Romance/' } ,
176+ { label : 'School Life' , value : '/genres/School+Life/' } ,
177+ { label : 'Sci-fi' , value : '/genres/Sci-fi/' } ,
178+ { label : 'Seinen' , value : '/genres/Seinen/' } ,
179+ { label : 'Shoujo' , value : '/genres/Shoujo/' } ,
180+ { label : 'Shounen Ai' , value : '/genres/Shounen+Ai/' } ,
181+ { label : 'Shounen' , value : '/genres/Shounen/' } ,
182+ { label : 'Slice of Life' , value : '/genres/Slice+of+Life/' } ,
183+ { label : 'Smut' , value : '/genres/Smut/' } ,
184+ { label : 'Sports' , value : '/genres/Sports/' } ,
185+ { label : 'Supernatural' , value : '/genres/Supernatural/' } ,
186+ { label : 'Tragedy' , value : '/genres/Tragedy/' } ,
187+ { label : 'Wuxia' , value : '/genres/Wuxia/' } ,
188+ { label : 'Xianxia' , value : '/genres/Xianxia/' } ,
189+ { label : 'Xuanhuan' , value : '/genres/Xuanhuan/' } ,
190+ { label : 'Yaoi' , value : '/genres/Yaoi/' } ,
191+ ] ,
192+ } ,
193+ } satisfies Filters ;
120194}
121195
122196export default new FreeWebNovel ( ) ;
0 commit comments