Skip to content

Commit e4e3415

Browse files
authored
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
1 parent 9380542 commit e4e3415

File tree

1 file changed

+254
-46
lines changed

1 file changed

+254
-46
lines changed

src/plugins/english/genesis.ts

Lines changed: 254 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class Genesis implements Plugin.PluginBase {
1818
icon = 'src/en/genesis/icon.png';
1919
customCSS = 'src/en/genesis/customCSS.css';
2020
site = 'https://genesistudio.com';
21-
version = '1.0.4';
21+
version = '1.0.5';
2222

2323
imageRequestInit?: Plugin.ImageRequestInit | undefined = {
2424
headers: {
@@ -54,12 +54,15 @@ class Genesis implements Plugin.PluginBase {
5454

5555
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
5656
const url = `${this.site}${novelPath}/__data.json?x-sveltekit-invalidated=001`;
57+
58+
// Fetch the novel's data in JSON format
5759
const json = await fetchApi(url).then(r => r.json());
5860
const nodes = json.nodes;
59-
const data = nodes
60-
.filter((node: { type: string }) => node.type === 'data')
61-
.map((node: { data: any }) => node.data)[0];
6261

62+
// Extract the main novel data from the nodes
63+
const data = this.extractNovelData(nodes);
64+
65+
// Initialize the novel object with default values
6366
const novel: Plugin.SourceNovel = {
6467
path: novelPath,
6568
name: '',
@@ -70,53 +73,258 @@ class Genesis implements Plugin.PluginBase {
7073
chapters: [],
7174
};
7275

76+
// Parse and assign novel metadata (title, cover, summary, author, etc.)
77+
this.populateNovelMetadata(novel, data);
78+
79+
// Parse the chapters if available and assign them to the novel object
80+
novel.chapters = this.extractChapters(data);
81+
82+
return novel;
83+
}
84+
85+
// Helper function to extract novel data from nodes
86+
extractNovelData(nodes: any[]): any {
87+
return nodes
88+
.filter((node: { type: string }) => node.type === 'data')
89+
.map((node: { data: any }) => node.data)[0];
90+
}
91+
92+
// Helper function to populate novel metadata
93+
populateNovelMetadata(novel: Plugin.SourceNovel, data: any): void {
7394
for (const key in data) {
7495
const value = data[key];
75-
if (typeof value === 'object' && value !== null) {
76-
if ('novel_title' in value) {
77-
novel.name = data[value.novel_title];
78-
novel.cover = data[value.cover];
79-
novel.summary = data[value.synopsis];
80-
novel.author = data[value.author];
81-
novel.genres = (data[value.genres] as number[])
96+
97+
if (
98+
typeof value === 'object' &&
99+
value !== null &&
100+
'novel_title' in value
101+
) {
102+
novel.name = data[value.novel_title] || 'Unknown Title';
103+
novel.cover = data[value.cover] || '';
104+
novel.summary = data[value.synopsis] || '';
105+
novel.author = data[value.author] || 'Unknown Author';
106+
novel.genres =
107+
(data[value.genres] as number[])
82108
.map((genreId: number) => data[genreId])
83-
.join(', ');
84-
novel.status = value.release_days ? 'Ongoing' : 'Completed';
85-
} else if ('chapters_list' in value) {
86-
const chaptersFunction = data[value.chapters_list];
87-
const chapterMatches = chaptersFunction.match(
88-
/'id':((?!_)\w+),'chapter_title':(?:'([^'\\]*(?:\\.[^'\\]*)*)'|(\w+\([^\)]+\))),'chapter_number':(\w+),'required_tier':(\w+),'date_created':([^,]*),/g,
89-
);
90-
91-
if (chapterMatches) {
92-
novel.chapters = chapterMatches
93-
.map((match: string) => {
94-
const [, id, title, , number, requiredTier, dateCreated] =
95-
match.match(
96-
/'id':(\w+),'chapter_title':(?:'([^'\\]*(?:\\.[^'\\]*)*)'|(\w+\([^\)]+\))),'chapter_number':(\w+),'required_tier':(\w+),'date_created':([^,]*),/,
97-
)!;
98-
99-
if (parseInt(requiredTier, 16) === 0) {
100-
return {
101-
name: `Chapter ${parseInt(number, 16)}: ${title || 'Unknown Title'}`,
102-
path: `/viewer/${parseInt(id, 16)}`,
103-
releaseTime: dateCreated.replace(/^'|'$/g, ''),
104-
chapterNumber: parseInt(number, 16),
105-
};
106-
}
107-
return null;
108-
})
109-
.filter(
110-
(
111-
chapter: Plugin.ChapterItem | null,
112-
): chapter is Plugin.ChapterItem => chapter !== null,
113-
);
114-
}
115-
}
109+
.join(', ') || 'Unknown Genre';
110+
novel.status = value.release_days ? 'Ongoing' : 'Completed';
111+
break; // Break the loop once metadata is found
112+
}
113+
}
114+
}
115+
116+
// Helper function to extract and format chapters
117+
extractChapters(data: any): Plugin.ChapterItem[] {
118+
for (const key in data) {
119+
const value = data[key];
120+
121+
// Change string here if the chapters are stored under a different key
122+
const chapterKey = 'chapters';
123+
if (typeof value === 'object' && value !== null && chapterKey in value) {
124+
const chapterData = this.decodeData(data[value[chapterKey]]);
125+
126+
// Object.values will give us an array of arrays (any[][])
127+
const chapterArrays: any[][] = Object.values(chapterData);
128+
129+
// Flatten and format the chapters
130+
return chapterArrays.flatMap((chapters: any[]) => {
131+
return chapters
132+
.map((chapter: any) => this.formatChapter(chapter))
133+
.filter(
134+
(chapter): chapter is Plugin.ChapterItem => chapter !== null,
135+
);
136+
});
116137
}
117138
}
118139

119-
return novel;
140+
return [];
141+
}
142+
143+
// Helper function to format an individual chapter
144+
formatChapter(chapter: any): Plugin.ChapterItem | null {
145+
const { id, chapter_title, chapter_number, required_tier, date_created } =
146+
chapter;
147+
148+
// Ensure required fields are present and valid
149+
if (
150+
id &&
151+
chapter_title &&
152+
chapter_number &&
153+
required_tier !== null &&
154+
date_created
155+
) {
156+
const number = parseInt(chapter_number, 10) || 0;
157+
const requiredTier = parseInt(required_tier, 10) || 0;
158+
159+
// Only process chapters with a 'requiredTier' of 0
160+
if (requiredTier === 0) {
161+
return {
162+
name: `Chapter ${number}: ${chapter_title}`,
163+
path: `/viewer/${id}`,
164+
releaseTime: date_created,
165+
chapterNumber: number,
166+
};
167+
}
168+
}
169+
170+
return null;
171+
}
172+
173+
decodeData(code: any) {
174+
const offset = this.getOffsetIndex(code);
175+
const params = this.getDecodeParams(code);
176+
const constant = this.getConstant(code);
177+
const data = this.getStringsArrayRaw(code);
178+
179+
const getDataAt = (x: number) => data[x - offset];
180+
181+
//reshuffle data array
182+
// eslint-disable-next-line no-constant-condition
183+
while (true) {
184+
try {
185+
const some_number = this.applyDecodeParams(params, getDataAt);
186+
if (some_number === constant) break;
187+
else data.push(data.shift());
188+
} catch (err) {
189+
data.push(data.shift());
190+
}
191+
}
192+
193+
return this.getChapterData(code, getDataAt);
194+
}
195+
196+
getOffsetIndex(code: string) {
197+
// @ts-ignore
198+
const string = /{(\w+)=\1-0x(?<offset>[0-9a-f]+);/.exec(code).groups.offset;
199+
return parseInt(string, 16);
200+
}
201+
202+
/**
203+
* @returns {string[]}
204+
*/
205+
getStringsArrayRaw(code: string) {
206+
// @ts-ignore
207+
let json = /function \w+\(\){var \w+=(?<array>\['.+']);/.exec(code).groups
208+
.array;
209+
210+
//replace string single quotes with double quotes and add escaped chars
211+
json = json.replace(/'(.+?)'([,\]])/g, (match, p1, p2) => {
212+
return `"${p1.replace(/\\x([0-9a-z]{2})/g, (match: any, p1: string) => {
213+
//hexadecimal unicode escape chars
214+
return String.fromCharCode(parseInt(p1, 16));
215+
})}"${p2}`;
216+
});
217+
218+
return JSON.parse(json);
219+
}
220+
221+
/**
222+
* @returns {{offset: number, divider: number, negated: boolean}[][]}
223+
*/
224+
getDecodeParams(code: string) {
225+
// @ts-ignore
226+
const jsDecodeInt = /while\(!!\[]\){try{var \w+=(?<code>.+?);/.exec(code)
227+
.groups.code;
228+
const decodeSections = jsDecodeInt.split('+');
229+
const params = [];
230+
for (const section of decodeSections) {
231+
params.push(this.decodeParamSection(section));
232+
}
233+
return params;
234+
}
235+
236+
/**
237+
* @param {string} section
238+
* @returns {{offset: number, divider: number, negated: boolean}[]}
239+
*/
240+
decodeParamSection(section: string) {
241+
const sections = section.split('*');
242+
const params = [];
243+
for (const section of sections) {
244+
// @ts-ignore
245+
const offsetStr = /parseInt\(\w+\(0x(?<offset>[0-9a-f]+)\)\)/.exec(
246+
section,
247+
).groups.offset;
248+
const offset = parseInt(offsetStr, 16);
249+
// @ts-ignore
250+
const dividerStr = /\/0x(?<divider>[0-9a-f]+)/.exec(section).groups
251+
.divider;
252+
const divider = parseInt(dividerStr, 16);
253+
const negated = section.includes('-');
254+
params.push({ offset, divider, negated });
255+
}
256+
return params;
257+
}
258+
259+
getConstant(code: string) {
260+
// @ts-ignore
261+
const constantStr = /}}}\(\w+,0x(?<constant>[0-9a-f]+)\),/.exec(code).groups
262+
.constant;
263+
return parseInt(constantStr, 16);
264+
}
265+
266+
getChapterData(
267+
code: string,
268+
getDataAt: { (x: number): any; (arg0: number): any },
269+
) {
270+
let chapterDataStr =
271+
// @ts-ignore
272+
/\),\(function\(\){var \w+=\w+;return(?<data>{.+?});/.exec(code).groups
273+
.data;
274+
275+
//replace hex with decimal
276+
chapterDataStr = chapterDataStr.replace(/:0x([0-9a-f]+)/g, (match, p1) => {
277+
const hex = parseInt(p1, 16);
278+
return `: ${hex}`;
279+
});
280+
281+
//replace ![] with false and !![] with true
282+
chapterDataStr = chapterDataStr
283+
.replace(/:!!\[]/g, ':true')
284+
.replace(/:!\[]/g, ':false');
285+
286+
//replace string single quotes with double quotes and add escaped chars
287+
chapterDataStr = chapterDataStr.replace(
288+
/'(.+?)'([,\]}:])/g,
289+
(match, p1, p2) => {
290+
return `"${p1.replace(/\\x([0-9a-z]{2})/g, (match: any, p1: string) => {
291+
//hexadecimal unicode escape chars
292+
return String.fromCharCode(parseInt(p1, 16));
293+
})}"${p2}`;
294+
},
295+
);
296+
297+
//parse the data getting methods
298+
chapterDataStr = chapterDataStr.replace(
299+
// @ts-ignore
300+
/:\w+\(0x(?<offset>[0-9a-f]+)\)/g,
301+
(match, p1) => {
302+
const offset = parseInt(p1, 16);
303+
return `:${JSON.stringify(getDataAt(offset))}`;
304+
},
305+
);
306+
307+
return JSON.parse(chapterDataStr);
308+
}
309+
310+
/**
311+
* @param {{offset: number, divider: number, negated: boolean}[][]} params
312+
* @param {function(number): string} getDataAt
313+
*/
314+
applyDecodeParams(
315+
params: { offset: number; divider: number; negated: boolean }[][],
316+
getDataAt: { (x: number): any; (arg0: any): string },
317+
) {
318+
let res = 0;
319+
for (const paramAdd of params) {
320+
let resInner = 1;
321+
for (const paramMul of paramAdd) {
322+
resInner *= parseInt(getDataAt(paramMul.offset)) / paramMul.divider;
323+
if (paramMul.negated) resInner *= -1;
324+
}
325+
res += resInner;
326+
}
327+
return res;
120328
}
121329

122330
async parseChapter(chapterPath: string): Promise<string> {
@@ -126,7 +334,7 @@ class Genesis implements Plugin.PluginBase {
126334
const data = nodes
127335
.filter((node: { type: string }) => node.type === 'data')
128336
.map((node: { data: any }) => node.data)[0];
129-
const content = data[19];
337+
const content = data[data[0].gs] ?? data[19];
130338
const footnotes = data[data[0].footnotes];
131339
return content + (footnotes ?? '');
132340
}

0 commit comments

Comments
 (0)