music-metadata 11.4.0 → 11.5.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/lib/ParserFactory.d.ts +1 -1
- package/lib/ParserFactory.js +9 -0
- package/lib/aiff/AiffParser.js +1 -0
- package/lib/apev2/APEv2Parser.js +1 -0
- package/lib/common/MetadataCollector.d.ts +2 -0
- package/lib/common/MetadataCollector.js +5 -1
- package/lib/core.js +1 -4
- package/lib/dsdiff/DsdiffParser.js +1 -0
- package/lib/dsf/DsfParser.js +1 -0
- package/lib/flac/FlacParser.js +1 -0
- package/lib/mp4/Atom.js +0 -1
- package/lib/mp4/AtomToken.d.ts +24 -2
- package/lib/mp4/AtomToken.js +37 -9
- package/lib/mp4/MP4Parser.d.ts +3 -1
- package/lib/mp4/MP4Parser.js +106 -58
- package/lib/mpeg/MpegParser.js +1 -0
- package/lib/musepack/MusepackParser.js +1 -0
- package/lib/ogg/opus/OpusParser.js +1 -0
- package/lib/ogg/speex/SpeexParser.js +1 -0
- package/lib/ogg/theora/TheoraParser.js +1 -0
- package/lib/ogg/vorbis/VorbisParser.js +1 -0
- package/lib/type.d.ts +9 -1
- package/lib/wav/WaveParser.js +1 -0
- package/lib/wavpack/WavPackParser.js +1 -0
- package/package.json +4 -4
package/lib/ParserFactory.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type MediaType } from 'media-typer';
|
|
2
2
|
import { type INativeMetadataCollector } from './common/MetadataCollector.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type IAudioMetadata, type IOptions, type ParserType } from './type.js';
|
|
4
4
|
import type { ITokenizer } from 'strtok3';
|
|
5
5
|
export interface IParserLoader {
|
|
6
6
|
/**
|
package/lib/ParserFactory.js
CHANGED
|
@@ -3,6 +3,7 @@ import ContentType from 'content-type';
|
|
|
3
3
|
import { parse as mimeTypeParse } from 'media-typer';
|
|
4
4
|
import initDebug from 'debug';
|
|
5
5
|
import { MetadataCollector } from './common/MetadataCollector.js';
|
|
6
|
+
import { TrackType } from './type.js';
|
|
6
7
|
import { mpegParserLoader } from './mpeg/MpegLoader.js';
|
|
7
8
|
import { CouldNotDetermineFileTypeError, UnsupportedFileTypeError } from './ParseError.js';
|
|
8
9
|
import { apeParserLoader } from './apev2/Apev2Loader.js';
|
|
@@ -89,6 +90,14 @@ export class ParserFactory {
|
|
|
89
90
|
const parser = new ParserImpl(metadata, tokenizer, opts ?? {});
|
|
90
91
|
debug(`Parser ${parserLoader.parserType} loaded`);
|
|
91
92
|
await parser.parse();
|
|
93
|
+
if (metadata.format.trackInfo) {
|
|
94
|
+
if (metadata.format.hasAudio === undefined) {
|
|
95
|
+
metadata.setFormat('hasAudio', !!metadata.format.trackInfo.find(track => track.type === TrackType.audio));
|
|
96
|
+
}
|
|
97
|
+
if (metadata.format.hasVideo === undefined) {
|
|
98
|
+
metadata.setFormat('hasVideo', !!metadata.format.trackInfo.find(track => track.type === TrackType.video));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
92
101
|
return metadata.toCommonMetadata();
|
|
93
102
|
}
|
|
94
103
|
/**
|
package/lib/aiff/AiffParser.js
CHANGED
|
@@ -38,6 +38,7 @@ export class AIFFParser extends BasicParser {
|
|
|
38
38
|
throw new AiffContentError(`Unsupported AIFF type: ${type}`);
|
|
39
39
|
}
|
|
40
40
|
this.metadata.setFormat('lossless', !this.isCompressed);
|
|
41
|
+
this.metadata.setAudioOnly();
|
|
41
42
|
try {
|
|
42
43
|
while (!this.tokenizer.fileInfo.size || this.tokenizer.fileInfo.size - this.tokenizer.position >= iff.Header.len) {
|
|
43
44
|
debug(`Reading AIFF chunk at offset=${this.tokenizer.position}`);
|
package/lib/apev2/APEv2Parser.js
CHANGED
|
@@ -96,6 +96,7 @@ export class APEv2Parser extends BasicParser {
|
|
|
96
96
|
this.ape.descriptor = descriptor;
|
|
97
97
|
const lenExp = descriptor.descriptorBytes - DescriptorParser.len;
|
|
98
98
|
const header = await (lenExp > 0 ? this.parseDescriptorExpansion(lenExp) : this.parseHeader());
|
|
99
|
+
this.metadata.setAudioOnly();
|
|
99
100
|
await this.tokenizer.ignore(header.forwardBytes);
|
|
100
101
|
return this.tryParseApeHeader();
|
|
101
102
|
}
|
|
@@ -24,6 +24,7 @@ export interface INativeMetadataCollector extends IWarningCollector {
|
|
|
24
24
|
setFormat(key: FormatId, value: AnyTagValue): void;
|
|
25
25
|
addTag(tagType: TagType, tagId: string, value: AnyTagValue): Promise<void>;
|
|
26
26
|
addStreamInfo(streamInfo: ITrackInfo): void;
|
|
27
|
+
setAudioOnly(): void;
|
|
27
28
|
}
|
|
28
29
|
/**
|
|
29
30
|
* Provided to the parser to uodate the metadata result.
|
|
@@ -51,6 +52,7 @@ export declare class MetadataCollector implements INativeMetadataCollector {
|
|
|
51
52
|
hasAny(): boolean;
|
|
52
53
|
addStreamInfo(streamInfo: ITrackInfo): void;
|
|
53
54
|
setFormat(key: FormatId, value: AnyTagValue): void;
|
|
55
|
+
setAudioOnly(): void;
|
|
54
56
|
addTag(tagType: TagType, tagId: string, value: AnyTagValue): Promise<void>;
|
|
55
57
|
addWarning(warning: string): void;
|
|
56
58
|
postMap(tagType: TagType | 'artificial', tag: IGenericTag): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TrackTypeValueToKeyMap } from '../type.js';
|
|
1
|
+
import { TrackTypeValueToKeyMap, } from '../type.js';
|
|
2
2
|
import initDebug from 'debug';
|
|
3
3
|
import { isSingleton, isUnique } from './GenericTagTypes.js';
|
|
4
4
|
import { CombinedTagMapper } from './CombinedTagMapper.js';
|
|
@@ -61,6 +61,10 @@ export class MetadataCollector {
|
|
|
61
61
|
this.opts.observer({ metadata: this, tag: { type: 'format', id: key, value } });
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
setAudioOnly() {
|
|
65
|
+
this.setFormat('hasAudio', true);
|
|
66
|
+
this.setFormat('hasVideo', false);
|
|
67
|
+
}
|
|
64
68
|
async addTag(tagType, tagId, value) {
|
|
65
69
|
debug(`tag ${tagType}.${tagId} = ${value}`);
|
|
66
70
|
if (!this.native[tagType]) {
|
package/lib/core.js
CHANGED
|
@@ -69,10 +69,7 @@ export function parseFromTokenizer(tokenizer, options) {
|
|
|
69
69
|
export function orderTags(nativeTags) {
|
|
70
70
|
const tags = {};
|
|
71
71
|
for (const { id, value } of nativeTags) {
|
|
72
|
-
|
|
73
|
-
tags[id] = [];
|
|
74
|
-
}
|
|
75
|
-
tags[id].push(value);
|
|
72
|
+
(tags[id] || (tags[id] = [])).push(value);
|
|
76
73
|
}
|
|
77
74
|
return tags;
|
|
78
75
|
}
|
|
@@ -20,6 +20,7 @@ export class DsdiffParser extends BasicParser {
|
|
|
20
20
|
const header = await this.tokenizer.readToken(ChunkHeader64);
|
|
21
21
|
if (header.chunkID !== 'FRM8')
|
|
22
22
|
throw new DsdiffContentParseError('Unexpected chunk-ID');
|
|
23
|
+
this.metadata.setAudioOnly();
|
|
23
24
|
const type = (await this.tokenizer.readToken(FourCcToken)).trim();
|
|
24
25
|
switch (type) {
|
|
25
26
|
case 'DSD':
|
package/lib/dsf/DsfParser.js
CHANGED
|
@@ -18,6 +18,7 @@ export class DsfParser extends AbstractID3Parser {
|
|
|
18
18
|
throw new DsdContentParseError('Invalid chunk signature');
|
|
19
19
|
this.metadata.setFormat('container', 'DSF');
|
|
20
20
|
this.metadata.setFormat('lossless', true);
|
|
21
|
+
this.metadata.setAudioOnly();
|
|
21
22
|
const dsdChunk = await this.tokenizer.readToken(DsdChunk);
|
|
22
23
|
if (dsdChunk.metadataPointer === BigInt(0)) {
|
|
23
24
|
debug("No ID3v2 tag present");
|
package/lib/flac/FlacParser.js
CHANGED
package/lib/mp4/Atom.js
CHANGED
|
@@ -43,7 +43,6 @@ export class Atom {
|
|
|
43
43
|
// "Container" atoms, contains nested atoms
|
|
44
44
|
case 'moov': // The Movie Atom: contains other atoms
|
|
45
45
|
case 'udta': // User defined atom
|
|
46
|
-
case 'trak':
|
|
47
46
|
case 'mdia': // Media atom
|
|
48
47
|
case 'minf': // Media Information Atom
|
|
49
48
|
case 'stbl': // The Sample Table Atom
|
package/lib/mp4/AtomToken.d.ts
CHANGED
|
@@ -121,7 +121,6 @@ export declare const Header: IToken<IAtomHeader>;
|
|
|
121
121
|
*/
|
|
122
122
|
export declare const ExtendedSize: IToken<bigint>;
|
|
123
123
|
export declare const ftyp: IGetToken<IAtomFtyp>;
|
|
124
|
-
export declare const tkhd: IGetToken<IAtomFtyp>;
|
|
125
124
|
/**
|
|
126
125
|
* Token: Movie Header Atom
|
|
127
126
|
*/
|
|
@@ -269,7 +268,7 @@ export interface ITrackHeaderAtom extends IVersionAndFlags {
|
|
|
269
268
|
volume: number;
|
|
270
269
|
}
|
|
271
270
|
/**
|
|
272
|
-
* Track Header Atoms structure
|
|
271
|
+
* Track Header Atoms structure (`tkhd`)
|
|
273
272
|
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25550
|
|
274
273
|
*/
|
|
275
274
|
export declare class TrackHeaderAtom implements IGetToken<ITrackHeaderAtom> {
|
|
@@ -462,4 +461,27 @@ export declare class TrackRunBox implements IGetToken<ITrackRunBox> {
|
|
|
462
461
|
constructor(len: number);
|
|
463
462
|
get(buf: Uint8Array, off: number): ITrackRunBox;
|
|
464
463
|
}
|
|
464
|
+
export interface IHandlerBox {
|
|
465
|
+
version: number;
|
|
466
|
+
flags: number;
|
|
467
|
+
componentType: string;
|
|
468
|
+
handlerType: string;
|
|
469
|
+
componentName: string;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* HandlerBox (`hdlr`)
|
|
473
|
+
*/
|
|
474
|
+
export declare class HandlerBox implements IGetToken<IHandlerBox> {
|
|
475
|
+
len: number;
|
|
476
|
+
constructor(len: number);
|
|
477
|
+
get(buf: Uint8Array, off: number): IHandlerBox;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Chapter Track Reference Box (`chap`)
|
|
481
|
+
*/
|
|
482
|
+
export declare class ChapterTrackReferenceBox implements IGetToken<number[]> {
|
|
483
|
+
len: number;
|
|
484
|
+
constructor(len: number);
|
|
485
|
+
get(buf: Uint8Array, off: number): number[];
|
|
486
|
+
}
|
|
465
487
|
export {};
|
package/lib/mp4/AtomToken.js
CHANGED
|
@@ -34,14 +34,6 @@ export const ftyp = {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
|
-
export const tkhd = {
|
|
38
|
-
len: 4,
|
|
39
|
-
get: (buf, off) => {
|
|
40
|
-
return {
|
|
41
|
-
type: new Token.StringType(4, 'ascii').get(buf, off)
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
37
|
/**
|
|
46
38
|
* Token: Movie Header Atom
|
|
47
39
|
*/
|
|
@@ -174,7 +166,7 @@ export class NameAtom {
|
|
|
174
166
|
}
|
|
175
167
|
}
|
|
176
168
|
/**
|
|
177
|
-
* Track Header Atoms structure
|
|
169
|
+
* Track Header Atoms structure (`tkhd`)
|
|
178
170
|
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25550
|
|
179
171
|
*/
|
|
180
172
|
export class TrackHeaderAtom {
|
|
@@ -494,4 +486,40 @@ export class TrackRunBox {
|
|
|
494
486
|
return trun;
|
|
495
487
|
}
|
|
496
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* HandlerBox (`hdlr`)
|
|
491
|
+
*/
|
|
492
|
+
export class HandlerBox {
|
|
493
|
+
constructor(len) {
|
|
494
|
+
this.len = len;
|
|
495
|
+
}
|
|
496
|
+
get(buf, off) {
|
|
497
|
+
const _flagOffset = off + 1;
|
|
498
|
+
const charTypeToken = new Token.StringType(4, 'utf-8');
|
|
499
|
+
return {
|
|
500
|
+
version: Token.INT8.get(buf, off),
|
|
501
|
+
flags: Token.UINT24_BE.get(buf, off + 1),
|
|
502
|
+
componentType: charTypeToken.get(buf, off + 4),
|
|
503
|
+
handlerType: charTypeToken.get(buf, off + 8),
|
|
504
|
+
componentName: new Token.StringType(this.len - 28, 'utf-8').get(buf, off + 28),
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Chapter Track Reference Box (`chap`)
|
|
510
|
+
*/
|
|
511
|
+
export class ChapterTrackReferenceBox {
|
|
512
|
+
constructor(len) {
|
|
513
|
+
this.len = len;
|
|
514
|
+
}
|
|
515
|
+
get(buf, off) {
|
|
516
|
+
let dynOffset = 0;
|
|
517
|
+
const trackIds = [];
|
|
518
|
+
while (dynOffset < this.len) {
|
|
519
|
+
trackIds.push(Token.UINT32_BE.get(buf, off + dynOffset));
|
|
520
|
+
dynOffset += 4;
|
|
521
|
+
}
|
|
522
|
+
return trackIds;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
497
525
|
//# sourceMappingURL=AtomToken.js.map
|
package/lib/mp4/MP4Parser.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ export declare class MP4Parser extends BasicParser {
|
|
|
4
4
|
private static read_BE_Integer;
|
|
5
5
|
private audioLengthInBytes;
|
|
6
6
|
private tracks;
|
|
7
|
-
private
|
|
7
|
+
private hasVideoTrack;
|
|
8
|
+
private hasAudioTrack;
|
|
8
9
|
parse(): Promise<void>;
|
|
9
10
|
handleAtom(atom: Atom, remaining: number): Promise<void>;
|
|
10
11
|
private getTrackDescription;
|
|
@@ -18,6 +19,7 @@ export declare class MP4Parser extends BasicParser {
|
|
|
18
19
|
*/
|
|
19
20
|
private parseMetadataItemData;
|
|
20
21
|
private parseValueAtom;
|
|
22
|
+
private parseTrackBox;
|
|
21
23
|
private parseTrackFragmentBox;
|
|
22
24
|
private atomParsers;
|
|
23
25
|
/**
|
package/lib/mp4/MP4Parser.js
CHANGED
|
@@ -4,7 +4,7 @@ import { BasicParser } from '../common/BasicParser.js';
|
|
|
4
4
|
import { Genres } from '../id3v1/ID3v1Parser.js';
|
|
5
5
|
import { Atom } from './Atom.js';
|
|
6
6
|
import * as AtomToken from './AtomToken.js';
|
|
7
|
-
import { Mp4ContentError } from './AtomToken.js';
|
|
7
|
+
import { ChapterTrackReferenceBox, Mp4ContentError, } from './AtomToken.js';
|
|
8
8
|
import { TrackType } from '../type.js';
|
|
9
9
|
import { uint8ArrayToHex, uint8ArrayToString } from 'uint8array-extras';
|
|
10
10
|
const debug = initDebug('music-metadata:parser:MP4');
|
|
@@ -92,7 +92,9 @@ function distinct(value, index, self) {
|
|
|
92
92
|
export class MP4Parser extends BasicParser {
|
|
93
93
|
constructor() {
|
|
94
94
|
super(...arguments);
|
|
95
|
-
this.tracks =
|
|
95
|
+
this.tracks = new Map();
|
|
96
|
+
this.hasVideoTrack = false;
|
|
97
|
+
this.hasAudioTrack = true;
|
|
96
98
|
this.atomParsers = {
|
|
97
99
|
/**
|
|
98
100
|
* Parse movie header (mvhd) atom
|
|
@@ -103,19 +105,6 @@ export class MP4Parser extends BasicParser {
|
|
|
103
105
|
this.metadata.setFormat('creationTime', mvhd.creationTime);
|
|
104
106
|
this.metadata.setFormat('modificationTime', mvhd.modificationTime);
|
|
105
107
|
},
|
|
106
|
-
/**
|
|
107
|
-
* Parse media header (mdhd) atom
|
|
108
|
-
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25615
|
|
109
|
-
*/
|
|
110
|
-
mdhd: async (len) => {
|
|
111
|
-
const mdhd_data = await this.tokenizer.readToken(new AtomToken.MdhdAtom(len));
|
|
112
|
-
// this.parse_mxhd(mdhd_data, this.currentTrack);
|
|
113
|
-
const td = this.getTrackDescription();
|
|
114
|
-
td.creationTime = mdhd_data.creationTime;
|
|
115
|
-
td.modificationTime = mdhd_data.modificationTime;
|
|
116
|
-
td.timeScale = mdhd_data.timeScale;
|
|
117
|
-
td.duration = mdhd_data.duration;
|
|
118
|
-
},
|
|
119
108
|
chap: async (len) => {
|
|
120
109
|
const td = this.getTrackDescription();
|
|
121
110
|
const trackIds = [];
|
|
@@ -125,11 +114,6 @@ export class MP4Parser extends BasicParser {
|
|
|
125
114
|
}
|
|
126
115
|
td.chapterList = trackIds;
|
|
127
116
|
},
|
|
128
|
-
tkhd: async (len) => {
|
|
129
|
-
const track = (await this.tokenizer.readToken(new AtomToken.TrackHeaderAtom(len)));
|
|
130
|
-
track.fragments = [];
|
|
131
|
-
this.tracks.push(track);
|
|
132
|
-
},
|
|
133
117
|
/**
|
|
134
118
|
* Parse mdat atom.
|
|
135
119
|
* Will scan for chapters
|
|
@@ -138,10 +122,10 @@ export class MP4Parser extends BasicParser {
|
|
|
138
122
|
this.audioLengthInBytes = len;
|
|
139
123
|
this.calculateBitRate();
|
|
140
124
|
if (this.options.includeChapters) {
|
|
141
|
-
const trackWithChapters = this.tracks.filter(track => track.chapterList);
|
|
125
|
+
const trackWithChapters = [...this.tracks.values()].filter(track => track.chapterList);
|
|
142
126
|
if (trackWithChapters.length === 1) {
|
|
143
127
|
const chapterTrackIds = trackWithChapters[0].chapterList;
|
|
144
|
-
const chapterTracks = this.tracks.filter(track => chapterTrackIds.indexOf(track.trackId) !== -1);
|
|
128
|
+
const chapterTracks = [...this.tracks.values()].filter(track => chapterTrackIds.indexOf(track.header.trackId) !== -1);
|
|
145
129
|
if (chapterTracks.length === 1) {
|
|
146
130
|
return this.parseChapterTrack(chapterTracks[0], trackWithChapters[0], len);
|
|
147
131
|
}
|
|
@@ -171,20 +155,6 @@ export class MP4Parser extends BasicParser {
|
|
|
171
155
|
const trackDescription = this.getTrackDescription();
|
|
172
156
|
trackDescription.soundSampleDescription = stsd.table.map(dfEntry => this.parseSoundSampleDescription(dfEntry));
|
|
173
157
|
},
|
|
174
|
-
/**
|
|
175
|
-
* sample-to-Chunk Atoms
|
|
176
|
-
*/
|
|
177
|
-
stsc: async (len) => {
|
|
178
|
-
const stsc = await this.tokenizer.readToken(new AtomToken.StscAtom(len));
|
|
179
|
-
this.getTrackDescription().sampleToChunkTable = stsc.entries;
|
|
180
|
-
},
|
|
181
|
-
/**
|
|
182
|
-
* time-to-sample table
|
|
183
|
-
*/
|
|
184
|
-
stts: async (len) => {
|
|
185
|
-
const stts = await this.tokenizer.readToken(new AtomToken.SttsAtom(len));
|
|
186
|
-
this.getTrackDescription().timeToSampleTable = stts.entries;
|
|
187
|
-
},
|
|
188
158
|
/**
|
|
189
159
|
* Parse sample-sizes atom ('stsz')
|
|
190
160
|
*/
|
|
@@ -194,13 +164,6 @@ export class MP4Parser extends BasicParser {
|
|
|
194
164
|
td.sampleSize = stsz.sampleSize;
|
|
195
165
|
td.sampleSizeTable = stsz.entries;
|
|
196
166
|
},
|
|
197
|
-
/**
|
|
198
|
-
* Parse chunk-offset atom ('stco')
|
|
199
|
-
*/
|
|
200
|
-
stco: async (len) => {
|
|
201
|
-
const stco = await this.tokenizer.readToken(new AtomToken.StcoAtom(len));
|
|
202
|
-
this.getTrackDescription().chunkOffsetTable = stco.entries; // remember chunk offsets
|
|
203
|
-
},
|
|
204
167
|
date: async (len) => {
|
|
205
168
|
const date = await this.tokenizer.readToken(new Token.StringType(len, 'utf-8'));
|
|
206
169
|
await this.addTag('date', date);
|
|
@@ -215,11 +178,10 @@ export class MP4Parser extends BasicParser {
|
|
|
215
178
|
}
|
|
216
179
|
return Number(token.get(array, 0));
|
|
217
180
|
}
|
|
218
|
-
getTrackById(trackId) {
|
|
219
|
-
return this.tracks.find(track => track.trackId === trackId);
|
|
220
|
-
}
|
|
221
181
|
async parse() {
|
|
222
|
-
this.
|
|
182
|
+
this.hasVideoTrack = false;
|
|
183
|
+
this.hasAudioTrack = true;
|
|
184
|
+
this.tracks.clear();
|
|
223
185
|
let remainingFileSize = this.tokenizer.fileInfo.size || 0;
|
|
224
186
|
while (!this.tokenizer.fileInfo.size || remainingFileSize > 0) {
|
|
225
187
|
try {
|
|
@@ -278,15 +240,15 @@ export class MP4Parser extends BasicParser {
|
|
|
278
240
|
if (formatList.length > 0) {
|
|
279
241
|
this.metadata.setFormat('codec', formatList.filter(distinct).join('+'));
|
|
280
242
|
}
|
|
281
|
-
const audioTracks = this.tracks.filter(track => {
|
|
243
|
+
const audioTracks = [...this.tracks.values()].filter(track => {
|
|
282
244
|
return track.soundSampleDescription.length >= 1 && track.soundSampleDescription[0].description && track.soundSampleDescription[0].description.numAudioChannels > 0;
|
|
283
245
|
});
|
|
284
246
|
if (audioTracks.length >= 1) {
|
|
285
247
|
const audioTrack = audioTracks[0];
|
|
286
|
-
if (audioTrack.timeScale > 0) {
|
|
287
|
-
if (audioTrack.duration > 0) {
|
|
248
|
+
if (audioTrack.media.header && audioTrack.media.header.timeScale > 0) {
|
|
249
|
+
if (audioTrack.media.header.duration > 0) {
|
|
288
250
|
debug('Using duration defined on audio track');
|
|
289
|
-
const duration = audioTrack.duration / audioTrack.timeScale; // calculate duration in seconds
|
|
251
|
+
const duration = audioTrack.media.header.duration / audioTrack.media.header.timeScale; // calculate duration in seconds
|
|
290
252
|
this.metadata.setFormat('duration', duration);
|
|
291
253
|
}
|
|
292
254
|
else if (audioTrack.fragments.length > 0) {
|
|
@@ -302,15 +264,15 @@ export class MP4Parser extends BasicParser {
|
|
|
302
264
|
totalTimeUnits += dur;
|
|
303
265
|
}
|
|
304
266
|
}
|
|
305
|
-
this.metadata.setFormat('duration', totalTimeUnits / audioTrack.timeScale);
|
|
267
|
+
this.metadata.setFormat('duration', totalTimeUnits / audioTrack.media.header.timeScale);
|
|
306
268
|
}
|
|
307
269
|
}
|
|
308
270
|
const ssd = audioTrack.soundSampleDescription[0];
|
|
309
|
-
if (ssd.description) {
|
|
271
|
+
if (ssd.description && audioTrack.media.header) {
|
|
310
272
|
this.metadata.setFormat('sampleRate', ssd.description.sampleRate);
|
|
311
273
|
this.metadata.setFormat('bitsPerSample', ssd.description.sampleSize);
|
|
312
274
|
this.metadata.setFormat('numberOfChannels', ssd.description.numAudioChannels);
|
|
313
|
-
if (audioTrack.timeScale === 0 && audioTrack.timeToSampleTable.length > 0) {
|
|
275
|
+
if (audioTrack.media.header.timeScale === 0 && audioTrack.timeToSampleTable.length > 0) {
|
|
314
276
|
const totalSampleSize = audioTrack.timeToSampleTable
|
|
315
277
|
.map(ttstEntry => ttstEntry.count * ttstEntry.duration)
|
|
316
278
|
.reduce((total, sampleSize) => total + sampleSize);
|
|
@@ -324,6 +286,8 @@ export class MP4Parser extends BasicParser {
|
|
|
324
286
|
}
|
|
325
287
|
this.calculateBitRate();
|
|
326
288
|
}
|
|
289
|
+
this.metadata.setFormat('hasAudio', this.hasAudioTrack);
|
|
290
|
+
this.metadata.setFormat('hasVideo', this.hasVideoTrack);
|
|
327
291
|
}
|
|
328
292
|
async handleAtom(atom, remaining) {
|
|
329
293
|
if (atom.parent) {
|
|
@@ -331,6 +295,12 @@ export class MP4Parser extends BasicParser {
|
|
|
331
295
|
case 'ilst':
|
|
332
296
|
case '<id>':
|
|
333
297
|
return this.parseMetadataItemData(atom);
|
|
298
|
+
case 'moov':
|
|
299
|
+
switch (atom.header.name) {
|
|
300
|
+
case 'trak':
|
|
301
|
+
return this.parseTrackBox(atom);
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
334
304
|
case 'moof':
|
|
335
305
|
switch (atom.header.name) {
|
|
336
306
|
case 'traf':
|
|
@@ -346,7 +316,9 @@ export class MP4Parser extends BasicParser {
|
|
|
346
316
|
await this.tokenizer.ignore(remaining);
|
|
347
317
|
}
|
|
348
318
|
getTrackDescription() {
|
|
349
|
-
|
|
319
|
+
// ToDo: pick the right track, not the last track!!!!
|
|
320
|
+
const tracks = [...this.tracks.values()];
|
|
321
|
+
return tracks[tracks.length - 1];
|
|
350
322
|
}
|
|
351
323
|
calculateBitRate() {
|
|
352
324
|
if (this.audioLengthInBytes && this.metadata.format.duration) {
|
|
@@ -459,6 +431,82 @@ export class MP4Parser extends BasicParser {
|
|
|
459
431
|
this.addWarning(`atom key=${tagKey}, has unknown well-known-type (data-type): ${dataAtom.type.type}`);
|
|
460
432
|
}
|
|
461
433
|
}
|
|
434
|
+
async parseTrackBox(trakBox) {
|
|
435
|
+
// @ts-ignore
|
|
436
|
+
const track = {
|
|
437
|
+
media: {},
|
|
438
|
+
fragments: []
|
|
439
|
+
};
|
|
440
|
+
await trakBox.readAtoms(this.tokenizer, async (child, remaining) => {
|
|
441
|
+
const payLoadLength = child.getPayloadLength(remaining);
|
|
442
|
+
switch (child.header.name) {
|
|
443
|
+
case 'chap': {
|
|
444
|
+
const chap = await this.tokenizer.readToken(new ChapterTrackReferenceBox(remaining));
|
|
445
|
+
track.chapterList = chap;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
case 'tkhd': // TrackHeaderBox
|
|
449
|
+
track.header = await this.tokenizer.readToken(new AtomToken.TrackHeaderAtom(payLoadLength));
|
|
450
|
+
break;
|
|
451
|
+
case 'hdlr': // TrackHeaderBox
|
|
452
|
+
track.handler = await this.tokenizer.readToken(new AtomToken.HandlerBox(payLoadLength));
|
|
453
|
+
switch (track.handler.handlerType) {
|
|
454
|
+
case 'audi':
|
|
455
|
+
debug('Contains audio track');
|
|
456
|
+
this.hasAudioTrack = true;
|
|
457
|
+
break;
|
|
458
|
+
case 'vide':
|
|
459
|
+
debug('Contains video track');
|
|
460
|
+
this.hasVideoTrack = true;
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
break;
|
|
464
|
+
case 'mdhd': { // Parse media header (mdhd) box
|
|
465
|
+
const mdhd_data = await this.tokenizer.readToken(new AtomToken.MdhdAtom(payLoadLength));
|
|
466
|
+
track.media.header = mdhd_data;
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
case 'stco': {
|
|
470
|
+
const stco = await this.tokenizer.readToken(new AtomToken.StcoAtom(payLoadLength));
|
|
471
|
+
track.chunkOffsetTable = stco.entries; // remember chunk offsets
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
case 'stsc': { // sample-to-Chunk box
|
|
475
|
+
const stsc = await this.tokenizer.readToken(new AtomToken.StscAtom(payLoadLength));
|
|
476
|
+
track.sampleToChunkTable = stsc.entries;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
case 'stsd': { // sample description box
|
|
480
|
+
const stsd = await this.tokenizer.readToken(new AtomToken.StsdAtom(payLoadLength));
|
|
481
|
+
track.soundSampleDescription = stsd.table.map(dfEntry => this.parseSoundSampleDescription(dfEntry));
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
case 'stts': { // time-to-sample table
|
|
485
|
+
const stts = await this.tokenizer.readToken(new AtomToken.SttsAtom(payLoadLength));
|
|
486
|
+
track.timeToSampleTable = stts.entries;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
case 'stsz': {
|
|
490
|
+
const stsz = await this.tokenizer.readToken(new AtomToken.StszAtom(payLoadLength));
|
|
491
|
+
track.sampleSize = stsz.sampleSize;
|
|
492
|
+
track.sampleSizeTable = stsz.entries;
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
case 'dinf':
|
|
496
|
+
case 'vmhd':
|
|
497
|
+
case 'smhd':
|
|
498
|
+
debug(`Ignoring: ${child.header.name}`);
|
|
499
|
+
await this.tokenizer.ignore(payLoadLength);
|
|
500
|
+
break;
|
|
501
|
+
default: {
|
|
502
|
+
debug(`Unexpected track box: ${child.header.name}`);
|
|
503
|
+
await this.tokenizer.ignore(payLoadLength);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}, trakBox.getPayloadLength(0));
|
|
507
|
+
// Register track
|
|
508
|
+
this.tracks.set(track.header.trackId, track);
|
|
509
|
+
}
|
|
462
510
|
parseTrackFragmentBox(trafBox) {
|
|
463
511
|
let tfhd;
|
|
464
512
|
return trafBox.readAtoms(this.tokenizer, async (child, remaining) => {
|
|
@@ -476,7 +524,7 @@ export class MP4Parser extends BasicParser {
|
|
|
476
524
|
const trackRunBox = new AtomToken.TrackRunBox(payLoadLength);
|
|
477
525
|
const trun = await this.tokenizer.readToken(trackRunBox);
|
|
478
526
|
if (tfhd) {
|
|
479
|
-
const track = this.
|
|
527
|
+
const track = this.tracks.get(tfhd.trackId);
|
|
480
528
|
track?.fragments.push({ header: tfhd, trackRun: trun });
|
|
481
529
|
}
|
|
482
530
|
break;
|
|
@@ -532,11 +580,11 @@ export class MP4Parser extends BasicParser {
|
|
|
532
580
|
debug(`Chapter ${i + 1}: ${title}`);
|
|
533
581
|
const chapter = {
|
|
534
582
|
title,
|
|
535
|
-
timeScale: chapterTrack.timeScale,
|
|
583
|
+
timeScale: chapterTrack.media.header ? chapterTrack.media.header.timeScale : 0,
|
|
536
584
|
start,
|
|
537
585
|
sampleOffset: this.findSampleOffset(track, this.tokenizer.position)
|
|
538
586
|
};
|
|
539
|
-
debug(`Chapter title=${chapter.title}, offset=${chapter.sampleOffset}/${
|
|
587
|
+
debug(`Chapter title=${chapter.title}, offset=${chapter.sampleOffset}/${track.header.duration}`); // ToDo, use media duration if required!!!
|
|
540
588
|
chapters.push(chapter);
|
|
541
589
|
}
|
|
542
590
|
this.metadata.setFormat('chapters', chapters);
|
package/lib/mpeg/MpegParser.js
CHANGED
|
@@ -27,6 +27,7 @@ export class OpusParser extends VorbisParser {
|
|
|
27
27
|
throw new OpusContentError("Illegal ogg/Opus magic-signature");
|
|
28
28
|
this.metadata.setFormat('sampleRate', this.idHeader.inputSampleRate);
|
|
29
29
|
this.metadata.setFormat('numberOfChannels', this.idHeader.channelCount);
|
|
30
|
+
this.metadata.setAudioOnly();
|
|
30
31
|
}
|
|
31
32
|
async parseFullPage(pageData) {
|
|
32
33
|
const magicSignature = new Token.StringType(8, 'ascii').get(pageData, 0);
|
|
@@ -34,6 +34,7 @@ export class TheoraParser {
|
|
|
34
34
|
this.metadata.setFormat('codec', 'Theora');
|
|
35
35
|
const idHeader = IdentificationHeader.get(pageData, 0);
|
|
36
36
|
this.metadata.setFormat('bitrate', idHeader.nombr);
|
|
37
|
+
this.metadata.setAudioOnly();
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
//# sourceMappingURL=TheoraParser.js.map
|
|
@@ -92,6 +92,7 @@ export class VorbisParser {
|
|
|
92
92
|
*/
|
|
93
93
|
parseFirstPage(_header, pageData) {
|
|
94
94
|
this.metadata.setFormat('codec', 'Vorbis I');
|
|
95
|
+
this.metadata.setAudioOnly();
|
|
95
96
|
debug('Parse first page');
|
|
96
97
|
// Parse Vorbis common header
|
|
97
98
|
const commonHeader = CommonHeader.get(pageData, 0);
|
package/lib/type.d.ts
CHANGED
|
@@ -370,7 +370,7 @@ export interface IRatio {
|
|
|
370
370
|
*/
|
|
371
371
|
dB: number;
|
|
372
372
|
}
|
|
373
|
-
export type FormatId = 'container' | 'duration' | 'bitrate' | 'sampleRate' | 'bitsPerSample' | 'codec' | 'tool' | 'codecProfile' | 'lossless' | 'numberOfChannels' | 'numberOfSamples' | 'audioMD5' | 'chapters' | 'modificationTime' | 'creationTime' | 'trackPeakLevel' | 'trackGain' | 'albumGain';
|
|
373
|
+
export type FormatId = 'container' | 'duration' | 'bitrate' | 'sampleRate' | 'bitsPerSample' | 'codec' | 'tool' | 'codecProfile' | 'lossless' | 'numberOfChannels' | 'numberOfSamples' | 'audioMD5' | 'chapters' | 'modificationTime' | 'creationTime' | 'trackPeakLevel' | 'trackGain' | 'albumGain' | 'hasAudio' | 'hasVideo';
|
|
374
374
|
export interface IAudioTrack {
|
|
375
375
|
samplingFrequency?: number;
|
|
376
376
|
outputSamplingFrequency?: number;
|
|
@@ -470,6 +470,14 @@ export interface IFormat {
|
|
|
470
470
|
readonly trackGain?: number;
|
|
471
471
|
readonly trackPeakLevel?: number;
|
|
472
472
|
readonly albumGain?: number;
|
|
473
|
+
/**
|
|
474
|
+
* Indicates if the audio files contains an audio stream
|
|
475
|
+
*/
|
|
476
|
+
hasAudio?: boolean;
|
|
477
|
+
/**
|
|
478
|
+
* Indicates if the media files contains a video stream
|
|
479
|
+
*/
|
|
480
|
+
hasVideo?: boolean;
|
|
473
481
|
}
|
|
474
482
|
export interface ITag {
|
|
475
483
|
id: string;
|
package/lib/wav/WaveParser.js
CHANGED
|
@@ -31,6 +31,7 @@ export class WaveParser extends BasicParser {
|
|
|
31
31
|
debug(`pos=${this.tokenizer.position}, parse: chunkID=${riffHeader.chunkID}`);
|
|
32
32
|
if (riffHeader.chunkID !== 'RIFF')
|
|
33
33
|
return; // Not RIFF format
|
|
34
|
+
this.metadata.setAudioOnly();
|
|
34
35
|
return this.parseRiffChunk(riffHeader.chunkSize).catch(err => {
|
|
35
36
|
if (!(err instanceof strtok3.EndOfStreamError)) {
|
|
36
37
|
throw err;
|
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.5.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Borewit",
|
|
7
7
|
"url": "https://github.com/Borewit"
|
|
@@ -110,18 +110,18 @@
|
|
|
110
110
|
"file-type": "^21.0.0",
|
|
111
111
|
"media-typer": "^1.1.0",
|
|
112
112
|
"strtok3": "^10.3.1",
|
|
113
|
-
"token-types": "^6.0.
|
|
113
|
+
"token-types": "^6.0.3",
|
|
114
114
|
"uint8array-extras": "^1.4.0"
|
|
115
115
|
},
|
|
116
116
|
"devDependencies": {
|
|
117
|
-
"@biomejs/biome": "2.0.
|
|
117
|
+
"@biomejs/biome": "2.0.6",
|
|
118
118
|
"@types/chai": "^5.2.2",
|
|
119
119
|
"@types/chai-as-promised": "^8.0.2",
|
|
120
120
|
"@types/content-type": "^1.1.9",
|
|
121
121
|
"@types/debug": "^4.1.12",
|
|
122
122
|
"@types/media-typer": "^1.1.3",
|
|
123
123
|
"@types/mocha": "^10.0.10",
|
|
124
|
-
"@types/node": "^24.0.
|
|
124
|
+
"@types/node": "^24.0.4",
|
|
125
125
|
"c8": "^10.1.3",
|
|
126
126
|
"chai": "^5.2.0",
|
|
127
127
|
"chai-as-promised": "^8.0.1",
|