diff --git a/plugins/english/novelight.ts b/plugins/english/novelight.ts index 92e0211db..4535668bc 100644 --- a/plugins/english/novelight.ts +++ b/plugins/english/novelight.ts @@ -1,5 +1,5 @@ import { load as parseHTML } from 'cheerio'; -import { fetchApi } from '@libs/fetch'; +import { fetchApi, FetchInit } from '@libs/fetch'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; import { Filters, FilterTypes } from '@libs/filterInputs'; @@ -7,13 +7,47 @@ import { defaultCover } from '@libs/defaultCover'; import dayjs from 'dayjs'; import { storage } from '@libs/storage'; +type ChapterPaginationOption = { + value: string; + start: number; +}; + +type RawChapter = { + name: string; + path: string; + releaseTime?: string; + isLocked: boolean; +}; + class Novelight implements Plugin.PagePlugin { id = 'novelight'; name = 'Novelight'; - version = '1.1.5'; + version = '1.1.6'; icon = 'src/en/novelight/icon.png'; site = 'https://novelight.net/'; + // Browser-like headers (important for Cloudflare-fronted sites, which + // may serve a bot-check page to requests without a User-Agent). + private headers = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36', + Referer: this.site, + 'Accept-Language': 'en-US,en;q=0.9', + }; + + // Throw (carrying the HTTP status) on a refused response so a + // runner-side block is reported INCONCLUSIVE per docs/testing.md + // instead of being parsed into a false empty-result FAIL. + private async fetchSite(url: string, init?: FetchInit) { + const res = await fetchApi(url, init); + if (!res.ok) { + throw Object.assign(new Error('Request failed: ' + res.status), { + status: res.status, + }); + } + return res; + } + hideLocked = storage.get('hideLocked'); pluginSettings = { hideLocked: { @@ -57,7 +91,9 @@ class Novelight implements Plugin.PagePlugin { url += `?&ordering=popularity&page=${pageNo}`; } - const body = await fetchApi(url).then(r => r.text()); + const body = await this.fetchSite(url, { headers: this.headers }).then(r => + r.text(), + ); const loadedCheerio = parseHTML(body); @@ -87,16 +123,22 @@ class Novelight implements Plugin.PagePlugin { async parseNovel( novelPath: string, ): Promise { - const body = await fetchApi(this.site + novelPath).then(r => r.text()); + const body = await this.fetchSite(this.site + novelPath, { + headers: this.headers, + }).then(r => r.text()); const loadedCheerio = parseHTML(body); + // The chapter select tiles the chapter list, so one app page per site + // range keeps `totalPages` and the served pages in agreement. + const totalPages = this.parseChapterPagination(loadedCheerio).length || 1; + const novel: Plugin.SourceNovel & { totalPages: number } = { path: novelPath, name: loadedCheerio('h1').text() || 'Untitled', cover: this.site + loadedCheerio('.poster > img').attr('src'), summary: loadedCheerio('section.text-info.section > p').text(), - totalPages: loadedCheerio('#select-pagination-chapter > option').length, + totalPages, chapters: [], }; @@ -139,21 +181,50 @@ class Novelight implements Plugin.PagePlugin { return novel; } - async parsePage(novelPath: string, page: string): Promise { - const rawBody = await fetchApi(this.site + novelPath).then(r => r.text()); - const csrftoken = rawBody?.match(/window\.CSRF_TOKEN = "([^"]+)"/)?.[1]; - const bookId = rawBody?.match(/const OBJECT_BY_COMMENT = ([0-9]+)/)?.[1]; - const totalPages = parseInt( - rawBody - ?.match(/