Skip to content

Commit 2c4d45b

Browse files
authored
refactor: Update Novelfire parsePage to Use Correct Fetch Request (#2120)
pluginSettings
1 parent e1f3caf commit 2c4d45b

1 file changed

Lines changed: 124 additions & 68 deletions

File tree

plugins/english/novelfire.ts

Lines changed: 124 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,34 @@ import { storage } from '@libs/storage';
99
class NovelFire implements Plugin.PluginBase {
1010
id = 'novelfire';
1111
name = 'Novel Fire';
12-
version = '1.2.0';
12+
version = '1.3.0';
1313
icon = 'src/en/novelfire/icon.png';
1414
site = 'https://novelfire.net/';
15-
15+
webStorageUtilized = true;
1616
novelList: string[] = [];
17+
draw = 0;
1718

18-
singlePage = storage.get('singlePage');
1919
pluginSettings = {
20+
pageLength: {
21+
value: '-1',
22+
label: 'Page Mode (Change if Broken)',
23+
type: 'Select',
24+
options: [
25+
{ label: '100 (Fallback Mode)', value: '100' },
26+
{ label: '200', value: '200' },
27+
{ label: '500', value: '500' },
28+
{ label: 'All', value: '-1' },
29+
],
30+
},
2031
singlePage: {
2132
value: '',
2233
label:
2334
'Force load all chapters on a single page (Slower & use more data)',
2435
type: 'Switch',
2536
},
2637
};
38+
singlePage = storage.get('singlePage');
39+
pageLength = storage.get('pageLength');
2740

2841
async getCheerio(url: string, search: boolean): Promise<CheerioAPI> {
2942
const r = await fetchApi(url);
@@ -46,18 +59,20 @@ class NovelFire implements Plugin.PluginBase {
4659
): Plugin.NovelItem[] {
4760
return loadedCheerio(selector)
4861
.map((_, el) => {
49-
const titleElement = loadedCheerio(el).find('.novel-title > a');
50-
const fallbackElement = loadedCheerio(el).find('a');
62+
const $el = loadedCheerio(el);
63+
const titleElement = $el.find('.novel-title > a');
64+
const fallbackElement = $el.find('a');
5165

5266
const novelName =
5367
titleElement.text() ||
5468
fallbackElement.attr('title') ||
5569
'No Title Found';
5670

57-
const imgElement = loadedCheerio(el).find('.novel-cover > img');
58-
const novelCover =
59-
this.site +
60-
deSlash(imgElement.attr('data-src') || imgElement.attr('src') || '');
71+
const imgElement = $el.find('.novel-cover > img');
72+
const rawSrc = imgElement.attr('data-src') ?? imgElement.attr('src');
73+
const novelCover = rawSrc
74+
? new URL(rawSrc, this.site).href
75+
: defaultCover;
6176

6277
const novelPath =
6378
titleElement.attr('href') || fallbackElement.attr('href');
@@ -67,7 +82,7 @@ class NovelFire implements Plugin.PluginBase {
6782
return {
6883
name: novelName,
6984
cover: novelCover,
70-
path: deSlash(novelPath.replace(this.site, '')),
85+
path: new URL(novelPath, this.site).pathname.substring(1),
7186
};
7287
})
7388
.get()
@@ -91,6 +106,7 @@ class NovelFire implements Plugin.PluginBase {
91106
): Promise<Plugin.NovelItem[]> {
92107
if (pageNo === 1) {
93108
this.novelList = [];
109+
this.draw = 0;
94110
}
95111
const url = this.site + 'search-adv';
96112
const params = new URLSearchParams();
@@ -121,9 +137,44 @@ class NovelFire implements Plugin.PluginBase {
121137
async getAllChapters(
122138
novelPath: string,
123139
post_id: string,
140+
page: string,
124141
): Promise<Plugin.ChapterItem[]> {
125-
const url = `${this.site}listChapterDataAjax?post_id=${post_id}`;
126-
const result = await fetchApi(url);
142+
const length = parseInt(this.pageLength) || -1;
143+
const url = `${this.site}ajax/listChapterDataAjax`;
144+
const start = length === -1 ? 0 : (parseInt(page) - 1) * length;
145+
this.draw++;
146+
const params = new URLSearchParams({
147+
draw: this.draw.toString(),
148+
'columns[0][data]': 'n_sort',
149+
'columns[0][name]': 'cmm_posts_detail.n_sort',
150+
'columns[0][searchable]': 'true',
151+
'columns[0][orderable]': 'true',
152+
'columns[0][search][value]': '',
153+
'columns[0][search][regex]': 'false',
154+
155+
'columns[1][data]': 'bookmark_created_at',
156+
'columns[1][name]': 'bookmark_chapters.created_at',
157+
'columns[1][searchable]': 'false',
158+
'columns[1][orderable]': 'true',
159+
'columns[1][search][value]': '',
160+
'columns[1][search][regex]': 'false',
161+
162+
'order[0][column]': '0',
163+
'order[0][dir]': 'asc',
164+
'order[0][name]': 'cmm_posts_detail.n_sort',
165+
166+
start: start.toString(),
167+
length: length.toString(),
168+
'search[value]': '',
169+
'search[regex]': 'false',
170+
post_id: post_id,
171+
only_bookmark: 'false',
172+
_: Date.now().toString(),
173+
});
174+
175+
const result = await fetchApi(`${url}?${params.toString()}`);
176+
if (result.status === 429) throw new NovelFireThrottlingError();
177+
127178
const body = await result.text();
128179

129180
if (body.includes('You are being rate limited')) {
@@ -135,7 +186,7 @@ class NovelFire implements Plugin.PluginBase {
135186
}
136187

137188
const json = JSON.parse(body);
138-
const chapters = json.data
189+
const chapters = (json.data || [])
139190
.map((index: { title?: string; slug: string; n_sort: number }) => {
140191
const chapterName = load(index.title || index.slug).text();
141192
const chapterPath = `${novelPath}/chapter-${index.n_sort}`;
@@ -223,13 +274,16 @@ class NovelFire implements Plugin.PluginBase {
223274
}
224275

225276
async parseNovel(
226-
novelPathRaw: string,
277+
novelPath: string,
227278
): Promise<Plugin.SourceNovel & { totalPages: number }> {
228-
const novelPath = deSlash(novelPathRaw);
279+
this.draw = 0;
229280
const $ = await this.getCheerio(this.site + novelPath, false);
230281
const baseUrl = this.site;
231282

232-
let post_id = '0';
283+
const post_id = $('#novel-report').attr('report-post_id');
284+
if (post_id) {
285+
storage.set(`novelfire_postid_${novelPath}`, post_id);
286+
}
233287

234288
const novel: Partial<Plugin.SourceNovel & { totalPages: number }> = {
235289
path: novelPath,
@@ -286,55 +340,68 @@ class NovelFire implements Plugin.PluginBase {
286340

287341
novel.rating = parseFloat($('.nub').text().trim());
288342

289-
post_id = $('#novel-report').attr('report-post_id') || '0';
290-
291-
try {
292-
novel.chapters = await this.getAllChapters(novelPath, post_id);
293-
} catch (error) {
294-
const totalChapters = $('.header-stats .icon-book-open')
295-
.parent()
296-
.text()
297-
.trim();
298-
novel.totalPages = Math.ceil(parseInt(totalChapters) / 50);
299-
if (this.singlePage) {
300-
novel.chapters = await this.getAllChaptersForce(
301-
novelPath,
302-
novel.totalPages,
303-
);
304-
if (novel.totalPages > 1 && novel.chapters.length > 50) {
305-
novel.totalPages = 1;
306-
}
307-
}
343+
const totalChapters = $('.header-stats i.icon-book-open')
344+
.parent()
345+
.text()
346+
.trim();
347+
const length = parseInt(this.pageLength) || -1;
348+
novel.totalPages =
349+
length === -1 ? 1 : Math.ceil(parseInt(totalChapters) / length) || 1;
350+
if (length === 100 && this.singlePage) {
351+
novel.chapters = await this.getAllChaptersForce(
352+
novel.path as string,
353+
novel.totalPages,
354+
);
355+
novel.totalPages = 1;
308356
}
309357

310358
return novel as Plugin.SourceNovel & { totalPages: number };
311359
}
312360

313361
async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> {
314-
const url = `${this.site}${novelPath}/chapters?page=${page}`;
315-
const result = await fetchApi(url);
316-
const body = await result.text();
317-
318-
const loadedCheerio = load(body);
319-
320-
const chapters = loadedCheerio('.chapter-list li')
321-
.map((index, ele) => {
322-
const chapterName =
323-
loadedCheerio(ele).find('a').attr('title') || 'No Title Found';
324-
const chapterPath = loadedCheerio(ele).find('a').attr('href');
325-
326-
if (!chapterPath) return null;
362+
const post_id = storage.get(`novelfire_postid_${novelPath}`);
363+
364+
if (post_id && !isNaN(Number(post_id))) {
365+
try {
366+
const chapters = await this.getAllChapters(novelPath, post_id, page);
367+
return { chapters };
368+
} catch (e) {
369+
// Fallback to scraping if AJAX fails
370+
}
371+
}
327372

328-
return {
329-
name: chapterName,
330-
path: deSlash(chapterPath.replace(this.site, '')),
331-
};
332-
})
333-
.get()
334-
.filter(chapter => chapter !== null) as Plugin.ChapterItem[];
373+
// Fallback only works for multiples of 100
374+
const length = parseInt(this.pageLength) || -1;
375+
if (length === 100) {
376+
const url = `${this.site}${novelPath}/chapters?page=${page}`;
377+
const result = await fetchApi(url);
378+
const body = await result.text();
379+
380+
const loadedCheerio = load(body);
381+
382+
const chapters = loadedCheerio('.chapter-list li')
383+
.map((index, ele) => {
384+
const chapterName =
385+
loadedCheerio(ele).find('a').attr('title') || 'No Title Found';
386+
const chapterPath = loadedCheerio(ele).find('a').attr('href');
387+
388+
if (!chapterPath) return null;
389+
390+
return {
391+
name: chapterName,
392+
path: new URL(chapterPath, this.site).pathname.substring(1),
393+
};
394+
})
395+
.get()
396+
.filter(chapter => chapter !== null) as Plugin.ChapterItem[];
397+
398+
return {
399+
chapters,
400+
};
401+
}
335402

336403
return {
337-
chapters,
404+
chapters: [],
338405
};
339406
}
340407

@@ -362,6 +429,7 @@ class NovelFire implements Plugin.PluginBase {
362429
): Promise<Plugin.NovelItem[]> {
363430
if (page === 1) {
364431
this.novelList = [];
432+
this.draw = 0;
365433
}
366434
const params = new URLSearchParams();
367435
params.append('keyword', searchTerm);
@@ -544,15 +612,3 @@ class NovelFireAjaxNotFound extends Error {
544612
this.name = 'NovelFireAjaxError';
545613
}
546614
}
547-
548-
function deSlash(url: string): string {
549-
let clean: string;
550-
551-
if (url.charAt(0) == '/') {
552-
clean = url.substring(1);
553-
} else {
554-
clean = url;
555-
}
556-
557-
return clean;
558-
}

0 commit comments

Comments
 (0)