Skip to content

Commit 6d1b67c

Browse files
committed
freewebnovel:
- removed freewebnovel signature - better error management - added genres - added filters
1 parent c7f6a9b commit 6d1b67c

File tree

1 file changed

+110
-36
lines changed

1 file changed

+110
-36
lines changed

plugins/english/freewebnovel.ts

Lines changed: 110 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,63 @@
11
import { Plugin } from '@typings/plugin';
22
import { fetchApi, fetchFile } from '@libs/fetch';
33
import { NovelStatus } from '@libs/novelStatus';
4-
import { load as parseHTML } from 'cheerio';
4+
import { CheerioAPI, load as parseHTML } from 'cheerio';
55
import qs from 'qs';
6+
import { Filters, FilterTypes } from '@libs/filterInputs';
67

78
class 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(/alert\((.*?)\)/)?.[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

122196
export default new FreeWebNovel();

0 commit comments

Comments
 (0)