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.
- package/LICENSE +339 -0
- package/README.MD +110 -0
- package/dist/bytemanip.d.ts +12 -0
- package/dist/bytemanip.d.ts.map +1 -0
- package/dist/bytemanip.js +71 -0
- package/dist/database-abstraction.d.ts +39 -0
- package/dist/database-abstraction.d.ts.map +1 -0
- package/dist/database-abstraction.js +288 -0
- package/dist/databases.d.ts +85 -0
- package/dist/databases.d.ts.map +1 -0
- package/dist/databases.js +303 -0
- package/dist/devices.d.ts +6 -0
- package/dist/devices.d.ts.map +1 -0
- package/dist/devices.js +9 -0
- package/dist/encryption.d.ts +16 -0
- package/dist/encryption.d.ts.map +1 -0
- package/dist/encryption.js +123 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +10 -0
- package/dist/filesystem/index.d.ts +2 -0
- package/dist/filesystem/index.d.ts.map +1 -0
- package/dist/filesystem/index.js +17 -0
- package/dist/filesystem/usb-mass-storage-webusb-filesystem.d.ts +44 -0
- package/dist/filesystem/usb-mass-storage-webusb-filesystem.d.ts.map +1 -0
- package/dist/filesystem/usb-mass-storage-webusb-filesystem.js +187 -0
- package/dist/functions.d.ts +12 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +73 -0
- package/dist/helpers.d.ts +9 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +47 -0
- package/dist/id3.d.ts +18 -0
- package/dist/id3.d.ts.map +1 -0
- package/dist/id3.js +137 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/init-data.d.ts +16 -0
- package/dist/init-data.d.ts.map +1 -0
- package/dist/init-data.js +18 -0
- package/dist/initialization.d.ts +4 -0
- package/dist/initialization.d.ts.map +1 -0
- package/dist/initialization.js +32 -0
- package/dist/sort.d.ts +13 -0
- package/dist/sort.d.ts.map +1 -0
- package/dist/sort.js +62 -0
- package/dist/tables.d.ts +19 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +101 -0
- package/dist/tagged-oma.d.ts +12 -0
- package/dist/tagged-oma.d.ts.map +1 -0
- package/dist/tagged-oma.js +175 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +90 -0
- 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;
|