fix: Genesis (#1939)
* Fix (genesis): Browse + Novel Parsing * Fix (genesis): Novel ToC parsing * Remove commented code * Fix (genesis): Mark locked + Hide locked option * Fix (genesis): Fix chapter names + path * Cleanup and comments * Fix (genesis): Parse chapters - keys * Fix (genesis): Parse keys for chapters * Fix (genesis): Allow gifs * Fix (genesis): Version bump * Semantic versioning
This commit is contained in:
+161
-425
@@ -1,16 +1,9 @@
|
|||||||
|
import { CheerioAPI, load } from 'cheerio';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
/**
|
import { storage } from '@libs/storage';
|
||||||
* Example for novel API:
|
|
||||||
* https://genesistudio.com/novels/dlh/__data.json?x-sveltekit-invalidated=001
|
|
||||||
* -> to view novel remove '__data.json?x-sveltekit-invalidated=001'
|
|
||||||
*
|
|
||||||
* Example for chapter API:
|
|
||||||
* https://genesistudio.com/viewer/2443/__data.json?x-sveltekit-invalidated=001
|
|
||||||
* -> to view chapter remove '__data.json?x-sveltekit-invalidated=001'
|
|
||||||
*/
|
|
||||||
|
|
||||||
class Genesis implements Plugin.PluginBase {
|
class Genesis implements Plugin.PluginBase {
|
||||||
id = 'genesistudio';
|
id = 'genesistudio';
|
||||||
@@ -18,7 +11,17 @@ class Genesis implements Plugin.PluginBase {
|
|||||||
icon = 'src/en/genesis/icon.png';
|
icon = 'src/en/genesis/icon.png';
|
||||||
customCSS = 'src/en/genesis/customCSS.css';
|
customCSS = 'src/en/genesis/customCSS.css';
|
||||||
site = 'https://genesistudio.com';
|
site = 'https://genesistudio.com';
|
||||||
version = '1.1.2';
|
api = 'https://api.genesistudio.com';
|
||||||
|
version = '2.0.0';
|
||||||
|
|
||||||
|
hideLocked = storage.get('hideLocked');
|
||||||
|
pluginSettings = {
|
||||||
|
hideLocked: {
|
||||||
|
value: '',
|
||||||
|
label: 'Hide locked chapters',
|
||||||
|
type: 'Switch',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
imageRequestInit?: Plugin.ImageRequestInit | undefined = {
|
imageRequestInit?: Plugin.ImageRequestInit | undefined = {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -26,434 +29,167 @@ class Genesis implements Plugin.PluginBase {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async parseNovels(json: any[]): Promise<Plugin.SourceNovel[]> {
|
async parseNovelJSON(json: any[]): Promise<Plugin.SourceNovel[]> {
|
||||||
return json.map((novel: any) => ({
|
return json.map((novel: any) => ({
|
||||||
name: novel.novel_title,
|
name: novel.novel_title,
|
||||||
path: `/novels/${novel.abbreviation}`,
|
path: `/novels/${novel.abbreviation}`.trim(),
|
||||||
cover: novel.cover,
|
cover: `${this.api}/storage/v1/object/public/directus/${novel.cover}.png`,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> {
|
||||||
pageNo: number,
|
// There is only one page of results, and no known page function, so do not try
|
||||||
{
|
|
||||||
showLatestNovels,
|
|
||||||
filters,
|
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
|
||||||
): Promise<Plugin.NovelItem[]> {
|
|
||||||
if (pageNo !== 1) return [];
|
if (pageNo !== 1) return [];
|
||||||
let link = `${this.site}/api/novels/search?`;
|
// Only 14 results, no use in sorting or status
|
||||||
if (showLatestNovels) {
|
// Also all novels are Ongoing with no Completed, can't test status filter
|
||||||
link += 'serialization=All&sort=Recent';
|
const link = `${this.site}/api/directus/novels?status=published&fields=["cover","novel_title","cover","abbreviation"]&limit=-1`;
|
||||||
} else {
|
|
||||||
if (filters?.genres.value.length) {
|
|
||||||
link += 'genres=' + filters.genres.value.join('&genres=') + '&';
|
|
||||||
}
|
|
||||||
if (filters?.storyStatus.value) {
|
|
||||||
link += 'serialization=' + `${filters.storyStatus.value}` + '&';
|
|
||||||
}
|
|
||||||
if (filters?.sort.value) {
|
|
||||||
link += 'sort=' + `${filters.sort.value}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const json = await fetchApi(link).then(r => r.json());
|
const json = await fetchApi(link).then(r => r.json());
|
||||||
return this.parseNovels(json);
|
return this.parseNovelJSON(json);
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to extract novel data from nodes
|
|
||||||
extractData(nodes: any[]): any {
|
|
||||||
return nodes
|
|
||||||
.filter((node: { type: string }) => node.type === 'data')
|
|
||||||
.map((node: { data: any }) => node.data)[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
const url = `${this.site}${novelPath}/__data.json?x-sveltekit-invalidated=001`;
|
const abbreviation = novelPath.replace('/novels/', '');
|
||||||
|
const url = `${this.site}/api/directus/novels/by-abbreviation/${abbreviation}`;
|
||||||
|
|
||||||
// Fetch the novel's data in JSON format
|
// Fetch the novel's data in JSON format
|
||||||
const json = await fetchApi(url).then(r => r.json());
|
const raw = await fetchApi(url);
|
||||||
const nodes = json.nodes;
|
const json = await raw.json();
|
||||||
|
|
||||||
// Extract the main novel data from the nodes
|
|
||||||
const data = this.extractData(nodes);
|
|
||||||
|
|
||||||
// Initialize the novel object with default values
|
// Initialize the novel object with default values
|
||||||
const novel: Plugin.SourceNovel = {
|
const parse = await this.parseNovelJSON([json]);
|
||||||
path: novelPath,
|
const novel: Plugin.SourceNovel = parse[0];
|
||||||
name: '',
|
novel.summary = json.synopsis;
|
||||||
cover: '',
|
novel.author = json.author;
|
||||||
summary: '',
|
const map: Record<string, string> = {
|
||||||
author: '',
|
ongoing: NovelStatus.Ongoing,
|
||||||
status: 'Unknown',
|
hiatus: NovelStatus.OnHiatus,
|
||||||
chapters: [],
|
dropped: NovelStatus.Cancelled,
|
||||||
|
cancelled: NovelStatus.Cancelled,
|
||||||
|
completed: NovelStatus.Completed,
|
||||||
|
unknown: NovelStatus.Unknown,
|
||||||
};
|
};
|
||||||
|
novel.status = map[json.serialization.toLowerCase()] ?? NovelStatus.Unknown;
|
||||||
// Parse and assign novel metadata (title, cover, summary, author, etc.)
|
if (json.cover) {
|
||||||
this.populateNovelMetadata(novel, data);
|
const url = `${this.site}/api/directus-file/${json.cover}`;
|
||||||
|
const imgJson = await (await fetchApi(url)).json();
|
||||||
// Parse the chapters if available and assign them to the novel object
|
console.log(imgJson.type);
|
||||||
novel.chapters = this.extractChapters(data);
|
novel.cover = `${this.api}/storage/v1/object/public/directus/${json.cover}.png`;
|
||||||
|
if (imgJson.type == 'image/gif') {
|
||||||
return novel;
|
novel.cover = novel.cover?.replace('.png', '.gif');
|
||||||
}
|
} else if (imgJson.type !== 'image/png') {
|
||||||
|
novel.cover = novel.cover?.replace(
|
||||||
// Helper function to populate novel metadata
|
'.png',
|
||||||
populateNovelMetadata(novel: Plugin.SourceNovel, data: any): void {
|
'.' + imgJson.type.toString().split('/')[1],
|
||||||
for (const key in data) {
|
|
||||||
const value = data[key];
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof value === 'object' &&
|
|
||||||
value !== null &&
|
|
||||||
'novel_title' in value
|
|
||||||
) {
|
|
||||||
novel.name = data[value.novel_title] || 'Unknown Title';
|
|
||||||
novel.cover = data[value.cover] || '';
|
|
||||||
novel.summary = data[value.synopsis] || '';
|
|
||||||
novel.author = data[value.author] || 'Unknown Author';
|
|
||||||
novel.genres =
|
|
||||||
(data[value.genres] as number[])
|
|
||||||
.map((genreId: number) => data[genreId])
|
|
||||||
.join(', ') || 'Unknown Genre';
|
|
||||||
novel.status = value.release_days ? 'Ongoing' : 'Completed';
|
|
||||||
break; // Break the loop once metadata is found
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to extract and format chapters
|
|
||||||
extractChapters(data: any): Plugin.ChapterItem[] {
|
|
||||||
const chapterKey = 'chapters';
|
|
||||||
const chapters: Plugin.ChapterItem[] = [];
|
|
||||||
|
|
||||||
// Iterate over each property in data to find chapter containers
|
|
||||||
for (const key in data) {
|
|
||||||
const value = data[key];
|
|
||||||
|
|
||||||
// Look for an object with a 'chapters' key
|
|
||||||
if (value && typeof value === 'object' && chapterKey in value) {
|
|
||||||
const chaptersIndexKey = value[chapterKey];
|
|
||||||
const chapterData = data[chaptersIndexKey];
|
|
||||||
if (!chapterData || typeof chapterData !== 'object') continue;
|
|
||||||
|
|
||||||
// Dynamically get chapter indices from every category (free, premium, etc.)
|
|
||||||
let allChapterIndices: number[] = [];
|
|
||||||
for (const category of Object.keys(chapterData)) {
|
|
||||||
const chapterIndices = data[chapterData[category]];
|
|
||||||
if (Array.isArray(chapterIndices)) {
|
|
||||||
allChapterIndices = allChapterIndices.concat(chapterIndices);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format each chapter and add only valid ones
|
|
||||||
const formattedChapters = allChapterIndices
|
|
||||||
.map((index: number) => data[index])
|
|
||||||
.map((chapter: any) => this.formatChapter(chapter, data))
|
|
||||||
.filter((chapter): chapter is Plugin.ChapterItem => chapter !== null);
|
|
||||||
|
|
||||||
chapters.push(...formattedChapters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapters;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to format an individual chapter
|
|
||||||
formatChapter(chapter: any, data: any): Plugin.ChapterItem | null {
|
|
||||||
const { id, chapter_title, chapter_number, required_tier, date_created } =
|
|
||||||
chapter;
|
|
||||||
|
|
||||||
// Destructure from data using computed property names based on chapter keys
|
|
||||||
const {
|
|
||||||
[id]: chapterId,
|
|
||||||
[chapter_title]: chapterTitle,
|
|
||||||
[chapter_number]: chapterNumberVal,
|
|
||||||
[required_tier]: requiredTierVal,
|
|
||||||
[date_created]: dateCreated,
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if (
|
|
||||||
chapterId &&
|
|
||||||
chapterTitle &&
|
|
||||||
chapterNumberVal >= 0 &&
|
|
||||||
requiredTierVal !== null &&
|
|
||||||
dateCreated
|
|
||||||
) {
|
|
||||||
const chapterNumber = parseInt(String(chapterNumberVal), 10) || 0;
|
|
||||||
const requiredTier = parseInt(String(requiredTierVal), 10) || 0;
|
|
||||||
|
|
||||||
// Only process chapters that do not require a premium tier
|
|
||||||
if (requiredTier === 0) {
|
|
||||||
return {
|
|
||||||
name: `Chapter ${chapterNumber} - ${chapterTitle}`,
|
|
||||||
path: `/viewer/${chapterId}`,
|
|
||||||
releaseTime: dateCreated,
|
|
||||||
chapterNumber: chapterNumber,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// // Helper function to extract and format chapters
|
|
||||||
// extractChapters(data: any): Plugin.ChapterItem[] {
|
|
||||||
// for (const key in data) {
|
|
||||||
// const value = data[key];
|
|
||||||
//
|
|
||||||
// // Change string here if the chapters are stored under a different key
|
|
||||||
// const chapterKey = 'chapters';
|
|
||||||
// if (typeof value === 'object' && value !== null && chapterKey in value) {
|
|
||||||
// const chapterData = this.decodeData(data[value[chapterKey]]);
|
|
||||||
//
|
|
||||||
// // Object.values will give us an array of arrays (any[][])
|
|
||||||
// const chapterArrays: any[][] = Object.values(chapterData);
|
|
||||||
//
|
|
||||||
// // Flatten and format the chapters
|
|
||||||
// return chapterArrays.flatMap((chapters: any[]) => {
|
|
||||||
// return chapters
|
|
||||||
// .map((chapter: any) => this.formatChapter(chapter))
|
|
||||||
// .filter(
|
|
||||||
// (chapter): chapter is Plugin.ChapterItem => chapter !== null,
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return [];
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Helper function to format an individual chapter
|
|
||||||
// formatChapter(chapter: any): Plugin.ChapterItem | null {
|
|
||||||
// const { id, chapter_title, chapter_number, required_tier, date_created } =
|
|
||||||
// chapter;
|
|
||||||
//
|
|
||||||
// // Ensure required fields are present and valid
|
|
||||||
// if (
|
|
||||||
// id &&
|
|
||||||
// chapter_title &&
|
|
||||||
// chapter_number >= 0 &&
|
|
||||||
// required_tier !== null &&
|
|
||||||
// date_created
|
|
||||||
// ) {
|
|
||||||
// const number = parseInt(chapter_number, 10) || 0;
|
|
||||||
// const requiredTier = parseInt(required_tier, 10) || 0;
|
|
||||||
//
|
|
||||||
// // Only process chapters with a 'requiredTier' of 0
|
|
||||||
// if (requiredTier === 0) {
|
|
||||||
// return {
|
|
||||||
// name: `Chapter ${number}: ${chapter_title}`,
|
|
||||||
// path: `/viewer/${id}`,
|
|
||||||
// releaseTime: date_created,
|
|
||||||
// chapterNumber: number,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// decodeData(code: any) {
|
|
||||||
// const offset = this.getOffsetIndex(code);
|
|
||||||
// const params = this.getDecodeParams(code);
|
|
||||||
// const constant = this.getConstant(code);
|
|
||||||
// const data = this.getStringsArrayRaw(code);
|
|
||||||
//
|
|
||||||
// const getDataAt = (x: number) => data[x - offset];
|
|
||||||
//
|
|
||||||
// //reshuffle data array
|
|
||||||
// // eslint-disable-next-line no-constant-condition
|
|
||||||
// while (true) {
|
|
||||||
// try {
|
|
||||||
// const some_number = this.applyDecodeParams(params, getDataAt);
|
|
||||||
// if (some_number === constant) break;
|
|
||||||
// else data.push(data.shift());
|
|
||||||
// } catch (err) {
|
|
||||||
// data.push(data.shift());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return this.getChapterData(code, getDataAt);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// getOffsetIndex(code: string) {
|
|
||||||
// // @ts-ignore
|
|
||||||
// const string = /{(\w+)=\1-0x(?<offset>[0-9a-f]+);/.exec(code).groups.offset;
|
|
||||||
// return parseInt(string, 16);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * @returns {string[]}
|
|
||||||
// */
|
|
||||||
// getStringsArrayRaw(code: string) {
|
|
||||||
// // @ts-ignore
|
|
||||||
// let json = /function \w+\(\){var \w+=(?<array>\['.+']);/.exec(code).groups
|
|
||||||
// .array;
|
|
||||||
//
|
|
||||||
// //replace string single quotes with double quotes and add escaped chars
|
|
||||||
// json = json.replace(/'(.+?)'([,\]])/g, (match, p1, p2) => {
|
|
||||||
// return `"${p1.replace(/\\x([0-9a-z]{2})/g, (match: any, p1: string) => {
|
|
||||||
// //hexadecimal unicode escape chars
|
|
||||||
// return String.fromCharCode(parseInt(p1, 16));
|
|
||||||
// })}"${p2}`;
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// return JSON.parse(json);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * @returns {{offset: number, divider: number, negated: boolean}[][]}
|
|
||||||
// */
|
|
||||||
// getDecodeParams(code: string) {
|
|
||||||
// // @ts-ignore
|
|
||||||
// const jsDecodeInt = /while\(!!\[]\){try{var \w+=(?<code>.+?);/.exec(code)
|
|
||||||
// .groups.code;
|
|
||||||
// const decodeSections = jsDecodeInt.split('+');
|
|
||||||
// const params = [];
|
|
||||||
// for (const section of decodeSections) {
|
|
||||||
// params.push(this.decodeParamSection(section));
|
|
||||||
// }
|
|
||||||
// return params;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * @param {string} section
|
|
||||||
// * @returns {{offset: number, divider: number, negated: boolean}[]}
|
|
||||||
// */
|
|
||||||
// decodeParamSection(section: string) {
|
|
||||||
// const sections = section.split('*');
|
|
||||||
// const params = [];
|
|
||||||
// for (const section of sections) {
|
|
||||||
// // @ts-ignore
|
|
||||||
// const offsetStr = /parseInt\(\w+\(0x(?<offset>[0-9a-f]+)\)\)/.exec(
|
|
||||||
// section,
|
|
||||||
// ).groups.offset;
|
|
||||||
// const offset = parseInt(offsetStr, 16);
|
|
||||||
// // @ts-ignore
|
|
||||||
// const dividerStr = /\/0x(?<divider>[0-9a-f]+)/.exec(section).groups
|
|
||||||
// .divider;
|
|
||||||
// const divider = parseInt(dividerStr, 16);
|
|
||||||
// const negated = section.includes('-');
|
|
||||||
// params.push({ offset, divider, negated });
|
|
||||||
// }
|
|
||||||
// return params;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// getConstant(code: string) {
|
|
||||||
// // @ts-ignore
|
|
||||||
// const constantStr = /}}}\(\w+,0x(?<constant>[0-9a-f]+)\),/.exec(code).groups
|
|
||||||
// .constant;
|
|
||||||
// return parseInt(constantStr, 16);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// getChapterData(
|
|
||||||
// code: string,
|
|
||||||
// getDataAt: { (x: number): any; (arg0: number): any },
|
|
||||||
// ) {
|
|
||||||
// let chapterDataStr =
|
|
||||||
// // @ts-ignore
|
|
||||||
// /\),\(function\(\){var \w+=\w+;return(?<data>{.+?});/.exec(code).groups
|
|
||||||
// .data;
|
|
||||||
//
|
|
||||||
// //replace hex with decimal
|
|
||||||
// chapterDataStr = chapterDataStr.replace(/:0x([0-9a-f]+)/g, (match, p1) => {
|
|
||||||
// const hex = parseInt(p1, 16);
|
|
||||||
// return `: ${hex}`;
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// //replace ![] with false and !![] with true
|
|
||||||
// chapterDataStr = chapterDataStr
|
|
||||||
// .replace(/:!!\[]/g, ':true')
|
|
||||||
// .replace(/:!\[]/g, ':false');
|
|
||||||
//
|
|
||||||
// //replace string single quotes with double quotes and add escaped chars
|
|
||||||
// chapterDataStr = chapterDataStr.replace(
|
|
||||||
// /'(.+?)'([,\]}:])/g,
|
|
||||||
// (match, p1, p2) => {
|
|
||||||
// return `"${p1.replace(/\\x([0-9a-z]{2})/g, (match: any, p1: string) => {
|
|
||||||
// //hexadecimal unicode escape chars
|
|
||||||
// return String.fromCharCode(parseInt(p1, 16));
|
|
||||||
// })}"${p2}`;
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// //parse the data getting methods
|
|
||||||
// chapterDataStr = chapterDataStr.replace(
|
|
||||||
// // @ts-ignore
|
|
||||||
// /:\w+\(0x(?<offset>[0-9a-f]+)\)/g,
|
|
||||||
// (match, p1) => {
|
|
||||||
// const offset = parseInt(p1, 16);
|
|
||||||
// return `:${JSON.stringify(getDataAt(offset))}`;
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// return JSON.parse(chapterDataStr);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * @param {{offset: number, divider: number, negated: boolean}[][]} params
|
|
||||||
// * @param {function(number): string} getDataAt
|
|
||||||
// */
|
|
||||||
// applyDecodeParams(
|
|
||||||
// params: { offset: number; divider: number; negated: boolean }[][],
|
|
||||||
// getDataAt: { (x: number): any; (arg0: any): string },
|
|
||||||
// ) {
|
|
||||||
// let res = 0;
|
|
||||||
// for (const paramAdd of params) {
|
|
||||||
// let resInner = 1;
|
|
||||||
// for (const paramMul of paramAdd) {
|
|
||||||
// resInner *= parseInt(getDataAt(paramMul.offset)) / paramMul.divider;
|
|
||||||
// if (paramMul.negated) resInner *= -1;
|
|
||||||
// }
|
|
||||||
// res += resInner;
|
|
||||||
// }
|
|
||||||
// return res;
|
|
||||||
// }
|
|
||||||
|
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
|
||||||
const url = `${this.site}${chapterPath}/__data.json?x-sveltekit-invalidated=001`;
|
|
||||||
|
|
||||||
// Fetch the novel's data in JSON format
|
|
||||||
const json = await fetchApi(url).then(r => r.json());
|
|
||||||
const nodes = json.nodes;
|
|
||||||
|
|
||||||
// Extract the main novel data from the nodes
|
|
||||||
const data = this.extractData(nodes);
|
|
||||||
|
|
||||||
// Look for chapter container with required fields
|
|
||||||
const contentKey = 'content';
|
|
||||||
const notesKey = 'notes';
|
|
||||||
const footnotesKey = 'footnotes';
|
|
||||||
|
|
||||||
// Iterate over each property in data to find chapter containers
|
|
||||||
for (const key in data) {
|
|
||||||
const mapping = data[key];
|
|
||||||
|
|
||||||
// Check container for keys that match the required fields
|
|
||||||
if (
|
|
||||||
mapping &&
|
|
||||||
typeof mapping === 'object' &&
|
|
||||||
contentKey in mapping &&
|
|
||||||
notesKey in mapping &&
|
|
||||||
footnotesKey in mapping
|
|
||||||
) {
|
|
||||||
// Retrieve the chapter's content, notes, and footnotes using the mapping.
|
|
||||||
const content = data[mapping[contentKey]];
|
|
||||||
const notes = data[mapping[notesKey]];
|
|
||||||
const footnotes = data[mapping[footnotesKey]];
|
|
||||||
|
|
||||||
// Return the combined parts with appropriate formatting
|
|
||||||
return (
|
|
||||||
content +
|
|
||||||
(notes ? `<h2>Notes</h2><br>${notes}` : '') +
|
|
||||||
(footnotes ?? '')
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no mapping object was found, return an empty string or handle appropriately.
|
// Parse the chapters if available and assign them to the novel object
|
||||||
return '';
|
novel.chapters = await this.extractChapters(json.id);
|
||||||
|
|
||||||
|
return novel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to extract and format chapters
|
||||||
|
async extractChapters(id: string): Plugin.ChapterItem[] {
|
||||||
|
const url = `${this.site}/api/novels-chapter/${id}`;
|
||||||
|
|
||||||
|
// Fetch the chapter data in JSON format
|
||||||
|
const raw = await fetchApi(url);
|
||||||
|
const json = await raw.json();
|
||||||
|
|
||||||
|
// Format each chapter and add only valid ones
|
||||||
|
const chapters = json.data.chapters
|
||||||
|
.map(index => {
|
||||||
|
const title = index.chapter_title;
|
||||||
|
const chapterPath = `/viewer/${index.id}`;
|
||||||
|
const isLocked = !index.isUnlocked;
|
||||||
|
if (this.hideLocked && isLocked) return null;
|
||||||
|
const chapterName = isLocked ? '🔒 ' + title : title;
|
||||||
|
const chapterNum = index.chapter_number;
|
||||||
|
|
||||||
|
if (!chapterPath) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: chapterName,
|
||||||
|
path: chapterPath,
|
||||||
|
chapterNumber: Number(chapterNum),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(chapter => chapter !== null) as Plugin.ChapterItem[];
|
||||||
|
|
||||||
|
return chapters;
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
|
const url = `${this.site}${chapterPath}`;
|
||||||
|
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;
|
||||||
|
|
||||||
|
let URLs = [];
|
||||||
|
let code;
|
||||||
|
|
||||||
|
// Find URL with API Key
|
||||||
|
const srcs = $('head')
|
||||||
|
.find('script')
|
||||||
|
.map(function () {
|
||||||
|
const src = $(this).attr('src');
|
||||||
|
if (src in URLs) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
URLs.push(src);
|
||||||
|
})
|
||||||
|
.toArray();
|
||||||
|
for (let 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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?select=id,chapter_title,chapter_number,chapter_content,status,novel&id=eq.${id}&status=eq.released`;
|
||||||
|
|
||||||
|
const chQuery = await fetchApi(path, {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchNovels(
|
async searchNovels(
|
||||||
@@ -461,18 +197,18 @@ class Genesis implements Plugin.PluginBase {
|
|||||||
pageNo: number,
|
pageNo: number,
|
||||||
): Promise<Plugin.SourceNovel[]> {
|
): Promise<Plugin.SourceNovel[]> {
|
||||||
if (pageNo !== 1) return [];
|
if (pageNo !== 1) return [];
|
||||||
const url = `${this.site}/api/novels/search?serialization=All&sort=Popular&title=${encodeURIComponent(searchTerm)}`;
|
// TODO: Figure out how to search
|
||||||
|
const url = `${this.site}/api/novels/search?title=${encodeURIComponent(searchTerm)}`;
|
||||||
const json = await fetchApi(url).then(r => r.json());
|
const json = await fetchApi(url).then(r => r.json());
|
||||||
return this.parseNovels(json);
|
return this.parseNovelJSON(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
filters = {
|
filters = {
|
||||||
sort: {
|
sort: {
|
||||||
label: 'Sort Results By',
|
label: 'Sort Results By',
|
||||||
value: 'Popular',
|
value: 'Date',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Popular', value: 'Popular' },
|
{ label: 'Date', value: 'Date' },
|
||||||
{ label: 'Recent', value: 'Recent' },
|
|
||||||
{ label: 'Views', value: 'Views' },
|
{ label: 'Views', value: 'Views' },
|
||||||
],
|
],
|
||||||
type: FilterTypes.Picker,
|
type: FilterTypes.Picker,
|
||||||
|
|||||||
Reference in New Issue
Block a user