music-metadata 11.10.4 → 11.10.5
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/lib/apev2/APEv2Parser.js +2 -2
- package/lib/common/Util.d.ts +3 -5
- package/lib/common/Util.js +15 -17
- package/lib/id3v2/FrameHeader.d.ts +31 -0
- package/lib/id3v2/FrameHeader.js +73 -0
- package/lib/id3v2/FrameParser.d.ts +1 -0
- package/lib/id3v2/FrameParser.js +87 -63
- package/lib/id3v2/ID3v2Parser.d.ts +0 -3
- package/lib/id3v2/ID3v2Parser.js +4 -60
- package/package.json +1 -1
package/lib/apev2/APEv2Parser.js
CHANGED
|
@@ -113,7 +113,7 @@ export class APEv2Parser extends BasicParser {
|
|
|
113
113
|
const tagItemHeader = await this.tokenizer.readToken(TagItemHeader);
|
|
114
114
|
bytesRemaining -= TagItemHeader.len + tagItemHeader.size;
|
|
115
115
|
await this.tokenizer.peekBuffer(keyBuffer, { length: Math.min(keyBuffer.length, bytesRemaining) });
|
|
116
|
-
let zero = util.findZero(keyBuffer
|
|
116
|
+
let zero = util.findZero(keyBuffer);
|
|
117
117
|
const key = await this.tokenizer.readToken(new StringType(zero, 'ascii'));
|
|
118
118
|
await this.tokenizer.ignore(1);
|
|
119
119
|
bytesRemaining -= key.length + 1;
|
|
@@ -131,7 +131,7 @@ export class APEv2Parser extends BasicParser {
|
|
|
131
131
|
else {
|
|
132
132
|
const picData = new Uint8Array(tagItemHeader.size);
|
|
133
133
|
await this.tokenizer.readBuffer(picData);
|
|
134
|
-
zero = util.findZero(picData
|
|
134
|
+
zero = util.findZero(picData);
|
|
135
135
|
const description = textDecode(picData.subarray(0, zero), 'utf-8');
|
|
136
136
|
const data = picData.subarray(zero + 1);
|
|
137
137
|
await this.metadata.addTag(tagFormat, key, {
|
package/lib/common/Util.d.ts
CHANGED
|
@@ -2,14 +2,12 @@ import type { IRatio } from '../type.js';
|
|
|
2
2
|
export type StringEncoding = 'ascii' | 'utf8' | 'utf-16le' | 'ucs2' | 'base64url' | 'latin1' | 'hex';
|
|
3
3
|
export declare function getBit(buf: Uint8Array, off: number, bit: number): boolean;
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Find delimiting zero in uint8Array
|
|
6
6
|
* @param uint8Array Uint8Array to find the zero delimiter in
|
|
7
|
-
* @param start Offset in uint8Array
|
|
8
|
-
* @param end Last position to parse in uint8Array
|
|
9
7
|
* @param encoding The string encoding used
|
|
10
|
-
* @return
|
|
8
|
+
* @return position in uint8Array where zero found, or uint8Array.length if not found
|
|
11
9
|
*/
|
|
12
|
-
export declare function findZero(uint8Array: Uint8Array,
|
|
10
|
+
export declare function findZero(uint8Array: Uint8Array, encoding?: StringEncoding): number;
|
|
13
11
|
export declare function trimRightNull(x: string): string;
|
|
14
12
|
/**
|
|
15
13
|
* Decode string
|
package/lib/common/Util.js
CHANGED
|
@@ -5,33 +5,31 @@ export function getBit(buf, off, bit) {
|
|
|
5
5
|
return (buf[off] & (1 << bit)) !== 0;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Find delimiting zero in uint8Array
|
|
9
9
|
* @param uint8Array Uint8Array to find the zero delimiter in
|
|
10
|
-
* @param start Offset in uint8Array
|
|
11
|
-
* @param end Last position to parse in uint8Array
|
|
12
10
|
* @param encoding The string encoding used
|
|
13
|
-
* @return
|
|
11
|
+
* @return position in uint8Array where zero found, or uint8Array.length if not found
|
|
14
12
|
*/
|
|
15
|
-
export function findZero(uint8Array,
|
|
16
|
-
|
|
13
|
+
export function findZero(uint8Array, encoding) {
|
|
14
|
+
const len = uint8Array.length;
|
|
17
15
|
if (encoding === 'utf-16le') {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
// Look for 0x00 0x00 on 2-byte boundary
|
|
17
|
+
for (let i = 0; i + 1 < len; i += 2) {
|
|
18
|
+
if (uint8Array[i] === 0 && uint8Array[i + 1] === 0)
|
|
19
|
+
return i;
|
|
22
20
|
}
|
|
23
|
-
return
|
|
21
|
+
return len;
|
|
24
22
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
// latin1 / utf8 / utf16be (caller typically handles utf16be separately or via decode)
|
|
24
|
+
for (let i = 0; i < len; i++) {
|
|
25
|
+
if (uint8Array[i] === 0)
|
|
26
|
+
return i;
|
|
29
27
|
}
|
|
30
|
-
return
|
|
28
|
+
return len;
|
|
31
29
|
}
|
|
32
30
|
export function trimRightNull(x) {
|
|
33
31
|
const pos0 = x.indexOf('\0');
|
|
34
|
-
return pos0 === -1 ? x : x.
|
|
32
|
+
return pos0 === -1 ? x : x.substring(0, pos0);
|
|
35
33
|
}
|
|
36
34
|
function swapBytes(uint8Array) {
|
|
37
35
|
const l = uint8Array.length;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ID3v2MajorVersion } from './ID3v2Token.js';
|
|
2
|
+
import type { IWarningCollector } from '../common/MetadataCollector.js';
|
|
3
|
+
export interface IFrameFlags {
|
|
4
|
+
status: {
|
|
5
|
+
tag_alter_preservation: boolean;
|
|
6
|
+
file_alter_preservation: boolean;
|
|
7
|
+
read_only: boolean;
|
|
8
|
+
};
|
|
9
|
+
format: {
|
|
10
|
+
grouping_identity: boolean;
|
|
11
|
+
compression: boolean;
|
|
12
|
+
encryption: boolean;
|
|
13
|
+
unsynchronisation: boolean;
|
|
14
|
+
data_length_indicator: boolean;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface IFrameHeader {
|
|
18
|
+
id: string;
|
|
19
|
+
length: number;
|
|
20
|
+
flags?: IFrameFlags;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Frame header length (bytes) depending on ID3v2 major version.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getFrameHeaderLength(majorVer: number): 6 | 10;
|
|
26
|
+
/**
|
|
27
|
+
* Factory: parse a frame header from its header bytes (6 for v2.2, 10 for v2.3/v2.4).
|
|
28
|
+
*
|
|
29
|
+
* Note: It only *parses* and does light validation. It does not read payload bytes.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readFrameHeader(uint8Array: Uint8Array, majorVer: ID3v2MajorVersion, warningCollector: IWarningCollector): IFrameHeader;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// lib/id3v2/FrameHeader.ts
|
|
2
|
+
import * as Token from 'token-types';
|
|
3
|
+
import * as util from '../common/Util.js';
|
|
4
|
+
import { UINT32SYNCSAFE } from './ID3v2Token.js';
|
|
5
|
+
import { textDecode } from '@borewit/text-codec';
|
|
6
|
+
import { Id3v2ContentError } from './FrameParser.js';
|
|
7
|
+
/**
|
|
8
|
+
* Frame header length (bytes) depending on ID3v2 major version.
|
|
9
|
+
*/
|
|
10
|
+
export function getFrameHeaderLength(majorVer) {
|
|
11
|
+
switch (majorVer) {
|
|
12
|
+
case 2: return 6;
|
|
13
|
+
case 3:
|
|
14
|
+
case 4: return 10;
|
|
15
|
+
default: throw makeUnexpectedMajorVersionError(majorVer);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function readFrameFlags(b) {
|
|
19
|
+
return {
|
|
20
|
+
status: {
|
|
21
|
+
tag_alter_preservation: util.getBit(b, 0, 6),
|
|
22
|
+
file_alter_preservation: util.getBit(b, 0, 5),
|
|
23
|
+
read_only: util.getBit(b, 0, 4)
|
|
24
|
+
},
|
|
25
|
+
format: {
|
|
26
|
+
grouping_identity: util.getBit(b, 1, 7),
|
|
27
|
+
compression: util.getBit(b, 1, 3),
|
|
28
|
+
encryption: util.getBit(b, 1, 2),
|
|
29
|
+
unsynchronisation: util.getBit(b, 1, 1),
|
|
30
|
+
data_length_indicator: util.getBit(b, 1, 0)
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Factory: parse a frame header from its header bytes (6 for v2.2, 10 for v2.3/v2.4).
|
|
36
|
+
*
|
|
37
|
+
* Note: It only *parses* and does light validation. It does not read payload bytes.
|
|
38
|
+
*/
|
|
39
|
+
export function readFrameHeader(uint8Array, majorVer, warningCollector) {
|
|
40
|
+
switch (majorVer) {
|
|
41
|
+
case 2:
|
|
42
|
+
return parseFrameHeaderV22(uint8Array, majorVer, warningCollector);
|
|
43
|
+
case 3:
|
|
44
|
+
case 4:
|
|
45
|
+
return parseFrameHeaderV23V24(uint8Array, majorVer, warningCollector);
|
|
46
|
+
default:
|
|
47
|
+
throw makeUnexpectedMajorVersionError(majorVer);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function parseFrameHeaderV22(uint8Array, majorVer, warningCollector) {
|
|
51
|
+
const header = {
|
|
52
|
+
id: textDecode(uint8Array.subarray(0, 3), 'ascii'),
|
|
53
|
+
length: Token.UINT24_BE.get(uint8Array, 3)
|
|
54
|
+
};
|
|
55
|
+
if (!header.id.match(/^[A-Z0-9]{3}$/)) {
|
|
56
|
+
warningCollector.addWarning(`Invalid ID3v2.${majorVer} frame-header-ID: ${header.id}`);
|
|
57
|
+
}
|
|
58
|
+
return header;
|
|
59
|
+
}
|
|
60
|
+
function parseFrameHeaderV23V24(uint8Array, majorVer, warningCollector) {
|
|
61
|
+
const header = {
|
|
62
|
+
id: textDecode(uint8Array.subarray(0, 4), 'ascii'),
|
|
63
|
+
length: (majorVer === 4 ? UINT32SYNCSAFE : Token.UINT32_BE).get(uint8Array, 4),
|
|
64
|
+
flags: readFrameFlags(uint8Array.subarray(8, 10))
|
|
65
|
+
};
|
|
66
|
+
if (!header.id.match(/^[A-Z0-9]{4}$/)) {
|
|
67
|
+
warningCollector.addWarning(`Invalid ID3v2.${majorVer} frame-header-ID: ${header.id}`);
|
|
68
|
+
}
|
|
69
|
+
return header;
|
|
70
|
+
}
|
|
71
|
+
function makeUnexpectedMajorVersionError(majorVer) {
|
|
72
|
+
throw new Id3v2ContentError(`Unexpected majorVer: ${majorVer}`);
|
|
73
|
+
}
|
package/lib/id3v2/FrameParser.js
CHANGED
|
@@ -7,6 +7,7 @@ import { makeUnexpectedFileContentError } from '../ParseError.js';
|
|
|
7
7
|
import { decodeUintBE } from '../common/Util.js';
|
|
8
8
|
const debug = initDebug('music-metadata:id3v2:frame-parser');
|
|
9
9
|
const defaultEnc = 'latin1'; // latin1 == iso-8859-1;
|
|
10
|
+
const urlEnc = { encoding: defaultEnc, bom: false };
|
|
10
11
|
export function parseGenre(origVal) {
|
|
11
12
|
// match everything inside parentheses
|
|
12
13
|
const genres = [];
|
|
@@ -90,7 +91,7 @@ export class FrameParser {
|
|
|
90
91
|
case 'PCST': {
|
|
91
92
|
let text;
|
|
92
93
|
try {
|
|
93
|
-
text = util.decodeString(uint8Array.subarray(1), encoding)
|
|
94
|
+
text = FrameParser.trimNullPadding(util.decodeString(uint8Array.subarray(1), encoding));
|
|
94
95
|
}
|
|
95
96
|
catch (error) {
|
|
96
97
|
if (error instanceof Error) {
|
|
@@ -135,7 +136,7 @@ export class FrameParser {
|
|
|
135
136
|
break;
|
|
136
137
|
}
|
|
137
138
|
case 'TXXX': {
|
|
138
|
-
const idAndData = FrameParser.readIdentifierAndData(uint8Array
|
|
139
|
+
const idAndData = FrameParser.readIdentifierAndData(uint8Array.subarray(1), encoding);
|
|
139
140
|
const textTag = {
|
|
140
141
|
description: idAndData.id,
|
|
141
142
|
text: this.splitValue(type, util.decodeString(idAndData.data, encoding).replace(/\x00+$/, ''))
|
|
@@ -147,28 +148,28 @@ export class FrameParser {
|
|
|
147
148
|
case 'APIC':
|
|
148
149
|
if (includeCovers) {
|
|
149
150
|
const pic = {};
|
|
150
|
-
|
|
151
|
+
uint8Array = uint8Array.subarray(1);
|
|
151
152
|
switch (this.major) {
|
|
152
153
|
case 2:
|
|
153
|
-
pic.format = util.decodeString(uint8Array.subarray(
|
|
154
|
-
|
|
154
|
+
pic.format = util.decodeString(uint8Array.subarray(0, 3), 'latin1'); // 'latin1'; // latin1 == iso-8859-1;
|
|
155
|
+
uint8Array = uint8Array.subarray(3);
|
|
155
156
|
break;
|
|
156
157
|
case 3:
|
|
157
158
|
case 4:
|
|
158
|
-
fzero = util.findZero(uint8Array,
|
|
159
|
-
pic.format = util.decodeString(uint8Array.subarray(
|
|
160
|
-
|
|
159
|
+
fzero = util.findZero(uint8Array, defaultEnc);
|
|
160
|
+
pic.format = util.decodeString(uint8Array.subarray(0, fzero), defaultEnc);
|
|
161
|
+
uint8Array = uint8Array.subarray(fzero + 1);
|
|
161
162
|
break;
|
|
162
163
|
default:
|
|
163
164
|
throw makeUnexpectedMajorVersionError(this.major);
|
|
164
165
|
}
|
|
165
166
|
pic.format = FrameParser.fixPictureMimeType(pic.format);
|
|
166
|
-
pic.type = AttachedPictureType[uint8Array[
|
|
167
|
-
|
|
168
|
-
fzero = util.findZero(uint8Array,
|
|
169
|
-
pic.description = util.decodeString(uint8Array.subarray(
|
|
170
|
-
|
|
171
|
-
pic.data = uint8Array
|
|
167
|
+
pic.type = AttachedPictureType[uint8Array[0]];
|
|
168
|
+
uint8Array = uint8Array.subarray(1);
|
|
169
|
+
fzero = util.findZero(uint8Array, encoding);
|
|
170
|
+
pic.description = util.decodeString(uint8Array.subarray(0, fzero), encoding);
|
|
171
|
+
uint8Array = uint8Array.subarray(fzero + nullTerminatorLength);
|
|
172
|
+
pic.data = uint8Array;
|
|
172
173
|
output = pic;
|
|
173
174
|
}
|
|
174
175
|
break;
|
|
@@ -178,7 +179,7 @@ export class FrameParser {
|
|
|
178
179
|
break;
|
|
179
180
|
case 'SYLT': {
|
|
180
181
|
const syltHeader = SyncTextHeader.get(uint8Array, 0);
|
|
181
|
-
|
|
182
|
+
uint8Array = uint8Array.subarray(SyncTextHeader.len);
|
|
182
183
|
const result = {
|
|
183
184
|
descriptor: '',
|
|
184
185
|
language: syltHeader.language,
|
|
@@ -187,12 +188,12 @@ export class FrameParser {
|
|
|
187
188
|
syncText: []
|
|
188
189
|
};
|
|
189
190
|
let readSyllables = false;
|
|
190
|
-
while (
|
|
191
|
-
const nullStr = FrameParser.readNullTerminatedString(uint8Array
|
|
192
|
-
|
|
191
|
+
while (uint8Array.length > 0) {
|
|
192
|
+
const nullStr = FrameParser.readNullTerminatedString(uint8Array, syltHeader.encoding);
|
|
193
|
+
uint8Array = uint8Array.subarray(nullStr.len);
|
|
193
194
|
if (readSyllables) {
|
|
194
|
-
const timestamp = Token.UINT32_BE.get(uint8Array,
|
|
195
|
-
|
|
195
|
+
const timestamp = Token.UINT32_BE.get(uint8Array, 0);
|
|
196
|
+
uint8Array = uint8Array.subarray(Token.UINT32_BE.len);
|
|
196
197
|
result.syncText.push({
|
|
197
198
|
text: nullStr.text,
|
|
198
199
|
timestamp
|
|
@@ -224,42 +225,52 @@ export class FrameParser {
|
|
|
224
225
|
break;
|
|
225
226
|
}
|
|
226
227
|
case 'UFID': {
|
|
227
|
-
const ufid = FrameParser.readIdentifierAndData(uint8Array,
|
|
228
|
+
const ufid = FrameParser.readIdentifierAndData(uint8Array, defaultEnc);
|
|
228
229
|
output = { owner_identifier: ufid.id, identifier: ufid.data };
|
|
229
230
|
break;
|
|
230
231
|
}
|
|
231
232
|
case 'PRIV': { // private frame
|
|
232
|
-
const priv = FrameParser.readIdentifierAndData(uint8Array,
|
|
233
|
+
const priv = FrameParser.readIdentifierAndData(uint8Array, defaultEnc);
|
|
233
234
|
output = { owner_identifier: priv.id, data: priv.data };
|
|
234
235
|
break;
|
|
235
236
|
}
|
|
236
237
|
case 'POPM': { // Popularimeter
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
uint8Array = uint8Array.subarray(offset);
|
|
239
|
+
const emailStr = FrameParser.readNullTerminatedString(uint8Array, urlEnc);
|
|
240
|
+
const email = emailStr.text;
|
|
241
|
+
uint8Array = uint8Array.subarray(emailStr.len);
|
|
242
|
+
if (uint8Array.length === 0) {
|
|
243
|
+
this.warningCollector.addWarning(`id3v2.${this.major} type=${type} POPM frame missing rating byte`);
|
|
244
|
+
output = { email, rating: 0, counter: undefined };
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
const rating = Token.UINT8.get(uint8Array, 0);
|
|
248
|
+
const counterBytes = uint8Array.subarray(Token.UINT8.len);
|
|
241
249
|
output = {
|
|
242
250
|
email,
|
|
243
|
-
rating
|
|
244
|
-
counter:
|
|
251
|
+
rating,
|
|
252
|
+
counter: counterBytes.length > 0 ? decodeUintBE(counterBytes) : undefined
|
|
245
253
|
};
|
|
246
254
|
break;
|
|
247
255
|
}
|
|
248
256
|
case 'GEOB': { // General encapsulated object
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
257
|
+
// [encoding] <MIME> 0x00 <filename> 0x00/0x00 0x00 <description> 0x00/0x00 0x00 <data>
|
|
258
|
+
const { encoding: geobEncoding, bom: geobBom } = TextEncodingToken.get(uint8Array, 0);
|
|
259
|
+
uint8Array = uint8Array.subarray(1);
|
|
260
|
+
const mimeTypeStr = FrameParser.readNullTerminatedString(uint8Array, urlEnc);
|
|
261
|
+
const mimeType = mimeTypeStr.text;
|
|
262
|
+
uint8Array = uint8Array.subarray(mimeTypeStr.len);
|
|
263
|
+
const filenameStr = FrameParser.readNullTerminatedString(uint8Array, { encoding: geobEncoding, bom: geobBom });
|
|
264
|
+
const filename = filenameStr.text;
|
|
265
|
+
uint8Array = uint8Array.subarray(filenameStr.len);
|
|
266
|
+
const descriptionStr = FrameParser.readNullTerminatedString(uint8Array, { encoding: geobEncoding, bom: geobBom });
|
|
267
|
+
const description = descriptionStr.text;
|
|
268
|
+
uint8Array = uint8Array.subarray(descriptionStr.len);
|
|
258
269
|
const geob = {
|
|
259
270
|
type: mimeType,
|
|
260
271
|
filename,
|
|
261
272
|
description,
|
|
262
|
-
data: uint8Array
|
|
273
|
+
data: uint8Array
|
|
263
274
|
};
|
|
264
275
|
output = geob;
|
|
265
276
|
break;
|
|
@@ -274,21 +285,26 @@ export class FrameParser {
|
|
|
274
285
|
case 'WPAY':
|
|
275
286
|
case 'WPUB':
|
|
276
287
|
// Decode URL
|
|
277
|
-
|
|
278
|
-
output = util.decodeString(uint8Array.subarray(offset, fzero), defaultEnc);
|
|
288
|
+
output = FrameParser.readNullTerminatedString(uint8Array, urlEnc).text;
|
|
279
289
|
break;
|
|
280
290
|
case 'WXXX': {
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
291
|
+
// [encoding] <description> 0x00/0x00 0x00 <url>
|
|
292
|
+
const { encoding: wxxxEncoding, bom: wxxxBom } = TextEncodingToken.get(uint8Array, 0);
|
|
293
|
+
uint8Array = uint8Array.subarray(1);
|
|
294
|
+
const descriptionStr = FrameParser.readNullTerminatedString(uint8Array, { encoding: wxxxEncoding, bom: wxxxBom });
|
|
295
|
+
const description = descriptionStr.text;
|
|
296
|
+
uint8Array = uint8Array.subarray(descriptionStr.len);
|
|
297
|
+
// URL is always ISO-8859-1
|
|
298
|
+
output = { description, url: FrameParser.trimNullPadding(util.decodeString(uint8Array, defaultEnc)) };
|
|
286
299
|
break;
|
|
287
300
|
}
|
|
288
301
|
case 'WFD':
|
|
289
|
-
case 'WFED':
|
|
290
|
-
|
|
302
|
+
case 'WFED': {
|
|
303
|
+
const { encoding: wfdEncoding, bom: wfdBom } = TextEncodingToken.get(uint8Array, 0);
|
|
304
|
+
uint8Array = uint8Array.subarray(1);
|
|
305
|
+
output = FrameParser.readNullTerminatedString(uint8Array, { encoding: wfdEncoding, bom: wfdBom }).text;
|
|
291
306
|
break;
|
|
307
|
+
}
|
|
292
308
|
case 'MCDI': {
|
|
293
309
|
// Music CD identifier
|
|
294
310
|
output = uint8Array.subarray(0, length);
|
|
@@ -301,18 +317,21 @@ export class FrameParser {
|
|
|
301
317
|
return output;
|
|
302
318
|
}
|
|
303
319
|
static readNullTerminatedString(uint8Array, encoding) {
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
320
|
+
const bomSize = encoding.bom ? 2 : 0;
|
|
321
|
+
const originalLen = uint8Array.length;
|
|
322
|
+
const valueArray = uint8Array.subarray(bomSize);
|
|
323
|
+
const zeroIndex = util.findZero(valueArray, encoding.encoding);
|
|
324
|
+
if (zeroIndex >= valueArray.length) {
|
|
325
|
+
// No terminator found, decode full buffer remainder
|
|
326
|
+
return {
|
|
327
|
+
text: util.decodeString(valueArray, encoding.encoding),
|
|
328
|
+
len: originalLen
|
|
329
|
+
};
|
|
312
330
|
}
|
|
331
|
+
const txt = valueArray.subarray(0, zeroIndex);
|
|
313
332
|
return {
|
|
314
333
|
text: util.decodeString(txt, encoding.encoding),
|
|
315
|
-
len:
|
|
334
|
+
len: bomSize + zeroIndex + FrameParser.getNullTerminatorLength(encoding.encoding)
|
|
316
335
|
};
|
|
317
336
|
}
|
|
318
337
|
static fixPictureMimeType(pictureType) {
|
|
@@ -361,16 +380,21 @@ export class FrameParser {
|
|
|
361
380
|
return FrameParser.trimArray(values);
|
|
362
381
|
}
|
|
363
382
|
static trimArray(values) {
|
|
364
|
-
return values.map(value =>
|
|
383
|
+
return values.map(value => FrameParser.trimNullPadding(value).trim());
|
|
384
|
+
}
|
|
385
|
+
static trimNullPadding(value) {
|
|
386
|
+
let end = value.length;
|
|
387
|
+
while (end > 0 && value.charCodeAt(end - 1) === 0) {
|
|
388
|
+
end--;
|
|
389
|
+
}
|
|
390
|
+
return end === value.length ? value : value.slice(0, end);
|
|
365
391
|
}
|
|
366
|
-
static readIdentifierAndData(uint8Array,
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
offset = fzero + FrameParser.getNullTerminatorLength(encoding);
|
|
370
|
-
return { id, data: uint8Array.subarray(offset, length) };
|
|
392
|
+
static readIdentifierAndData(uint8Array, encoding) {
|
|
393
|
+
const idStr = FrameParser.readNullTerminatedString(uint8Array, { encoding, bom: false });
|
|
394
|
+
return { id: idStr.text, data: uint8Array.subarray(idStr.len) };
|
|
371
395
|
}
|
|
372
396
|
static getNullTerminatorLength(enc) {
|
|
373
|
-
return enc
|
|
397
|
+
return enc.startsWith('utf-16') ? 2 : 1;
|
|
374
398
|
}
|
|
375
399
|
}
|
|
376
400
|
export class Id3v2ContentError extends makeUnexpectedFileContentError('id3v2') {
|
|
@@ -3,8 +3,6 @@ import type { IOptions } from '../type.js';
|
|
|
3
3
|
import type { INativeMetadataCollector } from '../common/MetadataCollector.js';
|
|
4
4
|
export declare class ID3v2Parser {
|
|
5
5
|
static removeUnsyncBytes(buffer: Uint8Array): Uint8Array;
|
|
6
|
-
private static getFrameHeaderLength;
|
|
7
|
-
private static readFrameFlags;
|
|
8
6
|
private static readFrameData;
|
|
9
7
|
/**
|
|
10
8
|
* Create a combined tag key, of tag & description
|
|
@@ -25,5 +23,4 @@ export declare class ID3v2Parser {
|
|
|
25
23
|
private handleTag;
|
|
26
24
|
private addTag;
|
|
27
25
|
private parseMetadata;
|
|
28
|
-
private readFrameHeader;
|
|
29
26
|
}
|
package/lib/id3v2/ID3v2Parser.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as Token from 'token-types';
|
|
2
|
-
import * as util from '../common/Util.js';
|
|
3
2
|
import { FrameParser, Id3v2ContentError } from './FrameParser.js';
|
|
4
|
-
import { ExtendedHeader, ID3v2Header
|
|
5
|
-
import {
|
|
3
|
+
import { ExtendedHeader, ID3v2Header } from './ID3v2Token.js';
|
|
4
|
+
import { getFrameHeaderLength, readFrameHeader } from './FrameHeader.js';
|
|
6
5
|
export class ID3v2Parser {
|
|
7
6
|
constructor() {
|
|
8
7
|
this.tokenizer = undefined;
|
|
@@ -26,33 +25,6 @@ export class ID3v2Parser {
|
|
|
26
25
|
}
|
|
27
26
|
return buffer.subarray(0, writeI);
|
|
28
27
|
}
|
|
29
|
-
static getFrameHeaderLength(majorVer) {
|
|
30
|
-
switch (majorVer) {
|
|
31
|
-
case 2:
|
|
32
|
-
return 6;
|
|
33
|
-
case 3:
|
|
34
|
-
case 4:
|
|
35
|
-
return 10;
|
|
36
|
-
default:
|
|
37
|
-
throw makeUnexpectedMajorVersionError(majorVer);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
static readFrameFlags(b) {
|
|
41
|
-
return {
|
|
42
|
-
status: {
|
|
43
|
-
tag_alter_preservation: util.getBit(b, 0, 6),
|
|
44
|
-
file_alter_preservation: util.getBit(b, 0, 5),
|
|
45
|
-
read_only: util.getBit(b, 0, 4)
|
|
46
|
-
},
|
|
47
|
-
format: {
|
|
48
|
-
grouping_identity: util.getBit(b, 1, 7),
|
|
49
|
-
compression: util.getBit(b, 1, 3),
|
|
50
|
-
encryption: util.getBit(b, 1, 2),
|
|
51
|
-
unsynchronisation: util.getBit(b, 1, 1),
|
|
52
|
-
data_length_indicator: util.getBit(b, 1, 0)
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
28
|
static readFrameData(uint8Array, frameHeader, majorVer, includeCovers, warningCollector) {
|
|
57
29
|
const frameParser = new FrameParser(majorVer, warningCollector);
|
|
58
30
|
switch (majorVer) {
|
|
@@ -127,14 +99,14 @@ export class ID3v2Parser {
|
|
|
127
99
|
while (true) {
|
|
128
100
|
if (offset === data.length)
|
|
129
101
|
break;
|
|
130
|
-
const frameHeaderLength =
|
|
102
|
+
const frameHeaderLength = getFrameHeaderLength(this.id3Header.version.major);
|
|
131
103
|
if (offset + frameHeaderLength > data.length) {
|
|
132
104
|
this.metadata.addWarning('Illegal ID3v2 tag length');
|
|
133
105
|
break;
|
|
134
106
|
}
|
|
135
107
|
const frameHeaderBytes = data.subarray(offset, offset + frameHeaderLength);
|
|
136
108
|
offset += frameHeaderLength;
|
|
137
|
-
const frameHeader =
|
|
109
|
+
const frameHeader = readFrameHeader(frameHeaderBytes, this.id3Header.version.major, this.metadata);
|
|
138
110
|
const frameDataBytes = data.subarray(offset, offset + frameHeader.length);
|
|
139
111
|
offset += frameHeader.length;
|
|
140
112
|
const values = ID3v2Parser.readFrameData(frameDataBytes, frameHeader, this.id3Header.version.major, !this.options.skipCovers, this.metadata);
|
|
@@ -144,34 +116,6 @@ export class ID3v2Parser {
|
|
|
144
116
|
}
|
|
145
117
|
return tags;
|
|
146
118
|
}
|
|
147
|
-
readFrameHeader(uint8Array, majorVer) {
|
|
148
|
-
let header;
|
|
149
|
-
switch (majorVer) {
|
|
150
|
-
case 2:
|
|
151
|
-
header = {
|
|
152
|
-
id: textDecode(uint8Array.subarray(0, 3), 'ascii'),
|
|
153
|
-
length: Token.UINT24_BE.get(uint8Array, 3)
|
|
154
|
-
};
|
|
155
|
-
if (!header.id.match(/[A-Z0-9]{3}/g)) {
|
|
156
|
-
this.metadata.addWarning(`Invalid ID3v2.${this.id3Header.version.major} frame-header-ID: ${header.id}`);
|
|
157
|
-
}
|
|
158
|
-
break;
|
|
159
|
-
case 3:
|
|
160
|
-
case 4:
|
|
161
|
-
header = {
|
|
162
|
-
id: textDecode(uint8Array.subarray(0, 4), 'ascii'),
|
|
163
|
-
length: (majorVer === 4 ? UINT32SYNCSAFE : Token.UINT32_BE).get(uint8Array, 4),
|
|
164
|
-
flags: ID3v2Parser.readFrameFlags(uint8Array.subarray(8, 10))
|
|
165
|
-
};
|
|
166
|
-
if (!header.id.match(/[A-Z0-9]{4}/g)) {
|
|
167
|
-
this.metadata.addWarning(`Invalid ID3v2.${this.id3Header.version.major} frame-header-ID: ${header.id}`);
|
|
168
|
-
}
|
|
169
|
-
break;
|
|
170
|
-
default:
|
|
171
|
-
throw makeUnexpectedMajorVersionError(majorVer);
|
|
172
|
-
}
|
|
173
|
-
return header;
|
|
174
|
-
}
|
|
175
119
|
}
|
|
176
120
|
function makeUnexpectedMajorVersionError(majorVer) {
|
|
177
121
|
throw new Id3v2ContentError(`Unexpected majorVer: ${majorVer}`);
|
package/package.json
CHANGED