Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 43 additions & 12 deletions plugins/english/fucknovelpia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,41 @@ class FuckNovelpia implements Plugin.PluginBase {
name = 'FuckNovelpia';
icon = 'src/en/fucknovelpia/icon.png';
site = 'https://fucknovelpia.com/';
version = '1.1.1';
version = '1.1.2';

browserHeaders = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept-Language': 'en-US,en;q=0.9',
};

sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

// The site intermittently swaps in anti-scrape pages (nginx 403, a custom
// "Blocked" 429 page, or a Cloudflare rate-limit page) whose bodies contain
// no reader markup. Parsing those silently produced empty chapters/novel
// lists (issue #2540), so surface them as errors instead, and wait out the
// rate limiter once before giving up.
async fetchHtml(url: string): Promise<string> {
let response = await fetchApi(url, { headers: this.browserHeaders });
if (response.status === 429) {
// Its 429s usually omit Retry-After, and Number(null) is 0, so parse it
// explicitly to keep the fallback wait instead of retrying instantly.
const retryAfter = parseInt(
response.headers.get('Retry-After') || '',
10,
);
const waitMs = (isNaN(retryAfter) ? 2 : Math.min(retryAfter, 10)) * 1000;
await this.sleep(waitMs);
response = await fetchApi(url, { headers: this.browserHeaders });
}
if (!response.ok) {
throw new Error(`FuckNovelpia returned HTTP ${response.status}`);
}
return await response.text();
}

// Returns false once the site has silently clamped us past the real last page.
hasRequestedPage(cheerio: CheerioAPI, requestedPage: number): boolean {
Expand Down Expand Up @@ -52,8 +86,7 @@ class FuckNovelpia implements Plugin.PluginBase {

async parseLatestUpdates() {
const link = this.site + 'updates.php';
const response = await fetchApi(link);
const html = await response.text();
const html = await this.fetchHtml(link);
const novels: Plugin.NovelItem[] = [];
let tempNovel: Partial<Plugin.NovelItem> = {};
const seen = new Set<string>();
Expand Down Expand Up @@ -168,8 +201,7 @@ class FuckNovelpia implements Plugin.PluginBase {

const link = this.site + 'search.php?' + params.toString();

const result = await fetchApi(link);
const body = await result.text();
const body = await this.fetchHtml(link);

const loadedCheerio = parseHTML(body);

Expand All @@ -178,8 +210,7 @@ class FuckNovelpia implements Plugin.PluginBase {
}

async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(this.site + novelPath);
const body = await result.text();
const body = await this.fetchHtml(this.site + novelPath);

const loadedCheerio = parseHTML(body);

Expand Down Expand Up @@ -267,14 +298,15 @@ class FuckNovelpia implements Plugin.PluginBase {
}

async parseChapter(chapterPath: string): Promise<string> {
const result = await fetchApi(this.site + chapterPath);
const body = await result.text();
const body = await this.fetchHtml(this.site + chapterPath);
const $ = parseHTML(body);

const chapter = $('.reader').first();

if (!chapter.length) {
return '';
// A 200 without reader markup is a changed/blocked page, not an empty
// chapter — never report a false empty-success.
throw new Error('Chapter content not found in page');
}

// Remove things that aren't part of the chapter
Expand Down Expand Up @@ -311,8 +343,7 @@ class FuckNovelpia implements Plugin.PluginBase {

const link = this.site + 'search.php?' + params.toString();

const result = await fetchApi(link);
const body = await result.text();
const body = await this.fetchHtml(link);
const loadedCheerio = parseHTML(body);

if (!this.hasRequestedPage(loadedCheerio, page)) return [];
Expand Down
Loading