music-metadata 11.8.3 → 11.10.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/README.md +3 -0
- package/lib/ParserFactory.js +1 -1
- package/lib/apev2/APEv2Parser.js +2 -2
- package/lib/asf/AsfObject.js +5 -5
- package/lib/asf/GUID.js +6 -6
- package/lib/common/FourCC.js +1 -1
- package/lib/common/GenericTagTypes.d.ts +1 -1
- package/lib/common/GenericTagTypes.js +2 -1
- package/lib/common/MetadataCollector.js +2 -2
- package/lib/common/Util.d.ts +5 -0
- package/lib/common/Util.js +12 -0
- package/lib/flac/FlacParser.js +10 -2
- package/lib/id3v2/FrameParser.js +23 -22
- package/lib/id3v2/ID3v24TagMapper.js +2 -1
- package/lib/id3v2/ID3v2Parser.js +7 -7
- package/lib/id3v2/ID3v2Token.d.ts +1 -1
- package/lib/id3v2/ID3v2Token.js +1 -1
- package/lib/lrc/LyricsParser.d.ts +2 -0
- package/lib/lrc/LyricsParser.js +23 -18
- package/lib/lyrics3/Lyrics3.js +2 -2
- package/lib/mp4/MP4Parser.js +2 -0
- package/lib/mp4/Mp4Loader.js +2 -2
- package/lib/mpeg/MpegParser.js +1 -1
- package/lib/ogg/OggParser.js +1 -1
- package/lib/ogg/vorbis/Vorbis.js +1 -1
- package/lib/ogg/vorbis/VorbisDecoder.js +2 -2
- package/lib/type.d.ts +1 -0
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -602,6 +602,9 @@ Returns a list of supported MIME-types. This may include some MIME-types which a
|
|
|
602
602
|
Note that enabling this option **does not guarantee** that duration will be available,
|
|
603
603
|
only that the parser will attempt to calculate it when possible, even if it requires reading the full file.
|
|
604
604
|
|
|
605
|
+
- `includeChapters`: `boolean` (default: `false`)
|
|
606
|
+
When `true`, the MP4 parser scans the `mdat` atom for chapters.
|
|
607
|
+
|
|
605
608
|
- `mkvUseIndex`: `boolean` (default: `false`)
|
|
606
609
|
|
|
607
610
|
When `true`, the parser uses the SeekHead index in Matroska (MKV) files to skip segment and cluster elements.
|
package/lib/ParserFactory.js
CHANGED
package/lib/apev2/APEv2Parser.js
CHANGED
|
@@ -132,8 +132,8 @@ export class APEv2Parser extends BasicParser {
|
|
|
132
132
|
const picData = new Uint8Array(tagItemHeader.size);
|
|
133
133
|
await this.tokenizer.readBuffer(picData);
|
|
134
134
|
zero = util.findZero(picData, 0, picData.length);
|
|
135
|
-
const description = textDecode(picData.
|
|
136
|
-
const data = picData.
|
|
135
|
+
const description = textDecode(picData.subarray(0, zero), 'utf-8');
|
|
136
|
+
const data = picData.subarray(zero + 1);
|
|
137
137
|
await this.metadata.addTag(tagFormat, key, {
|
|
138
138
|
description,
|
|
139
139
|
data
|
package/lib/asf/AsfObject.js
CHANGED
|
@@ -210,7 +210,7 @@ export class ContentDescriptionObjectState extends State {
|
|
|
210
210
|
if (length > 0) {
|
|
211
211
|
const tagName = ContentDescriptionObjectState.contentDescTags[i];
|
|
212
212
|
const end = pos + length;
|
|
213
|
-
tags.push({ id: tagName, value: parseUnicodeAttr(buf.
|
|
213
|
+
tags.push({ id: tagName, value: parseUnicodeAttr(buf.subarray(off + pos, off + end)) });
|
|
214
214
|
pos = end;
|
|
215
215
|
}
|
|
216
216
|
}
|
|
@@ -232,13 +232,13 @@ export class ExtendedContentDescriptionObjectState extends State {
|
|
|
232
232
|
for (let i = 0; i < attrCount; i += 1) {
|
|
233
233
|
const nameLen = view.getUint16(pos, true);
|
|
234
234
|
pos += 2;
|
|
235
|
-
const name = parseUnicodeAttr(buf.
|
|
235
|
+
const name = parseUnicodeAttr(buf.subarray(off + pos, off + pos + nameLen));
|
|
236
236
|
pos += nameLen;
|
|
237
237
|
const valueType = view.getUint16(pos, true);
|
|
238
238
|
pos += 2;
|
|
239
239
|
const valueLen = view.getUint16(pos, true);
|
|
240
240
|
pos += 2;
|
|
241
|
-
const value = buf.
|
|
241
|
+
const value = buf.subarray(off + pos, off + pos + valueLen);
|
|
242
242
|
pos += valueLen;
|
|
243
243
|
this.postProcessTag(tags, name, valueType, value);
|
|
244
244
|
}
|
|
@@ -298,9 +298,9 @@ export class MetadataObjectState extends State {
|
|
|
298
298
|
pos += 2;
|
|
299
299
|
const dataLen = view.getUint32(pos, true);
|
|
300
300
|
pos += 4;
|
|
301
|
-
const name = parseUnicodeAttr(uint8Array.
|
|
301
|
+
const name = parseUnicodeAttr(uint8Array.subarray(off + pos, off + pos + nameLen));
|
|
302
302
|
pos += nameLen;
|
|
303
|
-
const data = uint8Array.
|
|
303
|
+
const data = uint8Array.subarray(off + pos, off + pos + dataLen);
|
|
304
304
|
pos += dataLen;
|
|
305
305
|
this.postProcessTag(tags, name, dataType, data);
|
|
306
306
|
}
|
package/lib/asf/GUID.js
CHANGED
|
@@ -25,7 +25,7 @@ class GUID {
|
|
|
25
25
|
*/
|
|
26
26
|
static decode(objectId, offset = 0) {
|
|
27
27
|
const view = new DataView(objectId.buffer, offset);
|
|
28
|
-
const guid = `${view.getUint32(0, true).toString(16)}-${view.getUint16(4, true).toString(16)}-${view.getUint16(6, true).toString(16)}-${view.getUint16(8).toString(16)}-${uint8ArrayToHex(objectId.
|
|
28
|
+
const guid = `${view.getUint32(0, true).toString(16)}-${view.getUint16(4, true).toString(16)}-${view.getUint16(6, true).toString(16)}-${view.getUint16(8).toString(16)}-${uint8ArrayToHex(objectId.subarray(offset + 10, offset + 16))}`;
|
|
29
29
|
return guid.toUpperCase();
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
@@ -51,11 +51,11 @@ class GUID {
|
|
|
51
51
|
static encode(str) {
|
|
52
52
|
const bin = new Uint8Array(16);
|
|
53
53
|
const view = new DataView(bin.buffer);
|
|
54
|
-
view.setUint32(0, Number.parseInt(str.
|
|
55
|
-
view.setUint16(4, Number.parseInt(str.
|
|
56
|
-
view.setUint16(6, Number.parseInt(str.
|
|
57
|
-
bin.set(hexToUint8Array(str.
|
|
58
|
-
bin.set(hexToUint8Array(str.
|
|
54
|
+
view.setUint32(0, Number.parseInt(str.substring(0, 8), 16), true);
|
|
55
|
+
view.setUint16(4, Number.parseInt(str.substring(9, 13), 16), true);
|
|
56
|
+
view.setUint16(6, Number.parseInt(str.substring(14, 18), 16), true);
|
|
57
|
+
bin.set(hexToUint8Array(str.substring(19, 23)), 8);
|
|
58
|
+
bin.set(hexToUint8Array(str.substring(24)), 10);
|
|
59
59
|
return bin;
|
|
60
60
|
}
|
|
61
61
|
constructor(str) {
|
package/lib/common/FourCC.js
CHANGED
|
@@ -9,7 +9,7 @@ const validFourCC = /^[\x21-\x7e©][\x20-\x7e\x00()]{3}/;
|
|
|
9
9
|
export const FourCcToken = {
|
|
10
10
|
len: 4,
|
|
11
11
|
get: (buf, off) => {
|
|
12
|
-
const id = textDecode(buf.
|
|
12
|
+
const id = textDecode(buf.subarray(off, off + FourCcToken.len), 'latin1');
|
|
13
13
|
if (!id.match(validFourCC)) {
|
|
14
14
|
throw new FieldDecodingError(`FourCC contains invalid characters: ${util.a2hex(id)} "${id}"`);
|
|
15
15
|
}
|
|
@@ -4,7 +4,7 @@ export interface IGenericTag {
|
|
|
4
4
|
id: keyof ICommonTagsResult;
|
|
5
5
|
value: AnyTagValue;
|
|
6
6
|
}
|
|
7
|
-
export type GenericTagId = 'track' | 'disk' | 'year' | 'title' | 'artist' | 'artists' | 'albumartist' | 'album' | 'date' | 'originaldate' | 'originalyear' | 'releasedate' | 'comment' | 'genre' | 'picture' | 'composer' | 'lyrics' | 'albumsort' | 'titlesort' | 'work' | 'artistsort' | 'albumartistsort' | 'composersort' | 'lyricist' | 'writer' | 'conductor' | 'remixer' | 'arranger' | 'engineer' | 'technician' | 'producer' | 'djmixer' | 'mixer' | 'publisher' | 'label' | 'grouping' | 'subtitle' | 'discsubtitle' | 'totaltracks' | 'totaldiscs' | 'compilation' | 'rating' | 'bpm' | 'mood' | 'media' | 'catalognumber' | 'tvShow' | 'tvShowSort' | 'tvEpisode' | 'tvEpisodeId' | 'tvNetwork' | 'tvSeason' | 'podcast' | 'podcasturl' | 'releasestatus' | 'releasetype' | 'releasecountry' | 'script' | 'language' | 'copyright' | 'license' | 'encodedby' | 'encodersettings' | 'gapless' | 'barcode' | 'isrc' | 'asin' | 'musicbrainz_recordingid' | 'musicbrainz_trackid' | 'musicbrainz_albumid' | 'musicbrainz_artistid' | 'musicbrainz_albumartistid' | 'musicbrainz_releasegroupid' | 'musicbrainz_workid' | 'musicbrainz_trmid' | 'musicbrainz_discid' | 'acoustid_id' | 'acoustid_fingerprint' | 'musicip_puid' | 'musicip_fingerprint' | 'website' | 'performer:instrument' | 'peakLevel' | 'averageLevel' | 'notes' | 'key' | 'originalalbum' | 'originalartist' | 'discogs_artist_id' | 'discogs_label_id' | 'discogs_master_release_id' | 'discogs_rating' | 'discogs_release_id' | 'discogs_votes' | 'replaygain_track_gain' | 'replaygain_track_peak' | 'replaygain_album_gain' | 'replaygain_album_peak' | 'replaygain_track_minmax' | 'replaygain_album_minmax' | 'replaygain_undo' | 'description' | 'longDescription' | 'category' | 'hdVideo' | 'keywords' | 'movement' | 'movementIndex' | 'movementTotal' | 'podcastId' | 'showMovement' | 'stik';
|
|
7
|
+
export type GenericTagId = 'track' | 'disk' | 'year' | 'title' | 'artist' | 'artists' | 'albumartist' | 'album' | 'date' | 'originaldate' | 'originalyear' | 'releasedate' | 'comment' | 'genre' | 'picture' | 'composer' | 'lyrics' | 'albumsort' | 'titlesort' | 'work' | 'artistsort' | 'albumartistsort' | 'composersort' | 'lyricist' | 'writer' | 'conductor' | 'remixer' | 'arranger' | 'engineer' | 'technician' | 'producer' | 'djmixer' | 'mixer' | 'publisher' | 'label' | 'grouping' | 'subtitle' | 'discsubtitle' | 'totaltracks' | 'totaldiscs' | 'compilation' | 'rating' | 'bpm' | 'mood' | 'media' | 'catalognumber' | 'tvShow' | 'tvShowSort' | 'tvEpisode' | 'tvEpisodeId' | 'tvNetwork' | 'tvSeason' | 'podcast' | 'podcasturl' | 'releasestatus' | 'releasetype' | 'releasecountry' | 'script' | 'language' | 'copyright' | 'license' | 'encodedby' | 'encodersettings' | 'gapless' | 'barcode' | 'isrc' | 'asin' | 'musicbrainz_recordingid' | 'musicbrainz_trackid' | 'musicbrainz_albumid' | 'musicbrainz_artistid' | 'musicbrainz_albumartistid' | 'musicbrainz_releasegroupid' | 'musicbrainz_workid' | 'musicbrainz_trmid' | 'musicbrainz_discid' | 'acoustid_id' | 'acoustid_fingerprint' | 'musicip_puid' | 'musicip_fingerprint' | 'website' | 'performer:instrument' | 'peakLevel' | 'averageLevel' | 'notes' | 'key' | 'originalalbum' | 'originalartist' | 'discogs_artist_id' | 'discogs_label_id' | 'discogs_master_release_id' | 'discogs_rating' | 'discogs_release_id' | 'discogs_votes' | 'replaygain_track_gain' | 'replaygain_track_peak' | 'replaygain_album_gain' | 'replaygain_album_peak' | 'replaygain_track_minmax' | 'replaygain_album_minmax' | 'replaygain_undo' | 'description' | 'longDescription' | 'category' | 'hdVideo' | 'keywords' | 'movement' | 'movementIndex' | 'movementTotal' | 'podcastId' | 'showMovement' | 'stik' | 'playCounter';
|
|
8
8
|
export interface INativeTagMap {
|
|
9
9
|
[index: string]: GenericTagId;
|
|
10
10
|
}
|
|
@@ -112,7 +112,8 @@ const commonTags = {
|
|
|
112
112
|
movementTotal: defaultTagInfo,
|
|
113
113
|
podcastId: defaultTagInfo,
|
|
114
114
|
showMovement: defaultTagInfo,
|
|
115
|
-
stik: defaultTagInfo
|
|
115
|
+
stik: defaultTagInfo,
|
|
116
|
+
playCounter: defaultTagInfo
|
|
116
117
|
};
|
|
117
118
|
export const commonTagsKeys = /* @__PURE__ */ Object.keys(commonTags);
|
|
118
119
|
/**
|
|
@@ -5,7 +5,7 @@ import { CombinedTagMapper } from './CombinedTagMapper.js';
|
|
|
5
5
|
import { CommonTagMapper } from './GenericTagMapper.js';
|
|
6
6
|
import { toRatio } from './Util.js';
|
|
7
7
|
import { fileTypeFromBuffer } from 'file-type';
|
|
8
|
-
import {
|
|
8
|
+
import { parseLyrics } from '../lrc/LyricsParser.js';
|
|
9
9
|
const debug = initDebug('music-metadata:collector');
|
|
10
10
|
const TagPriority = ['matroska', 'APEv2', 'vorbis', 'ID3v2.4', 'ID3v2.3', 'ID3v2.2', 'exif', 'asf', 'iTunes', 'AIFF', 'ID3v1'];
|
|
11
11
|
/**
|
|
@@ -187,7 +187,7 @@ export class MetadataCollector {
|
|
|
187
187
|
break;
|
|
188
188
|
case 'lyrics':
|
|
189
189
|
if (typeof tag.value === 'string') {
|
|
190
|
-
tag.value =
|
|
190
|
+
tag.value = parseLyrics(tag.value);
|
|
191
191
|
}
|
|
192
192
|
break;
|
|
193
193
|
default:
|
package/lib/common/Util.d.ts
CHANGED
|
@@ -51,3 +51,8 @@ export declare function dbToRatio(dB: number): number;
|
|
|
51
51
|
* @param value string holding a ratio like '0.034' or '-7.54 dB'
|
|
52
52
|
*/
|
|
53
53
|
export declare function toRatio(value: string): IRatio | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Decode a big-endian unsigned integer from a Uint8Array.
|
|
56
|
+
* Supports dynamic length (1–8 bytes).
|
|
57
|
+
*/
|
|
58
|
+
export declare function decodeUintBE(uint8Array: Uint8Array): number;
|
package/lib/common/Util.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { StringType } from 'token-types';
|
|
2
2
|
import { FieldDecodingError } from '../ParseError.js';
|
|
3
|
+
import { getUintBE } from 'uint8array-extras';
|
|
3
4
|
export function getBit(buf, off, bit) {
|
|
4
5
|
return (buf[off] & (1 << bit)) !== 0;
|
|
5
6
|
}
|
|
@@ -140,3 +141,14 @@ export function toRatio(value) {
|
|
|
140
141
|
};
|
|
141
142
|
}
|
|
142
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Decode a big-endian unsigned integer from a Uint8Array.
|
|
146
|
+
* Supports dynamic length (1–8 bytes).
|
|
147
|
+
*/
|
|
148
|
+
export function decodeUintBE(uint8Array) {
|
|
149
|
+
if (uint8Array.length === 0) {
|
|
150
|
+
throw new Error("decodeUintBE: empty Uint8Array");
|
|
151
|
+
}
|
|
152
|
+
const view = new DataView(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength);
|
|
153
|
+
return getUintBE(view);
|
|
154
|
+
}
|
package/lib/flac/FlacParser.js
CHANGED
|
@@ -96,13 +96,21 @@ export class FlacParser extends AbstractID3Parser {
|
|
|
96
96
|
*/
|
|
97
97
|
async parseComment(data) {
|
|
98
98
|
const decoder = new VorbisDecoder(data, 0);
|
|
99
|
-
decoder.readStringUtf8();
|
|
99
|
+
const vendor = decoder.readStringUtf8();
|
|
100
|
+
if (vendor.length > 0) {
|
|
101
|
+
this.metadata.setFormat('tool', vendor);
|
|
102
|
+
}
|
|
100
103
|
const commentListLength = decoder.readInt32();
|
|
101
104
|
const tags = new Array(commentListLength);
|
|
102
105
|
for (let i = 0; i < commentListLength; i++) {
|
|
103
106
|
tags[i] = decoder.parseUserComment();
|
|
104
107
|
}
|
|
105
|
-
await Promise.all(tags.map(tag =>
|
|
108
|
+
await Promise.all(tags.map(tag => {
|
|
109
|
+
if (tag.key === 'ENCODER') {
|
|
110
|
+
this.metadata.setFormat('tool', tag.value);
|
|
111
|
+
}
|
|
112
|
+
return this.addTag(tag.key, tag.value);
|
|
113
|
+
}));
|
|
106
114
|
}
|
|
107
115
|
async parsePicture(dataLen) {
|
|
108
116
|
if (this.options.skipCovers) {
|
package/lib/id3v2/FrameParser.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as util from '../common/Util.js';
|
|
|
4
4
|
import { AttachedPictureType, SyncTextHeader, TextEncodingToken, TextHeader } from './ID3v2Token.js';
|
|
5
5
|
import { Genres } from '../id3v1/ID3v1Parser.js';
|
|
6
6
|
import { makeUnexpectedFileContentError } from '../ParseError.js';
|
|
7
|
+
import { decodeUintBE } from '../common/Util.js';
|
|
7
8
|
const debug = initDebug('music-metadata:id3v2:frame-parser');
|
|
8
9
|
const defaultEnc = 'latin1'; // latin1 == iso-8859-1;
|
|
9
10
|
export function parseGenre(origVal) {
|
|
@@ -54,7 +55,7 @@ function parseGenreCode(code) {
|
|
|
54
55
|
if (code === 'CR')
|
|
55
56
|
return 'Cover';
|
|
56
57
|
if (code.match(/^\d*$/)) {
|
|
57
|
-
return Genres[Number.parseInt(code)];
|
|
58
|
+
return Genres[Number.parseInt(code, 10)];
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
export class FrameParser {
|
|
@@ -89,7 +90,7 @@ export class FrameParser {
|
|
|
89
90
|
case 'PCST': {
|
|
90
91
|
let text;
|
|
91
92
|
try {
|
|
92
|
-
text = util.decodeString(uint8Array.
|
|
93
|
+
text = util.decodeString(uint8Array.subarray(1), encoding).replace(/\x00+$/, '');
|
|
93
94
|
}
|
|
94
95
|
catch (error) {
|
|
95
96
|
if (error instanceof Error) {
|
|
@@ -149,13 +150,13 @@ export class FrameParser {
|
|
|
149
150
|
offset += 1;
|
|
150
151
|
switch (this.major) {
|
|
151
152
|
case 2:
|
|
152
|
-
pic.format = util.decodeString(uint8Array.
|
|
153
|
+
pic.format = util.decodeString(uint8Array.subarray(offset, offset + 3), 'latin1'); // 'latin1'; // latin1 == iso-8859-1;
|
|
153
154
|
offset += 3;
|
|
154
155
|
break;
|
|
155
156
|
case 3:
|
|
156
157
|
case 4:
|
|
157
158
|
fzero = util.findZero(uint8Array, offset, length, defaultEnc);
|
|
158
|
-
pic.format = util.decodeString(uint8Array.
|
|
159
|
+
pic.format = util.decodeString(uint8Array.subarray(offset, fzero), defaultEnc);
|
|
159
160
|
offset = fzero + 1;
|
|
160
161
|
break;
|
|
161
162
|
default:
|
|
@@ -165,15 +166,15 @@ export class FrameParser {
|
|
|
165
166
|
pic.type = AttachedPictureType[uint8Array[offset]];
|
|
166
167
|
offset += 1;
|
|
167
168
|
fzero = util.findZero(uint8Array, offset, length, encoding);
|
|
168
|
-
pic.description = util.decodeString(uint8Array.
|
|
169
|
+
pic.description = util.decodeString(uint8Array.subarray(offset, fzero), encoding);
|
|
169
170
|
offset = fzero + nullTerminatorLength;
|
|
170
|
-
pic.data = uint8Array.
|
|
171
|
+
pic.data = uint8Array.subarray(offset, length);
|
|
171
172
|
output = pic;
|
|
172
173
|
}
|
|
173
174
|
break;
|
|
174
175
|
case 'CNT':
|
|
175
176
|
case 'PCNT':
|
|
176
|
-
output =
|
|
177
|
+
output = decodeUintBE(uint8Array);
|
|
177
178
|
break;
|
|
178
179
|
case 'SYLT': {
|
|
179
180
|
const syltHeader = SyncTextHeader.get(uint8Array, 0);
|
|
@@ -234,31 +235,31 @@ export class FrameParser {
|
|
|
234
235
|
}
|
|
235
236
|
case 'POPM': { // Popularimeter
|
|
236
237
|
fzero = util.findZero(uint8Array, offset, length, defaultEnc);
|
|
237
|
-
const email = util.decodeString(uint8Array.
|
|
238
|
+
const email = util.decodeString(uint8Array.subarray(offset, fzero), defaultEnc);
|
|
238
239
|
offset = fzero + 1;
|
|
239
|
-
const
|
|
240
|
+
const valueLen = length - offset - 1;
|
|
240
241
|
output = {
|
|
241
242
|
email,
|
|
242
243
|
rating: Token.UINT8.get(uint8Array, offset),
|
|
243
|
-
counter:
|
|
244
|
+
counter: valueLen > 0 ? util.decodeUintBE(uint8Array.subarray(offset + 1)) : undefined
|
|
244
245
|
};
|
|
245
246
|
break;
|
|
246
247
|
}
|
|
247
248
|
case 'GEOB': { // General encapsulated object
|
|
248
249
|
fzero = util.findZero(uint8Array, offset + 1, length, encoding);
|
|
249
|
-
const mimeType = util.decodeString(uint8Array.
|
|
250
|
+
const mimeType = util.decodeString(uint8Array.subarray(offset + 1, fzero), defaultEnc);
|
|
250
251
|
offset = fzero + 1;
|
|
251
252
|
fzero = util.findZero(uint8Array, offset, length, encoding);
|
|
252
|
-
const filename = util.decodeString(uint8Array.
|
|
253
|
+
const filename = util.decodeString(uint8Array.subarray(offset, fzero), defaultEnc);
|
|
253
254
|
offset = fzero + 1;
|
|
254
255
|
fzero = util.findZero(uint8Array, offset, length, encoding);
|
|
255
|
-
const description = util.decodeString(uint8Array.
|
|
256
|
+
const description = util.decodeString(uint8Array.subarray(offset, fzero), defaultEnc);
|
|
256
257
|
offset = fzero + 1;
|
|
257
258
|
const geob = {
|
|
258
259
|
type: mimeType,
|
|
259
260
|
filename,
|
|
260
261
|
description,
|
|
261
|
-
data: uint8Array.
|
|
262
|
+
data: uint8Array.subarray(offset, length)
|
|
262
263
|
};
|
|
263
264
|
output = geob;
|
|
264
265
|
break;
|
|
@@ -274,23 +275,23 @@ export class FrameParser {
|
|
|
274
275
|
case 'WPUB':
|
|
275
276
|
// Decode URL
|
|
276
277
|
fzero = util.findZero(uint8Array, offset + 1, length, encoding);
|
|
277
|
-
output = util.decodeString(uint8Array.
|
|
278
|
+
output = util.decodeString(uint8Array.subarray(offset, fzero), defaultEnc);
|
|
278
279
|
break;
|
|
279
280
|
case 'WXXX': {
|
|
280
281
|
// Decode URL
|
|
281
282
|
fzero = util.findZero(uint8Array, offset + 1, length, encoding);
|
|
282
|
-
const description = util.decodeString(uint8Array.
|
|
283
|
+
const description = util.decodeString(uint8Array.subarray(offset + 1, fzero), encoding);
|
|
283
284
|
offset = fzero + (encoding === 'utf-16le' ? 2 : 1);
|
|
284
|
-
output = { description, url: util.decodeString(uint8Array.
|
|
285
|
+
output = { description, url: util.decodeString(uint8Array.subarray(offset, length), defaultEnc) };
|
|
285
286
|
break;
|
|
286
287
|
}
|
|
287
288
|
case 'WFD':
|
|
288
289
|
case 'WFED':
|
|
289
|
-
output = util.decodeString(uint8Array.
|
|
290
|
+
output = util.decodeString(uint8Array.subarray(offset + 1, util.findZero(uint8Array, offset + 1, length, encoding)), encoding);
|
|
290
291
|
break;
|
|
291
292
|
case 'MCDI': {
|
|
292
293
|
// Music CD identifier
|
|
293
|
-
output = uint8Array.
|
|
294
|
+
output = uint8Array.subarray(0, length);
|
|
294
295
|
break;
|
|
295
296
|
}
|
|
296
297
|
default:
|
|
@@ -302,7 +303,7 @@ export class FrameParser {
|
|
|
302
303
|
static readNullTerminatedString(uint8Array, encoding) {
|
|
303
304
|
let offset = encoding.bom ? 2 : 0;
|
|
304
305
|
const zeroIndex = util.findZero(uint8Array, offset, uint8Array.length, encoding.encoding);
|
|
305
|
-
const txt = uint8Array.
|
|
306
|
+
const txt = uint8Array.subarray(offset, zeroIndex);
|
|
306
307
|
if (encoding.encoding === 'utf-16le') {
|
|
307
308
|
offset = zeroIndex + 2;
|
|
308
309
|
}
|
|
@@ -364,9 +365,9 @@ export class FrameParser {
|
|
|
364
365
|
}
|
|
365
366
|
static readIdentifierAndData(uint8Array, offset, length, encoding) {
|
|
366
367
|
const fzero = util.findZero(uint8Array, offset, length, encoding);
|
|
367
|
-
const id = util.decodeString(uint8Array.
|
|
368
|
+
const id = util.decodeString(uint8Array.subarray(offset, fzero), encoding);
|
|
368
369
|
offset = fzero + FrameParser.getNullTerminatorLength(encoding);
|
|
369
|
-
return { id, data: uint8Array.
|
|
370
|
+
return { id, data: uint8Array.subarray(offset, length) };
|
|
370
371
|
}
|
|
371
372
|
static getNullTerminatorLength(enc) {
|
|
372
373
|
return enc === 'utf-16le' ? 2 : 1;
|
|
@@ -137,7 +137,8 @@ const id3v24TagMap = {
|
|
|
137
137
|
TGID: 'podcastId',
|
|
138
138
|
TKWD: 'keywords',
|
|
139
139
|
WFED: 'podcasturl',
|
|
140
|
-
GRP1: 'grouping'
|
|
140
|
+
GRP1: 'grouping',
|
|
141
|
+
PCNT: 'playCounter',
|
|
141
142
|
};
|
|
142
143
|
export class ID3v24TagMapper extends CaseInsensitiveTagMap {
|
|
143
144
|
static toRating(popm) {
|
package/lib/id3v2/ID3v2Parser.js
CHANGED
|
@@ -24,7 +24,7 @@ export class ID3v2Parser {
|
|
|
24
24
|
if (readI < buffer.length) {
|
|
25
25
|
buffer[writeI++] = buffer[readI];
|
|
26
26
|
}
|
|
27
|
-
return buffer.
|
|
27
|
+
return buffer.subarray(0, writeI);
|
|
28
28
|
}
|
|
29
29
|
static getFrameHeaderLength(majorVer) {
|
|
30
30
|
switch (majorVer) {
|
|
@@ -64,7 +64,7 @@ export class ID3v2Parser {
|
|
|
64
64
|
uint8Array = ID3v2Parser.removeUnsyncBytes(uint8Array);
|
|
65
65
|
}
|
|
66
66
|
if (frameHeader.flags?.format.data_length_indicator) {
|
|
67
|
-
uint8Array = uint8Array.
|
|
67
|
+
uint8Array = uint8Array.subarray(4, uint8Array.length);
|
|
68
68
|
}
|
|
69
69
|
return frameParser.readData(uint8Array, frameHeader.id, includeCovers);
|
|
70
70
|
default:
|
|
@@ -132,10 +132,10 @@ export class ID3v2Parser {
|
|
|
132
132
|
this.metadata.addWarning('Illegal ID3v2 tag length');
|
|
133
133
|
break;
|
|
134
134
|
}
|
|
135
|
-
const frameHeaderBytes = data.
|
|
135
|
+
const frameHeaderBytes = data.subarray(offset, offset + frameHeaderLength);
|
|
136
136
|
offset += frameHeaderLength;
|
|
137
137
|
const frameHeader = this.readFrameHeader(frameHeaderBytes, this.id3Header.version.major);
|
|
138
|
-
const frameDataBytes = data.
|
|
138
|
+
const frameDataBytes = data.subarray(offset, offset + frameHeader.length);
|
|
139
139
|
offset += frameHeader.length;
|
|
140
140
|
const values = ID3v2Parser.readFrameData(frameDataBytes, frameHeader, this.id3Header.version.major, !this.options.skipCovers, this.metadata);
|
|
141
141
|
if (values) {
|
|
@@ -149,7 +149,7 @@ export class ID3v2Parser {
|
|
|
149
149
|
switch (majorVer) {
|
|
150
150
|
case 2:
|
|
151
151
|
header = {
|
|
152
|
-
id: textDecode(uint8Array.
|
|
152
|
+
id: textDecode(uint8Array.subarray(0, 3), 'ascii'),
|
|
153
153
|
length: Token.UINT24_BE.get(uint8Array, 3)
|
|
154
154
|
};
|
|
155
155
|
if (!header.id.match(/[A-Z0-9]{3}/g)) {
|
|
@@ -159,9 +159,9 @@ export class ID3v2Parser {
|
|
|
159
159
|
case 3:
|
|
160
160
|
case 4:
|
|
161
161
|
header = {
|
|
162
|
-
id: textDecode(uint8Array.
|
|
162
|
+
id: textDecode(uint8Array.subarray(0, 4), 'ascii'),
|
|
163
163
|
length: (majorVer === 4 ? UINT32SYNCSAFE : Token.UINT32_BE).get(uint8Array, 4),
|
|
164
|
-
flags: ID3v2Parser.readFrameFlags(uint8Array.
|
|
164
|
+
flags: ID3v2Parser.readFrameFlags(uint8Array.subarray(8, 10))
|
|
165
165
|
};
|
|
166
166
|
if (!header.id.match(/[A-Z0-9]{4}/g)) {
|
|
167
167
|
this.metadata.addWarning(`Invalid ID3v2.${this.id3Header.version.major} frame-header-ID: ${header.id}`);
|
|
@@ -48,7 +48,7 @@ export declare const LyricsContentType: {
|
|
|
48
48
|
};
|
|
49
49
|
export type LyricsContentType = typeof LyricsContentType[keyof typeof LyricsContentType];
|
|
50
50
|
export declare const TimestampFormat: {
|
|
51
|
-
|
|
51
|
+
notSynchronized: number;
|
|
52
52
|
mpegFrameNumber: number;
|
|
53
53
|
milliseconds: number;
|
|
54
54
|
};
|
package/lib/id3v2/ID3v2Token.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type ILyricsTag } from '../type.js';
|
|
2
|
+
export declare function parseLyrics(input: string): ILyricsTag;
|
|
3
|
+
export declare function toUnsyncedLyrics(lyrics: string): ILyricsTag;
|
|
2
4
|
/**
|
|
3
5
|
* Parse LRC (Lyrics) formatted text
|
|
4
6
|
* Ref: https://en.wikipedia.org/wiki/LRC_(file_format)
|
package/lib/lrc/LyricsParser.js
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { LyricsContentType, TimestampFormat } from '../type.js';
|
|
2
|
+
// Shared timestamp regex for LRC format
|
|
3
|
+
const TIMESTAMP_REGEX = /\[(\d{2}):(\d{2})\.(\d{2,3})]/;
|
|
4
|
+
export function parseLyrics(input) {
|
|
5
|
+
if (TIMESTAMP_REGEX.test(input)) {
|
|
6
|
+
return parseLrc(input);
|
|
7
|
+
}
|
|
8
|
+
return toUnsyncedLyrics(input);
|
|
9
|
+
}
|
|
10
|
+
export function toUnsyncedLyrics(lyrics) {
|
|
11
|
+
return {
|
|
12
|
+
contentType: LyricsContentType.lyrics,
|
|
13
|
+
timeStampFormat: TimestampFormat.notSynchronized,
|
|
14
|
+
text: lyrics.trim(),
|
|
15
|
+
syncText: [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
2
18
|
/**
|
|
3
19
|
* Parse LRC (Lyrics) formatted text
|
|
4
20
|
* Ref: https://en.wikipedia.org/wiki/LRC_(file_format)
|
|
@@ -7,34 +23,23 @@ import { LyricsContentType, TimestampFormat } from '../type.js';
|
|
|
7
23
|
export function parseLrc(lrcString) {
|
|
8
24
|
const lines = lrcString.split('\n');
|
|
9
25
|
const syncText = [];
|
|
10
|
-
// Regular expression to match LRC timestamps (e.g., [00:45.52] or [00:45.520])
|
|
11
|
-
const timestampRegex = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/;
|
|
12
26
|
for (const line of lines) {
|
|
13
|
-
const match = line.match(
|
|
27
|
+
const match = line.match(TIMESTAMP_REGEX);
|
|
14
28
|
if (match) {
|
|
15
29
|
const minutes = Number.parseInt(match[1], 10);
|
|
16
30
|
const seconds = Number.parseInt(match[2], 10);
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
// (e.g., .52 = 520 millseconds)
|
|
25
|
-
milliseconds = Number.parseInt(millisecondsStr, 10) * 10;
|
|
26
|
-
}
|
|
27
|
-
// Convert the timestamp to milliseconds, as per TimestampFormat.milliseconds
|
|
28
|
-
const timestamp = (minutes * 60 + seconds) * 1000 + milliseconds;
|
|
29
|
-
// Get the text portion of the line (e.g., "あの蝶は自由になれたかな")
|
|
30
|
-
const text = line.replace(timestampRegex, '').trim();
|
|
31
|
+
const ms = match[3].length === 3
|
|
32
|
+
? Number.parseInt(match[3], 10)
|
|
33
|
+
: Number.parseInt(match[3], 10) * 10;
|
|
34
|
+
const timestamp = (minutes * 60 + seconds) * 1000 + ms;
|
|
35
|
+
const text = line.replace(TIMESTAMP_REGEX, '').trim();
|
|
31
36
|
syncText.push({ timestamp, text });
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
|
-
// Creating the ILyricsTag object
|
|
35
39
|
return {
|
|
36
40
|
contentType: LyricsContentType.lyrics,
|
|
37
41
|
timeStampFormat: TimestampFormat.milliseconds,
|
|
42
|
+
text: syncText.map(line => line.text).join('\n'),
|
|
38
43
|
syncText,
|
|
39
44
|
};
|
|
40
45
|
}
|
package/lib/lyrics3/Lyrics3.js
CHANGED
|
@@ -8,9 +8,9 @@ export async function getLyricsHeaderLength(tokenizer) {
|
|
|
8
8
|
await tokenizer.readBuffer(buf, { position: fileSize - 143 });
|
|
9
9
|
tokenizer.setPosition(position); // Restore position
|
|
10
10
|
const txt = textDecode(buf, 'latin1');
|
|
11
|
-
const tag = txt.
|
|
11
|
+
const tag = txt.substring(6);
|
|
12
12
|
if (tag === endTag2) {
|
|
13
|
-
return Number.parseInt(txt.
|
|
13
|
+
return Number.parseInt(txt.substring(0, 6), 10) + 15;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
return 0;
|
package/lib/mp4/MP4Parser.js
CHANGED
package/lib/mp4/Mp4Loader.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const mp4ParserLoader = {
|
|
2
2
|
parserType: 'mp4',
|
|
3
|
-
extensions: ['.mp4', '.m4a', '.m4b', '.m4pa', 'm4v', 'm4r', '3gp'],
|
|
4
|
-
mimeTypes: ['audio/mp4', 'audio/m4a', 'video/m4v', 'video/mp4'],
|
|
3
|
+
extensions: ['.mp4', '.m4a', '.m4b', '.m4pa', 'm4v', 'm4r', '3gp', '.mov', '.movie', '.qt'],
|
|
4
|
+
mimeTypes: ['audio/mp4', 'audio/m4a', 'video/m4v', 'video/mp4', 'video/quicktime'],
|
|
5
5
|
async load() {
|
|
6
6
|
return (await import('./MP4Parser.js')).MP4Parser;
|
|
7
7
|
}
|
package/lib/mpeg/MpegParser.js
CHANGED
|
@@ -406,7 +406,7 @@ export class MpegParser extends AbstractID3Parser {
|
|
|
406
406
|
await this.skipSideInformation();
|
|
407
407
|
return false;
|
|
408
408
|
}
|
|
409
|
-
if (this.frameCount ===
|
|
409
|
+
if (this.frameCount === 4) {
|
|
410
410
|
// the stream is CBR if the first 3 frame bitrates are the same
|
|
411
411
|
if (this.areAllSame(this.bitrates)) {
|
|
412
412
|
// Actual calculation will be done in finalize
|
package/lib/ogg/OggParser.js
CHANGED
|
@@ -29,7 +29,7 @@ class OggStream {
|
|
|
29
29
|
debug('firstPage=%s, lastPage=%s, continued=%s', header.headerType.firstPage, header.headerType.lastPage, header.headerType.continued);
|
|
30
30
|
if (header.headerType.firstPage) {
|
|
31
31
|
this.metadata.setFormat('container', 'Ogg');
|
|
32
|
-
const idData = pageData.
|
|
32
|
+
const idData = pageData.subarray(0, 7); // Copy this portion
|
|
33
33
|
const asciiId = Array.from(idData)
|
|
34
34
|
.filter(b => b >= 32 && b <= 126) // Keep only printable ASCII
|
|
35
35
|
.map(b => String.fromCharCode(b))
|
package/lib/ogg/vorbis/Vorbis.js
CHANGED
|
@@ -38,7 +38,7 @@ export class VorbisPictureToken {
|
|
|
38
38
|
offset += 4;
|
|
39
39
|
const picDataLen = Token.UINT32_BE.get(buffer, offset);
|
|
40
40
|
offset += 4;
|
|
41
|
-
const data =
|
|
41
|
+
const data = buffer.slice(offset, offset + picDataLen);
|
|
42
42
|
return {
|
|
43
43
|
type,
|
|
44
44
|
format,
|
|
@@ -21,8 +21,8 @@ export class VorbisDecoder {
|
|
|
21
21
|
const v = this.readStringUtf8();
|
|
22
22
|
const idx = v.indexOf('=');
|
|
23
23
|
return {
|
|
24
|
-
key: v.
|
|
25
|
-
value: v.
|
|
24
|
+
key: v.substring(0, idx).toUpperCase(),
|
|
25
|
+
value: v.substring(idx + 1),
|
|
26
26
|
len: this.offset - offset0
|
|
27
27
|
};
|
|
28
28
|
}
|
package/lib/type.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "music-metadata",
|
|
3
3
|
"description": "Music metadata parser for Node.js, supporting virtual any audio and tag format.",
|
|
4
|
-
"version": "11.
|
|
4
|
+
"version": "11.10.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Borewit",
|
|
7
7
|
"url": "https://github.com/Borewit"
|
|
@@ -109,33 +109,33 @@
|
|
|
109
109
|
"@borewit/text-codec": "^0.2.0",
|
|
110
110
|
"@tokenizer/token": "^0.3.0",
|
|
111
111
|
"content-type": "^1.0.5",
|
|
112
|
-
"debug": "^4.4.
|
|
112
|
+
"debug": "^4.4.3",
|
|
113
113
|
"file-type": "^21.0.0",
|
|
114
114
|
"media-typer": "^1.1.0",
|
|
115
115
|
"strtok3": "^10.3.4",
|
|
116
116
|
"token-types": "^6.1.1",
|
|
117
|
-
"uint8array-extras": "^1.
|
|
117
|
+
"uint8array-extras": "^1.5.0"
|
|
118
118
|
},
|
|
119
119
|
"devDependencies": {
|
|
120
|
-
"@biomejs/biome": "2.
|
|
120
|
+
"@biomejs/biome": "2.3.4",
|
|
121
121
|
"@types/chai": "^5.2.2",
|
|
122
122
|
"@types/chai-as-promised": "^8.0.2",
|
|
123
123
|
"@types/content-type": "^1.1.9",
|
|
124
124
|
"@types/debug": "^4.1.12",
|
|
125
125
|
"@types/media-typer": "^1.1.3",
|
|
126
126
|
"@types/mocha": "^10.0.10",
|
|
127
|
-
"@types/node": "^24.
|
|
127
|
+
"@types/node": "^24.5.0",
|
|
128
128
|
"c8": "^10.1.3",
|
|
129
|
-
"chai": "^
|
|
130
|
-
"chai-as-promised": "^8.0.
|
|
131
|
-
"del-cli": "^
|
|
132
|
-
"mime": "^4.0
|
|
133
|
-
"mocha": "^11.7.
|
|
129
|
+
"chai": "^6.2.0",
|
|
130
|
+
"chai-as-promised": "^8.0.2",
|
|
131
|
+
"del-cli": "^7.0.0",
|
|
132
|
+
"mime": "^4.1.0",
|
|
133
|
+
"mocha": "^11.7.5",
|
|
134
134
|
"node-readable-to-web-readable-stream": "^0.4.2",
|
|
135
135
|
"remark-cli": "^12.0.1",
|
|
136
136
|
"remark-preset-lint-consistent": "^6.0.1",
|
|
137
137
|
"ts-node": "^10.9.2",
|
|
138
|
-
"typescript": "^5.9.
|
|
138
|
+
"typescript": "^5.9.3"
|
|
139
139
|
},
|
|
140
140
|
"engines": {
|
|
141
141
|
"node": ">=18"
|