music-metadata 11.15.0 → 11.16.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.
Files changed (103) hide show
  1. package/README.md +35 -32
  2. package/lib/ParserFactory.d.ts +1 -1
  3. package/lib/ParserFactory.js +17 -13
  4. package/lib/aiff/AiffParser.js +19 -12
  5. package/lib/aiff/AiffToken.d.ts +1 -1
  6. package/lib/aiff/AiffToken.js +2 -1
  7. package/lib/apev2/APEv2Parser.d.ts +4 -3
  8. package/lib/apev2/APEv2Parser.js +59 -16
  9. package/lib/apev2/APEv2TagMapper.js +1 -0
  10. package/lib/apev2/APEv2Token.d.ts +1 -1
  11. package/lib/apev2/APEv2Token.js +1 -1
  12. package/lib/asf/AsfGuid.js +56 -50
  13. package/lib/asf/AsfObject.d.ts +1 -1
  14. package/lib/asf/AsfObject.js +3 -2
  15. package/lib/asf/AsfParser.js +15 -7
  16. package/lib/asf/AsfTagMapper.d.ts +3 -2
  17. package/lib/asf/AsfTagMapper.js +21 -4
  18. package/lib/common/CaseInsensitiveTagMap.d.ts +1 -1
  19. package/lib/common/CombinedTagMapper.d.ts +2 -2
  20. package/lib/common/CombinedTagMapper.js +6 -6
  21. package/lib/common/FourCC.js +3 -2
  22. package/lib/common/GenericTagMapper.d.ts +1 -1
  23. package/lib/common/GenericTagTypes.js +1 -1
  24. package/lib/common/MetadataCollector.d.ts +3 -1
  25. package/lib/common/MetadataCollector.js +28 -9
  26. package/lib/common/Util.js +25 -17
  27. package/lib/core.d.ts +4 -4
  28. package/lib/core.js +18 -11
  29. package/lib/dsdiff/DsdiffParser.js +29 -16
  30. package/lib/dsf/DsfChunk.d.ts +1 -1
  31. package/lib/dsf/DsfParser.js +3 -5
  32. package/lib/ebml/EbmlIterator.d.ts +1 -1
  33. package/lib/ebml/EbmlIterator.js +35 -6
  34. package/lib/ebml/types.d.ts +1 -1
  35. package/lib/ebml/types.js +1 -1
  36. package/lib/flac/FlacParser.d.ts +1 -1
  37. package/lib/flac/FlacParser.js +6 -5
  38. package/lib/flac/FlacToken.d.ts +1 -1
  39. package/lib/flac/FlacToken.js +1 -1
  40. package/lib/id3v1/ID3v1Parser.d.ts +1 -1
  41. package/lib/id3v1/ID3v1Parser.js +212 -42
  42. package/lib/id3v2/AbstractID3Parser.js +5 -5
  43. package/lib/id3v2/FrameHeader.d.ts +1 -1
  44. package/lib/id3v2/FrameHeader.js +8 -5
  45. package/lib/id3v2/FrameParser.d.ts +2 -1
  46. package/lib/id3v2/FrameParser.js +53 -22
  47. package/lib/id3v2/ID3v22TagMapper.d.ts +1 -1
  48. package/lib/id3v2/ID3v22TagMapper.js +1 -1
  49. package/lib/id3v2/ID3v24TagMapper.js +3 -3
  50. package/lib/id3v2/ID3v2ChapterToken.js +2 -2
  51. package/lib/id3v2/ID3v2Parser.d.ts +1 -1
  52. package/lib/id3v2/ID3v2Parser.js +32 -16
  53. package/lib/id3v2/ID3v2Token.d.ts +2 -2
  54. package/lib/id3v2/ID3v2Token.js +2 -3
  55. package/lib/index.js +6 -3
  56. package/lib/lrc/LyricsParser.js +3 -5
  57. package/lib/matroska/MatroskaDtd.js +7 -3
  58. package/lib/matroska/MatroskaParser.js +22 -11
  59. package/lib/matroska/types.d.ts +2 -2
  60. package/lib/mp4/Atom.d.ts +4 -3
  61. package/lib/mp4/Atom.js +44 -11
  62. package/lib/mp4/AtomToken.d.ts +1 -1
  63. package/lib/mp4/AtomToken.js +30 -8
  64. package/lib/mp4/MP4Parser.d.ts +2 -0
  65. package/lib/mp4/MP4Parser.js +79 -54
  66. package/lib/mp4/MP4TagMapper.d.ts +2 -2
  67. package/lib/mpeg/MpegLoader.js +1 -1
  68. package/lib/mpeg/MpegParser.js +41 -19
  69. package/lib/mpeg/ReplayGainDataFormat.d.ts +2 -2
  70. package/lib/mpeg/ReplayGainDataFormat.js +1 -1
  71. package/lib/mpeg/XingTag.d.ts +1 -1
  72. package/lib/musepack/MusepackParser.js +2 -2
  73. package/lib/musepack/sv7/BitReader.js +1 -1
  74. package/lib/musepack/sv7/MpcSv7Parser.js +4 -3
  75. package/lib/musepack/sv7/StreamVersion7.js +3 -3
  76. package/lib/musepack/sv8/MpcSv8Parser.js +7 -5
  77. package/lib/musepack/sv8/StreamVersion8.js +2 -2
  78. package/lib/ogg/OggParser.js +16 -12
  79. package/lib/ogg/OggToken.js +1 -1
  80. package/lib/ogg/flac/FlacStream.d.ts +2 -2
  81. package/lib/ogg/flac/FlacStream.js +2 -2
  82. package/lib/ogg/opus/OpusStream.d.ts +2 -2
  83. package/lib/ogg/opus/OpusStream.js +8 -4
  84. package/lib/ogg/speex/SpeexStream.d.ts +2 -2
  85. package/lib/ogg/speex/SpeexStream.js +2 -1
  86. package/lib/ogg/theora/TheoraStream.d.ts +2 -2
  87. package/lib/ogg/vorbis/Vorbis.d.ts +1 -1
  88. package/lib/ogg/vorbis/VorbisDecoder.js +1 -1
  89. package/lib/ogg/vorbis/VorbisStream.d.ts +3 -3
  90. package/lib/ogg/vorbis/VorbisStream.js +14 -9
  91. package/lib/ogg/vorbis/VorbisTagMapper.js +4 -0
  92. package/lib/riff/RiffChunk.d.ts +3 -1
  93. package/lib/riff/RiffChunk.js +3 -2
  94. package/lib/riff/RiffInfoTagMap.d.ts +1 -1
  95. package/lib/type.d.ts +3 -3
  96. package/lib/type.js +1 -1
  97. package/lib/wav/WaveChunk.d.ts +1 -1
  98. package/lib/wav/WaveChunk.js +3 -2
  99. package/lib/wav/WaveParser.d.ts +6 -2
  100. package/lib/wav/WaveParser.js +75 -22
  101. package/lib/wavpack/WavPackParser.js +22 -13
  102. package/lib/wavpack/WavPackToken.js +3 -2
  103. package/package.json +3 -2
package/README.md CHANGED
@@ -84,7 +84,7 @@ Following tag header formats are supported:
84
84
  Following lyric formats are supported:
85
85
  - [LRC](https://en.wikipedia.org/wiki/LRC_(file_format))
86
86
  - Synchronized lyrics (SYLT)
87
- - Unsynchronized lyrics (USULT)
87
+ - Unsynchronized lyrics (USLT)
88
88
 
89
89
  Support for [MusicBrainz](https://musicbrainz.org/) tags as written by [Picard](https://picard.musicbrainz.org/).
90
90
  [ReplayGain](https://wiki.hydrogenaud.io/index.php?title=ReplayGain) tags are supported.
@@ -123,7 +123,7 @@ yarn add music-metadata
123
123
 
124
124
  **Node.js specific** functions to read an audio file or stream:
125
125
  1. **File Parsing**: Parse audio files directly from the filesystem using the [parseFile function](#parsefile-function)
126
- 1. **Stream Parsing**: Parse audio metadata from a Node.js [Readable stream](https://nodejs.org/api/stream.html#class-streamreadable) using the [parseStream function](#parsewebstream-function).
126
+ 1. **Stream Parsing**: Parse audio metadata from a Node.js [Readable stream](https://nodejs.org/api/stream.html#class-streamreadable) using the [parseStream function](#parsestream-function).
127
127
 
128
128
  **Cross-platform** functions available to read an audio file or stream:
129
129
 
@@ -136,8 +136,8 @@ There are multiple ways to parse (read) audio tracks:
136
136
  > [!NOTE]
137
137
  > Direct file access in Node.js is generally faster because it can 'jump' to various parts of the file without reading intermediate data.
138
138
 
139
- ### Node.js specific function
140
- These functions are tailored for Node.js environments and leverage Node.js-specific APIs,
139
+ ### Node.js specific functions
140
+ These functions use Node.js-specific APIs,
141
141
  making them incompatible with browser-based JavaScript engines.
142
142
 
143
143
  #### `parseFile` function
@@ -269,7 +269,7 @@ import { createReadStream } from 'fs';
269
269
 
270
270
  ### Cross-platform functions
271
271
  These functions are designed to be cross-platform,
272
- meaning it can be used in both Node.js and web browsers.
272
+ meaning they can be used in both Node.js and web browsers.
273
273
 
274
274
  #### `parseWebStream` function
275
275
 
@@ -295,14 +295,14 @@ parseWebStream(webStream: ReadableStream<Uint8Array>, fileInfo?: IFileInfo | str
295
295
  An object containing file-related information or a string representing the MIME-type of the audio stream.
296
296
  The fileInfo parameter can help the parser to correctly identify the audio format and may include:
297
297
 
298
- - `mimeType`: A string representing the [MIME-type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) (e.g., `audio/mpeg`).
298
+ - `mimeType`: A string representing the [MIME-type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) (e.g., `audio/mpeg`).
299
299
 
300
- If provided, it is assumed the streamed file content is to be the MIME-type.
301
- If not provided, the parser will attempt to determine the format based on the content of the stream.
300
+ If provided, it is assumed the streamed file content is to be the MIME-type.
301
+ If not provided, the parser will attempt to determine the format based on the content of the stream.
302
302
 
303
- - `size`: The total size of the audio stream in bytes (useful for streams with a known length).
303
+ - `size`: The total size of the audio stream in bytes (useful for streams with a known length).
304
304
 
305
- - `path`: A string representing the file path or filename, which can also assist in determining the format.
305
+ - `path`: A string representing the file path or filename, which can also assist in determining the format.
306
306
 
307
307
  - `options`: `IOptions` (optional)
308
308
 
@@ -351,19 +351,19 @@ The `response.body` provides a [`ReadableStream`](https://developer.mozilla.org/
351
351
  #### `parseBlob` function
352
352
 
353
353
  Parses metadata from an audio file represented as a [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob).
354
- This function is suitable for use in environments that support the ReadableStreamBYOBReader, which is **available in Node.js 20** and above.
354
+ This function reads slices of the Blob and supports both browser File objects and Node.js Blob objects.
355
355
 
356
356
  ##### Syntax
357
357
  ```ts
358
- parseBlob(blob: Blob, options?: IOptions = {}): Promise<IAudioMetadata>
358
+ parseBlob(blob: Blob, options?: IOptions): Promise<IAudioMetadata>
359
359
  ```
360
360
 
361
361
  ##### Parameters
362
362
 
363
363
  - `blob`: [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob)
364
364
 
365
- The Blob object containing the audio data to be parsed.
366
- This can be a file or any binary data. If the Blob is an instance of File, its name will be used as the file path in the metadata.
365
+ The Blob object containing the audio data to be parsed.
366
+ This can be a File selected by the user or a Blob containing audio data. The Blob size and MIME type are passed to the parser.
367
367
 
368
368
  - `options`: [IOptions](#ioptions-interface) (optional)
369
369
 
@@ -400,7 +400,7 @@ This function is particularly useful when you already have audio data in memory.
400
400
 
401
401
  ##### Syntax
402
402
  ```ts
403
- parseBuffer(buffer: Uint8Array, fileInfo?: IFileInfo | string, opts?: IOptions = {}): Promise<IAudioMetadata>
403
+ parseBuffer(uint8Array: Uint8Array, fileInfo?: IFileInfo | string, options?: IOptions): Promise<IAudioMetadata>
404
404
  ```
405
405
 
406
406
  ##### Parameters
@@ -518,12 +518,13 @@ readMetadata();
518
518
 
519
519
  ### Handling Parse Errors
520
520
 
521
- `music-metadata` provides a robust and extensible error handling system with custom error classes that inherit from the standard JavaScript `Error`.
522
- All possible parsing errors are part of a union type `UnionOfParseErrors`, ensuring that every error scenario is accounted for in your code.
521
+ `music-metadata` exports custom error classes that extend JavaScript `Error`.
522
+ Parsing can also reject with other errors, such as filesystem, stream, or tokenizer errors.
523
+ Handle caught values as `unknown` in TypeScript and narrow them before accessing error properties.
523
524
 
524
525
  #### Union of Parse Errors
525
526
 
526
- All parsing errors extend from the base class `ParseError` and are included in the `UnionOfParseErrors` type:
527
+ The `UnionOfParseErrors` type groups the following library error classes; it does not describe every possible rejection:
527
528
  ```ts
528
529
  export type UnionOfParseErrors =
529
530
  | CouldNotDetermineFileTypeError
@@ -545,10 +546,10 @@ export type UnionOfParseErrors =
545
546
 
546
547
  #### `orderTags` function
547
548
 
548
- Utility to Converts the native tags to a dictionary index on the tag identifier
549
+ Converts an array of native tags to a dictionary keyed by tag identifier. Each value is an array of tag values.
549
550
 
550
551
  ```ts
551
- orderTags(nativeTags: ITag[]): [tagId: string]: any[]
552
+ orderTags(nativeTags: ITag[]): INativeTagDict
552
553
  ```
553
554
 
554
555
  ```js
@@ -571,7 +572,7 @@ import { inspect } from 'util';
571
572
  Can be used to convert the normalized rating value to the 0..5 stars, where 0 an undefined rating, 1 the star the lowest rating and 5 the highest rating.
572
573
 
573
574
  ```ts
574
- ratingToStars(rating: number): number
575
+ ratingToStars(rating: number | undefined): number
575
576
  ```
576
577
  #### `selectCover` function
577
578
 
@@ -613,7 +614,7 @@ Returns a list of supported MIME-types. This may include some MIME-types which a
613
614
  - Metadata not listed in the SeekHead may be skipped.
614
615
  - If the SeekHead is missing, this option has no effect.
615
616
 
616
- - `observer`: `(update: MetadataEvent) => void;`:
617
+ - `observer`: `(update: IMetadataEvent) => void`:
617
618
 
618
619
  Callback function triggered when common tags or format properties are updated during parsing.
619
620
  Allows real-time monitoring of metadata as it becomes available.
@@ -628,7 +629,7 @@ Returns a list of supported MIME-types. This may include some MIME-types which a
628
629
  This is particularly beneficial for streaming input, as it avoids the need to read the entire stream.
629
630
 
630
631
  > [!NOTE]
631
- > - The `duration` option is typically included in most cases, but setting it to true ensures that the entire file is parsed if necessary to get an accurate duration.
632
+ > - `format.duration` may be available without enabling `duration`. Set `duration: true` to allow additional scanning when needed; duration can still be unavailable.
632
633
  > - Using `mkvUseIndex` can improve performance in Matroska files, but be aware of potential side effects, such as missing metadata due to skipped elements.
633
634
 
634
635
 
@@ -637,8 +638,9 @@ Returns a list of supported MIME-types. This may include some MIME-types which a
637
638
  If the returned promise resolves, the metadata (TypeScript `IAudioMetadata` interface) contains:
638
639
  - [`metadata.format`](#metadataformat) Audio format information
639
640
  - [`metadata.common`](#metadatacommon) Is a generic (abstract) way of reading metadata information.
640
- - [`metadata.trackInfo`](#metadatatrackInfo) Is a generic (abstract) way of reading metadata information.
641
- - `metadata.native` List of native (original) tags found in the parsed audio file.
641
+ - [`metadata.format.trackInfo`](#metadataformattrackinfo) Describes individual audio and video tracks when available.
642
+ - `metadata.native` Maps each tag format to an array of native (original) tags found in the parsed audio file.
643
+ - `metadata.quality.warnings` Contains non-fatal parsing warnings, each with a `message` string.
642
644
 
643
645
  #### `metadata.format`
644
646
 
@@ -660,15 +662,16 @@ Audio format information. Defined in the TypeScript `IFormat` interface:
660
662
  - `format.trackGain?: number` Track gain in dB
661
663
  - `format.albumGain?: number` Album gain in dB
662
664
 
663
- #### `metadata.trackInfo`
665
+ #### `metadata.format.trackInfo`
664
666
 
665
- To support advanced containers like [Matroska](https://wikipedia.org/wiki/Matroska) or [MPEG-4](https://en.wikipedia.org/wiki/MPEG-4), which may contain multiple audio and video tracks, the **experimental**- `metadata.trackInfo` has been added,
667
+ Containers such as Matroska and MPEG-4 can contain multiple audio and video tracks.
668
+ The experimental `metadata.format.trackInfo` property describes these individual tracks.
666
669
 
667
- `metadata.trackInfo` is either `undefined` or has an **array** of [trackInfo](#trackinfo)
670
+ `metadata.format.trackInfo` is an array of [trackInfo](#trackinfo) objects, empty when no track information is available.
668
671
 
669
672
  ##### trackInfo
670
673
 
671
- Audio format information. Defined in the TypeScript `IFormat` interface:
674
+ Individual track information. Defined in the TypeScript `ITrackInfo` interface:
672
675
  - `trackInfo.type?: TrackType` Track type
673
676
  - `trackInfo.codecName?: string` Codec name
674
677
  - `trackInfo.codecSettings?: string` Codec settings
@@ -685,7 +688,7 @@ Audio format information. Defined in the TypeScript `IFormat` interface:
685
688
  - `audioTrack.samplingFrequency?: number`
686
689
  - `audioTrack.outputSamplingFrequency?: number`
687
690
  - `audioTrack.channels?: number`
688
- - `audioTrack.channelPositions?: Buffer`
691
+ - `audioTrack.channelPositions?: Uint8Array`
689
692
  - `audioTrack.bitDepth?: number`
690
693
 
691
694
  ##### `trackInfo.videoTrack`
@@ -698,7 +701,7 @@ Audio format information. Defined in the TypeScript `IFormat` interface:
698
701
  - `videoTrack.displayHeight?: number`
699
702
  - `videoTrack.displayUnit?: number`
700
703
  - `videoTrack.aspectRatioType?: number`
701
- - `videoTrack.colourSpace?: Buffer`
704
+ - `videoTrack.colourSpace?: Uint8Array`
702
705
  - `videoTrack.gammaValue?: number`
703
706
 
704
707
  #### `metadata.common`
@@ -737,7 +740,7 @@ export interface IPicture {
737
740
  /**
738
741
  * Image data
739
742
  */
740
- data: Buffer;
743
+ data: Uint8Array;
741
744
  /**
742
745
  * Optional description
743
746
  */
@@ -1,7 +1,7 @@
1
1
  import { type MediaType } from 'media-typer';
2
+ import type { ITokenizer } from 'strtok3';
2
3
  import { type INativeMetadataCollector } from './common/MetadataCollector.js';
3
4
  import { type IAudioMetadata, type IOptions, type ParserType } from './type.js';
4
- import type { ITokenizer } from 'strtok3';
5
5
  export interface IParserLoader {
6
6
  /**
7
7
  * Returns a list of supported file extensions
@@ -1,24 +1,24 @@
1
- import { fileTypeFromBuffer } from 'file-type';
2
1
  import ContentType from 'content-type';
3
- import { parse as mimeTypeParse } from 'media-typer';
4
2
  import initDebug from 'debug';
5
- import { MetadataCollector } from './common/MetadataCollector.js';
6
- import { TrackType } from './type.js';
7
- import { mpegParserLoader } from './mpeg/MpegLoader.js';
8
- import { CouldNotDetermineFileTypeError, UnsupportedFileTypeError } from './ParseError.js';
3
+ import { fileTypeFromBuffer } from 'file-type';
4
+ import { parse as mimeTypeParse } from 'media-typer';
5
+ import { aiffParserLoader } from './aiff/AiffLoader.js';
9
6
  import { apeParserLoader } from './apev2/Apev2Loader.js';
10
7
  import { asfParserLoader } from './asf/AsfLoader.js';
8
+ import { MetadataCollector } from './common/MetadataCollector.js';
9
+ import { scanAppendingHeaders } from './core.js';
11
10
  import { dsdiffParserLoader } from './dsdiff/DsdiffLoader.js';
12
- import { aiffParserLoader } from './aiff/AiffLoader.js';
13
11
  import { dsfParserLoader } from './dsf/DsfLoader.js';
14
12
  import { flacParserLoader } from './flac/FlacLoader.js';
15
13
  import { matroskaParserLoader } from './matroska/MatroskaLoader.js';
16
14
  import { mp4ParserLoader } from './mp4/Mp4Loader.js';
15
+ import { mpegParserLoader } from './mpeg/MpegLoader.js';
17
16
  import { musepackParserLoader } from './musepack/MusepackLoader.js';
18
17
  import { oggParserLoader } from './ogg/OggLoader.js';
19
- import { wavpackParserLoader } from './wavpack/WavPackLoader.js';
18
+ import { CouldNotDetermineFileTypeError, UnsupportedFileTypeError } from './ParseError.js';
19
+ import { TrackType } from './type.js';
20
20
  import { riffParserLoader } from './wav/WaveLoader.js';
21
- import { scanAppendingHeaders } from './core.js';
21
+ import { wavpackParserLoader } from './wavpack/WavPackLoader.js';
22
22
  const debug = initDebug('music-metadata:parser:factory');
23
23
  export function parseHttpContentType(contentType) {
24
24
  const type = ContentType.parse(contentType);
@@ -47,7 +47,9 @@ export class ParserFactory {
47
47
  musepackParserLoader,
48
48
  dsfParserLoader,
49
49
  dsdiffParserLoader
50
- ].forEach(parser => { this.registerParser(parser); });
50
+ ].forEach(parser => {
51
+ this.registerParser(parser);
52
+ });
51
53
  }
52
54
  registerParser(parser) {
53
55
  this.parsers.push(parser);
@@ -73,7 +75,7 @@ export class ParserFactory {
73
75
  debug('Guess parser on content...');
74
76
  await tokenizer.peekBuffer(buf, { mayBeLess: true });
75
77
  const guessedType = await fileTypeFromBuffer(buf, { mpegOffsetTolerance: 10 });
76
- if (!guessedType || !guessedType.mime) {
78
+ if (!guessedType?.mime) {
77
79
  throw new CouldNotDetermineFileTypeError('Failed to determine audio format');
78
80
  }
79
81
  debug(`Guessed file type is mime=${guessedType.mime}, extension=${guessedType.ext}`);
@@ -105,15 +107,17 @@ export class ParserFactory {
105
107
  * @return Parser submodule name
106
108
  */
107
109
  findLoaderForExtension(filePath) {
108
- if (!filePath)
110
+ if (!filePath) {
109
111
  return;
112
+ }
110
113
  const extension = getExtension(filePath).toLocaleLowerCase() || filePath;
111
114
  return this.parsers.find(parser => parser.extensions.indexOf(extension) !== -1);
112
115
  }
113
116
  findLoaderForContentType(httpContentType) {
114
117
  let mime;
115
- if (!httpContentType)
118
+ if (!httpContentType) {
116
119
  return;
120
+ }
117
121
  try {
118
122
  mime = parseHttpContentType(httpContentType);
119
123
  }
@@ -1,12 +1,12 @@
1
- import * as Token from 'token-types';
2
1
  import initDebug from 'debug';
3
2
  import * as strtok3 from 'strtok3';
4
- import { ID3v2Parser } from '../id3v2/ID3v2Parser.js';
5
- import { FourCcToken } from '../common/FourCC.js';
3
+ import * as Token from 'token-types';
6
4
  import { BasicParser } from '../common/BasicParser.js';
5
+ import { FourCcToken } from '../common/FourCC.js';
6
+ import { ID3v2Parser } from '../id3v2/ID3v2Parser.js';
7
+ import * as iff from '../iff/index.js';
7
8
  import * as AiffToken from './AiffToken.js';
8
9
  import { AiffContentError, compressionTypes } from './AiffToken.js';
9
- import * as iff from '../iff/index.js';
10
10
  const debug = initDebug('music-metadata:parser:aiff');
11
11
  /**
12
12
  * AIFF - Audio Interchange File Format
@@ -22,8 +22,9 @@ export class AIFFParser extends BasicParser {
22
22
  }
23
23
  async parse() {
24
24
  const header = await this.tokenizer.readToken(iff.Header);
25
- if (header.chunkID !== 'FORM')
26
- throw new AiffContentError('Invalid Chunk-ID, expected \'FORM\''); // Not AIFF format
25
+ if (header.chunkID !== 'FORM') {
26
+ throw new AiffContentError("Invalid Chunk-ID, expected 'FORM'"); // Not AIFF format
27
+ }
27
28
  const type = await this.tokenizer.readToken(FourCcToken);
28
29
  switch (type) {
29
30
  case 'AIFF':
@@ -40,7 +41,8 @@ export class AIFFParser extends BasicParser {
40
41
  this.metadata.setFormat('lossless', !this.isCompressed);
41
42
  this.metadata.setAudioOnly();
42
43
  try {
43
- while (!this.tokenizer.fileInfo.size || this.tokenizer.fileInfo.size - this.tokenizer.position >= iff.Header.len) {
44
+ while (!this.tokenizer.fileInfo.size ||
45
+ this.tokenizer.fileInfo.size - this.tokenizer.position >= iff.Header.len) {
44
46
  debug(`Reading AIFF chunk at offset=${this.tokenizer.position}`);
45
47
  const chunkHeader = await this.tokenizer.readToken(iff.Header);
46
48
  const nextChunk = 2 * Math.round(chunkHeader.chunkSize / 2);
@@ -50,7 +52,7 @@ export class AIFFParser extends BasicParser {
50
52
  }
51
53
  catch (err) {
52
54
  if (err instanceof strtok3.EndOfStreamError) {
53
- debug("End-of-stream");
55
+ debug('End-of-stream');
54
56
  }
55
57
  else {
56
58
  throw err;
@@ -59,7 +61,8 @@ export class AIFFParser extends BasicParser {
59
61
  }
60
62
  async readData(header) {
61
63
  switch (header.chunkID) {
62
- case 'COMM': { // The Common Chunk
64
+ case 'COMM': {
65
+ // The Common Chunk
63
66
  if (this.isCompressed === null) {
64
67
  throw new AiffContentError('Failed to parse AIFF.COMM chunk when compression type is unknown');
65
68
  }
@@ -74,7 +77,8 @@ export class AIFFParser extends BasicParser {
74
77
  }
75
78
  return header.chunkSize;
76
79
  }
77
- case 'ID3 ': { // ID3-meta-data
80
+ case 'ID3 ': {
81
+ // ID3-meta-data
78
82
  const id3_data = await this.tokenizer.readToken(new Token.Uint8ArrayType(header.chunkSize));
79
83
  const rst = strtok3.fromBuffer(id3_data);
80
84
  await new ID3v2Parser().parse(this.metadata, rst, this.options);
@@ -82,7 +86,7 @@ export class AIFFParser extends BasicParser {
82
86
  }
83
87
  case 'SSND': // Sound Data Chunk
84
88
  if (this.metadata.format.duration) {
85
- this.metadata.setFormat('bitrate', 8 * header.chunkSize / this.metadata.format.duration);
89
+ this.metadata.setFormat('bitrate', (8 * header.chunkSize) / this.metadata.format.duration);
86
90
  }
87
91
  return 0;
88
92
  case 'NAME': // Sample name chunk
@@ -97,7 +101,10 @@ export class AIFFParser extends BasicParser {
97
101
  }
98
102
  async readTextChunk(header) {
99
103
  const value = await this.tokenizer.readToken(new Token.StringType(header.chunkSize, 'ascii'));
100
- const values = value.split('\0').map(v => v.trim()).filter(v => v?.length);
104
+ const values = value
105
+ .split('\0')
106
+ .map(v => v.trim())
107
+ .filter(v => v?.length);
101
108
  await Promise.all(values.map(v => this.metadata.addTag('AIFF', header.chunkID, v)));
102
109
  return header.chunkSize;
103
110
  }
@@ -1,5 +1,5 @@
1
- import type * as iff from '../iff/index.js';
2
1
  import type { IGetToken } from 'strtok3';
2
+ import type * as iff from '../iff/index.js';
3
3
  export declare const compressionTypes: {
4
4
  NONE: string;
5
5
  sowt: string;
@@ -18,8 +18,9 @@ export class Common {
18
18
  constructor(header, isAifc) {
19
19
  this.isAifc = isAifc;
20
20
  const minimumChunkSize = isAifc ? 22 : 18;
21
- if (header.chunkSize < minimumChunkSize)
21
+ if (header.chunkSize < minimumChunkSize) {
22
22
  throw new AiffContentError(`COMMON CHUNK size should always be at least ${minimumChunkSize}`);
23
+ }
23
24
  this.len = header.chunkSize;
24
25
  }
25
26
  get(buf, off) {
@@ -1,9 +1,9 @@
1
+ import type { IRandomAccessTokenizer } from 'strtok3';
1
2
  import * as strtok3 from 'strtok3';
2
- import type { IOptions, IApeHeader } from '../type.js';
3
- import type { INativeMetadataCollector } from '../common/MetadataCollector.js';
4
3
  import { BasicParser } from '../common/BasicParser.js';
4
+ import type { INativeMetadataCollector } from '../common/MetadataCollector.js';
5
+ import type { IApeHeader, IOptions } from '../type.js';
5
6
  import { type IFooter, type IHeader } from './APEv2Token.js';
6
- import type { IRandomAccessTokenizer } from 'strtok3';
7
7
  declare const ApeContentError_base: {
8
8
  new (message: string): {
9
9
  readonly fileType: string;
@@ -40,6 +40,7 @@ export declare class APEv2Parser extends BasicParser {
40
40
  tryParseApeHeader(): Promise<void>;
41
41
  parse(): Promise<void>;
42
42
  parseTags(footer: IFooter): Promise<void>;
43
+ private readTagValue;
43
44
  private parseDescriptorExpansion;
44
45
  private parseHeader;
45
46
  }
@@ -1,11 +1,11 @@
1
+ import { textDecode } from '@borewit/text-codec';
1
2
  import initDebug from 'debug';
2
3
  import * as strtok3 from 'strtok3';
3
4
  import { StringType } from 'token-types';
4
- import * as util from '../common/Util.js';
5
5
  import { BasicParser } from '../common/BasicParser.js';
6
- import { DataType, DescriptorParser, Header, TagFooter, TagItemHeader } from './APEv2Token.js';
6
+ import * as util from '../common/Util.js';
7
7
  import { makeUnexpectedFileContentError } from '../ParseError.js';
8
- import { textDecode } from '@borewit/text-codec';
8
+ import { DataType, DescriptorParser, Header, TagFooter, TagItemHeader } from './APEv2Token.js';
9
9
  const debug = initDebug('music-metadata:parser:APEv2');
10
10
  const tagFormat = 'APEv2';
11
11
  const preamble = 'APETAGEX';
@@ -61,8 +61,9 @@ export class APEv2Parser extends BasicParser {
61
61
  }
62
62
  static parseTagFooter(metadata, buffer, options) {
63
63
  const footer = TagFooter.get(buffer, buffer.length - TagFooter.len);
64
- if (footer.ID !== preamble)
64
+ if (footer.ID !== preamble) {
65
65
  throw new ApeContentError('Unexpected APEv2 Footer ID preamble value');
66
+ }
66
67
  strtok3.fromBuffer(buffer);
67
68
  const apeParser = new APEv2Parser(metadata, strtok3.fromBuffer(buffer), options);
68
69
  return apeParser.parseTags(footer);
@@ -72,7 +73,7 @@ export class APEv2Parser extends BasicParser {
72
73
  */
73
74
  async tryParseApeHeader() {
74
75
  if (this.tokenizer.fileInfo.size && this.tokenizer.fileInfo.size - this.tokenizer.position < TagFooter.len) {
75
- debug("No APEv2 header found, end-of-file reached");
76
+ debug('No APEv2 header found, end-of-file reached');
76
77
  return;
77
78
  }
78
79
  const footer = await this.tokenizer.peekToken(TagFooter);
@@ -91,8 +92,9 @@ export class APEv2Parser extends BasicParser {
91
92
  }
92
93
  async parse() {
93
94
  const descriptor = await this.tokenizer.readToken(DescriptorParser);
94
- if (descriptor.ID !== 'MAC ')
95
+ if (descriptor.ID !== 'MAC ') {
95
96
  throw new ApeContentError('Unexpected descriptor ID');
97
+ }
96
98
  this.ape.descriptor = descriptor;
97
99
  const lenExp = descriptor.descriptorBytes - DescriptorParser.len;
98
100
  const header = await (lenExp > 0 ? this.parseDescriptorExpansion(lenExp) : this.parseHeader());
@@ -111,15 +113,31 @@ export class APEv2Parser extends BasicParser {
111
113
  }
112
114
  // Only APEv2 tag has tag item headers
113
115
  const tagItemHeader = await this.tokenizer.readToken(TagItemHeader);
114
- bytesRemaining -= TagItemHeader.len + tagItemHeader.size;
115
- await this.tokenizer.peekBuffer(keyBuffer, { length: Math.min(keyBuffer.length, bytesRemaining) });
116
- let zero = util.findZero(keyBuffer);
116
+ bytesRemaining -= TagItemHeader.len;
117
+ // Reserve the value bytes before looking for the key terminator.
118
+ if (tagItemHeader.size >= bytesRemaining) {
119
+ throw new ApeContentError(`Invalid tag item size: ${tagItemHeader.size}`);
120
+ }
121
+ const keyBytes = await this.tokenizer.peekBuffer(keyBuffer, {
122
+ length: Math.min(keyBuffer.length, bytesRemaining - tagItemHeader.size),
123
+ mayBeLess: true
124
+ });
125
+ let zero = keyBuffer.subarray(0, keyBytes).indexOf(0);
126
+ if (zero === -1) {
127
+ throw new ApeContentError('Unterminated tag item key');
128
+ }
117
129
  const key = await this.tokenizer.readToken(new StringType(zero, 'ascii'));
118
130
  await this.tokenizer.ignore(1);
119
131
  bytesRemaining -= key.length + 1;
132
+ if (this.tokenizer.fileInfo.size !== undefined &&
133
+ tagItemHeader.size > this.tokenizer.fileInfo.size - this.tokenizer.position) {
134
+ throw new ApeContentError(`Invalid tag item size: ${tagItemHeader.size}`);
135
+ }
136
+ bytesRemaining -= tagItemHeader.size;
120
137
  switch (tagItemHeader.flags.dataType) {
121
- case DataType.text_utf8: { // utf-8 text-string
122
- const value = await this.tokenizer.readToken(new StringType(tagItemHeader.size, 'utf8'));
138
+ case DataType.text_utf8: {
139
+ // utf-8 text-string
140
+ const value = textDecode(await this.readTagValue(tagItemHeader.size), 'utf-8');
123
141
  const values = value.split(/\x00/g);
124
142
  await Promise.all(values.map(val => this.metadata.addTag(tagFormat, key, val)));
125
143
  break;
@@ -129,8 +147,7 @@ export class APEv2Parser extends BasicParser {
129
147
  await this.tokenizer.ignore(tagItemHeader.size);
130
148
  }
131
149
  else {
132
- const picData = new Uint8Array(tagItemHeader.size);
133
- await this.tokenizer.readBuffer(picData);
150
+ const picData = await this.readTagValue(tagItemHeader.size);
134
151
  zero = util.findZero(picData);
135
152
  const description = textDecode(picData.subarray(0, zero), 'utf-8');
136
153
  const data = picData.subarray(zero + 1);
@@ -152,6 +169,30 @@ export class APEv2Parser extends BasicParser {
152
169
  }
153
170
  }
154
171
  }
172
+ async readTagValue(size) {
173
+ const chunkSize = 64 * 1024;
174
+ if ((!this.tokenizer.supportsRandomAccess() || this.tokenizer.fileInfo.size === undefined) && size > chunkSize) {
175
+ // Stream sizes may be unknown or untrustworthy. Only allocate the full
176
+ // value after all of its bytes have actually been read.
177
+ const chunks = [];
178
+ for (let remaining = size; remaining > 0;) {
179
+ const chunk = new Uint8Array(Math.min(remaining, chunkSize));
180
+ await this.tokenizer.readBuffer(chunk);
181
+ chunks.push(chunk);
182
+ remaining -= chunk.length;
183
+ }
184
+ const value = new Uint8Array(size);
185
+ let offset = 0;
186
+ for (const chunk of chunks) {
187
+ value.set(chunk, offset);
188
+ offset += chunk.length;
189
+ }
190
+ return value;
191
+ }
192
+ const value = new Uint8Array(size);
193
+ await this.tokenizer.readBuffer(value);
194
+ return value;
195
+ }
155
196
  async parseDescriptorExpansion(lenExp) {
156
197
  await this.tokenizer.ignore(lenExp);
157
198
  return this.parseHeader();
@@ -160,7 +201,7 @@ export class APEv2Parser extends BasicParser {
160
201
  const header = await this.tokenizer.readToken(Header);
161
202
  // ToDo before
162
203
  this.metadata.setFormat('lossless', true);
163
- this.metadata.setFormat('container', 'Monkey\'s Audio');
204
+ this.metadata.setFormat('container', "Monkey's Audio");
164
205
  this.metadata.setFormat('bitsPerSample', header.bitsPerSample);
165
206
  this.metadata.setFormat('sampleRate', header.sampleRate);
166
207
  this.metadata.setFormat('numberOfChannels', header.channel);
@@ -169,8 +210,10 @@ export class APEv2Parser extends BasicParser {
169
210
  throw new ApeContentError('Missing APE descriptor');
170
211
  }
171
212
  return {
172
- forwardBytes: this.ape.descriptor.seekTableBytes + this.ape.descriptor.headerDataBytes +
173
- this.ape.descriptor.apeFrameDataBytes + this.ape.descriptor.terminatingDataBytes
213
+ forwardBytes: this.ape.descriptor.seekTableBytes +
214
+ this.ape.descriptor.headerDataBytes +
215
+ this.ape.descriptor.apeFrameDataBytes +
216
+ this.ape.descriptor.terminatingDataBytes
174
217
  };
175
218
  }
176
219
  }
@@ -9,6 +9,7 @@ const apev2TagMap = {
9
9
  'Album Artist': 'albumartist',
10
10
  Album: 'album',
11
11
  Year: 'date',
12
+ Date: 'date',
12
13
  Originalyear: 'originalyear',
13
14
  Originaldate: 'originaldate',
14
15
  Releasedate: 'releasedate',
@@ -58,7 +58,7 @@ export declare const DataType: {
58
58
  external_info: number;
59
59
  reserved: number;
60
60
  };
61
- export type DataType = typeof DataType[keyof typeof DataType];
61
+ export type DataType = (typeof DataType)[keyof typeof DataType];
62
62
  /**
63
63
  * APE_DESCRIPTOR: defines the sizes (and offsets) of all the pieces, as well as the MD5 checksum
64
64
  */
@@ -112,5 +112,5 @@ export function parseTagFlags(flags) {
112
112
  * @return {boolean} true if bit is 1; otherwise false
113
113
  */
114
114
  export function isBitSet(num, bit) {
115
- return (num & 1 << bit) !== 0;
115
+ return (num & (1 << bit)) !== 0;
116
116
  }