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
95 changes: 26 additions & 69 deletions plugins/english/genesis.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { load } from 'cheerio';
import { fetchApi } from '@libs/fetch';
import { Filters, FilterTypes } from '@libs/filterInputs';
import { Plugin } from '@/types/plugin';
Expand All @@ -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 = {
Expand Down Expand Up @@ -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;
Expand All @@ -138,75 +141,21 @@ class Genesis implements Plugin.PluginBase {
}

async parseChapter(chapterPath: string): Promise<string> {
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', '<br/>');
return ch;
return content.replaceAll('\n', '<br/>');
}

async searchNovels(
Expand Down Expand Up @@ -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;
};
};
Loading