diff --git a/plugins/english/genesis.ts b/plugins/english/genesis.ts index e6644b3b1..b596802b9 100644 --- a/plugins/english/genesis.ts +++ b/plugins/english/genesis.ts @@ -1,4 +1,3 @@ -import { load } from 'cheerio'; import { fetchApi } from '@libs/fetch'; import { Filters, FilterTypes } from '@libs/filterInputs'; import { Plugin } from '@/types/plugin'; @@ -13,7 +12,7 @@ class Genesis implements Plugin.PluginBase { customCSS = 'src/en/genesis/customCSS.css'; site = 'https://genesistudio.com'; api = 'https://api.genesistudio.com'; - version = '2.0.1'; + version = '2.0.3'; hideLocked = storage.get('hideLocked'); pluginSettings = { @@ -114,12 +113,16 @@ class Genesis implements Plugin.PluginBase { const raw = await fetchApi(url); const json: ChapterJSON = await raw.json(); - // Format each chapter and add only valid ones + // Only released chapters are listed; the pre-2.0.1 Supabase query filtered on status=eq.released + // (every chapter observed had status released: 5 of 5 and 157 of 157 in the listing, 2026-10-15) const chapters = json.data.chapters + .filter(index => index.status === 'released') .map(index => { const title = index.chapter_title; - const chapterPath = `/viewer/${index.id}`; const isLocked = !index.isUnlocked; + const chapterPath = isLocked + ? `/viewer/locked/${index.id}` + : `/viewer/${index.id}`; if (this.hideLocked && isLocked) return null; const chapterName = isLocked ? '🔒 ' + title : title; const chapterNum = index.chapter_number; @@ -138,75 +141,21 @@ class Genesis implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const url = `${this.site}${chapterPath}`; + if (chapterPath.startsWith('/viewer/locked/')) { + throw new Error('This chapter is locked on the site'); + } const id = chapterPath.replace('/viewer/', ''); - // Fetch the novel's data in JSON format - const raw = await fetchApi(url); - const $ = load(await raw.text()); - let external_api; - let apikey; - - const URLs: string[] = []; - let code; + // Anonymous requests for paid chapters get HTTP 401, error "Authentication required", + // and chapter_content "None" (checked 2026-10-15), so a refusal is treated as locked + const res = await fetchApi(`${this.site}/api/chapters/${id}/content`); + const json: ChapterContentJSON = await res.json(); - $('head script[src]').each((_, el) => { - const src = $(el).attr('src')!; - if (!URLs.includes(src)) { - URLs.push(src); - } - }); - - for (const src of URLs) { - const script = await fetchApi(`${this.site}${src}`); - const raw = await script.text(); - if (raw.includes('sb_publishable')) { - code = raw; - break; - } - } - if (!code) { - throw new Error('Failed to find API Key'); - } - // Find right segment of code - let arr = code.split(';'); - for (const seg of arr) { - if (seg.includes('sb_publishable')) { - code = seg; - break; - } + const content = json.data?.chapter_content; + if (!res.ok || json.error || !content || content === 'None') { + throw new Error('This chapter is locked on the site'); } - arr = code.split('"'); - for (const seg of arr) { - if (seg.includes('https')) { - external_api = seg; - continue; - } - if (seg.includes('sb_publishable')) { - apikey = seg; - continue; - } - } - - const path = `${external_api}/rest/v1/chapters`; - const search = new URLSearchParams({ - select: 'id,chapter_title,chapter_number,chapter_content,status,novel', - id: `eq.${id}`, - status: 'eq.released', - }); - - const chQuery = await fetchApi(`${path}?${search}`, { - method: 'GET', - headers: { - // Cookie: 'csrftoken=' + csrftoken, - Referer: this.site, - 'apikey': apikey, - 'x-client-info': 'supabase-ssr/0.7.0 createBrowserClient', - }, - }); - const json = await chQuery.json(); - const ch = json[0].chapter_content.replaceAll('\n', '
'); - return ch; + return content.replaceAll('\n', '
'); } async searchNovels( @@ -306,7 +255,15 @@ type ChapterJSON = { id: string; chapter_number: number; chapter_title: string; + status: string; isUnlocked: boolean; }[]; }; }; + +type ChapterContentJSON = { + error?: string; + data?: { + chapter_content?: string | null; + }; +};