music-metadata 12.2.0 → 12.2.2

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.
@@ -75,6 +75,10 @@ const encoderDict = {
75
75
  function distinct(value, index, self) {
76
76
  return self.indexOf(value) === index;
77
77
  }
78
+ /** Chapter offsets can be revisited even when the total input size is unknown. */
79
+ function supportsSeeking(tokenizer) {
80
+ return tokenizer.supportsRandomAccess() && 'setPosition' in tokenizer && typeof tokenizer.setPosition === 'function';
81
+ }
78
82
  /**
79
83
  * Determine if a track carries audio.
80
84
  *
@@ -173,7 +177,7 @@ export class MP4Parser extends BasicParser {
173
177
  const rootAtom = await Atom.readAtom(this.tokenizer, (atom, remaining) => this.handleAtom(atom, remaining), null, remainingFileSize);
174
178
  remainingFileSize = rootAtom.header.length === 0n ? 0 : remainingFileSize - Number(rootAtom.header.length);
175
179
  }
176
- if (this.mediaDataRanges.length > 0) {
180
+ if (this.mediaDataRanges.length > 0 && supportsSeeking(this.tokenizer)) {
177
181
  // Track references and sample tables may follow the media data in a trailing moov box.
178
182
  const tokenizer = this.tokenizer;
179
183
  const position = tokenizer.position;
@@ -626,7 +630,13 @@ export class MP4Parser extends BasicParser {
626
630
  case 'tkhd': // TrackHeaderBox
627
631
  track.header = await this.readToken(new AtomToken.TrackHeaderAtom(payLoadLength));
628
632
  break;
629
- case 'hdlr': // TrackHeaderBox
633
+ case 'hdlr': // Media handler; metadata and data handlers do not identify the track type.
634
+ // QuickTime File Format, Media atom: Handler reference atom ('hdlr').
635
+ // https://developer.apple.com/documentation/quicktime-file-format/media_atom/handler_reference_atom
636
+ if (child.parent?.header.name !== 'mdia' || child.parent.parent !== trakBox) {
637
+ await this.tokenizer.ignore(payLoadLength);
638
+ break;
639
+ }
630
640
  track.handler = await this.readToken(new AtomToken.HandlerBox(payLoadLength));
631
641
  if (track.handler.handlerType === 'audi' || track.handler.handlerType === 'soun') {
632
642
  this.hasAudioTrack = true;
@@ -782,7 +792,7 @@ export class MP4Parser extends BasicParser {
782
792
  mdat: async (len) => {
783
793
  const range = { offset: this.tokenizer.position, end: this.tokenizer.position + len };
784
794
  if (this.options.includeChapters) {
785
- if (this.tokenizer.supportsRandomAccess()) {
795
+ if (supportsSeeking(this.tokenizer)) {
786
796
  this.mediaDataRanges.push(range);
787
797
  }
788
798
  else {
@@ -160,7 +160,7 @@ export class MpegParser extends AbstractID3Parser {
160
160
  this.metadata.setFormat('container', header.container);
161
161
  this.metadata.setFormat('codec', header.codec);
162
162
  this.metadata.setFormat('lossless', false);
163
- this.metadata.setFormat('sampleRate', header.samplingRate);
163
+ this.metadata.setFormat('sampleRate', header.samplingRate ?? undefined);
164
164
  this.frameCount++;
165
165
  return header instanceof AdtsFrameHeader ? this.parseAdts(header) : this.parseMpegFrame(header);
166
166
  }
@@ -301,7 +301,7 @@ export class MpegParser extends AbstractID3Parser {
301
301
  if (infoTag.lame) {
302
302
  this.metadata.setFormat('tool', `LAME ${common.stripNulls(infoTag.lame.version)}`);
303
303
  if (infoTag.lame.extended) {
304
- this.metadata.setFormat('trackPeakLevel', infoTag.lame.extended.track_peak);
304
+ this.metadata.setFormat('trackPeakLevel', infoTag.lame.extended.track_peak ?? undefined);
305
305
  if (infoTag.lame.extended.track_gain) {
306
306
  this.metadata.setFormat('trackGain', infoTag.lame.extended.track_gain.adjustment);
307
307
  }
@@ -1,13 +1,13 @@
1
1
  import type { TagType } from '../common/GenericTagTypes.js';
2
2
  import { type INativeMetadataCollector, MetadataCollector } from '../common/MetadataCollector.js';
3
- import type { AnyTagValue, FormatId, INativeTags, IQualityInformation } from '../type.js';
3
+ import type { AnyTagValue, IFormat, INativeTags, IQualityInformation } from '../type.js';
4
4
  /** Keep format values local to a logical stream while sharing tags and warnings. */
5
5
  export declare class OggStreamMetadata extends MetadataCollector {
6
6
  private readonly parent;
7
7
  readonly native: INativeTags;
8
8
  readonly quality: IQualityInformation;
9
9
  constructor(parent: INativeMetadataCollector);
10
- setFormat(key: FormatId, value: AnyTagValue): void;
10
+ setFormat<K extends keyof IFormat>(key: K, value: IFormat[K]): void;
11
11
  registerTagType(tagType: TagType): void;
12
12
  addTag(tagType: TagType, tagId: string, value: AnyTagValue): Promise<void>;
13
13
  addWarning(warning: string): void;
@@ -5,6 +5,16 @@ export declare class VorbisDecoder {
5
5
  get remaining(): number;
6
6
  readInt32(): number;
7
7
  readStringUtf8(): string;
8
+ /** Decode the shared structure, leaving packet framing and metadata policy to callers. */
9
+ parseCommentList(createCountError?: (message: string) => Error): {
10
+ vendor: string;
11
+ comments: Generator<{
12
+ key: string;
13
+ value: string;
14
+ len: number;
15
+ }, void, unknown>;
16
+ };
17
+ private parseUserCommentList;
8
18
  parseUserComment(): {
9
19
  key: string;
10
20
  value: string;
@@ -30,6 +30,21 @@ export class VorbisDecoder {
30
30
  this.offset += len;
31
31
  return value;
32
32
  }
33
+ /** Decode the shared structure, leaving packet framing and metadata policy to callers. */
34
+ parseCommentList(createCountError = message => new VorbisCommentError(message)) {
35
+ const vendor = this.readStringUtf8();
36
+ return { vendor, comments: this.parseUserCommentList(createCountError) };
37
+ }
38
+ *parseUserCommentList(createCountError) {
39
+ const count = this.readInt32();
40
+ // Each entry needs at least its four-byte length, even for an empty comment.
41
+ if (count > Math.floor(this.remaining / 4)) {
42
+ throw createCountError(`Vorbis comment count ${count} exceeds remaining data`);
43
+ }
44
+ for (let i = 0; i < count; ++i) {
45
+ yield this.parseUserComment();
46
+ }
47
+ }
33
48
  parseUserComment() {
34
49
  const offset0 = this.offset;
35
50
  const v = this.readStringUtf8();
@@ -1,5 +1,4 @@
1
1
  import debugInit from 'debug';
2
- import * as Token from 'token-types';
3
2
  import { validateAllocation } from '../../common/Allocation.js';
4
3
  import { makeUnexpectedFileContentError } from '../../ParseError.js';
5
4
  import { CommonHeader, IdentificationHeader, VorbisPictureToken } from './Vorbis.js';
@@ -141,14 +140,9 @@ export class VorbisStream {
141
140
  * Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-840005.2
142
141
  */
143
142
  async parseUserCommentList(pageData, offset) {
144
- const strLen = Token.UINT32_LE.get(pageData, offset);
145
- offset += 4;
146
- // const vendorString = new Token.StringType(strLen, 'utf-8').get(pageData, offset);
147
- offset += strLen;
148
- let userCommentListLength = Token.UINT32_LE.get(pageData, offset);
149
- offset += 4;
150
- while (userCommentListLength-- > 0) {
151
- offset += await this.parseUserComment(pageData, offset);
143
+ const { comments } = new VorbisDecoder(pageData, offset).parseCommentList();
144
+ for (const tag of comments) {
145
+ await this.addTag(tag.key, tag.value);
152
146
  }
153
147
  this.isMetadataComplete = true;
154
148
  }
package/lib/type.d.ts CHANGED
@@ -388,7 +388,7 @@ export interface IRatio {
388
388
  */
389
389
  dB: number;
390
390
  }
391
- export type FormatId = 'container' | 'duration' | 'bitrate' | 'containerDuration' | 'overallBitrate' | 'sampleRate' | 'bitsPerSample' | 'codec' | 'tool' | 'codecProfile' | 'lossless' | 'numberOfChannels' | 'numberOfSamples' | 'audioMD5' | 'chapters' | 'modificationTime' | 'creationTime' | 'trackPeakLevel' | 'trackGain' | 'albumGain' | 'hasAudio' | 'hasVideo';
391
+ export type FormatId = keyof IFormat;
392
392
  export interface IAudioTrack {
393
393
  /** Number of decoded audio samples, when known. */
394
394
  numberOfSamples?: number;
@@ -603,6 +603,7 @@ export interface IOptions {
603
603
  /**
604
604
  * Maximum bytes buffered for a single metadata payload, accumulated Ogg packet, or tokenizer stream lookahead.
605
605
  * Defaults to 64 MiB (67108864 bytes). Must be a positive safe integer.
606
+ * Also caps file-type read buffers during media format and cover art MIME-type detection.
606
607
  * This is not a limit on total process memory or the size of skipped audio/video data.
607
608
  */
608
609
  maxMemoryAllocation?: number;
@@ -625,7 +626,8 @@ export interface IOptions {
625
626
  */
626
627
  includeChapters?: boolean;
627
628
  /**
628
- * Set observer for async callbacks to common or format.
629
+ * Called synchronously after common tag or format updates. Returned promises are not awaited.
630
+ * Unchanged primitive format values do not trigger a callback.
629
631
  */
630
632
  observer?: Observer;
631
633
  /**
@@ -1,10 +1,9 @@
1
1
  import initDebug from 'debug';
2
2
  import * as strtok3 from 'strtok3';
3
3
  import * as Token from 'token-types';
4
- import { getTokenizerOptions } from '../common/Allocation.js';
5
4
  import { BasicParser } from '../common/BasicParser.js';
6
5
  import { FourCcToken } from '../common/FourCC.js';
7
- import { ID3v2Parser } from '../id3v2/ID3v2Parser.js';
6
+ import { parseEmbeddedID3v2 } from '../id3v2/EmbeddedID3v2.js';
8
7
  import * as riff from '../riff/RiffChunk.js';
9
8
  import { BroadcastAudioExtensionChunk } from './BwfChunk.js';
10
9
  import * as WaveChunk from './WaveChunk.js';
@@ -101,9 +100,7 @@ export class WaveParser extends BasicParser {
101
100
  case 'id3 ': // The way Picard, FooBar currently stores, ID3 meta-data
102
101
  case 'ID3 ': {
103
102
  // The way Mp3Tags stores ID3 meta-data
104
- const id3_data = await this.readToken(new Token.Uint8ArrayType(header.chunkSize));
105
- const rst = strtok3.fromBuffer(id3_data, getTokenizerOptions(this.options));
106
- await new ID3v2Parser().parse(this.metadata, rst, this.options);
103
+ await parseEmbeddedID3v2(this.metadata, this.tokenizer, this.options, header.chunkSize);
107
104
  break;
108
105
  }
109
106
  case 'data': {
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": "12.2.0",
4
+ "version": "12.2.2",
5
5
  "author": {
6
6
  "name": "Borewit",
7
7
  "url": "https://github.com/Borewit"
@@ -122,9 +122,9 @@
122
122
  "@tokenizer/token": "^0.3.0",
123
123
  "content-type": "^3.1.1",
124
124
  "debug": "^4.4.3",
125
- "file-type": "^22.1.1",
125
+ "file-type": "^22.2.0",
126
126
  "media-typer": "^2.0.0",
127
- "strtok3": "^10.4.1",
127
+ "strtok3": "^10.6.0",
128
128
  "token-types": "^6.1.2",
129
129
  "uint8array-extras": "^1.6.0",
130
130
  "win-guid": "^0.2.1"
@@ -136,12 +136,12 @@
136
136
  "@types/debug": "^4.1.13",
137
137
  "@types/media-typer": "^1.1.3",
138
138
  "@types/mocha": "^10.0.10",
139
- "@types/node": "^26.6.4",
139
+ "@types/node": "^26.6.5",
140
140
  "c8": "^12.0.0",
141
141
  "chai": "^6.3.0",
142
142
  "chai-as-promised": "^8.0.2",
143
143
  "del-cli": "^8.0.1",
144
- "mime": "^4.1.0",
144
+ "mime": "^4.1.1",
145
145
  "mocha": "^12.0.3",
146
146
  "nodemon": "^3.1.14",
147
147
  "remark-cli": "^12.0.1",