music-metadata 11.9.0 → 11.10.1
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/common/MetadataCollector.js +2 -2
- package/lib/id3v2/FrameParser.js +1 -1
- 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/mp4/MP4Parser.js +2 -0
- package/lib/mp4/Mp4Loader.js +2 -2
- package/lib/ogg/OggParser.js +3 -1
- package/lib/ogg/OggToken.d.ts +1 -1
- package/lib/ogg/opus/OpusStream.d.ts +1 -1
- package/lib/ogg/opus/OpusStream.js +2 -2
- package/lib/ogg/vorbis/VorbisStream.d.ts +1 -1
- package/lib/ogg/vorbis/VorbisStream.js +1 -1
- package/package.json +8 -8
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.
|
|
@@ -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/id3v2/FrameParser.js
CHANGED
|
@@ -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/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/ogg/OggParser.js
CHANGED
|
@@ -84,6 +84,7 @@ export class OggParser extends BasicParser {
|
|
|
84
84
|
*/
|
|
85
85
|
async parse() {
|
|
86
86
|
this.streams = new Map();
|
|
87
|
+
let enfOfStream = false;
|
|
87
88
|
let header;
|
|
88
89
|
try {
|
|
89
90
|
do {
|
|
@@ -105,6 +106,7 @@ export class OggParser extends BasicParser {
|
|
|
105
106
|
catch (err) {
|
|
106
107
|
if (err instanceof EndOfStreamError) {
|
|
107
108
|
debug("Reached end-of-stream");
|
|
109
|
+
enfOfStream = true;
|
|
108
110
|
}
|
|
109
111
|
else if (err instanceof OggContentError) {
|
|
110
112
|
this.metadata.addWarning(`Corrupt Ogg content at ${this.tokenizer.position}`);
|
|
@@ -117,7 +119,7 @@ export class OggParser extends BasicParser {
|
|
|
117
119
|
this.metadata.addWarning(`End-of-stream reached before reaching last page in Ogg stream serial=${stream.streamSerial}`);
|
|
118
120
|
await stream.pageConsumer?.flush();
|
|
119
121
|
}
|
|
120
|
-
stream.pageConsumer?.calculateDuration();
|
|
122
|
+
stream.pageConsumer?.calculateDuration(enfOfStream);
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
125
|
}
|
package/lib/ogg/OggToken.d.ts
CHANGED
|
@@ -20,5 +20,5 @@ export declare class OpusStream extends VorbisStream {
|
|
|
20
20
|
*/
|
|
21
21
|
protected parseFirstPage(_header: IPageHeader, pageData: Uint8Array): void;
|
|
22
22
|
protected parseFullPage(pageData: Uint8Array): Promise<void>;
|
|
23
|
-
calculateDuration(): void;
|
|
23
|
+
calculateDuration(enfOfStream: boolean): void;
|
|
24
24
|
}
|
|
@@ -41,8 +41,8 @@ export class OpusStream extends VorbisStream {
|
|
|
41
41
|
break;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
calculateDuration() {
|
|
45
|
-
if (this.lastPageHeader && this.metadata.format.sampleRate && this.lastPageHeader.absoluteGranulePosition >= 0) {
|
|
44
|
+
calculateDuration(enfOfStream) {
|
|
45
|
+
if (this.lastPageHeader && (enfOfStream || this.lastPageHeader.headerType.lastPage) && this.metadata.format.sampleRate && this.lastPageHeader.absoluteGranulePosition >= 0) {
|
|
46
46
|
// Calculate duration
|
|
47
47
|
const pos_48bit = this.lastPageHeader.absoluteGranulePosition - this.idHeader.preSkip;
|
|
48
48
|
this.metadata.setFormat('numberOfSamples', pos_48bit);
|
|
@@ -37,7 +37,7 @@ export declare class VorbisStream implements IPageConsumer {
|
|
|
37
37
|
flush(): Promise<void>;
|
|
38
38
|
parseUserComment(pageData: Uint8Array, offset: number): Promise<number>;
|
|
39
39
|
addTag(id: string, value: string | IVorbisPicture): Promise<void>;
|
|
40
|
-
calculateDuration(): void;
|
|
40
|
+
calculateDuration(enfOfStream: boolean): void;
|
|
41
41
|
/**
|
|
42
42
|
* Parse first Ogg/Vorbis page
|
|
43
43
|
* @param _header
|
|
@@ -77,7 +77,7 @@ export class VorbisStream {
|
|
|
77
77
|
}
|
|
78
78
|
await this.metadata.addTag('vorbis', id, value);
|
|
79
79
|
}
|
|
80
|
-
calculateDuration() {
|
|
80
|
+
calculateDuration(enfOfStream) {
|
|
81
81
|
if (this.lastPageHeader && this.metadata.format.sampleRate && this.lastPageHeader.absoluteGranulePosition >= 0) {
|
|
82
82
|
// Calculate duration
|
|
83
83
|
this.metadata.setFormat('numberOfSamples', this.lastPageHeader.absoluteGranulePosition);
|
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.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Borewit",
|
|
7
7
|
"url": "https://github.com/Borewit"
|
|
@@ -110,32 +110,32 @@
|
|
|
110
110
|
"@tokenizer/token": "^0.3.0",
|
|
111
111
|
"content-type": "^1.0.5",
|
|
112
112
|
"debug": "^4.4.3",
|
|
113
|
-
"file-type": "^21.
|
|
113
|
+
"file-type": "^21.1.0",
|
|
114
114
|
"media-typer": "^1.1.0",
|
|
115
115
|
"strtok3": "^10.3.4",
|
|
116
116
|
"token-types": "^6.1.1",
|
|
117
117
|
"uint8array-extras": "^1.5.0"
|
|
118
118
|
},
|
|
119
119
|
"devDependencies": {
|
|
120
|
-
"@biomejs/biome": "2.
|
|
120
|
+
"@biomejs/biome": "2.3.5",
|
|
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": "^6.
|
|
129
|
+
"chai": "^6.2.1",
|
|
130
130
|
"chai-as-promised": "^8.0.2",
|
|
131
|
-
"del-cli": "^
|
|
131
|
+
"del-cli": "^7.0.0",
|
|
132
132
|
"mime": "^4.1.0",
|
|
133
|
-
"mocha": "^11.7.
|
|
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"
|