Improvement: Genesis: Fix (#1540)

* genesis: fix

* genesis: fix

* genesis: fix popularNovels

* genesis: add line breaks

* genesis: revert version

* genesis: fix popular novels
This commit is contained in:
Batorian
2025-03-01 03:39:02 +01:00
committed by GitHub
parent 748b9368cb
commit c1bd8cfed8
+351 -233
View File
@@ -18,7 +18,7 @@ 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.0.7'; version = '1.1.0';
imageRequestInit?: Plugin.ImageRequestInit | undefined = { imageRequestInit?: Plugin.ImageRequestInit | undefined = {
headers: { headers: {
@@ -36,22 +36,37 @@ class Genesis implements Plugin.PluginBase {
async popularNovels( async popularNovels(
pageNo: number, pageNo: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions, {
): Promise<Plugin.SourceNovel[]> { showLatestNovels,
filters,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
): Promise<Plugin.NovelItem[]> {
if (pageNo !== 1) return []; if (pageNo !== 1) return [];
let link = `${this.site}/api/search?`; let link = `${this.site}/api/novels/search?`;
if (showLatestNovels) { if (showLatestNovels) {
link += 'sort=Recent'; link += 'serialization=All&sort=Recent';
} else { } else {
if (filters!.genres.value) { if (filters?.genres.value.length) {
link += filters!.genres.value; 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}`;
} }
link += `&${filters!.storyStatus.value}&${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.parseNovels(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 url = `${this.site}${novelPath}/__data.json?x-sveltekit-invalidated=001`;
@@ -60,7 +75,7 @@ class Genesis implements Plugin.PluginBase {
const nodes = json.nodes; const nodes = json.nodes;
// Extract the main novel data from the nodes // Extract the main novel data from the nodes
const data = this.extractNovelData(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 novel: Plugin.SourceNovel = {
@@ -82,13 +97,6 @@ class Genesis implements Plugin.PluginBase {
return novel; return novel;
} }
// Helper function to extract novel data from nodes
extractNovelData(nodes: any[]): any {
return nodes
.filter((node: { type: string }) => node.type === 'data')
.map((node: { data: any }) => node.data)[0];
}
// Helper function to populate novel metadata // Helper function to populate novel metadata
populateNovelMetadata(novel: Plugin.SourceNovel, data: any): void { populateNovelMetadata(novel: Plugin.SourceNovel, data: any): void {
for (const key in data) { for (const key in data) {
@@ -115,54 +123,73 @@ class Genesis implements Plugin.PluginBase {
// Helper function to extract and format chapters // Helper function to extract and format chapters
extractChapters(data: any): Plugin.ChapterItem[] { 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) { for (const key in data) {
const value = data[key]; const value = data[key];
// Change string here if the chapters are stored under a different key // Look for an object with a 'chapters' key
const chapterKey = 'chapters'; if (value && typeof value === 'object' && chapterKey in value) {
if (typeof value === 'object' && value !== null && chapterKey in value) { const chaptersIndexKey = value[chapterKey];
const chapterData = this.decodeData(data[value[chapterKey]]); const chapterData = data[chaptersIndexKey];
if (!chapterData || typeof chapterData !== 'object') continue;
// Object.values will give us an array of arrays (any[][]) // Dynamically get chapter indices from every category (free, premium, etc.)
const chapterArrays: any[][] = Object.values(chapterData); let allChapterIndices: number[] = [];
for (const category of Object.keys(chapterData)) {
const chapterIndices = data[chapterData[category]];
if (Array.isArray(chapterIndices)) {
allChapterIndices = allChapterIndices.concat(chapterIndices);
}
}
// Flatten and format the chapters // Format each chapter and add only valid ones
return chapterArrays.flatMap((chapters: any[]) => { const formattedChapters = allChapterIndices
return chapters .map((index: number) => data[index])
.map((chapter: any) => this.formatChapter(chapter)) .map((chapter: any) => this.formatChapter(chapter, data))
.filter( .filter((chapter): chapter is Plugin.ChapterItem => chapter !== null);
(chapter): chapter is Plugin.ChapterItem => chapter !== null,
); chapters.push(...formattedChapters);
});
} }
} }
return []; return chapters;
} }
// Helper function to format an individual chapter // Helper function to format an individual chapter
formatChapter(chapter: any): Plugin.ChapterItem | null { formatChapter(chapter: any, data: any): Plugin.ChapterItem | null {
const { id, chapter_title, chapter_number, required_tier, date_created } = const { id, chapter_title, chapter_number, required_tier, date_created } =
chapter; chapter;
// Ensure required fields are present and valid // Destructure from data using computed property names based on chapter keys
if ( const {
id && [id]: chapterId,
chapter_title && [chapter_title]: chapterTitle,
chapter_number >= 0 && [chapter_number]: chapterNumberVal,
required_tier !== null && [required_tier]: requiredTierVal,
date_created [date_created]: dateCreated,
) { } = data;
const number = parseInt(chapter_number, 10) || 0;
const requiredTier = parseInt(required_tier, 10) || 0;
// Only process chapters with a 'requiredTier' of 0 // 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) { if (requiredTier === 0) {
return { return {
name: `Chapter ${number}: ${chapter_title}`, name: `Chapter ${chapterNumber} - ${chapterTitle}`,
path: `/viewer/${id}`, path: `/viewer/${chapterId}`,
releaseTime: date_created, releaseTime: dateCreated,
chapterNumber: number, chapterNumber: chapterNumber,
}; };
} }
} }
@@ -170,176 +197,267 @@ class Genesis implements Plugin.PluginBase {
return null; return null;
} }
decodeData(code: any) { // // Helper function to extract and format chapters
const offset = this.getOffsetIndex(code); // extractChapters(data: any): Plugin.ChapterItem[] {
const params = this.getDecodeParams(code); // for (const key in data) {
const constant = this.getConstant(code); // const value = data[key];
const data = this.getStringsArrayRaw(code); //
// // Change string here if the chapters are stored under a different key
const getDataAt = (x: number) => data[x - offset]; // const chapterKey = 'chapters';
// if (typeof value === 'object' && value !== null && chapterKey in value) {
//reshuffle data array // const chapterData = this.decodeData(data[value[chapterKey]]);
// eslint-disable-next-line no-constant-condition //
while (true) { // // Object.values will give us an array of arrays (any[][])
try { // const chapterArrays: any[][] = Object.values(chapterData);
const some_number = this.applyDecodeParams(params, getDataAt); //
if (some_number === constant) break; // // Flatten and format the chapters
else data.push(data.shift()); // return chapterArrays.flatMap((chapters: any[]) => {
} catch (err) { // return chapters
data.push(data.shift()); // .map((chapter: any) => this.formatChapter(chapter))
} // .filter(
} // (chapter): chapter is Plugin.ChapterItem => chapter !== null,
// );
return this.getChapterData(code, getDataAt); // });
} // }
// }
getOffsetIndex(code: string) { //
// @ts-ignore // return [];
const string = /{(\w+)=\1-0x(?<offset>[0-9a-f]+);/.exec(code).groups.offset; // }
return parseInt(string, 16); //
} // // Helper function to format an individual chapter
// formatChapter(chapter: any): Plugin.ChapterItem | null {
/** // const { id, chapter_title, chapter_number, required_tier, date_created } =
* @returns {string[]} // chapter;
*/ //
getStringsArrayRaw(code: string) { // // Ensure required fields are present and valid
// @ts-ignore // if (
let json = /function \w+\(\){var \w+=(?<array>\['.+']);/.exec(code).groups // id &&
.array; // chapter_title &&
// chapter_number >= 0 &&
//replace string single quotes with double quotes and add escaped chars // required_tier !== null &&
json = json.replace(/'(.+?)'([,\]])/g, (match, p1, p2) => { // date_created
return `"${p1.replace(/\\x([0-9a-z]{2})/g, (match: any, p1: string) => { // ) {
//hexadecimal unicode escape chars // const number = parseInt(chapter_number, 10) || 0;
return String.fromCharCode(parseInt(p1, 16)); // const requiredTier = parseInt(required_tier, 10) || 0;
})}"${p2}`; //
}); // // Only process chapters with a 'requiredTier' of 0
// if (requiredTier === 0) {
return JSON.parse(json); // return {
} // name: `Chapter ${number}: ${chapter_title}`,
// path: `/viewer/${id}`,
/** // releaseTime: date_created,
* @returns {{offset: number, divider: number, negated: boolean}[][]} // chapterNumber: number,
*/ // };
getDecodeParams(code: string) { // }
// @ts-ignore // }
const jsDecodeInt = /while\(!!\[]\){try{var \w+=(?<code>.+?);/.exec(code) //
.groups.code; // return null;
const decodeSections = jsDecodeInt.split('+'); // }
const params = []; //
for (const section of decodeSections) { // decodeData(code: any) {
params.push(this.decodeParamSection(section)); // const offset = this.getOffsetIndex(code);
} // const params = this.getDecodeParams(code);
return params; // const constant = this.getConstant(code);
} // const data = this.getStringsArrayRaw(code);
//
/** // const getDataAt = (x: number) => data[x - offset];
* @param {string} section //
* @returns {{offset: number, divider: number, negated: boolean}[]} // //reshuffle data array
*/ // // eslint-disable-next-line no-constant-condition
decodeParamSection(section: string) { // while (true) {
const sections = section.split('*'); // try {
const params = []; // const some_number = this.applyDecodeParams(params, getDataAt);
for (const section of sections) { // if (some_number === constant) break;
// @ts-ignore // else data.push(data.shift());
const offsetStr = /parseInt\(\w+\(0x(?<offset>[0-9a-f]+)\)\)/.exec( // } catch (err) {
section, // data.push(data.shift());
).groups.offset; // }
const offset = parseInt(offsetStr, 16); // }
// @ts-ignore //
const dividerStr = /\/0x(?<divider>[0-9a-f]+)/.exec(section).groups // return this.getChapterData(code, getDataAt);
.divider; // }
const divider = parseInt(dividerStr, 16); //
const negated = section.includes('-'); // getOffsetIndex(code: string) {
params.push({ offset, divider, negated }); // // @ts-ignore
} // const string = /{(\w+)=\1-0x(?<offset>[0-9a-f]+);/.exec(code).groups.offset;
return params; // return parseInt(string, 16);
} // }
//
getConstant(code: string) { // /**
// @ts-ignore // * @returns {string[]}
const constantStr = /}}}\(\w+,0x(?<constant>[0-9a-f]+)\),/.exec(code).groups // */
.constant; // getStringsArrayRaw(code: string) {
return parseInt(constantStr, 16); // // @ts-ignore
} // let json = /function \w+\(\){var \w+=(?<array>\['.+']);/.exec(code).groups
// .array;
getChapterData( //
code: string, // //replace string single quotes with double quotes and add escaped chars
getDataAt: { (x: number): any; (arg0: number): any }, // json = json.replace(/'(.+?)'([,\]])/g, (match, p1, p2) => {
) { // return `"${p1.replace(/\\x([0-9a-z]{2})/g, (match: any, p1: string) => {
let chapterDataStr = // //hexadecimal unicode escape chars
// @ts-ignore // return String.fromCharCode(parseInt(p1, 16));
/\),\(function\(\){var \w+=\w+;return(?<data>{.+?});/.exec(code).groups // })}"${p2}`;
.data; // });
//
//replace hex with decimal // return JSON.parse(json);
chapterDataStr = chapterDataStr.replace(/:0x([0-9a-f]+)/g, (match, p1) => { // }
const hex = parseInt(p1, 16); //
return `: ${hex}`; // /**
}); // * @returns {{offset: number, divider: number, negated: boolean}[][]}
// */
//replace ![] with false and !![] with true // getDecodeParams(code: string) {
chapterDataStr = chapterDataStr // // @ts-ignore
.replace(/:!!\[]/g, ':true') // const jsDecodeInt = /while\(!!\[]\){try{var \w+=(?<code>.+?);/.exec(code)
.replace(/:!\[]/g, ':false'); // .groups.code;
// const decodeSections = jsDecodeInt.split('+');
//replace string single quotes with double quotes and add escaped chars // const params = [];
chapterDataStr = chapterDataStr.replace( // for (const section of decodeSections) {
/'(.+?)'([,\]}:])/g, // params.push(this.decodeParamSection(section));
(match, p1, p2) => { // }
return `"${p1.replace(/\\x([0-9a-z]{2})/g, (match: any, p1: string) => { // return params;
//hexadecimal unicode escape chars // }
return String.fromCharCode(parseInt(p1, 16)); //
})}"${p2}`; // /**
}, // * @param {string} section
); // * @returns {{offset: number, divider: number, negated: boolean}[]}
// */
//parse the data getting methods // decodeParamSection(section: string) {
chapterDataStr = chapterDataStr.replace( // const sections = section.split('*');
// @ts-ignore // const params = [];
/:\w+\(0x(?<offset>[0-9a-f]+)\)/g, // for (const section of sections) {
(match, p1) => { // // @ts-ignore
const offset = parseInt(p1, 16); // const offsetStr = /parseInt\(\w+\(0x(?<offset>[0-9a-f]+)\)\)/.exec(
return `:${JSON.stringify(getDataAt(offset))}`; // section,
}, // ).groups.offset;
); // const offset = parseInt(offsetStr, 16);
// // @ts-ignore
return JSON.parse(chapterDataStr); // const dividerStr = /\/0x(?<divider>[0-9a-f]+)/.exec(section).groups
} // .divider;
// const divider = parseInt(dividerStr, 16);
/** // const negated = section.includes('-');
* @param {{offset: number, divider: number, negated: boolean}[][]} params // params.push({ offset, divider, negated });
* @param {function(number): string} getDataAt // }
*/ // return params;
applyDecodeParams( // }
params: { offset: number; divider: number; negated: boolean }[][], //
getDataAt: { (x: number): any; (arg0: any): string }, // getConstant(code: string) {
) { // // @ts-ignore
let res = 0; // const constantStr = /}}}\(\w+,0x(?<constant>[0-9a-f]+)\),/.exec(code).groups
for (const paramAdd of params) { // .constant;
let resInner = 1; // return parseInt(constantStr, 16);
for (const paramMul of paramAdd) { // }
resInner *= parseInt(getDataAt(paramMul.offset)) / paramMul.divider; //
if (paramMul.negated) resInner *= -1; // getChapterData(
} // code: string,
res += resInner; // getDataAt: { (x: number): any; (arg0: number): any },
} // ) {
return res; // 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> { async parseChapter(chapterPath: string): Promise<string> {
const url = `${this.site}${chapterPath}/__data.json?x-sveltekit-invalidated=001`; 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 json = await fetchApi(url).then(r => r.json());
const nodes = json.nodes; const nodes = json.nodes;
const data = nodes
.filter((node: { type: string }) => node.type === 'data') // Extract the main novel data from the nodes
.map((node: { data: any }) => node.data)[0]; const data = this.extractData(nodes);
const content = data[data[0].gs] ?? data[19];
const notes = data[data[0].notes]; const contentKey = 'content';
const footnotes = data[data[0].footnotes]; const notesKey = 'notes';
return ( const footnotesKey = 'footnotes';
content + (notes ? `<h2>Notes</h2>${notes}` : '') + (footnotes ?? '')
); // Iterate over each property in data to find chapter containers
for (const key in data) {
const mapping = data[key];
// Look for an object with a 'chapters' key
if (
mapping &&
typeof mapping === 'object' &&
contentKey in mapping &&
notesKey in mapping &&
footnotesKey in mapping
) {
// Use the found mapping object to determine the actual keys.
const contentMappingKey = mapping[contentKey];
const notesMappingKey = mapping[notesKey];
const footnotesMappingKey = mapping[footnotesKey];
// Retrieve the chapter's content, notes, and footnotes using the mapping.
// Provide a fallback for content if needed.
const content = data[contentMappingKey] ?? data[19];
const notes = data[notesMappingKey];
const footnotes = data[footnotesMappingKey];
return (
content +
(notes ? `<b>Notes</b><br>${notes}` : '') +
(footnotes ? `<b>Footnotes</b><br>${footnotes}` : '')
);
}
}
// If no mapping object was found, return an empty string or handle appropriately.
return '';
} }
async searchNovels( async searchNovels(
@@ -347,7 +465,7 @@ 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/search?serialization=All&sort=Popular&title=${searchTerm}`; const url = `${this.site}/api/novels/search?serialization=All&sort=Popular&title=${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.parseNovels(json);
} }
@@ -355,21 +473,21 @@ class Genesis implements Plugin.PluginBase {
filters = { filters = {
sort: { sort: {
label: 'Sort Results By', label: 'Sort Results By',
value: 'sort=Popular', value: 'Popular',
options: [ options: [
{ label: 'Popular', value: 'sort=Popular' }, { label: 'Popular', value: 'Popular' },
{ label: 'Recent', value: 'sort=Recent' }, { label: 'Recent', value: 'Recent' },
{ label: 'Views', value: 'sort=Views' }, { label: 'Views', value: 'Views' },
], ],
type: FilterTypes.Picker, type: FilterTypes.Picker,
}, },
storyStatus: { storyStatus: {
label: 'Status', label: 'Status',
value: 'serialization=All', value: 'All',
options: [ options: [
{ label: 'All', value: 'serialization=All' }, { label: 'All', value: 'All' },
{ label: 'Ongoing', value: 'serialization=Ongoing' }, { label: 'Ongoing', value: 'Ongoing' },
{ label: 'Completed', value: 'serialization=Completed' }, { label: 'Completed', value: 'Completed' },
], ],
type: FilterTypes.Picker, type: FilterTypes.Picker,
}, },
@@ -377,18 +495,18 @@ class Genesis implements Plugin.PluginBase {
label: 'Genres', label: 'Genres',
value: [], value: [],
options: [ options: [
{ label: 'Action', value: 'genres=Action' }, { label: 'Action', value: 'Action' },
{ label: 'Comedy', value: 'genres=Comedy' }, { label: 'Comedy', value: 'Comedy' },
{ label: 'Drama', value: 'genres=Drama' }, { label: 'Drama', value: 'Drama' },
{ label: 'Fantasy', value: 'genres=Fantasy' }, { label: 'Fantasy', value: 'Fantasy' },
{ label: 'Harem', value: 'genres=Harem' }, { label: 'Harem', value: 'Harem' },
{ label: 'Martial Arts', value: 'genres=Martial+Arts' }, { label: 'Martial Arts', value: 'Martial Arts' },
{ label: 'Modern', value: 'genres=Modern' }, { label: 'Modern', value: 'Modern' },
{ label: 'Mystery', value: 'genres=Mystery' }, { label: 'Mystery', value: 'Mystery' },
{ label: 'Psychological', value: 'genres=Psychological' }, { label: 'Psychological', value: 'Psychological' },
{ label: 'Romance', value: 'genres=Romance' }, { label: 'Romance', value: 'Romance' },
{ label: 'Slice of life', value: 'genres=Slice+of+Life' }, { label: 'Slice of life', value: 'Slice of Life' },
{ label: 'Tragedy', value: 'genres=Tragedy' }, { label: 'Tragedy', value: 'Tragedy' },
], ],
type: FilterTypes.CheckboxGroup, type: FilterTypes.CheckboxGroup,
}, },