From e4e341545ef631169136d5f4ec4e263c34d8ba6f Mon Sep 17 00:00:00 2001 From: Batorian <53831711+Batorian@users.noreply.github.com> Date: Wed, 16 Oct 2024 18:35:33 +0200 Subject: [PATCH] Improvement: [Genesis]: Fix Chapter List (#1211) * genesis: update chapter_content * genesis: fix chapter list * genesis: fix * genesis: update version * genesis: update chapter content * genesis: fix plugin (again) * genesis: revert version * genesis: fix chapters --- src/plugins/english/genesis.ts | 300 ++++++++++++++++++++++++++++----- 1 file changed, 254 insertions(+), 46 deletions(-) diff --git a/src/plugins/english/genesis.ts b/src/plugins/english/genesis.ts index 65f9e2b98..8422b4d44 100644 --- a/src/plugins/english/genesis.ts +++ b/src/plugins/english/genesis.ts @@ -18,7 +18,7 @@ class Genesis implements Plugin.PluginBase { icon = 'src/en/genesis/icon.png'; customCSS = 'src/en/genesis/customCSS.css'; site = 'https://genesistudio.com'; - version = '1.0.4'; + version = '1.0.5'; imageRequestInit?: Plugin.ImageRequestInit | undefined = { headers: { @@ -54,12 +54,15 @@ class Genesis implements Plugin.PluginBase { async parseNovel(novelPath: string): Promise { const url = `${this.site}${novelPath}/__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; - const data = nodes - .filter((node: { type: string }) => node.type === 'data') - .map((node: { data: any }) => node.data)[0]; + // Extract the main novel data from the nodes + const data = this.extractNovelData(nodes); + + // Initialize the novel object with default values const novel: Plugin.SourceNovel = { path: novelPath, name: '', @@ -70,53 +73,258 @@ class Genesis implements Plugin.PluginBase { chapters: [], }; + // Parse and assign novel metadata (title, cover, summary, author, etc.) + this.populateNovelMetadata(novel, data); + + // Parse the chapters if available and assign them to the novel object + novel.chapters = this.extractChapters(data); + + 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 + populateNovelMetadata(novel: Plugin.SourceNovel, data: any): void { for (const key in data) { const value = data[key]; - if (typeof value === 'object' && value !== null) { - if ('novel_title' in value) { - novel.name = data[value.novel_title]; - novel.cover = data[value.cover]; - novel.summary = data[value.synopsis]; - novel.author = data[value.author]; - novel.genres = (data[value.genres] as number[]) + + 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(', '); - novel.status = value.release_days ? 'Ongoing' : 'Completed'; - } else if ('chapters_list' in value) { - const chaptersFunction = data[value.chapters_list]; - const chapterMatches = chaptersFunction.match( - /'id':((?!_)\w+),'chapter_title':(?:'([^'\\]*(?:\\.[^'\\]*)*)'|(\w+\([^\)]+\))),'chapter_number':(\w+),'required_tier':(\w+),'date_created':([^,]*),/g, - ); - - if (chapterMatches) { - novel.chapters = chapterMatches - .map((match: string) => { - const [, id, title, , number, requiredTier, dateCreated] = - match.match( - /'id':(\w+),'chapter_title':(?:'([^'\\]*(?:\\.[^'\\]*)*)'|(\w+\([^\)]+\))),'chapter_number':(\w+),'required_tier':(\w+),'date_created':([^,]*),/, - )!; - - if (parseInt(requiredTier, 16) === 0) { - return { - name: `Chapter ${parseInt(number, 16)}: ${title || 'Unknown Title'}`, - path: `/viewer/${parseInt(id, 16)}`, - releaseTime: dateCreated.replace(/^'|'$/g, ''), - chapterNumber: parseInt(number, 16), - }; - } - return null; - }) - .filter( - ( - chapter: Plugin.ChapterItem | null, - ): chapter is Plugin.ChapterItem => chapter !== null, - ); - } - } + .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[] { + 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 novel; + 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 && + 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(?[0-9a-f]+);/.exec(code).groups.offset; + return parseInt(string, 16); + } + + /** + * @returns {string[]} + */ + getStringsArrayRaw(code: string) { + // @ts-ignore + let json = /function \w+\(\){var \w+=(?\['.+']);/.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+=(?.+?);/.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(?[0-9a-f]+)\)\)/.exec( + section, + ).groups.offset; + const offset = parseInt(offsetStr, 16); + // @ts-ignore + const dividerStr = /\/0x(?[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(?[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(?{.+?});/.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(?[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 { @@ -126,7 +334,7 @@ class Genesis implements Plugin.PluginBase { const data = nodes .filter((node: { type: string }) => node.type === 'data') .map((node: { data: any }) => node.data)[0]; - const content = data[19]; + const content = data[data[0].gs] ?? data[19]; const footnotes = data[data[0].footnotes]; return content + (footnotes ?? ''); }