music-metadata 10.5.0 → 10.6.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
@@ -26,7 +26,7 @@ Module: version 8 migrated from [CommonJS](https://en.wikipedia.org/wiki/CommonJ
26
26
  The distributed JavaScript codebase is compliant with the [ECMAScript 2020 (11th Edition)](https://en.wikipedia.org/wiki/ECMAScript_version_history#11th_Edition_%E2%80%93_ECMAScript_2020) standard.
27
27
 
28
28
  > [!NOTE]
29
- > See also [CommonJS Backward Compatibility](#commonjs-backward-compatibility)
29
+ > See also [CommonJS backward Compatibility](#commonjs-backward-compatibility)
30
30
 
31
31
  This module requires a [Node.js ≥ 16](https://nodejs.org/en/about/previous-releases) engine.
32
32
  It can also be used in a browser environment when bundled with a module bundler.
@@ -81,8 +81,13 @@ Following tag header formats are supported:
81
81
  - [RIFF](https://wikipedia.org/wiki/Resource_Interchange_File_Format)/INFO
82
82
  - [Vorbis comment](https://wikipedia.org/wiki/Vorbis_comment)
83
83
  - [AIFF](https://wikipedia.org/wiki/Audio_Interchange_File_Format)
84
-
85
- It allows many tags to be accessed in audio format, and tag format independent way.
84
+
85
+ Following lyric formats are supported:
86
+ - [LRC](https://en.wikipedia.org/wiki/LRC_(file_format))
87
+ - Synchronized lyrics (SYLT)
88
+ - Unsynchronized lyrics (USULT)
89
+ [
90
+ It allows many tags to be]() accessed in audio format, and tag format independent way.
86
91
 
87
92
  Support for [MusicBrainz](https://musicbrainz.org/) tags as written by [Picard](https://picard.musicbrainz.org/).
88
93
  [ReplayGain](https://wiki.hydrogenaud.io/index.php?title=ReplayGain) tags are supported.
@@ -768,7 +773,7 @@ Dependency list:
768
773
  - [@tokenizer-token](https://github.com/Borewit/tokenizer-token)
769
774
  - [peek-readable](https://github.com/Borewit/peek-readable)
770
775
 
771
- ## CommonJS Backward compatibility
776
+ ## CommonJS backward compatibility
772
777
 
773
778
  For legacy CommonJS projects needing to load the `music-metadata` ESM module, you can use the `loadMusicMetadata` function:
774
779
  ```js
@@ -310,7 +310,6 @@ export class MetadataObjectState extends State {
310
310
  }
311
311
  MetadataObjectState.guid = GUID.MetadataObject;
312
312
  // 4.8 Metadata Library Object (optional, 0 or 1)
313
- // biome-ignore lint/complexity/noStaticOnlyClass: Extends a non-static class
314
313
  export class MetadataLibraryObjectState extends MetadataObjectState {
315
314
  }
316
315
  MetadataLibraryObjectState.guid = GUID.MetadataLibraryObject;
@@ -5,6 +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
9
  const debug = initDebug('music-metadata:collector');
9
10
  const TagPriority = ['matroska', 'APEv2', 'vorbis', 'ID3v2.4', 'ID3v2.3', 'ID3v2.2', 'exif', 'asf', 'iTunes', 'AIFF', 'ID3v1'];
10
11
  /**
@@ -180,6 +181,11 @@ export class MetadataCollector {
180
181
  this.setGenericTag(tagType, { id: 'gapless', value: tag.value.text === '1' });
181
182
  }
182
183
  break;
184
+ case 'lyrics':
185
+ if (typeof tag.value === 'string') {
186
+ tag.value = parseLrc(tag.value);
187
+ }
188
+ break;
183
189
  default:
184
190
  // nothing to do
185
191
  }
@@ -1,6 +1,6 @@
1
1
  import type { IGetToken } from 'strtok3';
2
2
  import type { IChunkHeader64 } from '../iff/index.js';
3
- export { type IChunkHeader64 } from '../iff/index.js';
3
+ export type { IChunkHeader64 } from '../iff/index.js';
4
4
  /**
5
5
  * DSDIFF chunk header
6
6
  * The data-size encoding is deviating from EA-IFF 85
@@ -248,16 +248,17 @@ export class FrameParser {
248
248
  fzero = util.findZero(uint8Array, offset + 1, length, encoding);
249
249
  const mimeType = util.decodeString(uint8Array.slice(offset + 1, fzero), defaultEnc);
250
250
  offset = fzero + 1;
251
- fzero = util.findZero(uint8Array, offset, length - offset, encoding);
251
+ fzero = util.findZero(uint8Array, offset, length, encoding);
252
252
  const filename = util.decodeString(uint8Array.slice(offset, fzero), defaultEnc);
253
253
  offset = fzero + 1;
254
- fzero = util.findZero(uint8Array, offset, length - offset, encoding);
254
+ fzero = util.findZero(uint8Array, offset, length, encoding);
255
255
  const description = util.decodeString(uint8Array.slice(offset, fzero), defaultEnc);
256
+ offset = fzero + 1;
256
257
  const geob = {
257
258
  type: mimeType,
258
259
  filename,
259
260
  description,
260
- data: uint8Array.slice(offset + 1, length)
261
+ data: uint8Array.slice(offset, length)
261
262
  };
262
263
  output = geob;
263
264
  break;
@@ -0,0 +1,7 @@
1
+ import { type ILyricsTag } from '../type.js';
2
+ /**
3
+ * Parse LRC (Lyrics) formatted text
4
+ * Ref: https://en.wikipedia.org/wiki/LRC_(file_format)
5
+ * @param lrcString
6
+ */
7
+ export declare function parseLrc(lrcString: string): ILyricsTag;
@@ -0,0 +1,32 @@
1
+ import { LyricsContentType, TimestampFormat } from '../type.js';
2
+ /**
3
+ * Parse LRC (Lyrics) formatted text
4
+ * Ref: https://en.wikipedia.org/wiki/LRC_(file_format)
5
+ * @param lrcString
6
+ */
7
+ export function parseLrc(lrcString) {
8
+ const lines = lrcString.split('\n');
9
+ const syncText = [];
10
+ // Regular expression to match LRC timestamps (e.g., [00:45.52])
11
+ const timestampRegex = /\[(\d{2}):(\d{2})\.(\d{2})\]/;
12
+ for (const line of lines) {
13
+ const match = line.match(timestampRegex);
14
+ if (match) {
15
+ const minutes = Number.parseInt(match[1], 10);
16
+ const seconds = Number.parseInt(match[2], 10);
17
+ const hundredths = Number.parseInt(match[3], 10);
18
+ // Convert the timestamp to milliseconds, as per TimestampFormat.milliseconds
19
+ const timestamp = (minutes * 60 + seconds) * 1000 + hundredths * 10;
20
+ // Get the text portion of the line (e.g., "あの蝶は自由になれたかな")
21
+ const text = line.replace(timestampRegex, '').trim();
22
+ syncText.push({ timestamp, text });
23
+ }
24
+ }
25
+ // Creating the ILyricsTag object
26
+ return {
27
+ contentType: LyricsContentType.lyrics,
28
+ timeStampFormat: TimestampFormat.milliseconds,
29
+ syncText,
30
+ };
31
+ }
32
+ //# sourceMappingURL=LyricsParser.js.map
@@ -296,12 +296,12 @@ export class MpegParser extends AbstractID3Parser {
296
296
  this.metadata.setFormat('bitrate', mpegSize * 8 / format.duration);
297
297
  }
298
298
  }
299
- else if (this.tokenizer.fileInfo.size && format.codecProfile === 'CBR') {
299
+ if (this.tokenizer.fileInfo.size && format.codecProfile === 'CBR') {
300
300
  const mpegSize = this.tokenizer.fileInfo.size - this.mpegOffset - (hasID3v1 ? 128 : 0);
301
301
  if (this.frame_size !== null && this.samplesPerFrame !== null) {
302
302
  const numberOfSamples = Math.round(mpegSize / this.frame_size) * this.samplesPerFrame;
303
303
  this.metadata.setFormat('numberOfSamples', numberOfSamples);
304
- if (format.sampleRate) {
304
+ if (format.sampleRate && !format.duration) {
305
305
  const duration = numberOfSamples / format.sampleRate;
306
306
  debug("Calculate CBR duration based on file size: %s", duration);
307
307
  this.metadata.setFormat('duration', duration);
@@ -1,6 +1,6 @@
1
1
  import type { IGetToken } from 'strtok3';
2
2
  import type { IChunkHeader } from '../iff/index.js';
3
- export { type IChunkHeader } from '../iff/index.js';
3
+ export type { IChunkHeader } from '../iff/index.js';
4
4
  /**
5
5
  * Common RIFF chunk header
6
6
  */
package/lib/type.d.ts CHANGED
@@ -614,7 +614,7 @@ export interface IRandomReader {
614
614
  */
615
615
  randomRead(buffer: Uint8Array, offset: number, length: number, position: number): Promise<number>;
616
616
  }
617
- interface ILyricsText {
617
+ export interface ILyricsText {
618
618
  text: string;
619
619
  timestamp?: number;
620
620
  }
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": "10.5.0",
4
+ "version": "10.6.0",
5
5
  "author": {
6
6
  "name": "Borewit",
7
7
  "url": "https://github.com/Borewit"
@@ -85,7 +85,9 @@
85
85
  "info",
86
86
  "parse",
87
87
  "parser",
88
- "bwf"
88
+ "bwf",
89
+ "slt",
90
+ "lyrics"
89
91
  ],
90
92
  "scripts": {
91
93
  "clean": "del-cli 'lib/**/*.js' 'lib/**/*.js.map' 'lib/**/*.d.ts' 'src/**/*.d.ts' 'test/**/*.js' 'test/**/*.js.map' 'test/**/*.js' 'test/**/*.js.map' 'doc-gen/**/*.js' 'doc-gen/**/*.js.map'",
@@ -106,32 +108,32 @@
106
108
  "@tokenizer/token": "^0.3.0",
107
109
  "content-type": "^1.0.5",
108
110
  "debug": "^4.3.7",
109
- "file-type": "^19.5.0",
111
+ "file-type": "^19.6.0",
110
112
  "media-typer": "^1.1.0",
111
- "strtok3": "^9.0.0",
113
+ "strtok3": "^9.0.1",
112
114
  "token-types": "^6.0.0",
113
115
  "uint8array-extras": "^1.4.0"
114
116
  },
115
117
  "devDependencies": {
116
- "@biomejs/biome": "1.8.3",
117
- "@types/chai": "^4.3.19",
118
- "@types/chai-as-promised": "^8.0.0",
118
+ "@biomejs/biome": "1.9.4",
119
+ "@types/chai": "^5.0.1",
120
+ "@types/chai-as-promised": "^8.0.1",
119
121
  "@types/content-type": "^1.1.8",
120
122
  "@types/debug": "^4.1.12",
121
123
  "@types/media-typer": "^1.1.3",
122
- "@types/mocha": "^10.0.7",
123
- "@types/node": "^22.5.4",
124
+ "@types/mocha": "^10.0.9",
125
+ "@types/node": "^22.9.0",
124
126
  "c8": "^10.1.2",
125
- "chai": "^5.1.1",
127
+ "chai": "^5.1.2",
126
128
  "chai-as-promised": "^8.0.0",
127
- "del-cli": "^5.1.0",
129
+ "del-cli": "^6.0.0",
128
130
  "mime": "^4.0.4",
129
- "mocha": "^10.7.3",
131
+ "mocha": "^10.8.2",
130
132
  "prettier": "^3.3.3",
131
133
  "remark-cli": "^12.0.1",
132
134
  "remark-preset-lint-consistent": "^6.0.0",
133
135
  "ts-node": "^10.9.2",
134
- "typescript": "^5.5.3"
136
+ "typescript": "^5.6.3"
135
137
  },
136
138
  "engines": {
137
139
  "node": ">=16.0.0"