music-metadata 7.11.7 → 7.12.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.txt +9 -0
- package/README.md +434 -432
- package/lib/ParserFactory.d.ts +48 -49
- package/lib/ParserFactory.js +252 -251
- package/lib/aiff/AiffParser.d.ts +14 -15
- package/lib/aiff/AiffParser.js +84 -85
- package/lib/aiff/AiffToken.d.ts +22 -22
- package/lib/aiff/AiffToken.js +43 -43
- package/lib/apev2/APEv2Parser.d.ts +30 -30
- package/lib/apev2/APEv2Parser.js +161 -162
- package/lib/apev2/APEv2TagMapper.d.ts +4 -4
- package/lib/apev2/APEv2TagMapper.js +86 -86
- package/lib/apev2/APEv2Token.d.ts +100 -100
- package/lib/apev2/APEv2Token.js +126 -126
- package/lib/asf/AsfObject.d.ts +319 -319
- package/lib/asf/AsfObject.js +384 -384
- package/lib/asf/AsfParser.d.ts +17 -17
- package/lib/asf/AsfParser.js +135 -135
- package/lib/asf/AsfTagMapper.d.ts +7 -7
- package/lib/asf/AsfTagMapper.js +95 -95
- package/lib/asf/AsfUtil.d.ts +13 -13
- package/lib/asf/AsfUtil.js +40 -40
- package/lib/asf/GUID.d.ts +84 -86
- package/lib/asf/GUID.js +121 -123
- package/lib/common/BasicParser.d.ts +17 -17
- package/lib/common/BasicParser.js +18 -18
- package/lib/common/CaseInsensitiveTagMap.d.ts +10 -10
- package/lib/common/CaseInsensitiveTagMap.js +21 -21
- package/lib/common/CombinedTagMapper.d.ts +19 -19
- package/lib/common/CombinedTagMapper.js +51 -51
- package/lib/common/FourCC.d.ts +6 -6
- package/lib/common/FourCC.js +28 -28
- package/lib/common/GenericTagMapper.d.ts +51 -51
- package/lib/common/GenericTagMapper.js +55 -55
- package/lib/common/GenericTagTypes.d.ts +33 -33
- package/lib/common/GenericTagTypes.js +131 -131
- package/lib/common/MetadataCollector.d.ts +76 -76
- package/lib/common/MetadataCollector.js +275 -275
- package/lib/common/RandomFileReader.d.ts +22 -20
- package/lib/common/RandomFileReader.js +34 -37
- package/lib/common/RandomUint8ArrayReader.d.ts +18 -18
- package/lib/common/RandomUint8ArrayReader.js +25 -25
- package/lib/common/Util.d.ts +57 -58
- package/lib/common/Util.js +157 -162
- package/lib/core.d.ts +48 -48
- package/lib/core.js +90 -90
- package/lib/dsdiff/DsdiffParser.d.ts +14 -14
- package/lib/dsdiff/DsdiffParser.js +143 -143
- package/lib/dsdiff/DsdiffToken.d.ts +9 -9
- package/lib/dsdiff/DsdiffToken.js +21 -21
- package/lib/dsf/DsfChunk.d.ts +86 -86
- package/lib/dsf/DsfChunk.js +54 -54
- package/lib/dsf/DsfParser.d.ts +9 -9
- package/lib/dsf/DsfParser.js +56 -56
- package/lib/flac/FlacParser.d.ts +28 -28
- package/lib/flac/FlacParser.js +175 -175
- package/lib/id3v1/ID3v1Parser.d.ts +13 -13
- package/lib/id3v1/ID3v1Parser.js +134 -134
- package/lib/id3v1/ID3v1TagMap.d.ts +4 -4
- package/lib/id3v1/ID3v1TagMap.js +22 -22
- package/lib/id3v2/AbstractID3Parser.d.ts +17 -17
- package/lib/id3v2/AbstractID3Parser.js +60 -60
- package/lib/id3v2/FrameParser.d.ts +32 -32
- package/lib/id3v2/FrameParser.js +329 -329
- package/lib/id3v2/ID3v22TagMapper.d.ts +9 -9
- package/lib/id3v2/ID3v22TagMapper.js +55 -55
- package/lib/id3v2/ID3v24TagMapper.d.ts +14 -14
- package/lib/id3v2/ID3v24TagMapper.js +193 -193
- package/lib/id3v2/ID3v2Parser.d.ts +29 -29
- package/lib/id3v2/ID3v2Parser.js +184 -194
- package/lib/id3v2/ID3v2Token.d.ts +73 -73
- package/lib/id3v2/ID3v2Token.js +106 -106
- package/lib/iff/index.d.ts +33 -33
- package/lib/iff/index.js +19 -19
- package/lib/index.d.ts +45 -45
- package/lib/index.js +74 -74
- package/lib/lyrics3/Lyrics3.d.ts +3 -3
- package/lib/lyrics3/Lyrics3.js +17 -17
- package/lib/matroska/MatroskaDtd.d.ts +8 -8
- package/lib/matroska/MatroskaDtd.js +279 -279
- package/lib/matroska/MatroskaParser.d.ts +37 -37
- package/lib/matroska/MatroskaParser.js +235 -235
- package/lib/matroska/MatroskaTagMapper.d.ts +4 -4
- package/lib/matroska/MatroskaTagMapper.js +35 -35
- package/lib/matroska/types.d.ts +175 -175
- package/lib/matroska/types.js +33 -32
- package/lib/mp4/Atom.d.ts +16 -16
- package/lib/mp4/Atom.js +70 -70
- package/lib/mp4/AtomToken.d.ts +395 -395
- package/lib/mp4/AtomToken.js +406 -406
- package/lib/mp4/MP4Parser.d.ts +30 -30
- package/lib/mp4/MP4Parser.js +511 -511
- package/lib/mp4/MP4TagMapper.d.ts +5 -5
- package/lib/mp4/MP4TagMapper.js +115 -115
- package/lib/mpeg/ExtendedLameHeader.d.ts +27 -27
- package/lib/mpeg/ExtendedLameHeader.js +31 -31
- package/lib/mpeg/MpegParser.d.ts +49 -49
- package/lib/mpeg/MpegParser.js +524 -529
- package/lib/mpeg/ReplayGainDataFormat.d.ts +55 -55
- package/lib/mpeg/ReplayGainDataFormat.js +69 -69
- package/lib/mpeg/XingTag.d.ts +45 -45
- package/lib/mpeg/XingTag.js +69 -69
- package/lib/musepack/index.d.ts +5 -5
- package/lib/musepack/index.js +32 -32
- package/lib/musepack/sv7/BitReader.d.ts +13 -13
- package/lib/musepack/sv7/BitReader.js +54 -54
- package/lib/musepack/sv7/MpcSv7Parser.d.ts +8 -8
- package/lib/musepack/sv7/MpcSv7Parser.js +46 -46
- package/lib/musepack/sv7/StreamVersion7.d.ts +28 -28
- package/lib/musepack/sv7/StreamVersion7.js +41 -41
- package/lib/musepack/sv8/MpcSv8Parser.d.ts +6 -6
- package/lib/musepack/sv8/MpcSv8Parser.js +55 -55
- package/lib/musepack/sv8/StreamVersion8.d.ts +40 -40
- package/lib/musepack/sv8/StreamVersion8.js +80 -80
- package/lib/ogg/Ogg.d.ts +72 -72
- package/lib/ogg/Ogg.js +2 -2
- package/lib/ogg/OggParser.d.ts +23 -23
- package/lib/ogg/OggParser.js +126 -126
- package/lib/ogg/opus/Opus.d.ts +48 -48
- package/lib/ogg/opus/Opus.js +28 -28
- package/lib/ogg/opus/OpusParser.d.ts +25 -25
- package/lib/ogg/opus/OpusParser.js +56 -56
- package/lib/ogg/speex/Speex.d.ts +36 -36
- package/lib/ogg/speex/Speex.js +31 -31
- package/lib/ogg/speex/SpeexParser.d.ts +22 -22
- package/lib/ogg/speex/SpeexParser.js +35 -35
- package/lib/ogg/theora/Theora.d.ts +20 -20
- package/lib/ogg/theora/Theora.js +23 -23
- package/lib/ogg/theora/TheoraParser.d.ts +28 -28
- package/lib/ogg/theora/TheoraParser.js +44 -44
- package/lib/ogg/vorbis/Vorbis.d.ts +69 -79
- package/lib/ogg/vorbis/Vorbis.js +78 -78
- package/lib/ogg/vorbis/VorbisDecoder.d.ts +12 -12
- package/lib/ogg/vorbis/VorbisDecoder.js +32 -32
- package/lib/ogg/vorbis/VorbisParser.d.ts +36 -36
- package/lib/ogg/vorbis/VorbisParser.js +128 -128
- package/lib/ogg/vorbis/VorbisTagMapper.d.ts +7 -7
- package/lib/ogg/vorbis/VorbisTagMapper.js +132 -132
- package/lib/riff/RiffChunk.d.ts +16 -16
- package/lib/riff/RiffChunk.js +32 -32
- package/lib/riff/RiffInfoTagMap.d.ts +10 -10
- package/lib/riff/RiffInfoTagMap.js +37 -37
- package/lib/type.d.ts +592 -599
- package/lib/type.js +5 -13
- package/lib/wav/BwfChunk.d.ts +17 -0
- package/lib/wav/BwfChunk.js +28 -0
- package/lib/wav/WaveChunk.d.ts +64 -64
- package/lib/wav/WaveChunk.js +65 -65
- package/lib/wav/WaveParser.d.ts +24 -24
- package/lib/wav/WaveParser.js +156 -144
- package/lib/wavpack/WavPackParser.d.ts +14 -14
- package/lib/wavpack/WavPackParser.js +99 -105
- package/lib/wavpack/WavPackToken.d.ts +64 -64
- package/lib/wavpack/WavPackToken.js +76 -76
- package/package.json +150 -142
package/lib/id3v2/FrameParser.js
CHANGED
|
@@ -1,329 +1,329 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FrameParser = exports.parseGenre = void 0;
|
|
4
|
-
const initDebug = require("debug");
|
|
5
|
-
const Token = require("token-types");
|
|
6
|
-
const util = require("../common/Util");
|
|
7
|
-
const ID3v2Token_1 = require("./ID3v2Token");
|
|
8
|
-
const ID3v1Parser_1 = require("../id3v1/ID3v1Parser");
|
|
9
|
-
const debug = initDebug('music-metadata:id3v2:frame-parser');
|
|
10
|
-
const defaultEnc = 'latin1'; // latin1 == iso-8859-1;
|
|
11
|
-
function parseGenre(origVal) {
|
|
12
|
-
// match everything inside parentheses
|
|
13
|
-
const genres = [];
|
|
14
|
-
let code;
|
|
15
|
-
let word = '';
|
|
16
|
-
for (const c of origVal) {
|
|
17
|
-
if (typeof code === 'string') {
|
|
18
|
-
if (c === '(' && code === '') {
|
|
19
|
-
word += '(';
|
|
20
|
-
code = undefined;
|
|
21
|
-
}
|
|
22
|
-
else if (c === ')') {
|
|
23
|
-
if (word !== '') {
|
|
24
|
-
genres.push(word);
|
|
25
|
-
word = '';
|
|
26
|
-
}
|
|
27
|
-
const genre = parseGenreCode(code);
|
|
28
|
-
if (genre) {
|
|
29
|
-
genres.push(genre);
|
|
30
|
-
}
|
|
31
|
-
code = undefined;
|
|
32
|
-
}
|
|
33
|
-
else
|
|
34
|
-
code += c;
|
|
35
|
-
}
|
|
36
|
-
else if (c === '(') {
|
|
37
|
-
code = '';
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
word += c;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (word) {
|
|
44
|
-
if (genres.length === 0 && word.match(/^\d*$/)) {
|
|
45
|
-
word = ID3v1Parser_1.Genres[word];
|
|
46
|
-
}
|
|
47
|
-
genres.push(word);
|
|
48
|
-
}
|
|
49
|
-
return genres;
|
|
50
|
-
}
|
|
51
|
-
exports.parseGenre = parseGenre;
|
|
52
|
-
function parseGenreCode(code) {
|
|
53
|
-
if (code === 'RX')
|
|
54
|
-
return 'Remix';
|
|
55
|
-
if (code === 'CR')
|
|
56
|
-
return 'Cover';
|
|
57
|
-
if (code.match(/^\d*$/)) {
|
|
58
|
-
return ID3v1Parser_1.Genres[code];
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
class FrameParser {
|
|
62
|
-
/**
|
|
63
|
-
* Create id3v2 frame parser
|
|
64
|
-
* @param major - Major version, e.g. (4) for id3v2.4
|
|
65
|
-
* @param warningCollector - Used to collect decode issue
|
|
66
|
-
*/
|
|
67
|
-
constructor(major, warningCollector) {
|
|
68
|
-
this.major = major;
|
|
69
|
-
this.warningCollector = warningCollector;
|
|
70
|
-
}
|
|
71
|
-
readData(b, type, includeCovers) {
|
|
72
|
-
if (b.length === 0) {
|
|
73
|
-
this.warningCollector.addWarning(`id3v2.${this.major} header has empty tag type=${type}`);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
const { encoding, bom } = ID3v2Token_1.TextEncodingToken.get(b, 0);
|
|
77
|
-
const length = b.length;
|
|
78
|
-
let offset = 0;
|
|
79
|
-
let output = []; // ToDo
|
|
80
|
-
const nullTerminatorLength = FrameParser.getNullTerminatorLength(encoding);
|
|
81
|
-
let fzero;
|
|
82
|
-
const out = {};
|
|
83
|
-
debug(`Parsing tag type=${type}, encoding=${encoding}, bom=${bom}`);
|
|
84
|
-
switch (type !== 'TXXX' && type[0] === 'T' ? 'T*' : type) {
|
|
85
|
-
case 'T*': // 4.2.1. Text information frames - details
|
|
86
|
-
case 'IPLS': // v2.3: Involved people list
|
|
87
|
-
case 'MVIN':
|
|
88
|
-
case 'MVNM':
|
|
89
|
-
case 'PCS':
|
|
90
|
-
case 'PCST':
|
|
91
|
-
let text;
|
|
92
|
-
try {
|
|
93
|
-
text = util.decodeString(b.subarray(1), encoding).replace(/\x00+$/, '');
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
this.warningCollector.addWarning(`id3v2.${this.major} type=${type} header has invalid string value: ${error.message}`);
|
|
97
|
-
}
|
|
98
|
-
switch (type) {
|
|
99
|
-
case 'TMCL': // Musician credits list
|
|
100
|
-
case 'TIPL': // Involved people list
|
|
101
|
-
case 'IPLS': // Involved people list
|
|
102
|
-
output = this.splitValue(type, text);
|
|
103
|
-
output = FrameParser.functionList(output);
|
|
104
|
-
break;
|
|
105
|
-
case 'TRK':
|
|
106
|
-
case 'TRCK':
|
|
107
|
-
case 'TPOS':
|
|
108
|
-
output = text;
|
|
109
|
-
break;
|
|
110
|
-
case 'TCOM':
|
|
111
|
-
case 'TEXT':
|
|
112
|
-
case 'TOLY':
|
|
113
|
-
case 'TOPE':
|
|
114
|
-
case 'TPE1':
|
|
115
|
-
case 'TSRC':
|
|
116
|
-
// id3v2.3 defines that TCOM, TEXT, TOLY, TOPE & TPE1 values are separated by /
|
|
117
|
-
output = this.splitValue(type, text);
|
|
118
|
-
break;
|
|
119
|
-
case 'TCO':
|
|
120
|
-
case 'TCON':
|
|
121
|
-
output = this.splitValue(type, text).map(v => parseGenre(v)).reduce((acc, val) => acc.concat(val), []);
|
|
122
|
-
break;
|
|
123
|
-
case 'PCS':
|
|
124
|
-
case 'PCST':
|
|
125
|
-
// TODO: Why `default` not results `1` but `''`?
|
|
126
|
-
output = this.major >= 4 ? this.splitValue(type, text) : [text];
|
|
127
|
-
output = (Array.isArray(output) && output[0] === '') ? 1 : 0;
|
|
128
|
-
break;
|
|
129
|
-
default:
|
|
130
|
-
output = this.major >= 4 ? this.splitValue(type, text) : [text];
|
|
131
|
-
}
|
|
132
|
-
break;
|
|
133
|
-
case 'TXXX':
|
|
134
|
-
output = FrameParser.readIdentifierAndData(b, offset + 1, length, encoding);
|
|
135
|
-
output = {
|
|
136
|
-
description: output.id,
|
|
137
|
-
text: this.splitValue(type, util.decodeString(output.data, encoding).replace(/\x00+$/, ''))
|
|
138
|
-
};
|
|
139
|
-
break;
|
|
140
|
-
case 'PIC':
|
|
141
|
-
case 'APIC':
|
|
142
|
-
if (includeCovers) {
|
|
143
|
-
const pic = {};
|
|
144
|
-
offset += 1;
|
|
145
|
-
switch (this.major) {
|
|
146
|
-
case 2:
|
|
147
|
-
pic.format = util.decodeString(b.slice(offset, offset + 3), 'latin1'); // 'latin1'; // latin1 == iso-8859-1;
|
|
148
|
-
offset += 3;
|
|
149
|
-
break;
|
|
150
|
-
case 3:
|
|
151
|
-
case 4:
|
|
152
|
-
fzero = util.findZero(b, offset, length, defaultEnc);
|
|
153
|
-
pic.format = util.decodeString(b.slice(offset, fzero), defaultEnc);
|
|
154
|
-
offset = fzero + 1;
|
|
155
|
-
break;
|
|
156
|
-
default:
|
|
157
|
-
throw new Error('Warning: unexpected major versionIndex: ' + this.major);
|
|
158
|
-
}
|
|
159
|
-
pic.format = FrameParser.fixPictureMimeType(pic.format);
|
|
160
|
-
pic.type = ID3v2Token_1.AttachedPictureType[b[offset]];
|
|
161
|
-
offset += 1;
|
|
162
|
-
fzero = util.findZero(b, offset, length, encoding);
|
|
163
|
-
pic.description = util.decodeString(b.slice(offset, fzero), encoding);
|
|
164
|
-
offset = fzero + nullTerminatorLength;
|
|
165
|
-
pic.data = Buffer.from(b.slice(offset, length));
|
|
166
|
-
output = pic;
|
|
167
|
-
}
|
|
168
|
-
break;
|
|
169
|
-
case 'CNT':
|
|
170
|
-
case 'PCNT':
|
|
171
|
-
output = Token.UINT32_BE.get(b, 0);
|
|
172
|
-
break;
|
|
173
|
-
case 'SYLT':
|
|
174
|
-
// skip text encoding (1 byte),
|
|
175
|
-
// language (3 bytes),
|
|
176
|
-
// time stamp format (1 byte),
|
|
177
|
-
// content tagTypes (1 byte),
|
|
178
|
-
// content descriptor (1 byte)
|
|
179
|
-
offset += 7;
|
|
180
|
-
output = [];
|
|
181
|
-
while (offset < length) {
|
|
182
|
-
const txt = b.slice(offset, offset = util.findZero(b, offset, length, encoding));
|
|
183
|
-
offset += 5; // push offset forward one + 4 byte timestamp
|
|
184
|
-
output.push(util.decodeString(txt, encoding));
|
|
185
|
-
}
|
|
186
|
-
break;
|
|
187
|
-
case 'ULT':
|
|
188
|
-
case 'USLT':
|
|
189
|
-
case 'COM':
|
|
190
|
-
case 'COMM':
|
|
191
|
-
offset += 1;
|
|
192
|
-
out.language = util.decodeString(b.slice(offset, offset + 3), defaultEnc);
|
|
193
|
-
offset += 3;
|
|
194
|
-
fzero = util.findZero(b, offset, length, encoding);
|
|
195
|
-
out.description = util.decodeString(b.slice(offset, fzero), encoding);
|
|
196
|
-
offset = fzero + nullTerminatorLength;
|
|
197
|
-
out.text = util.decodeString(b.slice(offset, length), encoding).replace(/\x00+$/, '');
|
|
198
|
-
output = [out];
|
|
199
|
-
break;
|
|
200
|
-
case 'UFID':
|
|
201
|
-
output = FrameParser.readIdentifierAndData(b, offset, length, defaultEnc);
|
|
202
|
-
output = { owner_identifier: output.id, identifier: output.data };
|
|
203
|
-
break;
|
|
204
|
-
case 'PRIV': // private frame
|
|
205
|
-
output = FrameParser.readIdentifierAndData(b, offset, length, defaultEnc);
|
|
206
|
-
output = { owner_identifier: output.id, data: output.data };
|
|
207
|
-
break;
|
|
208
|
-
case 'POPM': // Popularimeter
|
|
209
|
-
fzero = util.findZero(b, offset, length, defaultEnc);
|
|
210
|
-
const email = util.decodeString(b.slice(offset, fzero), defaultEnc);
|
|
211
|
-
offset = fzero + 1;
|
|
212
|
-
const dataLen = length - offset;
|
|
213
|
-
output = {
|
|
214
|
-
email,
|
|
215
|
-
rating: b.readUInt8(offset),
|
|
216
|
-
counter: dataLen >= 5 ? b.readUInt32BE(offset + 1) : undefined
|
|
217
|
-
};
|
|
218
|
-
break;
|
|
219
|
-
case 'GEOB': { // General encapsulated object
|
|
220
|
-
fzero = util.findZero(b, offset + 1, length, encoding);
|
|
221
|
-
const mimeType = util.decodeString(b.slice(offset + 1, fzero), defaultEnc);
|
|
222
|
-
offset = fzero + 1;
|
|
223
|
-
fzero = util.findZero(b, offset, length - offset, encoding);
|
|
224
|
-
const filename = util.decodeString(b.slice(offset, fzero), defaultEnc);
|
|
225
|
-
offset = fzero + 1;
|
|
226
|
-
fzero = util.findZero(b, offset, length - offset, encoding);
|
|
227
|
-
const description = util.decodeString(b.slice(offset, fzero), defaultEnc);
|
|
228
|
-
output = {
|
|
229
|
-
type: mimeType,
|
|
230
|
-
filename,
|
|
231
|
-
description,
|
|
232
|
-
data: b.slice(offset + 1, length)
|
|
233
|
-
};
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
// W-Frames:
|
|
237
|
-
case 'WCOM':
|
|
238
|
-
case 'WCOP':
|
|
239
|
-
case 'WOAF':
|
|
240
|
-
case 'WOAR':
|
|
241
|
-
case 'WOAS':
|
|
242
|
-
case 'WORS':
|
|
243
|
-
case 'WPAY':
|
|
244
|
-
case 'WPUB':
|
|
245
|
-
// Decode URL
|
|
246
|
-
output = util.decodeString(b.slice(offset, fzero), defaultEnc);
|
|
247
|
-
break;
|
|
248
|
-
case 'WXXX': {
|
|
249
|
-
// Decode URL
|
|
250
|
-
fzero = util.findZero(b, offset + 1, length, encoding);
|
|
251
|
-
const description = util.decodeString(b.slice(offset + 1, fzero), encoding);
|
|
252
|
-
offset = fzero + (encoding === 'utf16le' ? 2 : 1);
|
|
253
|
-
output = { description, url: util.decodeString(b.slice(offset, length), defaultEnc) };
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
case 'WFD':
|
|
257
|
-
case 'WFED':
|
|
258
|
-
output = util.decodeString(b.slice(offset + 1, util.findZero(b, offset + 1, length, encoding)), encoding);
|
|
259
|
-
break;
|
|
260
|
-
case 'MCDI': {
|
|
261
|
-
// Music CD identifier
|
|
262
|
-
output = b.slice(0, length);
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
default:
|
|
266
|
-
debug('Warning: unsupported id3v2-tag-type: ' + type);
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
269
|
-
return output;
|
|
270
|
-
}
|
|
271
|
-
static fixPictureMimeType(pictureType) {
|
|
272
|
-
pictureType = pictureType.toLocaleLowerCase();
|
|
273
|
-
switch (pictureType) {
|
|
274
|
-
case 'jpg':
|
|
275
|
-
return 'image/jpeg';
|
|
276
|
-
case 'png':
|
|
277
|
-
return 'image/png';
|
|
278
|
-
}
|
|
279
|
-
return pictureType;
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Converts TMCL (Musician credits list) or TIPL (Involved people list)
|
|
283
|
-
* @param entries
|
|
284
|
-
*/
|
|
285
|
-
static functionList(entries) {
|
|
286
|
-
const res = {};
|
|
287
|
-
for (let i = 0; i + 1 < entries.length; i += 2) {
|
|
288
|
-
const names = entries[i + 1].split(',');
|
|
289
|
-
res[entries[i]] = res.hasOwnProperty(entries[i]) ? res[entries[i]].concat(names) : names;
|
|
290
|
-
}
|
|
291
|
-
return res;
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* id3v2.4 defines that multiple T* values are separated by 0x00
|
|
295
|
-
* id3v2.3 defines that TCOM, TEXT, TOLY, TOPE & TPE1 values are separated by /
|
|
296
|
-
* @param tag - Tag name
|
|
297
|
-
* @param text - Concatenated tag value
|
|
298
|
-
* @returns Split tag value
|
|
299
|
-
*/
|
|
300
|
-
splitValue(tag, text) {
|
|
301
|
-
let values;
|
|
302
|
-
if (this.major < 4) {
|
|
303
|
-
values = text.split(/\x00/g);
|
|
304
|
-
if (values.length > 1) {
|
|
305
|
-
this.warningCollector.addWarning(`ID3v2.${this.major} ${tag} uses non standard null-separator.`);
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
values = text.split(/\//g);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
values = text.split(/\x00/g);
|
|
313
|
-
}
|
|
314
|
-
return FrameParser.trimArray(values);
|
|
315
|
-
}
|
|
316
|
-
static trimArray(values) {
|
|
317
|
-
return values.map(value => value.replace(/\x00+$/, '').trim());
|
|
318
|
-
}
|
|
319
|
-
static readIdentifierAndData(b, offset, length, encoding) {
|
|
320
|
-
const fzero = util.findZero(b, offset, length, encoding);
|
|
321
|
-
const id = util.decodeString(b.slice(offset, fzero), encoding);
|
|
322
|
-
offset = fzero + FrameParser.getNullTerminatorLength(encoding);
|
|
323
|
-
return { id, data: b.slice(offset, length) };
|
|
324
|
-
}
|
|
325
|
-
static getNullTerminatorLength(enc) {
|
|
326
|
-
return enc === 'utf16le' ? 2 : 1;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
exports.FrameParser = FrameParser;
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FrameParser = exports.parseGenre = void 0;
|
|
4
|
+
const initDebug = require("debug");
|
|
5
|
+
const Token = require("token-types");
|
|
6
|
+
const util = require("../common/Util");
|
|
7
|
+
const ID3v2Token_1 = require("./ID3v2Token");
|
|
8
|
+
const ID3v1Parser_1 = require("../id3v1/ID3v1Parser");
|
|
9
|
+
const debug = initDebug('music-metadata:id3v2:frame-parser');
|
|
10
|
+
const defaultEnc = 'latin1'; // latin1 == iso-8859-1;
|
|
11
|
+
function parseGenre(origVal) {
|
|
12
|
+
// match everything inside parentheses
|
|
13
|
+
const genres = [];
|
|
14
|
+
let code;
|
|
15
|
+
let word = '';
|
|
16
|
+
for (const c of origVal) {
|
|
17
|
+
if (typeof code === 'string') {
|
|
18
|
+
if (c === '(' && code === '') {
|
|
19
|
+
word += '(';
|
|
20
|
+
code = undefined;
|
|
21
|
+
}
|
|
22
|
+
else if (c === ')') {
|
|
23
|
+
if (word !== '') {
|
|
24
|
+
genres.push(word);
|
|
25
|
+
word = '';
|
|
26
|
+
}
|
|
27
|
+
const genre = parseGenreCode(code);
|
|
28
|
+
if (genre) {
|
|
29
|
+
genres.push(genre);
|
|
30
|
+
}
|
|
31
|
+
code = undefined;
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
code += c;
|
|
35
|
+
}
|
|
36
|
+
else if (c === '(') {
|
|
37
|
+
code = '';
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
word += c;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (word) {
|
|
44
|
+
if (genres.length === 0 && word.match(/^\d*$/)) {
|
|
45
|
+
word = ID3v1Parser_1.Genres[word];
|
|
46
|
+
}
|
|
47
|
+
genres.push(word);
|
|
48
|
+
}
|
|
49
|
+
return genres;
|
|
50
|
+
}
|
|
51
|
+
exports.parseGenre = parseGenre;
|
|
52
|
+
function parseGenreCode(code) {
|
|
53
|
+
if (code === 'RX')
|
|
54
|
+
return 'Remix';
|
|
55
|
+
if (code === 'CR')
|
|
56
|
+
return 'Cover';
|
|
57
|
+
if (code.match(/^\d*$/)) {
|
|
58
|
+
return ID3v1Parser_1.Genres[code];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
class FrameParser {
|
|
62
|
+
/**
|
|
63
|
+
* Create id3v2 frame parser
|
|
64
|
+
* @param major - Major version, e.g. (4) for id3v2.4
|
|
65
|
+
* @param warningCollector - Used to collect decode issue
|
|
66
|
+
*/
|
|
67
|
+
constructor(major, warningCollector) {
|
|
68
|
+
this.major = major;
|
|
69
|
+
this.warningCollector = warningCollector;
|
|
70
|
+
}
|
|
71
|
+
readData(b, type, includeCovers) {
|
|
72
|
+
if (b.length === 0) {
|
|
73
|
+
this.warningCollector.addWarning(`id3v2.${this.major} header has empty tag type=${type}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const { encoding, bom } = ID3v2Token_1.TextEncodingToken.get(b, 0);
|
|
77
|
+
const length = b.length;
|
|
78
|
+
let offset = 0;
|
|
79
|
+
let output = []; // ToDo
|
|
80
|
+
const nullTerminatorLength = FrameParser.getNullTerminatorLength(encoding);
|
|
81
|
+
let fzero;
|
|
82
|
+
const out = {};
|
|
83
|
+
debug(`Parsing tag type=${type}, encoding=${encoding}, bom=${bom}`);
|
|
84
|
+
switch (type !== 'TXXX' && type[0] === 'T' ? 'T*' : type) {
|
|
85
|
+
case 'T*': // 4.2.1. Text information frames - details
|
|
86
|
+
case 'IPLS': // v2.3: Involved people list
|
|
87
|
+
case 'MVIN':
|
|
88
|
+
case 'MVNM':
|
|
89
|
+
case 'PCS':
|
|
90
|
+
case 'PCST':
|
|
91
|
+
let text;
|
|
92
|
+
try {
|
|
93
|
+
text = util.decodeString(b.subarray(1), encoding).replace(/\x00+$/, '');
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
this.warningCollector.addWarning(`id3v2.${this.major} type=${type} header has invalid string value: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
switch (type) {
|
|
99
|
+
case 'TMCL': // Musician credits list
|
|
100
|
+
case 'TIPL': // Involved people list
|
|
101
|
+
case 'IPLS': // Involved people list
|
|
102
|
+
output = this.splitValue(type, text);
|
|
103
|
+
output = FrameParser.functionList(output);
|
|
104
|
+
break;
|
|
105
|
+
case 'TRK':
|
|
106
|
+
case 'TRCK':
|
|
107
|
+
case 'TPOS':
|
|
108
|
+
output = text;
|
|
109
|
+
break;
|
|
110
|
+
case 'TCOM':
|
|
111
|
+
case 'TEXT':
|
|
112
|
+
case 'TOLY':
|
|
113
|
+
case 'TOPE':
|
|
114
|
+
case 'TPE1':
|
|
115
|
+
case 'TSRC':
|
|
116
|
+
// id3v2.3 defines that TCOM, TEXT, TOLY, TOPE & TPE1 values are separated by /
|
|
117
|
+
output = this.splitValue(type, text);
|
|
118
|
+
break;
|
|
119
|
+
case 'TCO':
|
|
120
|
+
case 'TCON':
|
|
121
|
+
output = this.splitValue(type, text).map(v => parseGenre(v)).reduce((acc, val) => acc.concat(val), []);
|
|
122
|
+
break;
|
|
123
|
+
case 'PCS':
|
|
124
|
+
case 'PCST':
|
|
125
|
+
// TODO: Why `default` not results `1` but `''`?
|
|
126
|
+
output = this.major >= 4 ? this.splitValue(type, text) : [text];
|
|
127
|
+
output = (Array.isArray(output) && output[0] === '') ? 1 : 0;
|
|
128
|
+
break;
|
|
129
|
+
default:
|
|
130
|
+
output = this.major >= 4 ? this.splitValue(type, text) : [text];
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
case 'TXXX':
|
|
134
|
+
output = FrameParser.readIdentifierAndData(b, offset + 1, length, encoding);
|
|
135
|
+
output = {
|
|
136
|
+
description: output.id,
|
|
137
|
+
text: this.splitValue(type, util.decodeString(output.data, encoding).replace(/\x00+$/, ''))
|
|
138
|
+
};
|
|
139
|
+
break;
|
|
140
|
+
case 'PIC':
|
|
141
|
+
case 'APIC':
|
|
142
|
+
if (includeCovers) {
|
|
143
|
+
const pic = {};
|
|
144
|
+
offset += 1;
|
|
145
|
+
switch (this.major) {
|
|
146
|
+
case 2:
|
|
147
|
+
pic.format = util.decodeString(b.slice(offset, offset + 3), 'latin1'); // 'latin1'; // latin1 == iso-8859-1;
|
|
148
|
+
offset += 3;
|
|
149
|
+
break;
|
|
150
|
+
case 3:
|
|
151
|
+
case 4:
|
|
152
|
+
fzero = util.findZero(b, offset, length, defaultEnc);
|
|
153
|
+
pic.format = util.decodeString(b.slice(offset, fzero), defaultEnc);
|
|
154
|
+
offset = fzero + 1;
|
|
155
|
+
break;
|
|
156
|
+
default:
|
|
157
|
+
throw new Error('Warning: unexpected major versionIndex: ' + this.major);
|
|
158
|
+
}
|
|
159
|
+
pic.format = FrameParser.fixPictureMimeType(pic.format);
|
|
160
|
+
pic.type = ID3v2Token_1.AttachedPictureType[b[offset]];
|
|
161
|
+
offset += 1;
|
|
162
|
+
fzero = util.findZero(b, offset, length, encoding);
|
|
163
|
+
pic.description = util.decodeString(b.slice(offset, fzero), encoding);
|
|
164
|
+
offset = fzero + nullTerminatorLength;
|
|
165
|
+
pic.data = Buffer.from(b.slice(offset, length));
|
|
166
|
+
output = pic;
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
case 'CNT':
|
|
170
|
+
case 'PCNT':
|
|
171
|
+
output = Token.UINT32_BE.get(b, 0);
|
|
172
|
+
break;
|
|
173
|
+
case 'SYLT':
|
|
174
|
+
// skip text encoding (1 byte),
|
|
175
|
+
// language (3 bytes),
|
|
176
|
+
// time stamp format (1 byte),
|
|
177
|
+
// content tagTypes (1 byte),
|
|
178
|
+
// content descriptor (1 byte)
|
|
179
|
+
offset += 7;
|
|
180
|
+
output = [];
|
|
181
|
+
while (offset < length) {
|
|
182
|
+
const txt = b.slice(offset, offset = util.findZero(b, offset, length, encoding));
|
|
183
|
+
offset += 5; // push offset forward one + 4 byte timestamp
|
|
184
|
+
output.push(util.decodeString(txt, encoding));
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
case 'ULT':
|
|
188
|
+
case 'USLT':
|
|
189
|
+
case 'COM':
|
|
190
|
+
case 'COMM':
|
|
191
|
+
offset += 1;
|
|
192
|
+
out.language = util.decodeString(b.slice(offset, offset + 3), defaultEnc);
|
|
193
|
+
offset += 3;
|
|
194
|
+
fzero = util.findZero(b, offset, length, encoding);
|
|
195
|
+
out.description = util.decodeString(b.slice(offset, fzero), encoding);
|
|
196
|
+
offset = fzero + nullTerminatorLength;
|
|
197
|
+
out.text = util.decodeString(b.slice(offset, length), encoding).replace(/\x00+$/, '');
|
|
198
|
+
output = [out];
|
|
199
|
+
break;
|
|
200
|
+
case 'UFID':
|
|
201
|
+
output = FrameParser.readIdentifierAndData(b, offset, length, defaultEnc);
|
|
202
|
+
output = { owner_identifier: output.id, identifier: output.data };
|
|
203
|
+
break;
|
|
204
|
+
case 'PRIV': // private frame
|
|
205
|
+
output = FrameParser.readIdentifierAndData(b, offset, length, defaultEnc);
|
|
206
|
+
output = { owner_identifier: output.id, data: output.data };
|
|
207
|
+
break;
|
|
208
|
+
case 'POPM': // Popularimeter
|
|
209
|
+
fzero = util.findZero(b, offset, length, defaultEnc);
|
|
210
|
+
const email = util.decodeString(b.slice(offset, fzero), defaultEnc);
|
|
211
|
+
offset = fzero + 1;
|
|
212
|
+
const dataLen = length - offset;
|
|
213
|
+
output = {
|
|
214
|
+
email,
|
|
215
|
+
rating: b.readUInt8(offset),
|
|
216
|
+
counter: dataLen >= 5 ? b.readUInt32BE(offset + 1) : undefined
|
|
217
|
+
};
|
|
218
|
+
break;
|
|
219
|
+
case 'GEOB': { // General encapsulated object
|
|
220
|
+
fzero = util.findZero(b, offset + 1, length, encoding);
|
|
221
|
+
const mimeType = util.decodeString(b.slice(offset + 1, fzero), defaultEnc);
|
|
222
|
+
offset = fzero + 1;
|
|
223
|
+
fzero = util.findZero(b, offset, length - offset, encoding);
|
|
224
|
+
const filename = util.decodeString(b.slice(offset, fzero), defaultEnc);
|
|
225
|
+
offset = fzero + 1;
|
|
226
|
+
fzero = util.findZero(b, offset, length - offset, encoding);
|
|
227
|
+
const description = util.decodeString(b.slice(offset, fzero), defaultEnc);
|
|
228
|
+
output = {
|
|
229
|
+
type: mimeType,
|
|
230
|
+
filename,
|
|
231
|
+
description,
|
|
232
|
+
data: b.slice(offset + 1, length)
|
|
233
|
+
};
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
// W-Frames:
|
|
237
|
+
case 'WCOM':
|
|
238
|
+
case 'WCOP':
|
|
239
|
+
case 'WOAF':
|
|
240
|
+
case 'WOAR':
|
|
241
|
+
case 'WOAS':
|
|
242
|
+
case 'WORS':
|
|
243
|
+
case 'WPAY':
|
|
244
|
+
case 'WPUB':
|
|
245
|
+
// Decode URL
|
|
246
|
+
output = util.decodeString(b.slice(offset, fzero), defaultEnc);
|
|
247
|
+
break;
|
|
248
|
+
case 'WXXX': {
|
|
249
|
+
// Decode URL
|
|
250
|
+
fzero = util.findZero(b, offset + 1, length, encoding);
|
|
251
|
+
const description = util.decodeString(b.slice(offset + 1, fzero), encoding);
|
|
252
|
+
offset = fzero + (encoding === 'utf16le' ? 2 : 1);
|
|
253
|
+
output = { description, url: util.decodeString(b.slice(offset, length), defaultEnc) };
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
case 'WFD':
|
|
257
|
+
case 'WFED':
|
|
258
|
+
output = util.decodeString(b.slice(offset + 1, util.findZero(b, offset + 1, length, encoding)), encoding);
|
|
259
|
+
break;
|
|
260
|
+
case 'MCDI': {
|
|
261
|
+
// Music CD identifier
|
|
262
|
+
output = b.slice(0, length);
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
default:
|
|
266
|
+
debug('Warning: unsupported id3v2-tag-type: ' + type);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
return output;
|
|
270
|
+
}
|
|
271
|
+
static fixPictureMimeType(pictureType) {
|
|
272
|
+
pictureType = pictureType.toLocaleLowerCase();
|
|
273
|
+
switch (pictureType) {
|
|
274
|
+
case 'jpg':
|
|
275
|
+
return 'image/jpeg';
|
|
276
|
+
case 'png':
|
|
277
|
+
return 'image/png';
|
|
278
|
+
}
|
|
279
|
+
return pictureType;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Converts TMCL (Musician credits list) or TIPL (Involved people list)
|
|
283
|
+
* @param entries
|
|
284
|
+
*/
|
|
285
|
+
static functionList(entries) {
|
|
286
|
+
const res = {};
|
|
287
|
+
for (let i = 0; i + 1 < entries.length; i += 2) {
|
|
288
|
+
const names = entries[i + 1].split(',');
|
|
289
|
+
res[entries[i]] = res.hasOwnProperty(entries[i]) ? res[entries[i]].concat(names) : names;
|
|
290
|
+
}
|
|
291
|
+
return res;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* id3v2.4 defines that multiple T* values are separated by 0x00
|
|
295
|
+
* id3v2.3 defines that TCOM, TEXT, TOLY, TOPE & TPE1 values are separated by /
|
|
296
|
+
* @param tag - Tag name
|
|
297
|
+
* @param text - Concatenated tag value
|
|
298
|
+
* @returns Split tag value
|
|
299
|
+
*/
|
|
300
|
+
splitValue(tag, text) {
|
|
301
|
+
let values;
|
|
302
|
+
if (this.major < 4) {
|
|
303
|
+
values = text.split(/\x00/g);
|
|
304
|
+
if (values.length > 1) {
|
|
305
|
+
this.warningCollector.addWarning(`ID3v2.${this.major} ${tag} uses non standard null-separator.`);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
values = text.split(/\//g);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
values = text.split(/\x00/g);
|
|
313
|
+
}
|
|
314
|
+
return FrameParser.trimArray(values);
|
|
315
|
+
}
|
|
316
|
+
static trimArray(values) {
|
|
317
|
+
return values.map(value => value.replace(/\x00+$/, '').trim());
|
|
318
|
+
}
|
|
319
|
+
static readIdentifierAndData(b, offset, length, encoding) {
|
|
320
|
+
const fzero = util.findZero(b, offset, length, encoding);
|
|
321
|
+
const id = util.decodeString(b.slice(offset, fzero), encoding);
|
|
322
|
+
offset = fzero + FrameParser.getNullTerminatorLength(encoding);
|
|
323
|
+
return { id, data: b.slice(offset, length) };
|
|
324
|
+
}
|
|
325
|
+
static getNullTerminatorLength(enc) {
|
|
326
|
+
return enc === 'utf16le' ? 2 : 1;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
exports.FrameParser = FrameParser;
|