@probityrules/jsmediatags 4.0.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/CHANGELOG.md +45 -0
- package/LICENSE.md +36 -0
- package/README.md +548 -0
- package/build/ArrayBufferFileReader.d.ts +12 -0
- package/build/ArrayBufferFileReader.js +27 -0
- package/build/ArrayFileReader.d.ts +11 -0
- package/build/ArrayFileReader.js +30 -0
- package/build/BlobFileReader.d.ts +12 -0
- package/build/BlobFileReader.js +47 -0
- package/build/ByteArrayUtils.d.ts +9 -0
- package/build/ByteArrayUtils.js +46 -0
- package/build/ChunkedFileData.d.ts +28 -0
- package/build/ChunkedFileData.js +171 -0
- package/build/DecodedString.d.ts +6 -0
- package/build/DecodedString.js +2 -0
- package/build/FLACTagContents.d.ts +19 -0
- package/build/FLACTagContents.js +54 -0
- package/build/FLACTagReader.d.ts +103 -0
- package/build/FLACTagReader.js +320 -0
- package/build/ID3v1TagReader.d.ts +10 -0
- package/build/ID3v1TagReader.js +176 -0
- package/build/ID3v2FrameReader.d.ts +25 -0
- package/build/ID3v2FrameReader.js +582 -0
- package/build/ID3v2TagContents.d.ts +82 -0
- package/build/ID3v2TagContents.js +318 -0
- package/build/ID3v2TagReader.d.ts +13 -0
- package/build/ID3v2TagReader.js +118 -0
- package/build/MP4TagContents.d.ts +17 -0
- package/build/MP4TagContents.js +52 -0
- package/build/MP4TagReader.d.ts +19 -0
- package/build/MP4TagReader.js +291 -0
- package/build/MediaFileReader.d.ts +46 -0
- package/build/MediaFileReader.js +168 -0
- package/build/MediaTagReader.d.ts +18 -0
- package/build/MediaTagReader.js +76 -0
- package/build/NodeFileReader.d.ts +12 -0
- package/build/NodeFileReader.js +103 -0
- package/build/ReactNativeFileReader.d.ts +12 -0
- package/build/ReactNativeFileReader.js +48 -0
- package/build/StringUtils.d.ts +7 -0
- package/build/StringUtils.js +102 -0
- package/build/XhrFileReader.d.ts +41 -0
- package/build/XhrFileReader.js +238 -0
- package/build/jsmediatags.d.ts +45 -0
- package/build/jsmediatags.js +219 -0
- package/build/registerNodeFileReaders.d.ts +5 -0
- package/build/registerNodeFileReaders.js +19 -0
- package/build/registerNodeFileReaders.noop.d.ts +2 -0
- package/build/registerNodeFileReaders.noop.js +3 -0
- package/build/types.d.ts +77 -0
- package/build/types.js +2 -0
- package/dist/jsmediatags.min.js +2 -0
- package/package.json +110 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const MediaTagReader = require('./MediaTagReader');
|
|
3
|
+
const MediaFileReader = require('./MediaFileReader');
|
|
4
|
+
class MP4TagReader extends MediaTagReader {
|
|
5
|
+
static getTagIdentifierByteRange() {
|
|
6
|
+
// The tag identifier is located in [4, 8] but since we'll need to reader
|
|
7
|
+
// the header of the first block anyway, we load it instead to avoid
|
|
8
|
+
// making two requests.
|
|
9
|
+
return {
|
|
10
|
+
offset: 0,
|
|
11
|
+
length: 16
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
static canReadTagFormat(tagIdentifier) {
|
|
15
|
+
var id = String.fromCharCode.apply(String, tagIdentifier.slice(4, 8));
|
|
16
|
+
return id === "ftyp";
|
|
17
|
+
}
|
|
18
|
+
_loadData(mediaFileReader, callbacks) {
|
|
19
|
+
// MP4 metadata isn't located in a specific location of the file. Roughly
|
|
20
|
+
// speaking, it's composed of blocks chained together like a linked list.
|
|
21
|
+
// These blocks are called atoms (or boxes).
|
|
22
|
+
// Each atom of the list can have its own child linked list. Atoms in this
|
|
23
|
+
// situation do not possess any data and are called "container" as they only
|
|
24
|
+
// contain other atoms.
|
|
25
|
+
// Other atoms represent a particular set of data, like audio, video or
|
|
26
|
+
// metadata. In order to find and load all the interesting atoms we need
|
|
27
|
+
// to traverse the entire linked list of atoms and only load the ones
|
|
28
|
+
// associated with metadata.
|
|
29
|
+
// The metadata atoms can be find under the "moov.udta.meta.ilst" hierarchy.
|
|
30
|
+
var self = this;
|
|
31
|
+
// Load the header of the first atom
|
|
32
|
+
mediaFileReader.loadRange([0, 16], {
|
|
33
|
+
onSuccess: function () {
|
|
34
|
+
self._loadAtom(mediaFileReader, 0, "", callbacks);
|
|
35
|
+
},
|
|
36
|
+
onError: callbacks.onError
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
_loadAtom(mediaFileReader, offset, parentAtomFullName, callbacks) {
|
|
40
|
+
if (offset >= mediaFileReader.getSize()) {
|
|
41
|
+
callbacks.onSuccess();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
var self = this;
|
|
45
|
+
// 8 is the size of the atomSize and atomName fields.
|
|
46
|
+
// When reading the current block we always read 8 more bytes in order
|
|
47
|
+
// to also read the header of the next block.
|
|
48
|
+
var atomSize = mediaFileReader.getLongAt(offset, true);
|
|
49
|
+
if (atomSize == 0 || isNaN(atomSize)) {
|
|
50
|
+
callbacks.onSuccess();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
var atomName = mediaFileReader.getStringAt(offset + 4, 4);
|
|
54
|
+
// console.log(parentAtomFullName, atomName, atomSize);
|
|
55
|
+
// Container atoms (no actual data)
|
|
56
|
+
if (this._isContainerAtom(atomName)) {
|
|
57
|
+
if (atomName == "meta") {
|
|
58
|
+
// The "meta" atom breaks convention and is a container with data.
|
|
59
|
+
offset += 4; // next_item_id (uint32)
|
|
60
|
+
}
|
|
61
|
+
var atomFullName = (parentAtomFullName ? parentAtomFullName + "." : "") + atomName;
|
|
62
|
+
if (atomFullName === "moov.udta.meta.ilst") {
|
|
63
|
+
mediaFileReader.loadRange([offset, offset + atomSize], callbacks);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
mediaFileReader.loadRange([offset + 8, offset + 8 + 8], {
|
|
67
|
+
onSuccess: function () {
|
|
68
|
+
self._loadAtom(mediaFileReader, offset + 8, atomFullName, callbacks);
|
|
69
|
+
},
|
|
70
|
+
onError: callbacks.onError
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
mediaFileReader.loadRange([offset + atomSize, offset + atomSize + 8], {
|
|
76
|
+
onSuccess: function () {
|
|
77
|
+
self._loadAtom(mediaFileReader, offset + atomSize, parentAtomFullName, callbacks);
|
|
78
|
+
},
|
|
79
|
+
onError: callbacks.onError
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
_isContainerAtom(atomName) {
|
|
84
|
+
return ["moov", "udta", "meta", "ilst"].indexOf(atomName) >= 0;
|
|
85
|
+
}
|
|
86
|
+
_canReadAtom(atomName) {
|
|
87
|
+
return atomName !== "----";
|
|
88
|
+
}
|
|
89
|
+
_parseData(data, tagsToRead) {
|
|
90
|
+
const tags = {};
|
|
91
|
+
tagsToRead = this._expandShortcutTags(tagsToRead);
|
|
92
|
+
this._readAtom(tags, data, 0, data.getSize(), tagsToRead);
|
|
93
|
+
for (const name in SHORTCUTS) {
|
|
94
|
+
if (SHORTCUTS.hasOwnProperty(name)) {
|
|
95
|
+
const raw = tags[SHORTCUTS[name]];
|
|
96
|
+
const tag = raw;
|
|
97
|
+
if (tag) {
|
|
98
|
+
if (name === "track") {
|
|
99
|
+
const td = tag.data;
|
|
100
|
+
tags[name] = td.track;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
tags[name] = tag.data;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
type: "MP4",
|
|
110
|
+
ftyp: data.getStringAt(8, 4),
|
|
111
|
+
version: data.getLongAt(12, true),
|
|
112
|
+
tags: tags,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
_readAtom(tags, data, offset, length, tagsToRead, parentAtomFullName, indent) {
|
|
116
|
+
indent = indent === undefined ? "" : indent + " ";
|
|
117
|
+
var seek = offset;
|
|
118
|
+
while (seek < offset + length) {
|
|
119
|
+
var atomSize = data.getLongAt(seek, true);
|
|
120
|
+
if (atomSize == 0) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
var atomName = data.getStringAt(seek + 4, 4);
|
|
124
|
+
// console.log(seek, parentAtomFullName, atomName, atomSize);
|
|
125
|
+
if (this._isContainerAtom(atomName)) {
|
|
126
|
+
if (atomName == "meta") {
|
|
127
|
+
seek += 4; // next_item_id (uint32)
|
|
128
|
+
}
|
|
129
|
+
var atomFullName = (parentAtomFullName ? parentAtomFullName + "." : "") + atomName;
|
|
130
|
+
this._readAtom(tags, data, seek + 8, atomSize - 8, tagsToRead, atomFullName, indent);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Value atoms
|
|
134
|
+
if ((!tagsToRead || tagsToRead.indexOf(atomName) >= 0) &&
|
|
135
|
+
parentAtomFullName === "moov.udta.meta.ilst" &&
|
|
136
|
+
this._canReadAtom(atomName)) {
|
|
137
|
+
tags[atomName] = this._readMetadataAtom(data, seek);
|
|
138
|
+
}
|
|
139
|
+
seek += atomSize;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
_readMetadataAtom(data, offset) {
|
|
143
|
+
// 16: size + name + size + "data" (4 bytes each)
|
|
144
|
+
// 8: 1 byte atom version & 3 bytes atom flags + 4 bytes NULL space
|
|
145
|
+
// 8: 4 bytes track + 4 bytes total
|
|
146
|
+
const METADATA_HEADER = 16;
|
|
147
|
+
var atomSize = data.getLongAt(offset, true);
|
|
148
|
+
var atomName = data.getStringAt(offset + 4, 4);
|
|
149
|
+
var klass = data.getInteger24At(offset + METADATA_HEADER + 1, true);
|
|
150
|
+
let type = TYPES[String(klass)];
|
|
151
|
+
let atomData;
|
|
152
|
+
var bigEndian = true;
|
|
153
|
+
if (atomName == "trkn") {
|
|
154
|
+
atomData = {
|
|
155
|
+
track: data.getShortAt(offset + METADATA_HEADER + 10, bigEndian),
|
|
156
|
+
total: data.getShortAt(offset + METADATA_HEADER + 14, bigEndian),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
else if (atomName == "disk") {
|
|
160
|
+
atomData = {
|
|
161
|
+
disk: data.getShortAt(offset + METADATA_HEADER + 10, bigEndian),
|
|
162
|
+
total: data.getShortAt(offset + METADATA_HEADER + 14, bigEndian),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
var atomHeader = METADATA_HEADER + 4 + 4;
|
|
167
|
+
var dataStart = offset + atomHeader;
|
|
168
|
+
var dataLength = atomSize - atomHeader;
|
|
169
|
+
if (atomName === "covr" && type === "uint8") {
|
|
170
|
+
type = "jpeg";
|
|
171
|
+
}
|
|
172
|
+
switch (type) {
|
|
173
|
+
case "text":
|
|
174
|
+
atomData = data
|
|
175
|
+
.getStringWithCharsetAt(dataStart, dataLength, "utf-8")
|
|
176
|
+
.toString();
|
|
177
|
+
break;
|
|
178
|
+
case "uint8":
|
|
179
|
+
atomData = data.getShortAt(dataStart, false);
|
|
180
|
+
break;
|
|
181
|
+
case "int":
|
|
182
|
+
case "uint": {
|
|
183
|
+
const intReader = type == "int"
|
|
184
|
+
? dataLength == 1
|
|
185
|
+
? data.getSByteAt
|
|
186
|
+
: dataLength == 2
|
|
187
|
+
? data.getSShortAt
|
|
188
|
+
: dataLength == 4
|
|
189
|
+
? data.getSLongAt
|
|
190
|
+
: data.getLongAt
|
|
191
|
+
: dataLength == 1
|
|
192
|
+
? data.getByteAt
|
|
193
|
+
: dataLength == 2
|
|
194
|
+
? data.getShortAt
|
|
195
|
+
: data.getLongAt;
|
|
196
|
+
atomData = intReader.call(data, dataStart + (dataLength == 8 ? 4 : 0), true);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case "jpeg":
|
|
200
|
+
case "png":
|
|
201
|
+
atomData = {
|
|
202
|
+
format: "image/" + type,
|
|
203
|
+
data: data.getBytesAt(dataStart, dataLength),
|
|
204
|
+
};
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
id: atomName,
|
|
210
|
+
size: atomSize,
|
|
211
|
+
description: ATOM_DESCRIPTIONS[atomName] || "Unknown",
|
|
212
|
+
data: atomData
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
getShortcuts() {
|
|
216
|
+
return SHORTCUTS;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/*
|
|
220
|
+
* https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35
|
|
221
|
+
*/
|
|
222
|
+
const TYPES = {
|
|
223
|
+
"0": "uint8",
|
|
224
|
+
"1": "text",
|
|
225
|
+
"13": "jpeg",
|
|
226
|
+
"14": "png",
|
|
227
|
+
"21": "int",
|
|
228
|
+
"22": "uint"
|
|
229
|
+
};
|
|
230
|
+
const ATOM_DESCRIPTIONS = {
|
|
231
|
+
"©alb": "Album",
|
|
232
|
+
"©ART": "Artist",
|
|
233
|
+
"aART": "Album Artist",
|
|
234
|
+
"©day": "Release Date",
|
|
235
|
+
"©nam": "Title",
|
|
236
|
+
"©gen": "Genre",
|
|
237
|
+
"gnre": "Genre",
|
|
238
|
+
"trkn": "Track Number",
|
|
239
|
+
"©wrt": "Composer",
|
|
240
|
+
"©too": "Encoding Tool",
|
|
241
|
+
"©enc": "Encoded By",
|
|
242
|
+
"cprt": "Copyright",
|
|
243
|
+
"covr": "Cover Art",
|
|
244
|
+
"©grp": "Grouping",
|
|
245
|
+
"keyw": "Keywords",
|
|
246
|
+
"©lyr": "Lyrics",
|
|
247
|
+
"©cmt": "Comment",
|
|
248
|
+
"tmpo": "Tempo",
|
|
249
|
+
"cpil": "Compilation",
|
|
250
|
+
"disk": "Disc Number",
|
|
251
|
+
"tvsh": "TV Show Name",
|
|
252
|
+
"tven": "TV Episode ID",
|
|
253
|
+
"tvsn": "TV Season",
|
|
254
|
+
"tves": "TV Episode",
|
|
255
|
+
"tvnn": "TV Network",
|
|
256
|
+
"desc": "Description",
|
|
257
|
+
"ldes": "Long Description",
|
|
258
|
+
"sonm": "Sort Name",
|
|
259
|
+
"soar": "Sort Artist",
|
|
260
|
+
"soaa": "Sort Album",
|
|
261
|
+
"soco": "Sort Composer",
|
|
262
|
+
"sosn": "Sort Show",
|
|
263
|
+
"purd": "Purchase Date",
|
|
264
|
+
"pcst": "Podcast",
|
|
265
|
+
"purl": "Podcast URL",
|
|
266
|
+
"catg": "Category",
|
|
267
|
+
"hdvd": "HD Video",
|
|
268
|
+
"stik": "Media Type",
|
|
269
|
+
"rtng": "Content Rating",
|
|
270
|
+
"pgap": "Gapless Playback",
|
|
271
|
+
"apID": "Purchase Account",
|
|
272
|
+
"sfID": "Country Code",
|
|
273
|
+
"atID": "Artist ID",
|
|
274
|
+
"cnID": "Catalog ID",
|
|
275
|
+
"plID": "Collection ID",
|
|
276
|
+
"geID": "Genre ID",
|
|
277
|
+
"xid ": "Vendor Information",
|
|
278
|
+
"flvr": "Codec Flavor"
|
|
279
|
+
};
|
|
280
|
+
const SHORTCUTS = {
|
|
281
|
+
"title": "©nam",
|
|
282
|
+
"artist": "©ART",
|
|
283
|
+
"album": "©alb",
|
|
284
|
+
"year": "©day",
|
|
285
|
+
"comment": "©cmt",
|
|
286
|
+
"track": "trkn",
|
|
287
|
+
"genre": "©gen",
|
|
288
|
+
"picture": "covr",
|
|
289
|
+
"lyrics": "©lyr"
|
|
290
|
+
};
|
|
291
|
+
module.exports = MP4TagReader;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { DecodedString } from "./DecodedString";
|
|
2
|
+
import type { LoadCallbackType, CharsetType } from "./types";
|
|
3
|
+
declare class MediaFileReader {
|
|
4
|
+
protected _isInitialized: boolean;
|
|
5
|
+
protected _size: number;
|
|
6
|
+
constructor(_path?: unknown);
|
|
7
|
+
/**
|
|
8
|
+
* Decides if this media file reader is able to read the given file.
|
|
9
|
+
*/
|
|
10
|
+
static canReadFile(_file: unknown): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* This function needs to be called before any other function.
|
|
13
|
+
* Loads the necessary initial information from the file.
|
|
14
|
+
*/
|
|
15
|
+
init(callbacks: LoadCallbackType): void;
|
|
16
|
+
_init(callbacks: LoadCallbackType): void;
|
|
17
|
+
/**
|
|
18
|
+
* @param range The start and end indexes of the range to load.
|
|
19
|
+
* Ex: [0, 7] load bytes 0 to 7 inclusive.
|
|
20
|
+
*/
|
|
21
|
+
loadRange(range: [number, number], callbacks: LoadCallbackType): void;
|
|
22
|
+
/**
|
|
23
|
+
* @return The size of the file in bytes.
|
|
24
|
+
*/
|
|
25
|
+
getSize(): number;
|
|
26
|
+
getByteAt(offset: number): number;
|
|
27
|
+
getBytesAt(offset: number, length: number): Array<number>;
|
|
28
|
+
isBitSetAt(offset: number, bit: number): boolean;
|
|
29
|
+
getSByteAt(offset: number): number;
|
|
30
|
+
getShortAt(offset: number, isBigEndian: boolean): number;
|
|
31
|
+
getSShortAt(offset: number, isBigEndian: boolean): number;
|
|
32
|
+
getLongAt(offset: number, isBigEndian: boolean): number;
|
|
33
|
+
getSLongAt(offset: number, isBigEndian: boolean): number;
|
|
34
|
+
getInteger24At(offset: number, isBigEndian: boolean): number;
|
|
35
|
+
getStringAt(offset: number, length: number): string;
|
|
36
|
+
getStringWithCharsetAt(offset: number, length: number, charset?: CharsetType | null): DecodedString;
|
|
37
|
+
getCharAt(offset: number): string;
|
|
38
|
+
/**
|
|
39
|
+
* The ID3v2 tag/frame size is encoded with four bytes where the most
|
|
40
|
+
* significant bit (bit 7) is set to zero in every byte, making a total of 28
|
|
41
|
+
* bits. The zeroed bits are ignored, so a 257 bytes long tag is represented
|
|
42
|
+
* as $00 00 02 01.
|
|
43
|
+
*/
|
|
44
|
+
getSynchsafeInteger32At(offset: number): number;
|
|
45
|
+
}
|
|
46
|
+
export = MediaFileReader;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const StringUtils = require("./StringUtils");
|
|
3
|
+
class MediaFileReader {
|
|
4
|
+
constructor(_path) {
|
|
5
|
+
this._isInitialized = false;
|
|
6
|
+
this._size = 0;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Decides if this media file reader is able to read the given file.
|
|
10
|
+
*/
|
|
11
|
+
static canReadFile(_file) {
|
|
12
|
+
throw new Error("Must implement canReadFile function");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* This function needs to be called before any other function.
|
|
16
|
+
* Loads the necessary initial information from the file.
|
|
17
|
+
*/
|
|
18
|
+
init(callbacks) {
|
|
19
|
+
const self = this;
|
|
20
|
+
if (this._isInitialized) {
|
|
21
|
+
setTimeout(callbacks.onSuccess, 1);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
this._init({
|
|
25
|
+
onSuccess: function () {
|
|
26
|
+
self._isInitialized = true;
|
|
27
|
+
callbacks.onSuccess();
|
|
28
|
+
},
|
|
29
|
+
onError: callbacks.onError,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
_init(callbacks) {
|
|
34
|
+
throw new Error("Must implement init function");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* @param range The start and end indexes of the range to load.
|
|
38
|
+
* Ex: [0, 7] load bytes 0 to 7 inclusive.
|
|
39
|
+
*/
|
|
40
|
+
loadRange(range, callbacks) {
|
|
41
|
+
throw new Error("Must implement loadRange function");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* @return The size of the file in bytes.
|
|
45
|
+
*/
|
|
46
|
+
getSize() {
|
|
47
|
+
if (!this._isInitialized) {
|
|
48
|
+
throw new Error("init() must be called first.");
|
|
49
|
+
}
|
|
50
|
+
return this._size;
|
|
51
|
+
}
|
|
52
|
+
getByteAt(offset) {
|
|
53
|
+
throw new Error("Must implement getByteAt function");
|
|
54
|
+
}
|
|
55
|
+
getBytesAt(offset, length) {
|
|
56
|
+
const bytes = new Array(length);
|
|
57
|
+
for (let i = 0; i < length; i++) {
|
|
58
|
+
bytes[i] = this.getByteAt(offset + i);
|
|
59
|
+
}
|
|
60
|
+
return bytes;
|
|
61
|
+
}
|
|
62
|
+
isBitSetAt(offset, bit) {
|
|
63
|
+
const iByte = this.getByteAt(offset);
|
|
64
|
+
return (iByte & (1 << bit)) != 0;
|
|
65
|
+
}
|
|
66
|
+
getSByteAt(offset) {
|
|
67
|
+
const iByte = this.getByteAt(offset);
|
|
68
|
+
if (iByte > 127) {
|
|
69
|
+
return iByte - 256;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
return iByte;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
getShortAt(offset, isBigEndian) {
|
|
76
|
+
let iShort = isBigEndian
|
|
77
|
+
? (this.getByteAt(offset) << 8) + this.getByteAt(offset + 1)
|
|
78
|
+
: (this.getByteAt(offset + 1) << 8) + this.getByteAt(offset);
|
|
79
|
+
if (iShort < 0) {
|
|
80
|
+
iShort += 65536;
|
|
81
|
+
}
|
|
82
|
+
return iShort;
|
|
83
|
+
}
|
|
84
|
+
getSShortAt(offset, isBigEndian) {
|
|
85
|
+
const iUShort = this.getShortAt(offset, isBigEndian);
|
|
86
|
+
if (iUShort > 32767) {
|
|
87
|
+
return iUShort - 65536;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
return iUShort;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
getLongAt(offset, isBigEndian) {
|
|
94
|
+
const iByte1 = this.getByteAt(offset), iByte2 = this.getByteAt(offset + 1), iByte3 = this.getByteAt(offset + 2), iByte4 = this.getByteAt(offset + 3);
|
|
95
|
+
let iLong = isBigEndian
|
|
96
|
+
? (((((iByte1 << 8) + iByte2) << 8) + iByte3) << 8) + iByte4
|
|
97
|
+
: (((((iByte4 << 8) + iByte3) << 8) + iByte2) << 8) + iByte1;
|
|
98
|
+
if (iLong < 0) {
|
|
99
|
+
iLong += 4294967296;
|
|
100
|
+
}
|
|
101
|
+
return iLong;
|
|
102
|
+
}
|
|
103
|
+
getSLongAt(offset, isBigEndian) {
|
|
104
|
+
const iULong = this.getLongAt(offset, isBigEndian);
|
|
105
|
+
if (iULong > 2147483647) {
|
|
106
|
+
return iULong - 4294967296;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
return iULong;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
getInteger24At(offset, isBigEndian) {
|
|
113
|
+
const iByte1 = this.getByteAt(offset), iByte2 = this.getByteAt(offset + 1), iByte3 = this.getByteAt(offset + 2);
|
|
114
|
+
let iInteger = isBigEndian
|
|
115
|
+
? (((iByte1 << 8) + iByte2) << 8) + iByte3
|
|
116
|
+
: (((iByte3 << 8) + iByte2) << 8) + iByte1;
|
|
117
|
+
if (iInteger < 0) {
|
|
118
|
+
iInteger += 16777216;
|
|
119
|
+
}
|
|
120
|
+
return iInteger;
|
|
121
|
+
}
|
|
122
|
+
getStringAt(offset, length) {
|
|
123
|
+
const string = [];
|
|
124
|
+
for (let i = offset, j = 0; i < offset + length; i++, j++) {
|
|
125
|
+
string[j] = String.fromCharCode(this.getByteAt(i));
|
|
126
|
+
}
|
|
127
|
+
return string.join("");
|
|
128
|
+
}
|
|
129
|
+
getStringWithCharsetAt(offset, length, charset) {
|
|
130
|
+
const bytes = this.getBytesAt(offset, length);
|
|
131
|
+
let string;
|
|
132
|
+
switch ((charset || "").toLowerCase()) {
|
|
133
|
+
case "utf-16":
|
|
134
|
+
case "utf-16le":
|
|
135
|
+
case "utf-16be":
|
|
136
|
+
string = StringUtils.readUTF16String(bytes, charset === "utf-16be");
|
|
137
|
+
break;
|
|
138
|
+
case "utf-8":
|
|
139
|
+
string = StringUtils.readUTF8String(bytes);
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
string = StringUtils.readNullTerminatedString(bytes);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
return string;
|
|
146
|
+
}
|
|
147
|
+
getCharAt(offset) {
|
|
148
|
+
return String.fromCharCode(this.getByteAt(offset));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The ID3v2 tag/frame size is encoded with four bytes where the most
|
|
152
|
+
* significant bit (bit 7) is set to zero in every byte, making a total of 28
|
|
153
|
+
* bits. The zeroed bits are ignored, so a 257 bytes long tag is represented
|
|
154
|
+
* as $00 00 02 01.
|
|
155
|
+
*/
|
|
156
|
+
getSynchsafeInteger32At(offset) {
|
|
157
|
+
const size1 = this.getByteAt(offset);
|
|
158
|
+
const size2 = this.getByteAt(offset + 1);
|
|
159
|
+
const size3 = this.getByteAt(offset + 2);
|
|
160
|
+
const size4 = this.getByteAt(offset + 3);
|
|
161
|
+
const size = size4 & 0x7f |
|
|
162
|
+
((size3 & 0x7f) << 7) |
|
|
163
|
+
((size2 & 0x7f) << 14) |
|
|
164
|
+
((size1 & 0x7f) << 21);
|
|
165
|
+
return size;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
module.exports = MediaFileReader;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
declare const MediaFileReader: any;
|
|
2
|
+
import type { CallbackType, LoadCallbackType, ByteRange, TagType } from "./types";
|
|
3
|
+
declare class MediaTagReader {
|
|
4
|
+
private _mediaFileReader;
|
|
5
|
+
private _tags;
|
|
6
|
+
constructor(mediaFileReader: InstanceType<typeof MediaFileReader>);
|
|
7
|
+
static getTagIdentifierByteRange(): ByteRange;
|
|
8
|
+
static canReadTagFormat(_tagIdentifier: Array<number>): boolean;
|
|
9
|
+
setTagsToRead(tags: string[] | null): this;
|
|
10
|
+
read(callbacks: CallbackType<TagType>): void;
|
|
11
|
+
read(): Promise<TagType>;
|
|
12
|
+
readAsync(): Promise<TagType>;
|
|
13
|
+
getShortcuts(): Record<string, string | string[]>;
|
|
14
|
+
_loadData(_mediaFileReader: InstanceType<typeof MediaFileReader>, _callbacks: LoadCallbackType): void;
|
|
15
|
+
_parseData(_mediaFileReader: InstanceType<typeof MediaFileReader>, _tags: string[] | null): TagType;
|
|
16
|
+
_expandShortcutTags(tagsWithShortcuts: string[] | null): string[] | null;
|
|
17
|
+
}
|
|
18
|
+
export = MediaTagReader;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const MediaFileReader = require("./MediaFileReader");
|
|
3
|
+
class MediaTagReader {
|
|
4
|
+
constructor(mediaFileReader) {
|
|
5
|
+
this._mediaFileReader = mediaFileReader;
|
|
6
|
+
this._tags = null;
|
|
7
|
+
}
|
|
8
|
+
static getTagIdentifierByteRange() {
|
|
9
|
+
throw new Error("Must implement");
|
|
10
|
+
}
|
|
11
|
+
static canReadTagFormat(_tagIdentifier) {
|
|
12
|
+
throw new Error("Must implement");
|
|
13
|
+
}
|
|
14
|
+
setTagsToRead(tags) {
|
|
15
|
+
this._tags = tags;
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
read(callbacks) {
|
|
19
|
+
if (callbacks === undefined) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
this.read({ onSuccess: resolve, onError: reject });
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const self = this;
|
|
25
|
+
this._mediaFileReader.init({
|
|
26
|
+
onSuccess: function () {
|
|
27
|
+
self._loadData(self._mediaFileReader, {
|
|
28
|
+
onSuccess: function () {
|
|
29
|
+
let tags;
|
|
30
|
+
try {
|
|
31
|
+
tags = self._parseData(self._mediaFileReader, self._tags);
|
|
32
|
+
}
|
|
33
|
+
catch (ex) {
|
|
34
|
+
const err = ex;
|
|
35
|
+
if (callbacks.onError) {
|
|
36
|
+
callbacks.onError({
|
|
37
|
+
type: "parseData",
|
|
38
|
+
info: err.message,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
callbacks.onSuccess(tags);
|
|
44
|
+
},
|
|
45
|
+
onError: callbacks.onError,
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
onError: callbacks.onError,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
readAsync() {
|
|
52
|
+
return this.read();
|
|
53
|
+
}
|
|
54
|
+
getShortcuts() {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
_loadData(_mediaFileReader, _callbacks) {
|
|
58
|
+
throw new Error("Must implement _loadData function");
|
|
59
|
+
}
|
|
60
|
+
_parseData(_mediaFileReader, _tags) {
|
|
61
|
+
throw new Error("Must implement _parseData function");
|
|
62
|
+
}
|
|
63
|
+
_expandShortcutTags(tagsWithShortcuts) {
|
|
64
|
+
if (!tagsWithShortcuts) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
let tags = [];
|
|
68
|
+
const shortcuts = this.getShortcuts();
|
|
69
|
+
for (let i = 0, tagOrShortcut; (tagOrShortcut = tagsWithShortcuts[i]); i++) {
|
|
70
|
+
tags = tags.concat(shortcuts[tagOrShortcut] ||
|
|
71
|
+
[tagOrShortcut]);
|
|
72
|
+
}
|
|
73
|
+
return tags;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
module.exports = MediaTagReader;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare const MediaFileReader: any;
|
|
2
|
+
import type { LoadCallbackType } from "./types";
|
|
3
|
+
declare class NodeFileReader extends MediaFileReader {
|
|
4
|
+
private _path;
|
|
5
|
+
private _fileData;
|
|
6
|
+
constructor(path: string);
|
|
7
|
+
static canReadFile(file: unknown): boolean;
|
|
8
|
+
getByteAt(offset: number): number;
|
|
9
|
+
_init(callbacks: LoadCallbackType): void;
|
|
10
|
+
loadRange(range: [number, number], callbacks: LoadCallbackType): void;
|
|
11
|
+
}
|
|
12
|
+
export = NodeFileReader;
|