networkwm-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +339 -0
  2. package/README.MD +110 -0
  3. package/dist/bytemanip.d.ts +12 -0
  4. package/dist/bytemanip.d.ts.map +1 -0
  5. package/dist/bytemanip.js +71 -0
  6. package/dist/database-abstraction.d.ts +39 -0
  7. package/dist/database-abstraction.d.ts.map +1 -0
  8. package/dist/database-abstraction.js +288 -0
  9. package/dist/databases.d.ts +85 -0
  10. package/dist/databases.d.ts.map +1 -0
  11. package/dist/databases.js +303 -0
  12. package/dist/devices.d.ts +6 -0
  13. package/dist/devices.d.ts.map +1 -0
  14. package/dist/devices.js +9 -0
  15. package/dist/encryption.d.ts +16 -0
  16. package/dist/encryption.d.ts.map +1 -0
  17. package/dist/encryption.js +123 -0
  18. package/dist/errors.d.ts +4 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +10 -0
  21. package/dist/filesystem/index.d.ts +2 -0
  22. package/dist/filesystem/index.d.ts.map +1 -0
  23. package/dist/filesystem/index.js +17 -0
  24. package/dist/filesystem/usb-mass-storage-webusb-filesystem.d.ts +44 -0
  25. package/dist/filesystem/usb-mass-storage-webusb-filesystem.d.ts.map +1 -0
  26. package/dist/filesystem/usb-mass-storage-webusb-filesystem.js +187 -0
  27. package/dist/functions.d.ts +12 -0
  28. package/dist/functions.d.ts.map +1 -0
  29. package/dist/functions.js +73 -0
  30. package/dist/helpers.d.ts +9 -0
  31. package/dist/helpers.d.ts.map +1 -0
  32. package/dist/helpers.js +47 -0
  33. package/dist/id3.d.ts +18 -0
  34. package/dist/id3.d.ts.map +1 -0
  35. package/dist/id3.js +137 -0
  36. package/dist/index.d.ts +10 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +28 -0
  39. package/dist/init-data.d.ts +16 -0
  40. package/dist/init-data.d.ts.map +1 -0
  41. package/dist/init-data.js +18 -0
  42. package/dist/initialization.d.ts +4 -0
  43. package/dist/initialization.d.ts.map +1 -0
  44. package/dist/initialization.js +32 -0
  45. package/dist/sort.d.ts +13 -0
  46. package/dist/sort.d.ts.map +1 -0
  47. package/dist/sort.js +62 -0
  48. package/dist/tables.d.ts +19 -0
  49. package/dist/tables.d.ts.map +1 -0
  50. package/dist/tables.js +101 -0
  51. package/dist/tagged-oma.d.ts +12 -0
  52. package/dist/tagged-oma.d.ts.map +1 -0
  53. package/dist/tagged-oma.js +175 -0
  54. package/dist/utils.d.ts +9 -0
  55. package/dist/utils.d.ts.map +1 -0
  56. package/dist/utils.js +90 -0
  57. package/package.json +43 -0
@@ -0,0 +1,288 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DatabaseAbstraction = void 0;
4
+ const himd_js_1 = require("himd-js");
5
+ const databases_1 = require("./databases");
6
+ const sort_1 = require("./sort");
7
+ const initialization_1 = require("./initialization");
8
+ const filesystem_1 = require("./filesystem");
9
+ const tagged_oma_1 = require("./tagged-oma");
10
+ const helpers_1 = require("./helpers");
11
+ class DatabaseAbstraction {
12
+ constructor(filesystem) {
13
+ this.filesystem = filesystem;
14
+ this.lastTotalDuration = 0;
15
+ this.allTracks = [];
16
+ this.deletedTracks = [];
17
+ this.database = new databases_1.DatabaseManager(filesystem);
18
+ }
19
+ static async create(filesystem) {
20
+ const db = new DatabaseAbstraction(filesystem);
21
+ await db.database.init();
22
+ db._create();
23
+ return db;
24
+ }
25
+ _create() {
26
+ // Parse the group info file.
27
+ this.lastTotalDuration = 0;
28
+ this.content1ArtistAlbumTrack = contentDescriptionPairFromFiles(this.database.parsedTreeFiles["01TREE03.DAT"], this.database.parsedGroupInfoFiles["03GINF03.DAT"]);
29
+ this.deletedTracks = [];
30
+ this.allTracks = this.database.globalContentInfoFile.map((globalEntry, systemIndex) => {
31
+ var _a, _b;
32
+ // Locate the track index
33
+ const trackIndex = (_b = (_a = this.content1ArtistAlbumTrack.find(e => e.tracks.includes(systemIndex + 1))) === null || _a === void 0 ? void 0 : _a.tracks.indexOf(systemIndex + 1)) !== null && _b !== void 0 ? _b : 1;
34
+ this.lastTotalDuration += globalEntry.trackDuration;
35
+ const codecId = globalEntry.codecInfo[0];
36
+ const codecParams = globalEntry.codecInfo.slice(1);
37
+ const codecInfo = { codecId, codecInfo: codecParams };
38
+ const codecName = (0, himd_js_1.getCodecName)(codecInfo);
39
+ const codecKBPS = (0, himd_js_1.getKBPS)(codecInfo);
40
+ if (globalEntry.trackDuration === 0)
41
+ this.deletedTracks.push(systemIndex + 1);
42
+ return {
43
+ album: globalEntry.contents["TALB"],
44
+ artist: globalEntry.contents["TPE1"],
45
+ codecInfo: globalEntry.codecInfo,
46
+ encryptionState: globalEntry.encryptionState,
47
+ genre: globalEntry.contents["TCON"],
48
+ oneElementLength: globalEntry.oneElementLength,
49
+ title: globalEntry.contents["TIT2"],
50
+ trackDuration: globalEntry.trackDuration,
51
+ trackNumber: trackIndex,
52
+ systemIndex: systemIndex + 1,
53
+ codecName, codecKBPS
54
+ };
55
+ });
56
+ }
57
+ addNewTrack(trackInfo, codecInfo) {
58
+ const codecName = (0, himd_js_1.getCodecName)(codecInfo);
59
+ const codecKBPS = (0, himd_js_1.getKBPS)(codecInfo);
60
+ const newObject = {
61
+ ...trackInfo,
62
+ codecInfo: new Uint8Array([codecInfo.codecId, ...codecInfo.codecInfo.subarray(0, 3)]),
63
+ encryptionState: new Uint8Array([0, 1]),
64
+ oneElementLength: 128,
65
+ systemIndex: -1,
66
+ codecName, codecKBPS
67
+ };
68
+ // Do we have any free gaps after deleted tracks?
69
+ if (this.deletedTracks.length > 0) {
70
+ // Reuse it instead.
71
+ const reusedIndex = this.deletedTracks.splice(0, 1)[0];
72
+ this.allTracks.splice(reusedIndex - 1, 1, newObject);
73
+ newObject.systemIndex = reusedIndex;
74
+ return reusedIndex;
75
+ }
76
+ const idx = this.allTracks.push(newObject);
77
+ newObject.systemIndex = idx;
78
+ return newObject.systemIndex;
79
+ }
80
+ async uploadTrack(trackInfo, codec, rawData, session, callback) {
81
+ // If trackInfo.trackNumber == -1, it's the next one of this particular album
82
+ if (trackInfo.trackNumber == -1) {
83
+ trackInfo.trackNumber = this.allTracks
84
+ .filter(e => e.album === trackInfo.album && e.artist === trackInfo.artist)
85
+ .reduce((prev, c) => Math.max(prev, c.trackNumber), -1) + 1;
86
+ }
87
+ // Step 1 - Create the encrypted OMA which will later be written to the device's storage
88
+ const encryptedOMA = (0, tagged_oma_1.createTaggedEncryptedOMA)(rawData, trackInfo, codec);
89
+ // Step 2 - write track to the database
90
+ const globalTrackIndex = this.addNewTrack({
91
+ ...trackInfo,
92
+ trackDuration: encryptedOMA.duration,
93
+ }, codec);
94
+ // Step 3 - write track to the filesystem
95
+ const fh = await this.database.filesystem.open((0, helpers_1.resolvePathFromGlobalIndex)(globalTrackIndex), 'rw');
96
+ const data = encryptedOMA.data;
97
+ let remaining = data.length;
98
+ let i = 0;
99
+ callback === null || callback === void 0 ? void 0 : callback(i, data.length);
100
+ while (remaining) {
101
+ const toWrite = Math.min(2048, remaining);
102
+ await fh.write(data.slice(i, i + toWrite));
103
+ i += toWrite;
104
+ remaining -= toWrite;
105
+ callback === null || callback === void 0 ? void 0 : callback(i, data.length);
106
+ }
107
+ await fh.close();
108
+ // Step 4 - write MAC
109
+ session === null || session === void 0 ? void 0 : session.writeTrackMac(globalTrackIndex - 1, encryptedOMA.maclistValue);
110
+ }
111
+ async flushUpdates() {
112
+ this.reserializeDatabase();
113
+ this.database.reserializeTables();
114
+ return this.database.rewriteTables();
115
+ }
116
+ async deleteTrack(systemIndex) {
117
+ const track = this.allTracks[systemIndex - 1];
118
+ // Wipe metadata information.
119
+ track.trackDuration = 0;
120
+ track.album = "";
121
+ track.artist = "";
122
+ track.title = "";
123
+ this.deletedTracks.push(systemIndex);
124
+ // Sort
125
+ this.deletedTracks.sort((a, b) => a - b);
126
+ // Delete the file.
127
+ await this.filesystem.delete((0, helpers_1.resolvePathFromGlobalIndex)(systemIndex));
128
+ }
129
+ reserializeDatabase() {
130
+ const instrs = [
131
+ // TREE01 - Groups (I use [aritst - album] > [trackNumber])
132
+ {
133
+ metadataCreator: e => ({
134
+ TIT2: e.album,
135
+ TPE1: e.artist,
136
+ TCON: '',
137
+ TSOP: '',
138
+ PICP: '',
139
+ PIC0: '',
140
+ }),
141
+ sorting: [[{ var: 'artist' }, { literal: '-----' }, { var: 'album' }], [{ var: 'trackNumber' }]],
142
+ },
143
+ // TREE02 - [artist] > [title]
144
+ {
145
+ metadataCreator: e => ({ TIT2: e.artist }),
146
+ sorting: [[{ var: 'artist' }], [{ var: 'title' }]],
147
+ },
148
+ // TREE03 - [album] > [trackNumber]
149
+ {
150
+ metadataCreator: e => ({ TIT2: e.album }),
151
+ sorting: [[{ var: 'album' }], [{ var: 'trackNumber' }]],
152
+ },
153
+ // TREE04 - [genre] > [title]
154
+ // ...who came up with this order??
155
+ {
156
+ metadataCreator: e => ({ TIT2: e.genre }),
157
+ sorting: [[{ var: 'genre' }], [{ var: 'title' }]],
158
+ },
159
+ ];
160
+ for (let fileIndex = 0; fileIndex < instrs.length; fileIndex++) {
161
+ const sortingInstr = instrs[fileIndex];
162
+ const sorted = (0, sort_1.complexSort)(sortingInstr.sorting, this.allTracks.filter(e => e.trackDuration > 0));
163
+ const entries = [];
164
+ for (const _group of sorted) {
165
+ const group = _group;
166
+ const any = group.contents[0];
167
+ const metadata = sortingInstr.metadataCreator(any);
168
+ entries.push({
169
+ flags: 256,
170
+ metadata,
171
+ oneElementLength: 128,
172
+ tracks: group.contents.map(e => e.systemIndex),
173
+ });
174
+ }
175
+ const [tree, group] = contentDescriptionPairToFiles(this.allTracks, entries);
176
+ this.database.parsedGroupInfoFiles[`03GINF${(fileIndex + 1).toString().padStart(2, '0')}.DAT`] = group;
177
+ this.database.parsedTreeFiles[`01TREE${(fileIndex + 1).toString().padStart(2, '0')}.DAT`] = tree;
178
+ }
179
+ // Rebuild tree metadata (update total duration)
180
+ this.database.globalContentInfoFile = this.allTracks.map(e => ({
181
+ codecInfo: e.codecInfo,
182
+ contents: {
183
+ TIT2: e.title,
184
+ TPE1: e.artist,
185
+ TALB: e.album,
186
+ TCON: e.genre,
187
+ TSOP: e.artist,
188
+ },
189
+ encryptionState: e.encryptionState,
190
+ oneElementLength: e.oneElementLength,
191
+ trackDuration: e.trackDuration,
192
+ }));
193
+ let newDuration = this.allTracks.reduce((a, v) => a + v.trackDuration, 0);
194
+ this.database.rewriteTotalDuration(this.lastTotalDuration, newDuration);
195
+ this.lastTotalDuration = newDuration;
196
+ }
197
+ getTracksSortedArtistAlbum() {
198
+ return (0, sort_1.complexSort)([[{ var: 'artist' }], [{ var: 'album' }], [{ var: 'trackNumber' }]], this.allTracks.filter(e => e.trackDuration > 0));
199
+ }
200
+ async eraseAll() {
201
+ // Essentially reinitialize the filesystem.
202
+ // Destroy all audio files
203
+ const fs = this.database.filesystem;
204
+ // Due to how HIMDFilesystem abstraction works, delete() would immediately flush the FAT changes.
205
+ // Here, they will be cached.
206
+ const fsDelete = (fs instanceof filesystem_1.UMSCNWJSFilesystem) ? fs.fatfs.delete.bind(fs.fatfs) : fs.delete.bind(fs);
207
+ async function recurseDelete(dir) {
208
+ for (let file of await fs.list(dir)) {
209
+ if (file.type === 'directory') {
210
+ await recurseDelete(file.name);
211
+ }
212
+ else {
213
+ await fsDelete(file.name);
214
+ }
215
+ }
216
+ await fsDelete(dir);
217
+ }
218
+ await recurseDelete("/OMGAUDIO");
219
+ if (fs instanceof filesystem_1.UMSCNWJSFilesystem) {
220
+ await fs.fatfs.flushMetadataChanges();
221
+ }
222
+ await (0, initialization_1.initializeNW)(fs);
223
+ this.database = new databases_1.DatabaseManager(this.filesystem);
224
+ await this.database.init();
225
+ this._create();
226
+ }
227
+ async renameTrack(systemIndex, metadata) {
228
+ this.allTracks[systemIndex - 1].album = metadata.album;
229
+ this.allTracks[systemIndex - 1].artist = metadata.artist;
230
+ this.allTracks[systemIndex - 1].title = metadata.title;
231
+ this.allTracks[systemIndex - 1].trackNumber = metadata.trackNumber;
232
+ // TODO: Is this necessary??
233
+ // Update the metadata within the OMA file
234
+ const handle = await this.database.filesystem.open((0, helpers_1.resolvePathFromGlobalIndex)(systemIndex), 'rw');
235
+ if (!handle)
236
+ return;
237
+ await (0, tagged_oma_1.updateMetadata)(handle, metadata);
238
+ }
239
+ }
240
+ exports.DatabaseAbstraction = DatabaseAbstraction;
241
+ function contentDescriptionPairFromFiles(treeFile, groupFile) {
242
+ // Step 1: Parse GROUP file. Create `groups`:
243
+ let groups = groupFile.map(e => ({
244
+ // TPLB / tree
245
+ flags: -1,
246
+ tracks: [],
247
+ // GROUP:
248
+ metadata: e.contents,
249
+ oneElementLength: e.oneElementLength,
250
+ }));
251
+ let groupsSortedInLookupOrder = [...treeFile.mapStartBounds].sort((a, b) => b.firstTrackApplicableInTPLB - a.firstTrackApplicableInTPLB);
252
+ // Step 2: Traverse trees - update groups one by one
253
+ main: for (let i = 0; i < treeFile.tplb.length; i++) {
254
+ let checkedIndex = i + 1;
255
+ for (let group of groupsSortedInLookupOrder) {
256
+ if (checkedIndex >= group.firstTrackApplicableInTPLB) {
257
+ // Found!
258
+ groups[group.groupInfoIndex - 1].flags = group.flags;
259
+ groups[group.groupInfoIndex - 1].tracks.push(treeFile.tplb[i]);
260
+ continue main;
261
+ }
262
+ }
263
+ throw new Error("Group not found! Data is corrupted.");
264
+ }
265
+ return groups;
266
+ }
267
+ function contentDescriptionPairToFiles(allContentRef, content) {
268
+ // Assume pairs are sorted correctly.
269
+ const tree = {
270
+ mapStartBounds: [],
271
+ tplb: [],
272
+ };
273
+ const groups = content.map((etr, index) => {
274
+ let obj = {
275
+ oneElementLength: 128,
276
+ totalDuration: etr.tracks.reduce((p, c) => p + allContentRef[c - 1].trackDuration, 0),
277
+ contents: etr.metadata,
278
+ };
279
+ tree.mapStartBounds.push({
280
+ firstTrackApplicableInTPLB: tree.tplb.length + 1,
281
+ flags: etr.flags,
282
+ groupInfoIndex: index + 1,
283
+ });
284
+ tree.tplb.push(...etr.tracks);
285
+ return obj;
286
+ });
287
+ return [tree, groups];
288
+ }
@@ -0,0 +1,85 @@
1
+ import { HiMDFilesystem, HiMDCodecName } from "himd-js";
2
+ import { TableFile } from "./tables";
3
+ export interface TreeFile {
4
+ mapStartBounds: {
5
+ firstTrackApplicableInTPLB: number;
6
+ groupInfoIndex: number;
7
+ flags: number;
8
+ }[];
9
+ tplb: number[];
10
+ }
11
+ export interface ContentEntry {
12
+ encryptionState: Uint8Array;
13
+ codecInfo: Uint8Array;
14
+ trackDuration: number;
15
+ oneElementLength: number;
16
+ contents: {
17
+ [key: string]: string;
18
+ };
19
+ }
20
+ export interface GroupEntry {
21
+ totalDuration: number;
22
+ oneElementLength: number;
23
+ contents: {
24
+ [key: string]: string;
25
+ };
26
+ }
27
+ export interface TrackMetadata {
28
+ album: string;
29
+ artist: string;
30
+ title: string;
31
+ genre: string;
32
+ trackDuration: number;
33
+ trackNumber: number;
34
+ }
35
+ export declare class DatabaseManager {
36
+ filesystem: HiMDFilesystem;
37
+ tableFiles: {
38
+ [fileName: string]: TableFile;
39
+ };
40
+ parsedTreeFiles: {
41
+ [fileName: string]: TreeFile;
42
+ };
43
+ parsedGroupInfoFiles: {
44
+ [fileName: string]: GroupEntry[];
45
+ };
46
+ globalContentInfoFile: ContentEntry[];
47
+ constructor(filesystem: HiMDFilesystem);
48
+ init(): Promise<void>;
49
+ reserializeTables(): void;
50
+ rewriteTables(): Promise<void>;
51
+ protected getGlobalTrack(track: number): {
52
+ album: string;
53
+ artist: string;
54
+ genre: string;
55
+ title: string;
56
+ duration: number;
57
+ codecName: HiMDCodecName;
58
+ codecKBPS: number;
59
+ };
60
+ rewriteTotalDuration(oldValue: number, newValue: number): void;
61
+ listContentGroups(): {
62
+ groupName: string | null;
63
+ contents: {
64
+ title: string;
65
+ artist: string;
66
+ genre: string;
67
+ album: string;
68
+ duration: number;
69
+ codecName: HiMDCodecName;
70
+ codecKBPS: number;
71
+ }[];
72
+ }[];
73
+ listContentArtists(): {
74
+ [artist: string]: {
75
+ [album: string]: {
76
+ track: string;
77
+ index: -1;
78
+ duration: number;
79
+ codecName: HiMDCodecName;
80
+ codecKBPS: number;
81
+ }[];
82
+ };
83
+ };
84
+ }
85
+ //# sourceMappingURL=databases.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"databases.d.ts","sourceRoot":"","sources":["../src/databases.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,cAAc,EAAyB,aAAa,EAAE,MAAM,SAAS,CAAC;AAI1F,OAAO,EAAc,SAAS,EAAkB,MAAM,UAAU,CAAC;AA4DjE,MAAM,WAAW,QAAQ;IAAE,cAAc,EAAE;QAAE,0BAA0B,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAC;AAC3I,MAAM,WAAW,YAAY;IAAG,eAAe,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,UAAU,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAC,CAAA;CAAC;AACvK,MAAM,WAAW,UAAU;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAC,CAAA;CAAC;AAChH,MAAM,WAAW,aAAa;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC;AACxI,qBAAa,eAAe;IAML,UAAU,EAAE,cAAc;IAL7C,UAAU,EAAE;QAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAA;KAAC,CAAK;IAChD,eAAe,EAAE;QAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAA;KAAC,CAAM;IACrD,oBAAoB,EAAE;QAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAAA;KAAC,CAAM;IAC9D,qBAAqB,EAAE,YAAY,EAAE,CAAM;gBAExB,UAAU,EAAE,cAAc;IAEvC,IAAI;IAoFH,iBAAiB;IAsEX,aAAa;IAU1B,SAAS,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM;;;;;;;;;IAkB/B,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;IAYvD,iBAAiB,IAAI;QAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,aAAa,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAC,EAAE;IAkC9L,kBAAkB,IAAI;QAAC,CAAC,MAAM,EAAE,MAAM,GAAG;YAAC,CAAC,KAAK,EAAE,MAAM,GAAG;gBAAC,KAAK,EAAE,MAAM,CAAC;gBAAC,KAAK,EAAE,CAAC,CAAC,CAAC;gBAAC,QAAQ,EAAE,MAAM,CAAC;gBAAC,SAAS,EAAE,aAAa,CAAC;gBAAC,SAAS,EAAE,MAAM,CAAA;aAAE,EAAE,CAAA;SAAC,CAAA;KAAC;CAwBnK"}
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DatabaseManager = void 0;
4
+ const node_mass_storage_1 = require("node-mass-storage");
5
+ const himd_js_1 = require("himd-js");
6
+ const bytemanip_1 = require("./bytemanip");
7
+ const id3_1 = require("./id3");
8
+ const tables_1 = require("./tables");
9
+ const utils_1 = require("./utils");
10
+ const FILES_TO_LOAD = [
11
+ // Group definition files:
12
+ "01TREE01.DAT",
13
+ "01TREE02.DAT",
14
+ "01TREE03.DAT",
15
+ "01TREE04.DAT",
16
+ // Tree keyinfo file:
17
+ "02TREINF.DAT",
18
+ // Group string resource files:
19
+ "03GINF01.DAT",
20
+ "03GINF02.DAT",
21
+ "03GINF03.DAT",
22
+ "03GINF04.DAT",
23
+ // Global content descriptor file:
24
+ "04CNTINF.DAT",
25
+ ];
26
+ const textEncoder = new TextEncoder();
27
+ const textDecoderStd = new TextDecoder();
28
+ const textDecoderUTF16 = new TextDecoder("UTF-16BE");
29
+ function readPackedTags(data, offset, elementsCount, elementLength) {
30
+ const contents = {};
31
+ for (let i = 0; i < elementsCount; i++) {
32
+ let tagNameB, encodingType, rawContents;
33
+ [tagNameB, offset] = (0, bytemanip_1.readBytes)(data, offset, 4);
34
+ [encodingType, offset] = (0, bytemanip_1.readUint16)(data, offset);
35
+ [rawContents, offset] = (0, bytemanip_1.readBytes)(data, offset, elementLength - 6);
36
+ const tagName = textDecoderStd.decode(tagNameB);
37
+ (0, utils_1.assert)(encodingType === 2, "Invalid string encoding!");
38
+ // Trim all zeros:
39
+ let rawContentsStr = textDecoderUTF16.decode(rawContents);
40
+ let newLength = rawContentsStr.length;
41
+ while (rawContentsStr.charCodeAt(newLength - 1) === 0)
42
+ --newLength;
43
+ rawContentsStr = rawContentsStr.substring(0, newLength);
44
+ contents[tagName] = rawContentsStr.trim();
45
+ }
46
+ return [contents, offset];
47
+ }
48
+ function writePackedTags(tags, elementLength) {
49
+ let offset = 0;
50
+ let content = new Uint8Array(Object.keys(tags).length * elementLength);
51
+ for (let [k, v] of Object.entries(tags)) {
52
+ if (k.startsWith("_"))
53
+ continue;
54
+ content.set(textEncoder.encode(k), offset);
55
+ offset += 4;
56
+ content.set((0, id3_1.encodeUTF16BEStringEA3)(v, true), offset + 1);
57
+ offset += elementLength - 4;
58
+ }
59
+ return content;
60
+ }
61
+ ;
62
+ class DatabaseManager {
63
+ constructor(filesystem) {
64
+ this.filesystem = filesystem;
65
+ this.tableFiles = {};
66
+ this.parsedTreeFiles = {};
67
+ this.parsedGroupInfoFiles = {};
68
+ this.globalContentInfoFile = [];
69
+ }
70
+ async init() {
71
+ for (let file of FILES_TO_LOAD) {
72
+ const fd = await this.filesystem.open('OMGAUDIO/' + file, 'ro');
73
+ const contents = await fd.read();
74
+ const table = (0, tables_1.parseTable)(contents);
75
+ this.tableFiles[file] = table;
76
+ if (file.startsWith("03GINF")) {
77
+ // Group info file. Parse it.
78
+ // Make sure the format is readable
79
+ (0, utils_1.assert)(table.name === 'GPIF', "Invalid group info table name");
80
+ (0, utils_1.assert)(table.classes.length === 1, "Invalid class amount in GINF");
81
+ (0, utils_1.assert)(table.classes[0].className === "GPFB", "Invalid class name in GINF");
82
+ // Known format - we're dealing with the standard map
83
+ this.parsedGroupInfoFiles[file] = [];
84
+ for (let entry of table.contents[0].elements) {
85
+ // Read the header
86
+ const data = new DataView(entry.buffer);
87
+ let trackId, elementsCount, elementLength;
88
+ let offset = 8;
89
+ [trackId, offset] = (0, bytemanip_1.readUint32)(data, offset);
90
+ [elementsCount, offset] = (0, bytemanip_1.readUint16)(data, offset);
91
+ [elementLength, offset] = (0, bytemanip_1.readUint16)(data, offset);
92
+ (0, utils_1.assert)(elementLength > 0x10, "The group info table does not make sense.");
93
+ // Read and parse every element
94
+ let contents;
95
+ [contents, offset] = readPackedTags(data, offset, elementsCount, elementLength);
96
+ // Bundle the info
97
+ this.parsedGroupInfoFiles[file].push({ totalDuration: trackId, oneElementLength: elementLength, contents });
98
+ }
99
+ }
100
+ else if (file.startsWith("01TREE")) {
101
+ // Tree file. Parse
102
+ // Make sure the format is known / readable
103
+ (0, utils_1.assert)(table.name === "TREE", "Invalid root tree table name!");
104
+ (0, utils_1.assert)(table.classes.length === 2, "Invalid amount of classes in tree file!");
105
+ (0, utils_1.assert)(table.classes[0].className === "GPLB", "Invalid Groupinfo-match class in tree file!");
106
+ (0, utils_1.assert)(table.classes[1].className === "TPLB", "Invalid track index in tree file!");
107
+ // Ok - format is good
108
+ // Parse the entries.
109
+ const gplbEntries = [];
110
+ for (let gplbEntry of table.contents[0].elements) {
111
+ const data = new DataView(gplbEntry.buffer);
112
+ let offset = 0;
113
+ let groupInfoIndex, firstTrackApplicableInTPLB, flags;
114
+ [groupInfoIndex, offset] = (0, bytemanip_1.readUint16)(data, offset);
115
+ [flags, offset] = (0, bytemanip_1.readUint16)(data, offset);
116
+ [firstTrackApplicableInTPLB, offset] = (0, bytemanip_1.readUint16)(data, offset);
117
+ gplbEntries.push({ firstTrackApplicableInTPLB, flags, groupInfoIndex });
118
+ }
119
+ // Parse TPLB
120
+ const tplbEntries = [];
121
+ for (let e of table.contents[1].elements) {
122
+ tplbEntries.push((0, bytemanip_1.getUint16)(e));
123
+ }
124
+ this.parsedTreeFiles[file] = { mapStartBounds: gplbEntries, tplb: tplbEntries };
125
+ }
126
+ }
127
+ // Parse global content info file.
128
+ {
129
+ const rootTable = this.tableFiles["04CNTINF.DAT"];
130
+ (0, utils_1.assert)(rootTable.name === 'CNIF', "Invalid root table name");
131
+ (0, utils_1.assert)(rootTable.classes.length === 1, "Invalid class amount in root");
132
+ (0, utils_1.assert)(rootTable.classes[0].className === "CNFB", "Invalid class name in root");
133
+ for (let contentBlock of rootTable.contents[0].elements) {
134
+ const data = new DataView(contentBlock.buffer);
135
+ let zeros, encryptionState, codecInfo, trackId, elementsCount, elementLength, contents;
136
+ let offset = 0;
137
+ [zeros, offset] = (0, bytemanip_1.readBytes)(data, offset, 2);
138
+ [encryptionState, offset] = (0, bytemanip_1.readBytes)(data, offset, 2);
139
+ [codecInfo, offset] = (0, bytemanip_1.readBytes)(data, offset, 4);
140
+ [trackId, offset] = (0, bytemanip_1.readUint32)(data, offset);
141
+ [elementsCount, offset] = (0, bytemanip_1.readUint16)(data, offset);
142
+ [elementLength, offset] = (0, bytemanip_1.readUint16)(data, offset);
143
+ [contents, offset] = readPackedTags(data, offset, elementsCount, elementLength);
144
+ (0, utils_1.assert)(zeros.every(e => e === 0), "Unexpected data in root content block header");
145
+ this.globalContentInfoFile.push({ codecInfo, contents, oneElementLength: elementLength, encryptionState, trackDuration: trackId });
146
+ }
147
+ }
148
+ }
149
+ reserializeTables() {
150
+ // Write global content info file
151
+ {
152
+ const rootTable = this.tableFiles["04CNTINF.DAT"];
153
+ rootTable.contents[0].elements = [];
154
+ for (let contentBlock of this.globalContentInfoFile) {
155
+ const content = new Uint8Array(rootTable.contents[0].oneElementLength);
156
+ let offset = 0;
157
+ content.set([0, 0], offset);
158
+ offset += 2;
159
+ content.set(contentBlock.encryptionState, offset);
160
+ offset += 2;
161
+ content.set(contentBlock.codecInfo, offset);
162
+ offset += 4;
163
+ content.set((0, bytemanip_1.writeUint32)(contentBlock.trackDuration), offset);
164
+ offset += 4;
165
+ content.set((0, bytemanip_1.writeUint16)(Object.keys(contentBlock.contents).length), offset);
166
+ offset += 2;
167
+ content.set((0, bytemanip_1.writeUint16)(contentBlock.oneElementLength), offset);
168
+ offset += 2;
169
+ // Write all entries
170
+ content.set(writePackedTags(contentBlock.contents, contentBlock.oneElementLength), offset);
171
+ rootTable.contents[0].elements.push(content);
172
+ }
173
+ }
174
+ // Rebuild groupinfo
175
+ for (let groupInfoFile in this.parsedGroupInfoFiles) {
176
+ const parsed = this.parsedGroupInfoFiles[groupInfoFile];
177
+ const table = this.tableFiles[groupInfoFile];
178
+ table.contents[0].elements = [];
179
+ for (let element of parsed) {
180
+ let content = new Uint8Array(table.contents[0].oneElementLength).fill(0);
181
+ let offset = 8;
182
+ content.set((0, bytemanip_1.writeUint32)(element.totalDuration), offset);
183
+ offset += 4;
184
+ content.set((0, bytemanip_1.writeUint16)(Object.keys(element.contents).length), offset);
185
+ offset += 2;
186
+ content.set((0, bytemanip_1.writeUint16)(element.oneElementLength), offset);
187
+ offset += 2;
188
+ content.set(writePackedTags(element.contents, element.oneElementLength), offset);
189
+ table.contents[0].elements.push(content);
190
+ }
191
+ }
192
+ // Rebuild trees
193
+ for (let treeFile in this.parsedTreeFiles) {
194
+ const parsed = this.parsedTreeFiles[treeFile];
195
+ const table = this.tableFiles[treeFile];
196
+ table.contents[0].elements = [];
197
+ // Serialize the maps
198
+ for (let mapEntry of parsed.mapStartBounds) {
199
+ const entry = new Uint8Array(table.contents[0].oneElementLength);
200
+ let offset = 0;
201
+ entry.set((0, bytemanip_1.writeUint16)(mapEntry.groupInfoIndex), offset);
202
+ offset += 2;
203
+ entry.set((0, bytemanip_1.writeUint16)(mapEntry.flags), offset);
204
+ offset += 2;
205
+ entry.set((0, bytemanip_1.writeUint16)(mapEntry.firstTrackApplicableInTPLB), offset);
206
+ table.contents[0].elements.push(entry);
207
+ }
208
+ // Serialize the tplb
209
+ table.contents[1].elements = [];
210
+ for (let tplbEntry of parsed.tplb) {
211
+ table.contents[1].elements.push((0, bytemanip_1.writeUint16)(tplbEntry));
212
+ }
213
+ }
214
+ }
215
+ async rewriteTables() {
216
+ this.reserializeTables();
217
+ for (let filename in this.tableFiles) {
218
+ const tableContents = (0, tables_1.serializeTable)(this.tableFiles[filename], filename.startsWith("01TREE"));
219
+ const fd = await this.filesystem.open("OMGAUDIO/" + filename, 'rw');
220
+ await fd.write(tableContents);
221
+ await fd.close();
222
+ }
223
+ }
224
+ getGlobalTrack(track) {
225
+ const globalTrack = this.globalContentInfoFile[track - 1];
226
+ const codecId = globalTrack.codecInfo[0];
227
+ const codecParams = globalTrack.codecInfo.slice(1);
228
+ const codecInfo = { codecId, codecInfo: codecParams };
229
+ const codecName = (0, himd_js_1.getCodecName)(codecInfo);
230
+ const codecKBPS = (0, himd_js_1.getKBPS)(codecInfo);
231
+ return {
232
+ album: globalTrack.contents['TALB'],
233
+ artist: globalTrack.contents['TPE1'],
234
+ genre: globalTrack.contents['TCON'],
235
+ title: globalTrack.contents['TIT2'],
236
+ duration: Math.ceil(globalTrack.trackDuration / 1000),
237
+ codecName, codecKBPS,
238
+ };
239
+ }
240
+ rewriteTotalDuration(oldValue, newValue) {
241
+ const newKeyAsUint = (0, node_mass_storage_1.getBEUint32AsBytes)(newValue);
242
+ for (let entry of this.tableFiles["02TREINF.DAT"].contents[0].elements.slice(0, 4)) {
243
+ // After index 4, there be dragons
244
+ if ((0, node_mass_storage_1.getBEUint32)(entry.slice(8, 8 + 4)) === oldValue) {
245
+ entry.set(newKeyAsUint, 8);
246
+ }
247
+ }
248
+ }
249
+ // TODO: TRACK ORDERING
250
+ listContentGroups() {
251
+ const groupedEncountered = [];
252
+ const groups = [];
253
+ // 01TREE01.DAT is groups
254
+ const tree = this.parsedTreeFiles["01TREE01.DAT"];
255
+ const desc = this.parsedGroupInfoFiles["03GINF01.DAT"];
256
+ for (let trackIndex = 0; trackIndex < tree.tplb.length; trackIndex++) {
257
+ const track = tree.tplb[trackIndex];
258
+ let gplbEntry = -1;
259
+ // If Sony can depend on GPLBs being ordered correctly, so can I.
260
+ for (let i = tree.mapStartBounds.length - 1; i >= 0; i--) {
261
+ const gplb = tree.mapStartBounds[i];
262
+ if ((trackIndex + 1) >= gplb.firstTrackApplicableInTPLB) {
263
+ gplbEntry = gplb.groupInfoIndex;
264
+ break;
265
+ }
266
+ }
267
+ if (gplbEntry !== -1) {
268
+ groupedEncountered.push(track);
269
+ if (groups.length < gplbEntry) {
270
+ groups.push({ groupName: desc[gplbEntry - 1].contents['TIT2'], contents: [] });
271
+ }
272
+ groups[gplbEntry - 1].contents.push(this.getGlobalTrack(track));
273
+ }
274
+ }
275
+ let ungrouped = Array(this.globalContentInfoFile.length).fill(0).map((_, i) => i + 1).filter(e => !groupedEncountered.includes(e));
276
+ const ungroupedGroup = { groupName: null, contents: ungrouped.map(this.getGlobalTrack.bind(this)) };
277
+ groups.splice(0, 0, ungroupedGroup);
278
+ return groups;
279
+ }
280
+ listContentArtists() {
281
+ const artists = {};
282
+ for (let i = 1; i <= this.globalContentInfoFile.length; i++) {
283
+ const track = this.getGlobalTrack(i);
284
+ if (!(track.artist in artists)) {
285
+ artists[track.artist] = {};
286
+ }
287
+ const artist = artists[track.artist];
288
+ if (!(track.album in artist)) {
289
+ artist[track.album] = [];
290
+ }
291
+ const album = artist[track.album];
292
+ album.push({
293
+ track: track.title,
294
+ index: -1,
295
+ duration: track.duration,
296
+ codecName: track.codecName,
297
+ codecKBPS: track.codecKBPS,
298
+ });
299
+ }
300
+ return artists;
301
+ }
302
+ }
303
+ exports.DatabaseManager = DatabaseManager;