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 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 { parseLrc } from '../lrc/LyricsParser.js';
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 = parseLrc(tag.value);
190
+ tag.value = parseLyrics(tag.value);
191
191
  }
192
192
  break;
193
193
  default:
@@ -55,7 +55,7 @@ function parseGenreCode(code) {
55
55
  if (code === 'CR')
56
56
  return 'Cover';
57
57
  if (code.match(/^\d*$/)) {
58
- return Genres[Number.parseInt(code)];
58
+ return Genres[Number.parseInt(code, 10)];
59
59
  }
60
60
  }
61
61
  export class FrameParser {
@@ -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
- notSynchronized0: number;
51
+ notSynchronized: number;
52
52
  mpegFrameNumber: number;
53
53
  milliseconds: number;
54
54
  };
@@ -40,7 +40,7 @@ export const LyricsContentType = {
40
40
  trivia_pop: 6,
41
41
  };
42
42
  export const TimestampFormat = {
43
- notSynchronized0: 0,
43
+ notSynchronized: 0,
44
44
  mpegFrameNumber: 1,
45
45
  milliseconds: 2
46
46
  };
@@ -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)
@@ -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(timestampRegex);
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 millisecondsStr = match[3];
18
- let milliseconds;
19
- if (millisecondsStr.length === 3) {
20
- // (e.g., .521 = 521 millseconds)
21
- milliseconds = Number.parseInt(millisecondsStr, 10);
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
  }
@@ -300,6 +300,8 @@ export class MP4Parser extends BasicParser {
300
300
  switch (atom.header.name) {
301
301
  case 'trak':
302
302
  return this.parseTrackBox(atom);
303
+ case 'udta':
304
+ return this.parseTrackBox(atom);
303
305
  }
304
306
  break;
305
307
  case 'moof':
@@ -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.9.0",
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.2.4",
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.3.1",
127
+ "@types/node": "^24.5.0",
128
128
  "c8": "^10.1.3",
129
- "chai": "^6.0.1",
129
+ "chai": "^6.2.0",
130
130
  "chai-as-promised": "^8.0.2",
131
- "del-cli": "^6.0.0",
131
+ "del-cli": "^7.0.0",
132
132
  "mime": "^4.1.0",
133
- "mocha": "^11.7.2",
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.2"
138
+ "typescript": "^5.9.3"
139
139
  },
140
140
  "engines": {
141
141
  "node": ">=18"