music-metadata 7.12.1 → 7.12.4
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 -9
- package/README.md +434 -434
- package/lib/ParserFactory.d.ts +48 -48
- package/lib/ParserFactory.js +252 -252
- package/lib/aiff/AiffParser.d.ts +14 -14
- package/lib/aiff/AiffParser.js +84 -84
- 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 +164 -161
- 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 +381 -381
- 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 +38 -38
- package/lib/asf/GUID.d.ts +84 -84
- package/lib/asf/GUID.js +121 -121
- 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 -22
- package/lib/common/RandomFileReader.js +34 -34
- package/lib/common/RandomUint8ArrayReader.d.ts +18 -18
- package/lib/common/RandomUint8ArrayReader.js +25 -25
- package/lib/common/Util.d.ts +57 -57
- package/lib/common/Util.js +157 -157
- 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 +31 -31
- 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 +28 -28
- package/lib/id3v2/ID3v2Parser.js +182 -182
- 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 +32 -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 -524
- 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 -69
- 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 -592
- package/lib/type.js +5 -5
- package/lib/wav/BwfChunk.d.ts +17 -17
- package/lib/wav/BwfChunk.js +29 -28
- 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 +158 -156
- package/lib/wavpack/WavPackParser.d.ts +14 -14
- package/lib/wavpack/WavPackParser.js +99 -99
- package/lib/wavpack/WavPackToken.d.ts +64 -64
- package/lib/wavpack/WavPackToken.js +76 -76
- package/package.json +150 -150
|
@@ -1,276 +1,276 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.joinArtists = exports.MetadataCollector = void 0;
|
|
4
|
-
const type_1 = require("../type");
|
|
5
|
-
const
|
|
6
|
-
const GenericTagTypes_1 = require("./GenericTagTypes");
|
|
7
|
-
const CombinedTagMapper_1 = require("./CombinedTagMapper");
|
|
8
|
-
const GenericTagMapper_1 = require("./GenericTagMapper");
|
|
9
|
-
const Util_1 = require("./Util");
|
|
10
|
-
const FileType = require("file-type/core");
|
|
11
|
-
const debug =
|
|
12
|
-
const TagPriority = ['matroska', 'APEv2', 'vorbis', 'ID3v2.4', 'ID3v2.3', 'ID3v2.2', 'exif', 'asf', 'iTunes', 'ID3v1'];
|
|
13
|
-
/**
|
|
14
|
-
* Provided to the parser to uodate the metadata result.
|
|
15
|
-
* Responsible for triggering async updates
|
|
16
|
-
*/
|
|
17
|
-
class MetadataCollector {
|
|
18
|
-
constructor(opts) {
|
|
19
|
-
this.opts = opts;
|
|
20
|
-
this.format = {
|
|
21
|
-
tagTypes: [],
|
|
22
|
-
trackInfo: []
|
|
23
|
-
};
|
|
24
|
-
this.native = {};
|
|
25
|
-
this.common = {
|
|
26
|
-
track: { no: null, of: null },
|
|
27
|
-
disk: { no: null, of: null },
|
|
28
|
-
movementIndex: {}
|
|
29
|
-
};
|
|
30
|
-
this.quality = {
|
|
31
|
-
warnings: []
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* Keeps track of origin priority for each mapped id
|
|
35
|
-
*/
|
|
36
|
-
this.commonOrigin = {};
|
|
37
|
-
/**
|
|
38
|
-
* Maps a tag type to a priority
|
|
39
|
-
*/
|
|
40
|
-
this.originPriority = {};
|
|
41
|
-
this.tagMapper = new CombinedTagMapper_1.CombinedTagMapper();
|
|
42
|
-
let priority = 1;
|
|
43
|
-
for (const tagType of TagPriority) {
|
|
44
|
-
this.originPriority[tagType] = priority++;
|
|
45
|
-
}
|
|
46
|
-
this.originPriority.artificial = 500; // Filled using alternative tags
|
|
47
|
-
this.originPriority.id3v1 = 600; // Consider worst due to field length limit
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* @returns {boolean} true if one or more tags have been found
|
|
51
|
-
*/
|
|
52
|
-
hasAny() {
|
|
53
|
-
return Object.keys(this.native).length > 0;
|
|
54
|
-
}
|
|
55
|
-
addStreamInfo(streamInfo) {
|
|
56
|
-
debug(`streamInfo: type=${type_1.TrackType[streamInfo.type]}, codec=${streamInfo.codecName}`);
|
|
57
|
-
this.format.trackInfo.push(streamInfo);
|
|
58
|
-
}
|
|
59
|
-
setFormat(key, value) {
|
|
60
|
-
debug(`format: ${key} = ${value}`);
|
|
61
|
-
this.format[key] = value; // as any to override readonly
|
|
62
|
-
if (this.opts.observer) {
|
|
63
|
-
this.opts.observer({ metadata: this, tag: { type: 'format', id: key, value } });
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
addTag(tagType, tagId, value) {
|
|
67
|
-
debug(`tag ${tagType}.${tagId} = ${value}`);
|
|
68
|
-
if (!this.native[tagType]) {
|
|
69
|
-
this.format.tagTypes.push(tagType);
|
|
70
|
-
this.native[tagType] = [];
|
|
71
|
-
}
|
|
72
|
-
this.native[tagType].push({ id: tagId, value });
|
|
73
|
-
this.toCommon(tagType, tagId, value);
|
|
74
|
-
}
|
|
75
|
-
addWarning(warning) {
|
|
76
|
-
this.quality.warnings.push({ message: warning });
|
|
77
|
-
}
|
|
78
|
-
postMap(tagType, tag) {
|
|
79
|
-
// Common tag (alias) found
|
|
80
|
-
// check if we need to do something special with common tag
|
|
81
|
-
// if the event has been aliased then we need to clean it before
|
|
82
|
-
// it is emitted to the user. e.g. genre (20) -> Electronic
|
|
83
|
-
switch (tag.id) {
|
|
84
|
-
case 'artist':
|
|
85
|
-
if (this.commonOrigin.artist === this.originPriority[tagType]) {
|
|
86
|
-
// Assume the artist field is used as artists
|
|
87
|
-
return this.postMap('artificial', { id: 'artists', value: tag.value });
|
|
88
|
-
}
|
|
89
|
-
if (!this.common.artists) {
|
|
90
|
-
// Fill artists using artist source
|
|
91
|
-
this.setGenericTag('artificial', { id: 'artists', value: tag.value });
|
|
92
|
-
}
|
|
93
|
-
break;
|
|
94
|
-
case 'artists':
|
|
95
|
-
if (!this.common.artist || this.commonOrigin.artist === this.originPriority.artificial) {
|
|
96
|
-
if (!this.common.artists || this.common.artists.indexOf(tag.value) === -1) {
|
|
97
|
-
// Fill artist using artists source
|
|
98
|
-
const artists = (this.common.artists || []).concat([tag.value]);
|
|
99
|
-
const value = joinArtists(artists);
|
|
100
|
-
const artistTag = { id: 'artist', value };
|
|
101
|
-
this.setGenericTag('artificial', artistTag);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
break;
|
|
105
|
-
case 'picture':
|
|
106
|
-
this.postFixPicture(tag.value).then(picture => {
|
|
107
|
-
if (picture !== null) {
|
|
108
|
-
tag.value = picture;
|
|
109
|
-
this.setGenericTag(tagType, tag);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
return;
|
|
113
|
-
case 'totaltracks':
|
|
114
|
-
this.common.track.of = GenericTagMapper_1.CommonTagMapper.toIntOrNull(tag.value);
|
|
115
|
-
return;
|
|
116
|
-
case 'totaldiscs':
|
|
117
|
-
this.common.disk.of = GenericTagMapper_1.CommonTagMapper.toIntOrNull(tag.value);
|
|
118
|
-
return;
|
|
119
|
-
case 'movementTotal':
|
|
120
|
-
this.common.movementIndex.of = GenericTagMapper_1.CommonTagMapper.toIntOrNull(tag.value);
|
|
121
|
-
return;
|
|
122
|
-
case 'track':
|
|
123
|
-
case 'disk':
|
|
124
|
-
case 'movementIndex':
|
|
125
|
-
const of = this.common[tag.id].of; // store of value, maybe maybe overwritten
|
|
126
|
-
this.common[tag.id] = GenericTagMapper_1.CommonTagMapper.normalizeTrack(tag.value);
|
|
127
|
-
this.common[tag.id].of = of != null ? of : this.common[tag.id].of;
|
|
128
|
-
return;
|
|
129
|
-
case 'bpm':
|
|
130
|
-
case 'year':
|
|
131
|
-
case 'originalyear':
|
|
132
|
-
tag.value = parseInt(tag.value, 10);
|
|
133
|
-
break;
|
|
134
|
-
case 'date':
|
|
135
|
-
// ToDo: be more strict on 'YYYY...'
|
|
136
|
-
const year = parseInt(tag.value.substr(0, 4), 10);
|
|
137
|
-
if (!isNaN(year)) {
|
|
138
|
-
this.common.year = year;
|
|
139
|
-
}
|
|
140
|
-
break;
|
|
141
|
-
case 'discogs_label_id':
|
|
142
|
-
case 'discogs_release_id':
|
|
143
|
-
case 'discogs_master_release_id':
|
|
144
|
-
case 'discogs_artist_id':
|
|
145
|
-
case 'discogs_votes':
|
|
146
|
-
tag.value = typeof tag.value === 'string' ? parseInt(tag.value, 10) : tag.value;
|
|
147
|
-
break;
|
|
148
|
-
case 'replaygain_track_gain':
|
|
149
|
-
case 'replaygain_track_peak':
|
|
150
|
-
case 'replaygain_album_gain':
|
|
151
|
-
case 'replaygain_album_peak':
|
|
152
|
-
tag.value = (0, Util_1.toRatio)(tag.value);
|
|
153
|
-
break;
|
|
154
|
-
case 'replaygain_track_minmax':
|
|
155
|
-
tag.value = tag.value.split(',').map(v => parseInt(v, 10));
|
|
156
|
-
break;
|
|
157
|
-
case 'replaygain_undo':
|
|
158
|
-
const minMix = tag.value.split(',').map(v => parseInt(v, 10));
|
|
159
|
-
tag.value = {
|
|
160
|
-
leftChannel: minMix[0],
|
|
161
|
-
rightChannel: minMix[1]
|
|
162
|
-
};
|
|
163
|
-
break;
|
|
164
|
-
case 'gapless': // iTunes gap-less flag
|
|
165
|
-
case 'compilation':
|
|
166
|
-
case 'podcast':
|
|
167
|
-
case 'showMovement':
|
|
168
|
-
tag.value = tag.value === '1' || tag.value === 1; // boolean
|
|
169
|
-
break;
|
|
170
|
-
case 'isrc': // Only keep unique values
|
|
171
|
-
if (this.common[tag.id] && this.common[tag.id].indexOf(tag.value) !== -1)
|
|
172
|
-
return;
|
|
173
|
-
break;
|
|
174
|
-
default:
|
|
175
|
-
// nothing to do
|
|
176
|
-
}
|
|
177
|
-
if (tag.value !== null) {
|
|
178
|
-
this.setGenericTag(tagType, tag);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Convert native tags to common tags
|
|
183
|
-
* @returns {IAudioMetadata} Native + common tags
|
|
184
|
-
*/
|
|
185
|
-
toCommonMetadata() {
|
|
186
|
-
return {
|
|
187
|
-
format: this.format,
|
|
188
|
-
native: this.native,
|
|
189
|
-
quality: this.quality,
|
|
190
|
-
common: this.common
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Fix some common issues with picture object
|
|
195
|
-
* @param picture Picture
|
|
196
|
-
*/
|
|
197
|
-
async postFixPicture(picture) {
|
|
198
|
-
if (picture.data && picture.data.length > 0) {
|
|
199
|
-
if (!picture.format) {
|
|
200
|
-
const fileType = await FileType.fromBuffer(picture.data);
|
|
201
|
-
if (fileType) {
|
|
202
|
-
picture.format = fileType.mime;
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
picture.format = picture.format.toLocaleLowerCase();
|
|
209
|
-
switch (picture.format) {
|
|
210
|
-
case 'image/jpg':
|
|
211
|
-
picture.format = 'image/jpeg'; // ToDo: register warning
|
|
212
|
-
}
|
|
213
|
-
return picture;
|
|
214
|
-
}
|
|
215
|
-
this.addWarning(`Empty picture tag found`);
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Convert native tag to common tags
|
|
220
|
-
*/
|
|
221
|
-
toCommon(tagType, tagId, value) {
|
|
222
|
-
const tag = { id: tagId, value };
|
|
223
|
-
const genericTag = this.tagMapper.mapTag(tagType, tag, this);
|
|
224
|
-
if (genericTag) {
|
|
225
|
-
this.postMap(tagType, genericTag);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Set generic tag
|
|
230
|
-
*/
|
|
231
|
-
setGenericTag(tagType, tag) {
|
|
232
|
-
debug(`common.${tag.id} = ${tag.value}`);
|
|
233
|
-
const prio0 = this.commonOrigin[tag.id] || 1000;
|
|
234
|
-
const prio1 = this.originPriority[tagType];
|
|
235
|
-
if ((0, GenericTagTypes_1.isSingleton)(tag.id)) {
|
|
236
|
-
if (prio1 <= prio0) {
|
|
237
|
-
this.common[tag.id] = tag.value;
|
|
238
|
-
this.commonOrigin[tag.id] = prio1;
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
return debug(`Ignore native tag (singleton): ${tagType}.${tag.id} = ${tag.value}`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
if (prio1 === prio0) {
|
|
246
|
-
if (!(0, GenericTagTypes_1.isUnique)(tag.id) || this.common[tag.id].indexOf(tag.value) === -1) {
|
|
247
|
-
this.common[tag.id].push(tag.value);
|
|
248
|
-
}
|
|
249
|
-
else {
|
|
250
|
-
debug(`Ignore duplicate value: ${tagType}.${tag.id} = ${tag.value}`);
|
|
251
|
-
}
|
|
252
|
-
// no effect? this.commonOrigin[tag.id] = prio1;
|
|
253
|
-
}
|
|
254
|
-
else if (prio1 < prio0) {
|
|
255
|
-
this.common[tag.id] = [tag.value];
|
|
256
|
-
this.commonOrigin[tag.id] = prio1;
|
|
257
|
-
}
|
|
258
|
-
else {
|
|
259
|
-
return debug(`Ignore native tag (list): ${tagType}.${tag.id} = ${tag.value}`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (this.opts.observer) {
|
|
263
|
-
this.opts.observer({ metadata: this, tag: { type: 'common', id: tag.id, value: tag.value } });
|
|
264
|
-
}
|
|
265
|
-
// ToDo: trigger metadata event
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
exports.MetadataCollector = MetadataCollector;
|
|
269
|
-
function joinArtists(artists) {
|
|
270
|
-
if (artists.length > 2) {
|
|
271
|
-
return artists.slice(0, artists.length - 1).join(', ') + ' & ' + artists[artists.length - 1];
|
|
272
|
-
}
|
|
273
|
-
return artists.join(' & ');
|
|
274
|
-
}
|
|
275
|
-
exports.joinArtists = joinArtists;
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.joinArtists = exports.MetadataCollector = void 0;
|
|
4
|
+
const type_1 = require("../type");
|
|
5
|
+
const debug_1 = require("debug");
|
|
6
|
+
const GenericTagTypes_1 = require("./GenericTagTypes");
|
|
7
|
+
const CombinedTagMapper_1 = require("./CombinedTagMapper");
|
|
8
|
+
const GenericTagMapper_1 = require("./GenericTagMapper");
|
|
9
|
+
const Util_1 = require("./Util");
|
|
10
|
+
const FileType = require("file-type/core");
|
|
11
|
+
const debug = (0, debug_1.default)('music-metadata:collector');
|
|
12
|
+
const TagPriority = ['matroska', 'APEv2', 'vorbis', 'ID3v2.4', 'ID3v2.3', 'ID3v2.2', 'exif', 'asf', 'iTunes', 'ID3v1'];
|
|
13
|
+
/**
|
|
14
|
+
* Provided to the parser to uodate the metadata result.
|
|
15
|
+
* Responsible for triggering async updates
|
|
16
|
+
*/
|
|
17
|
+
class MetadataCollector {
|
|
18
|
+
constructor(opts) {
|
|
19
|
+
this.opts = opts;
|
|
20
|
+
this.format = {
|
|
21
|
+
tagTypes: [],
|
|
22
|
+
trackInfo: []
|
|
23
|
+
};
|
|
24
|
+
this.native = {};
|
|
25
|
+
this.common = {
|
|
26
|
+
track: { no: null, of: null },
|
|
27
|
+
disk: { no: null, of: null },
|
|
28
|
+
movementIndex: {}
|
|
29
|
+
};
|
|
30
|
+
this.quality = {
|
|
31
|
+
warnings: []
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Keeps track of origin priority for each mapped id
|
|
35
|
+
*/
|
|
36
|
+
this.commonOrigin = {};
|
|
37
|
+
/**
|
|
38
|
+
* Maps a tag type to a priority
|
|
39
|
+
*/
|
|
40
|
+
this.originPriority = {};
|
|
41
|
+
this.tagMapper = new CombinedTagMapper_1.CombinedTagMapper();
|
|
42
|
+
let priority = 1;
|
|
43
|
+
for (const tagType of TagPriority) {
|
|
44
|
+
this.originPriority[tagType] = priority++;
|
|
45
|
+
}
|
|
46
|
+
this.originPriority.artificial = 500; // Filled using alternative tags
|
|
47
|
+
this.originPriority.id3v1 = 600; // Consider worst due to field length limit
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* @returns {boolean} true if one or more tags have been found
|
|
51
|
+
*/
|
|
52
|
+
hasAny() {
|
|
53
|
+
return Object.keys(this.native).length > 0;
|
|
54
|
+
}
|
|
55
|
+
addStreamInfo(streamInfo) {
|
|
56
|
+
debug(`streamInfo: type=${type_1.TrackType[streamInfo.type]}, codec=${streamInfo.codecName}`);
|
|
57
|
+
this.format.trackInfo.push(streamInfo);
|
|
58
|
+
}
|
|
59
|
+
setFormat(key, value) {
|
|
60
|
+
debug(`format: ${key} = ${value}`);
|
|
61
|
+
this.format[key] = value; // as any to override readonly
|
|
62
|
+
if (this.opts.observer) {
|
|
63
|
+
this.opts.observer({ metadata: this, tag: { type: 'format', id: key, value } });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
addTag(tagType, tagId, value) {
|
|
67
|
+
debug(`tag ${tagType}.${tagId} = ${value}`);
|
|
68
|
+
if (!this.native[tagType]) {
|
|
69
|
+
this.format.tagTypes.push(tagType);
|
|
70
|
+
this.native[tagType] = [];
|
|
71
|
+
}
|
|
72
|
+
this.native[tagType].push({ id: tagId, value });
|
|
73
|
+
this.toCommon(tagType, tagId, value);
|
|
74
|
+
}
|
|
75
|
+
addWarning(warning) {
|
|
76
|
+
this.quality.warnings.push({ message: warning });
|
|
77
|
+
}
|
|
78
|
+
postMap(tagType, tag) {
|
|
79
|
+
// Common tag (alias) found
|
|
80
|
+
// check if we need to do something special with common tag
|
|
81
|
+
// if the event has been aliased then we need to clean it before
|
|
82
|
+
// it is emitted to the user. e.g. genre (20) -> Electronic
|
|
83
|
+
switch (tag.id) {
|
|
84
|
+
case 'artist':
|
|
85
|
+
if (this.commonOrigin.artist === this.originPriority[tagType]) {
|
|
86
|
+
// Assume the artist field is used as artists
|
|
87
|
+
return this.postMap('artificial', { id: 'artists', value: tag.value });
|
|
88
|
+
}
|
|
89
|
+
if (!this.common.artists) {
|
|
90
|
+
// Fill artists using artist source
|
|
91
|
+
this.setGenericTag('artificial', { id: 'artists', value: tag.value });
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
case 'artists':
|
|
95
|
+
if (!this.common.artist || this.commonOrigin.artist === this.originPriority.artificial) {
|
|
96
|
+
if (!this.common.artists || this.common.artists.indexOf(tag.value) === -1) {
|
|
97
|
+
// Fill artist using artists source
|
|
98
|
+
const artists = (this.common.artists || []).concat([tag.value]);
|
|
99
|
+
const value = joinArtists(artists);
|
|
100
|
+
const artistTag = { id: 'artist', value };
|
|
101
|
+
this.setGenericTag('artificial', artistTag);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
case 'picture':
|
|
106
|
+
this.postFixPicture(tag.value).then(picture => {
|
|
107
|
+
if (picture !== null) {
|
|
108
|
+
tag.value = picture;
|
|
109
|
+
this.setGenericTag(tagType, tag);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
case 'totaltracks':
|
|
114
|
+
this.common.track.of = GenericTagMapper_1.CommonTagMapper.toIntOrNull(tag.value);
|
|
115
|
+
return;
|
|
116
|
+
case 'totaldiscs':
|
|
117
|
+
this.common.disk.of = GenericTagMapper_1.CommonTagMapper.toIntOrNull(tag.value);
|
|
118
|
+
return;
|
|
119
|
+
case 'movementTotal':
|
|
120
|
+
this.common.movementIndex.of = GenericTagMapper_1.CommonTagMapper.toIntOrNull(tag.value);
|
|
121
|
+
return;
|
|
122
|
+
case 'track':
|
|
123
|
+
case 'disk':
|
|
124
|
+
case 'movementIndex':
|
|
125
|
+
const of = this.common[tag.id].of; // store of value, maybe maybe overwritten
|
|
126
|
+
this.common[tag.id] = GenericTagMapper_1.CommonTagMapper.normalizeTrack(tag.value);
|
|
127
|
+
this.common[tag.id].of = of != null ? of : this.common[tag.id].of;
|
|
128
|
+
return;
|
|
129
|
+
case 'bpm':
|
|
130
|
+
case 'year':
|
|
131
|
+
case 'originalyear':
|
|
132
|
+
tag.value = parseInt(tag.value, 10);
|
|
133
|
+
break;
|
|
134
|
+
case 'date':
|
|
135
|
+
// ToDo: be more strict on 'YYYY...'
|
|
136
|
+
const year = parseInt(tag.value.substr(0, 4), 10);
|
|
137
|
+
if (!isNaN(year)) {
|
|
138
|
+
this.common.year = year;
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
case 'discogs_label_id':
|
|
142
|
+
case 'discogs_release_id':
|
|
143
|
+
case 'discogs_master_release_id':
|
|
144
|
+
case 'discogs_artist_id':
|
|
145
|
+
case 'discogs_votes':
|
|
146
|
+
tag.value = typeof tag.value === 'string' ? parseInt(tag.value, 10) : tag.value;
|
|
147
|
+
break;
|
|
148
|
+
case 'replaygain_track_gain':
|
|
149
|
+
case 'replaygain_track_peak':
|
|
150
|
+
case 'replaygain_album_gain':
|
|
151
|
+
case 'replaygain_album_peak':
|
|
152
|
+
tag.value = (0, Util_1.toRatio)(tag.value);
|
|
153
|
+
break;
|
|
154
|
+
case 'replaygain_track_minmax':
|
|
155
|
+
tag.value = tag.value.split(',').map(v => parseInt(v, 10));
|
|
156
|
+
break;
|
|
157
|
+
case 'replaygain_undo':
|
|
158
|
+
const minMix = tag.value.split(',').map(v => parseInt(v, 10));
|
|
159
|
+
tag.value = {
|
|
160
|
+
leftChannel: minMix[0],
|
|
161
|
+
rightChannel: minMix[1]
|
|
162
|
+
};
|
|
163
|
+
break;
|
|
164
|
+
case 'gapless': // iTunes gap-less flag
|
|
165
|
+
case 'compilation':
|
|
166
|
+
case 'podcast':
|
|
167
|
+
case 'showMovement':
|
|
168
|
+
tag.value = tag.value === '1' || tag.value === 1; // boolean
|
|
169
|
+
break;
|
|
170
|
+
case 'isrc': // Only keep unique values
|
|
171
|
+
if (this.common[tag.id] && this.common[tag.id].indexOf(tag.value) !== -1)
|
|
172
|
+
return;
|
|
173
|
+
break;
|
|
174
|
+
default:
|
|
175
|
+
// nothing to do
|
|
176
|
+
}
|
|
177
|
+
if (tag.value !== null) {
|
|
178
|
+
this.setGenericTag(tagType, tag);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Convert native tags to common tags
|
|
183
|
+
* @returns {IAudioMetadata} Native + common tags
|
|
184
|
+
*/
|
|
185
|
+
toCommonMetadata() {
|
|
186
|
+
return {
|
|
187
|
+
format: this.format,
|
|
188
|
+
native: this.native,
|
|
189
|
+
quality: this.quality,
|
|
190
|
+
common: this.common
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Fix some common issues with picture object
|
|
195
|
+
* @param picture Picture
|
|
196
|
+
*/
|
|
197
|
+
async postFixPicture(picture) {
|
|
198
|
+
if (picture.data && picture.data.length > 0) {
|
|
199
|
+
if (!picture.format) {
|
|
200
|
+
const fileType = await FileType.fromBuffer(picture.data);
|
|
201
|
+
if (fileType) {
|
|
202
|
+
picture.format = fileType.mime;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
picture.format = picture.format.toLocaleLowerCase();
|
|
209
|
+
switch (picture.format) {
|
|
210
|
+
case 'image/jpg':
|
|
211
|
+
picture.format = 'image/jpeg'; // ToDo: register warning
|
|
212
|
+
}
|
|
213
|
+
return picture;
|
|
214
|
+
}
|
|
215
|
+
this.addWarning(`Empty picture tag found`);
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Convert native tag to common tags
|
|
220
|
+
*/
|
|
221
|
+
toCommon(tagType, tagId, value) {
|
|
222
|
+
const tag = { id: tagId, value };
|
|
223
|
+
const genericTag = this.tagMapper.mapTag(tagType, tag, this);
|
|
224
|
+
if (genericTag) {
|
|
225
|
+
this.postMap(tagType, genericTag);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Set generic tag
|
|
230
|
+
*/
|
|
231
|
+
setGenericTag(tagType, tag) {
|
|
232
|
+
debug(`common.${tag.id} = ${tag.value}`);
|
|
233
|
+
const prio0 = this.commonOrigin[tag.id] || 1000;
|
|
234
|
+
const prio1 = this.originPriority[tagType];
|
|
235
|
+
if ((0, GenericTagTypes_1.isSingleton)(tag.id)) {
|
|
236
|
+
if (prio1 <= prio0) {
|
|
237
|
+
this.common[tag.id] = tag.value;
|
|
238
|
+
this.commonOrigin[tag.id] = prio1;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
return debug(`Ignore native tag (singleton): ${tagType}.${tag.id} = ${tag.value}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
if (prio1 === prio0) {
|
|
246
|
+
if (!(0, GenericTagTypes_1.isUnique)(tag.id) || this.common[tag.id].indexOf(tag.value) === -1) {
|
|
247
|
+
this.common[tag.id].push(tag.value);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
debug(`Ignore duplicate value: ${tagType}.${tag.id} = ${tag.value}`);
|
|
251
|
+
}
|
|
252
|
+
// no effect? this.commonOrigin[tag.id] = prio1;
|
|
253
|
+
}
|
|
254
|
+
else if (prio1 < prio0) {
|
|
255
|
+
this.common[tag.id] = [tag.value];
|
|
256
|
+
this.commonOrigin[tag.id] = prio1;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
return debug(`Ignore native tag (list): ${tagType}.${tag.id} = ${tag.value}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (this.opts.observer) {
|
|
263
|
+
this.opts.observer({ metadata: this, tag: { type: 'common', id: tag.id, value: tag.value } });
|
|
264
|
+
}
|
|
265
|
+
// ToDo: trigger metadata event
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
exports.MetadataCollector = MetadataCollector;
|
|
269
|
+
function joinArtists(artists) {
|
|
270
|
+
if (artists.length > 2) {
|
|
271
|
+
return artists.slice(0, artists.length - 1).join(', ') + ' & ' + artists[artists.length - 1];
|
|
272
|
+
}
|
|
273
|
+
return artists.join(' & ');
|
|
274
|
+
}
|
|
275
|
+
exports.joinArtists = joinArtists;
|
|
276
276
|
//# sourceMappingURL=MetadataCollector.js.map
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
import { IRandomReader } from '../type';
|
|
3
|
-
/**
|
|
4
|
-
* Provides abstract file access via the IRandomRead interface
|
|
5
|
-
*/
|
|
6
|
-
export declare class RandomFileReader implements IRandomReader {
|
|
7
|
-
private readonly fileHandle;
|
|
8
|
-
filePath: string;
|
|
9
|
-
fileSize: number;
|
|
10
|
-
private constructor();
|
|
11
|
-
/**
|
|
12
|
-
* Read from a given position of an abstracted file or buffer.
|
|
13
|
-
* @param buffer {Buffer} is the buffer that the data will be written to.
|
|
14
|
-
* @param offset {number} is the offset in the buffer to start writing at.
|
|
15
|
-
* @param length {number}is an integer specifying the number of bytes to read.
|
|
16
|
-
* @param position {number} is an argument specifying where to begin reading from in the file.
|
|
17
|
-
* @return {Promise<number>} bytes read
|
|
18
|
-
*/
|
|
19
|
-
randomRead(buffer: Buffer, offset: number, length: number, position: number): Promise<number>;
|
|
20
|
-
close(): Promise<void>;
|
|
21
|
-
static init(filePath: string, fileSize: number): Promise<RandomFileReader>;
|
|
22
|
-
}
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { IRandomReader } from '../type';
|
|
3
|
+
/**
|
|
4
|
+
* Provides abstract file access via the IRandomRead interface
|
|
5
|
+
*/
|
|
6
|
+
export declare class RandomFileReader implements IRandomReader {
|
|
7
|
+
private readonly fileHandle;
|
|
8
|
+
filePath: string;
|
|
9
|
+
fileSize: number;
|
|
10
|
+
private constructor();
|
|
11
|
+
/**
|
|
12
|
+
* Read from a given position of an abstracted file or buffer.
|
|
13
|
+
* @param buffer {Buffer} is the buffer that the data will be written to.
|
|
14
|
+
* @param offset {number} is the offset in the buffer to start writing at.
|
|
15
|
+
* @param length {number}is an integer specifying the number of bytes to read.
|
|
16
|
+
* @param position {number} is an argument specifying where to begin reading from in the file.
|
|
17
|
+
* @return {Promise<number>} bytes read
|
|
18
|
+
*/
|
|
19
|
+
randomRead(buffer: Buffer, offset: number, length: number, position: number): Promise<number>;
|
|
20
|
+
close(): Promise<void>;
|
|
21
|
+
static init(filePath: string, fileSize: number): Promise<RandomFileReader>;
|
|
22
|
+
}
|