11import { fetchApi } from '@libs/fetch' ;
22import { Plugin } from '@/types/plugin' ;
33import { Filters } from '@libs/filterInputs' ;
4- import { load as loadCheerio } from 'cheerio' ;
54import { NovelStatus } from '@libs/novelStatus' ;
6- import dayjs from 'dayjs' ;
75
8- class FictionZonePlugin implements Plugin . PagePlugin {
6+ class FictionZonePlugin implements Plugin . PluginBase {
97 id = 'fictionzone' ;
108 name = 'Fiction Zone' ;
119 icon = 'src/en/fictionzone/icon.png' ;
1210 site = 'https://fictionzone.net' ;
13- version = '1.0.1 ' ;
11+ version = '1.0.2 ' ;
1412 filters : Filters | undefined = undefined ;
15- imageRequestInit ?: Plugin . ImageRequestInit | undefined = undefined ;
16-
17- //flag indicates whether access to LocalStorage, SesesionStorage is required.
18- webStorageUtilized ?: boolean ;
19-
20- cachedNovelIds = new Map < string , string > ( ) ;
2113
2214 async popularNovels (
2315 pageNo : number ,
@@ -26,176 +18,96 @@ class FictionZonePlugin implements Plugin.PagePlugin {
2618 filters,
2719 } : Plugin . PopularNovelsOptions < typeof this . filters > ,
2820 ) : Promise < Plugin . NovelItem [ ] > {
29- return await this . getPage ( this . site + '/library?page=' + pageNo ) ;
30- }
31-
32- async getPage ( url : string ) {
33- const req = await fetchApi ( url ) ;
34- const body = await req . text ( ) ;
35- const loadedCheerio = loadCheerio ( body ) ;
36-
37- return loadedCheerio ( 'div.novel-card' )
38- . map ( ( i , el ) => {
39- const novelName = loadedCheerio ( el ) . find ( 'a > div.title > h1' ) . text ( ) ;
40- const novelCover = loadedCheerio ( el ) . find ( 'img' ) . attr ( 'src' ) ;
41- const novelUrl = loadedCheerio ( el ) . find ( 'a' ) . attr ( 'href' ) ;
42-
43- return {
44- name : novelName ,
45- cover : novelCover ,
46- path : novelUrl ! . replace ( / ^ \/ / , '' ) . replace ( / \/ $ / , '' ) ,
47- } ;
48- } )
49- . toArray ( ) ;
50- }
51-
52- async parseNovel (
53- novelPath : string ,
54- ) : Promise < Plugin . SourceNovel & { totalPages : number } > {
55- const req = await fetchApi ( this . site + '/' + novelPath ) ;
56- const body = await req . text ( ) ;
57- const loadedCheerio = loadCheerio ( body ) ;
58-
59- const novel : Plugin . SourceNovel & { totalPages : number } = {
60- path : novelPath ,
61- name : loadedCheerio ( 'div.novel-title > h1' ) . text ( ) ,
62- totalPages : 1 ,
63- } ;
64-
65- // novel.artist = '';
66- novel . author = loadedCheerio ( 'div.novel-author > content' ) . text ( ) ;
67- novel . cover = loadedCheerio ( 'div.novel-img > img' ) . attr ( 'src' ) ;
68- novel . genres = [
69- ...loadedCheerio ( 'div.genres > .items > span' )
70- . map ( ( i , el ) => loadedCheerio ( el ) . text ( ) )
71- . toArray ( ) ,
72- ...loadedCheerio ( 'div.tags > .items > a' )
73- . map ( ( i , el ) => loadedCheerio ( el ) . text ( ) )
74- . toArray ( ) ,
75- ] . join ( ',' ) ;
76- const status = loadedCheerio ( 'div.novel-status > div.content' )
77- . text ( )
78- . trim ( ) ;
79- if ( status === 'Ongoing' ) novel . status = NovelStatus . Ongoing ;
80- novel . summary = loadedCheerio ( '#synopsis > div.content' ) . text ( ) ;
81-
82- const nuxtData = loadedCheerio ( 'script#__NUXT_DATA__' ) . html ( ) ;
83- const parsed = JSON . parse ( nuxtData ! ) ;
84- let last = null ;
85- for ( const a of parsed ) {
86- if ( typeof a === 'string' && a . startsWith ( 'novel_covers/' ) ) break ;
87- last = a ;
88- }
89- this . cachedNovelIds . set ( novelPath , last . toString ( ) ) ;
90- // @ts -ignore
91- novel . chapters = loadedCheerio (
92- 'div.chapters > div.list-wrapper > div.items > a.chapter' ,
93- )
94- . map ( ( i , el ) => {
95- const chapterName = loadedCheerio ( el ) . find ( 'span.chapter-title' ) . text ( ) ;
96- const chapterUrl = loadedCheerio ( el )
97- . attr ( 'href' )
98- ?. replace ( / ^ \/ / , '' )
99- . replace ( / \/ $ / , '' ) ;
100- const uploadTime = this . parseAgoDate (
101- loadedCheerio ( el ) . find ( 'span.update-date' ) . text ( ) ,
102- ) ;
103-
104- return {
105- name : chapterName ,
106- releaseTime : uploadTime ,
107- path : chapterUrl ?. replace ( / ^ \/ / , '' ) . replace ( / \/ $ / , '' ) ,
108- } ;
109- } )
110- . toArray ( )
111- . filter ( chap => ! ! chap . path ) ;
112- novel . totalPages = parseInt (
113- loadedCheerio ( 'div.chapters ul.el-pager > li:last-child' ) . text ( ) ,
21+ return await this . getPage (
22+ `/platform/browse?page=${ pageNo } &page_size=20&sort_by=${ showLatestNovels ? 'created_at' : 'bookmark_count' } &sort_order=desc&include_genres=true` ,
11423 ) ;
115-
116- return novel ;
11724 }
11825
119- async parsePage ( novelPath : string , page : string ) : Promise < Plugin . SourcePage > {
120- let id = this . cachedNovelIds . get ( novelPath ) ;
121- if ( ! id ) {
122- await this . parseNovel ( novelPath ) ;
123- id = this . cachedNovelIds . get ( novelPath ) ;
124- }
125-
126- const data = await fetchApi ( this . site + '/api/__api_party/api-v1' , {
26+ async getData ( url : string ) {
27+ return await fetchApi ( this . site + '/api/__api_party/fictionzone' , {
12728 method : 'POST' ,
12829 headers : {
12930 'Content-Type' : 'application/json' ,
31+ 'Accept' : 'application/json' ,
13032 } ,
13133 body : JSON . stringify ( {
132- 'path' : '/chapter/all/' + id ,
133- 'query' : { 'page' : parseInt ( page ) } ,
134- 'headers' : { 'content-type' : 'application/json' } ,
135- 'method' : 'get' ,
34+ 'path' : url ,
35+ 'headers' : [
36+ [ 'content-type' , 'application/json' ] ,
37+ [ 'x-request-time' , new Date ( ) . toISOString ( ) ] ,
38+ ] ,
39+ 'method' : 'GET' ,
13640 } ) ,
13741 } ) . then ( r => r . json ( ) ) ;
42+ }
43+
44+ async getPage ( url : string ) {
45+ const data = await this . getData ( url ) ;
46+
47+ return data . data . novels . map ( ( n : any ) => ( {
48+ name : n . title ,
49+ cover : `https://cdn.fictionzone.net/insecure/rs:fill:165:250/${ n . image } .webp` ,
50+ path : `novel/${ n . slug } ` ,
51+ } ) ) ;
52+ }
53+
54+ async getChapterPage ( id : string , novelPath : string ) {
55+ const data = await this . getData ( '/platform/chapter-lists?novel_id=' + id ) ;
56+
57+ return data . data . chapters . map ( ( n : any ) => ( {
58+ name : n . title ,
59+ number : n . chapter_number ,
60+ date : n . published_date
61+ ? new Date ( n . published_date ) . toISOString ( )
62+ : undefined ,
63+ path : `${ novelPath } /${ n . chapter_id } |/platform/chapter-content?novel_id=${ id } &chapter_id=${ n . chapter_id } ` ,
64+ } ) ) ;
65+ }
66+
67+ async parseNovel ( novelPath : string ) : Promise < Plugin . SourceNovel > {
68+ const novelSlug = novelPath . replace ( 'novel/' , '' ) ;
69+ const data = await this . getData (
70+ `/platform/novel-details?slug=${ novelSlug } ` ,
71+ ) ;
72+
13873 return {
139- chapters : data . _data . map ( ( c : any ) => ( {
140- name : c . title ,
141- releaseTime : new Date ( c . created_at ) . toISOString ( ) ,
142- path : novelPath + '/' + c . slug ,
143- } ) ) ,
74+ path : novelPath ,
75+ name : data . data . title ,
76+ cover : `https://cdn.fictionzone.net/insecure/rs:fill:165:250/${ data . data . image } .webp` ,
77+ genres : [
78+ ...data . data . genres . map ( ( g : any ) => g . name ) ,
79+ ...data . data . tags . map ( ( g : any ) => g . name ) ,
80+ ] . join ( ',' ) ,
81+ status :
82+ data . data . status == 1
83+ ? NovelStatus . Ongoing
84+ : data . data . status == 0
85+ ? NovelStatus . Completed
86+ : NovelStatus . Unknown ,
87+ author :
88+ data . data . contributors . filter ( ( c : any ) => c . role == 'author' ) [ 0 ]
89+ ?. display_name || '' ,
90+ summary : data . data . synopsis ,
91+ chapters : await this . getChapterPage ( data . data . id , novelPath ) ,
14492 } ;
14593 }
14694
14795 async parseChapter ( chapterPath : string ) : Promise < string > {
148- const req = await fetchApi ( this . site + '/' + chapterPath ) ;
149- const body = await req . text ( ) ;
150- const loadedCheerio = loadCheerio ( body ) ;
151- return loadedCheerio ( 'div.chapter-content' ) . html ( ) || '' ;
96+ const data = await this . getData ( chapterPath . split ( '|' ) [ 1 ] ) ;
97+ return '<p>' + data . data . content . replaceAll ( '\n' , '</p><p>' ) + '</p>' ;
15298 }
15399
154100 async searchNovels (
155101 searchTerm : string ,
156102 pageNo : number ,
157103 ) : Promise < Plugin . NovelItem [ ] > {
158104 return await this . getPage (
159- this . site +
160- '/library?query=' +
161- encodeURIComponent ( searchTerm ) +
162- '&page=' +
163- pageNo +
164- '&sort=views-all' ,
105+ `/platform/browse?search=${ encodeURIComponent ( searchTerm ) } &page=${ pageNo } &page_size=20&search_in_synopsis=true&sort_by=bookmark_count&sort_order=desc&include_genres=true` ,
165106 ) ;
166107 }
167108
168- parseAgoDate ( date : string | undefined ) {
169- //parseMadaraDate
170- if ( date ?. includes ( 'ago' ) ) {
171- const dayJSDate = dayjs ( new Date ( ) ) ; // today
172- const timeAgo = date . match ( / \d + / ) ?. [ 0 ] || '' ;
173- const timeAgoInt = parseInt ( timeAgo , 10 ) ;
174-
175- if ( ! timeAgo ) return null ; // there is no number!
176-
177- if ( date . includes ( 'hours ago' ) || date . includes ( 'hour ago' ) ) {
178- dayJSDate . subtract ( timeAgoInt , 'hours' ) ; // go back N hours
179- }
180-
181- if ( date . includes ( 'days ago' ) || date . includes ( 'day ago' ) ) {
182- dayJSDate . subtract ( timeAgoInt , 'days' ) ; // go back N days
183- }
184-
185- if ( date . includes ( 'months ago' ) || date . includes ( 'month ago' ) ) {
186- dayJSDate . subtract ( timeAgoInt , 'months' ) ; // go back N months
187- }
188-
189- if ( date . includes ( 'years ago' ) || date . includes ( 'year ago' ) ) {
190- dayJSDate . subtract ( timeAgoInt , 'years' ) ; // go back N years
191- }
192-
193- return dayJSDate . toISOString ( ) ;
194- }
195- return null ; // there is no "ago" so give up
196- }
197-
198- resolveUrl = ( path : string , isNovel ?: boolean ) => this . site + '/' + path ;
109+ resolveUrl = ( path : string , isNovel ?: boolean ) =>
110+ this . site + '/' + path . split ( '|' ) [ 0 ] ;
199111}
200112
201113export default new FictionZonePlugin ( ) ;
0 commit comments