music-metadata 11.9.0 → 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/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/package.json +7 -7
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/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"
|
|
@@ -117,25 +117,25 @@
|
|
|
117
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": "^6.0
|
|
129
|
+
"chai": "^6.2.0",
|
|
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"
|