music-metadata 12.2.1 → 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.
- package/README.md +4 -0
- package/lib/ParserFactory.d.ts +1 -1
- package/lib/ParserFactory.js +4 -3
- package/lib/aiff/AiffParser.js +2 -5
- package/lib/common/Allocation.js +2 -2
- package/lib/common/CombinedTagMapper.js +0 -1
- package/lib/common/MetadataCollector.d.ts +5 -5
- package/lib/common/MetadataCollector.js +12 -4
- package/lib/dsdiff/DsdiffParser.js +2 -6
- package/lib/dsf/DsfParser.js +1 -1
- package/lib/ebml/EbmlIterator.d.ts +19 -15
- package/lib/ebml/EbmlIterator.js +46 -38
- package/lib/ebml/types.d.ts +36 -8
- package/lib/ebml/types.js +2 -1
- package/lib/flac/FlacParser.js +4 -12
- package/lib/id3v1/ID3v1Parser.d.ts +1 -1
- package/lib/id3v1/ID3v1Parser.js +2 -1
- package/lib/id3v2/EmbeddedID3v2.d.ts +5 -0
- package/lib/id3v2/EmbeddedID3v2.js +29 -0
- package/lib/matroska/MatroskaDtd.d.ts +630 -2
- package/lib/matroska/MatroskaDtd.js +57 -40
- package/lib/matroska/MatroskaParser.js +119 -55
- package/lib/matroska/types.d.ts +31 -106
- package/lib/mp4/MP4Parser.js +6 -2
- package/lib/mpeg/MpegParser.js +2 -2
- package/lib/ogg/OggStreamMetadata.d.ts +2 -2
- package/lib/ogg/vorbis/VorbisDecoder.d.ts +10 -0
- package/lib/ogg/vorbis/VorbisDecoder.js +15 -0
- package/lib/ogg/vorbis/VorbisStream.js +3 -9
- package/lib/type.d.ts +4 -2
- package/lib/wav/WaveParser.js +2 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -620,6 +620,7 @@ Returns a list of supported MIME-types. This may include some MIME-types which a
|
|
|
620
620
|
Maximum bytes buffered for a single metadata payload, accumulated Ogg packet, or tokenizer stream lookahead.
|
|
621
621
|
Must be a positive safe integer. Parsing rejects payloads above this limit before allocating them.
|
|
622
622
|
The parser configures this limit once on its `strtok3` tokenizers, including tokenizers for embedded tags.
|
|
623
|
+
The same limit applies to `file-type` read buffers when detecting the media format or a cover art MIME-type.
|
|
623
624
|
Oversized tokenizer allocations or stream lookahead reject with `strtok3.BufferSizeError`.
|
|
624
625
|
When using `parseFromTokenizer()`, also configure `maxBufferSize` on the supplied tokenizer at creation.
|
|
625
626
|
Increase it for files with unusually large metadata or cover art, or lower it for constrained environments.
|
|
@@ -641,6 +642,9 @@ Returns a list of supported MIME-types. This may include some MIME-types which a
|
|
|
641
642
|
|
|
642
643
|
Callback function triggered when common tags or format properties are updated during parsing.
|
|
643
644
|
Allows real-time monitoring of metadata as it becomes available.
|
|
645
|
+
The callback runs synchronously after the update; returned promises are not awaited.
|
|
646
|
+
Repeated assignments of unchanged primitive format values do not trigger callbacks.
|
|
647
|
+
Keep callback work lightweight, or batch expensive operations in the consuming application.
|
|
644
648
|
|
|
645
649
|
- `skipCovers`: `boolean` (default: `false`)
|
|
646
650
|
|
package/lib/ParserFactory.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type MediaType } from 'media-typer';
|
|
2
|
-
import type
|
|
2
|
+
import { type ITokenizer } from 'strtok3';
|
|
3
3
|
import { type INativeMetadataCollector } from './common/MetadataCollector.js';
|
|
4
4
|
import { type IAudioMetadata, type IOptions, type ParserType } from './type.js';
|
|
5
5
|
export interface IParserLoader {
|
package/lib/ParserFactory.js
CHANGED
|
@@ -2,6 +2,7 @@ import { parse as parseContentType } from 'content-type';
|
|
|
2
2
|
import initDebug from 'debug';
|
|
3
3
|
import { fileTypeFromBuffer } from 'file-type';
|
|
4
4
|
import { parse as mimeTypeParse } from 'media-typer';
|
|
5
|
+
import { isRandomAccessTokenizer } from 'strtok3';
|
|
5
6
|
import { aiffParserLoader } from './aiff/AiffLoader.js';
|
|
6
7
|
import { apeParserLoader } from './apev2/Apev2Loader.js';
|
|
7
8
|
import { asfParserLoader } from './asf/AsfLoader.js';
|
|
@@ -59,8 +60,8 @@ export class ParserFactory {
|
|
|
59
60
|
this.parsers.push(parser);
|
|
60
61
|
}
|
|
61
62
|
async parse(tokenizer, parserLoader, opts = {}) {
|
|
62
|
-
getMaxMemoryAllocation(opts);
|
|
63
|
-
if (tokenizer
|
|
63
|
+
const maxBufferSize = getMaxMemoryAllocation(opts);
|
|
64
|
+
if (isRandomAccessTokenizer(tokenizer)) {
|
|
64
65
|
debug('tokenizer supports random-access, scanning for appending headers');
|
|
65
66
|
await scanAppendingHeaders(tokenizer, opts);
|
|
66
67
|
}
|
|
@@ -79,7 +80,7 @@ export class ParserFactory {
|
|
|
79
80
|
// Parser could not be determined on MIME-type or extension
|
|
80
81
|
debug('Guess parser on content...');
|
|
81
82
|
await tokenizer.peekBuffer(buf, { mayBeLess: true });
|
|
82
|
-
const guessedType = await fileTypeFromBuffer(buf, { mpegOffsetTolerance: 10 });
|
|
83
|
+
const guessedType = await fileTypeFromBuffer(buf, { mpegOffsetTolerance: 10, maxBufferSize });
|
|
83
84
|
if (!guessedType?.mime) {
|
|
84
85
|
throw new CouldNotDetermineFileTypeError('Failed to determine audio format');
|
|
85
86
|
}
|
package/lib/aiff/AiffParser.js
CHANGED
|
@@ -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 {
|
|
6
|
+
import { parseEmbeddedID3v2 } from '../id3v2/EmbeddedID3v2.js';
|
|
8
7
|
import * as iff from '../iff/index.js';
|
|
9
8
|
import * as AiffToken from './AiffToken.js';
|
|
10
9
|
import { AiffContentError, compressionTypes } from './AiffToken.js';
|
|
@@ -77,9 +76,7 @@ export class AIFFParser extends BasicParser {
|
|
|
77
76
|
}
|
|
78
77
|
case 'ID3 ': {
|
|
79
78
|
// ID3-meta-data
|
|
80
|
-
|
|
81
|
-
const rst = strtok3.fromBuffer(id3_data, getTokenizerOptions(this.options));
|
|
82
|
-
await new ID3v2Parser().parse(this.metadata, rst, this.options);
|
|
79
|
+
await parseEmbeddedID3v2(this.metadata, this.tokenizer, this.options, header.chunkSize);
|
|
83
80
|
return header.chunkSize;
|
|
84
81
|
}
|
|
85
82
|
case 'SSND': // Sound Data Chunk
|
package/lib/common/Allocation.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EndOfStreamError } from 'strtok3';
|
|
1
|
+
import { EndOfStreamError, isRandomAccessTokenizer } from 'strtok3';
|
|
2
2
|
import { UnexpectedFileContentError } from '../ParseError.js';
|
|
3
3
|
const defaultMaxMemoryAllocation = 64 * 1024 * 1024;
|
|
4
4
|
const readChunkSize = 64 * 1024;
|
|
@@ -33,7 +33,7 @@ function validateAvailableBytes(tokenizer, length, position) {
|
|
|
33
33
|
export async function readBuffer(tokenizer, length, options, position = tokenizer.position, makeError = allocationError) {
|
|
34
34
|
validateAllocation(length, options, makeError);
|
|
35
35
|
validateAvailableBytes(tokenizer, length, position);
|
|
36
|
-
if (
|
|
36
|
+
if (!isRandomAccessTokenizer(tokenizer) && length > readChunkSize) {
|
|
37
37
|
const chunks = [];
|
|
38
38
|
for (let remaining = length; remaining > 0;) {
|
|
39
39
|
const chunk = new Uint8Array(Math.min(remaining, readChunkSize));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AnyTagValue, type
|
|
1
|
+
import { type AnyTagValue, type IAudioMetadata, type ICommonTagsResult, type IFormat, type INativeTags, type IOptions, type IQualityInformation, type ITrackInfo } from '../type.js';
|
|
2
2
|
import { type IGenericTag, type TagType } from './GenericTagTypes.js';
|
|
3
3
|
/**
|
|
4
4
|
* Combines all generic-tag-mappers for each tag type
|
|
@@ -21,15 +21,15 @@ export interface INativeMetadataCollector extends IWarningCollector {
|
|
|
21
21
|
* @returns {boolean} true if one or more tags have been found
|
|
22
22
|
*/
|
|
23
23
|
hasAny(): boolean;
|
|
24
|
-
setFormat(key:
|
|
24
|
+
setFormat<K extends keyof IFormat>(key: K, value: IFormat[K]): void;
|
|
25
25
|
registerTagType(tagType: TagType): void;
|
|
26
26
|
addTag(tagType: TagType, tagId: string, value: AnyTagValue): Promise<void>;
|
|
27
27
|
addStreamInfo(streamInfo: ITrackInfo): void;
|
|
28
28
|
setAudioOnly(): void;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* Provided to the parser to
|
|
32
|
-
* Responsible for
|
|
31
|
+
* Provided to the parser to update the metadata result.
|
|
32
|
+
* Responsible for notifying observers synchronously after updates.
|
|
33
33
|
*/
|
|
34
34
|
export declare class MetadataCollector implements INativeMetadataCollector {
|
|
35
35
|
readonly format: IFormat;
|
|
@@ -52,7 +52,7 @@ export declare class MetadataCollector implements INativeMetadataCollector {
|
|
|
52
52
|
*/
|
|
53
53
|
hasAny(): boolean;
|
|
54
54
|
addStreamInfo(streamInfo: ITrackInfo): void;
|
|
55
|
-
setFormat(key:
|
|
55
|
+
setFormat<K extends keyof IFormat>(key: K, value: IFormat[K]): void;
|
|
56
56
|
setAudioOnly(): void;
|
|
57
57
|
registerTagType(tagType: TagType): void;
|
|
58
58
|
addTag(tagType: TagType, tagId: string, value: AnyTagValue): Promise<void>;
|
|
@@ -2,6 +2,7 @@ import initDebug from 'debug';
|
|
|
2
2
|
import { fileTypeFromBuffer } from 'file-type';
|
|
3
3
|
import { parseLyrics } from '../lrc/LyricsParser.js';
|
|
4
4
|
import { TrackTypeValueToKeyMap } from '../type.js';
|
|
5
|
+
import { getMaxMemoryAllocation } from './Allocation.js';
|
|
5
6
|
import { CombinedTagMapper } from './CombinedTagMapper.js';
|
|
6
7
|
import { CommonTagMapper } from './GenericTagMapper.js';
|
|
7
8
|
import { isSingleton, isUnique } from './GenericTagTypes.js';
|
|
@@ -23,8 +24,8 @@ const TagPriority = [
|
|
|
23
24
|
'ID3v1'
|
|
24
25
|
];
|
|
25
26
|
/**
|
|
26
|
-
* Provided to the parser to
|
|
27
|
-
* Responsible for
|
|
27
|
+
* Provided to the parser to update the metadata result.
|
|
28
|
+
* Responsible for notifying observers synchronously after updates.
|
|
28
29
|
*/
|
|
29
30
|
export class MetadataCollector {
|
|
30
31
|
format = {
|
|
@@ -73,8 +74,13 @@ export class MetadataCollector {
|
|
|
73
74
|
this.format.trackInfo.push(streamInfo);
|
|
74
75
|
}
|
|
75
76
|
setFormat(key, value) {
|
|
77
|
+
// Objects may have been mutated in place, so only skip unchanged primitives.
|
|
78
|
+
if ((value === null || typeof value !== 'object') && Object.is(this.format[key], value)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
76
81
|
debug(`format: ${key} = ${value}`);
|
|
77
|
-
|
|
82
|
+
// Allow internal writes while preserving each property's value type.
|
|
83
|
+
this.format[key] = value;
|
|
78
84
|
if (this.opts?.observer) {
|
|
79
85
|
this.opts.observer({ metadata: this, tag: { type: 'format', id: key, value } });
|
|
80
86
|
}
|
|
@@ -260,7 +266,9 @@ export class MetadataCollector {
|
|
|
260
266
|
async postFixPicture(picture) {
|
|
261
267
|
if (picture.data && picture.data.length > 0) {
|
|
262
268
|
if (!picture.format) {
|
|
263
|
-
const fileType = await fileTypeFromBuffer(Uint8Array.from(picture.data)
|
|
269
|
+
const fileType = await fileTypeFromBuffer(Uint8Array.from(picture.data), {
|
|
270
|
+
maxBufferSize: getMaxMemoryAllocation(this.opts ?? {})
|
|
271
|
+
}); // ToDO: remove Buffer
|
|
264
272
|
if (fileType) {
|
|
265
273
|
picture.format = fileType.mime;
|
|
266
274
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import initDebug from 'debug';
|
|
2
|
-
import * as strtok3 from 'strtok3';
|
|
3
2
|
import * as Token from 'token-types';
|
|
4
|
-
import { getTokenizerOptions } from '../common/Allocation.js';
|
|
5
3
|
import { BasicParser } from '../common/BasicParser.js';
|
|
6
4
|
import { FourCcToken } from '../common/FourCC.js';
|
|
7
|
-
import {
|
|
5
|
+
import { parseEmbeddedID3v2 } from '../id3v2/EmbeddedID3v2.js';
|
|
8
6
|
import { makeUnexpectedFileContentError } from '../ParseError.js';
|
|
9
7
|
import { ChunkHeader64 } from './DsdiffToken.js';
|
|
10
8
|
const debug = initDebug('music-metadata:parser:dsdiff');
|
|
@@ -94,9 +92,7 @@ export class DsdiffParser extends BasicParser {
|
|
|
94
92
|
}
|
|
95
93
|
case 'ID3': {
|
|
96
94
|
// Unofficial ID3 tag support
|
|
97
|
-
|
|
98
|
-
const rst = strtok3.fromBuffer(id3_data, getTokenizerOptions(this.options));
|
|
99
|
-
await new ID3v2Parser().parse(this.metadata, rst, this.options);
|
|
95
|
+
await parseEmbeddedID3v2(this.metadata, this.tokenizer, this.options, Number(header.chunkSize));
|
|
100
96
|
break;
|
|
101
97
|
}
|
|
102
98
|
case 'DSD':
|
package/lib/dsf/DsfParser.js
CHANGED
|
@@ -64,7 +64,7 @@ export class DsfParser extends AbstractID3Parser {
|
|
|
64
64
|
this.metadata.setFormat('numberOfChannels', formatChunk.channelNum);
|
|
65
65
|
this.metadata.setFormat('sampleRate', formatChunk.samplingFrequency);
|
|
66
66
|
this.metadata.setFormat('bitsPerSample', formatChunk.bitsPerSample);
|
|
67
|
-
this.metadata.setFormat('numberOfSamples', formatChunk.sampleCount);
|
|
67
|
+
this.metadata.setFormat('numberOfSamples', Number(formatChunk.sampleCount));
|
|
68
68
|
this.metadata.setFormat('duration', Number(formatChunk.sampleCount) / formatChunk.samplingFrequency);
|
|
69
69
|
const bitrate = formatChunk.bitsPerSample * formatChunk.samplingFrequency * formatChunk.channelNum;
|
|
70
70
|
this.metadata.setFormat('bitrate', bitrate);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ITokenizer } from 'strtok3';
|
|
2
2
|
import type { IOptions } from '../type.js';
|
|
3
|
-
import { type
|
|
3
|
+
import { type DecodedElement, type ElementEvent, type IElementContext, type IElementType, type ValueType } from './types.js';
|
|
4
4
|
declare const EbmlContentError_base: {
|
|
5
5
|
new (message: string): {
|
|
6
6
|
message: string;
|
|
@@ -16,13 +16,8 @@ declare const EbmlContentError_base: {
|
|
|
16
16
|
};
|
|
17
17
|
export declare class EbmlContentError extends EbmlContentError_base {
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
parent: ILinkedElementType | undefined;
|
|
22
|
-
readonly container?: {
|
|
23
|
-
[id: number]: ILinkedElementType;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
19
|
+
/** @deprecated Use the per-visit IElementContext instead. */
|
|
20
|
+
export type ILinkedElementType = IElementContext;
|
|
26
21
|
export declare const ParseAction: {
|
|
27
22
|
readonly ReadNext: 0;
|
|
28
23
|
readonly IgnoreElement: 2;
|
|
@@ -31,12 +26,19 @@ export declare const ParseAction: {
|
|
|
31
26
|
readonly SkipElement: 5;
|
|
32
27
|
};
|
|
33
28
|
export type ParseAction = (typeof ParseAction)[keyof typeof ParseAction];
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
/** Traversal callbacks and optional subtree retention. */
|
|
30
|
+
export type IElementListener<S extends IElementType = IElementType> = {
|
|
31
|
+
startNext: (element: IElementContext) => ParseAction | Promise<ParseAction>;
|
|
32
|
+
/** Legacy untyped events; new consumers should use onElement. */
|
|
33
|
+
elementValue?: (element: IElementContext, value: ValueType, offset: number) => Promise<void>;
|
|
34
|
+
onElement?: (event: ElementEvent<S>) => Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Collect only subtrees rooted at matching containers. Descendants of a selected
|
|
37
|
+
* container are retained until its event completes; other ancestors stay empty.
|
|
38
|
+
* Omit this callback to collect the entire document (legacy iterator behavior).
|
|
39
|
+
* Leaf events are delivered even when their values are not retained.
|
|
40
|
+
*/
|
|
41
|
+
collect?: (element: IElementContext) => boolean;
|
|
40
42
|
};
|
|
41
43
|
/**
|
|
42
44
|
* Extensible Binary Meta Language (EBML) iterator
|
|
@@ -57,14 +59,16 @@ export declare class EbmlIterator {
|
|
|
57
59
|
* @param tokenizer
|
|
58
60
|
*/
|
|
59
61
|
constructor(tokenizer: ITokenizer, options?: IOptions);
|
|
60
|
-
iterate(dtdElement:
|
|
62
|
+
iterate<const S extends IElementType>(dtdElement: S, posDone: number, listener: IElementListener<S>): Promise<DecodedElement<S>>;
|
|
61
63
|
private parseContainer;
|
|
64
|
+
private collect;
|
|
62
65
|
private ignore;
|
|
63
66
|
private readVintData;
|
|
64
67
|
private readElement;
|
|
65
68
|
private readFloat;
|
|
66
69
|
private readFlag;
|
|
67
70
|
private readUint;
|
|
71
|
+
private readInt;
|
|
68
72
|
private readString;
|
|
69
73
|
private readBuffer;
|
|
70
74
|
}
|
package/lib/ebml/EbmlIterator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import initDebug from 'debug';
|
|
2
|
-
import { EndOfStreamError } from 'strtok3';
|
|
2
|
+
import { EndOfStreamError, isRandomAccessTokenizer } from 'strtok3';
|
|
3
3
|
import * as Token from 'token-types';
|
|
4
4
|
import { Float32_BE, Float64_BE, StringType, UINT8 } from 'token-types';
|
|
5
5
|
import { getMaxMemoryAllocation, readBuffer } from '../common/Allocation.js';
|
|
@@ -38,6 +38,7 @@ export class EbmlIterator {
|
|
|
38
38
|
this.tokenizer = tokenizer;
|
|
39
39
|
this.elementHeader = new Uint8Array(Math.min(12, getMaxMemoryAllocation(options)));
|
|
40
40
|
this.parserMap.set(DataType.uint, e => this.readUint(e));
|
|
41
|
+
this.parserMap.set(DataType.int, e => this.readInt(e));
|
|
41
42
|
this.parserMap.set(DataType.string, e => this.readString(e));
|
|
42
43
|
this.parserMap.set(DataType.binary, e => this.readBuffer(e));
|
|
43
44
|
this.parserMap.set(DataType.uid, async (e) => this.readBuffer(e));
|
|
@@ -45,9 +46,20 @@ export class EbmlIterator {
|
|
|
45
46
|
this.parserMap.set(DataType.float, e => this.readFloat(e));
|
|
46
47
|
}
|
|
47
48
|
async iterate(dtdElement, posDone, listener) {
|
|
48
|
-
|
|
49
|
+
const root = { ...dtdElement, id: 0, parent: undefined };
|
|
50
|
+
const tree = await this.parseContainer(root, posDone, {
|
|
51
|
+
startNext: listener.startNext,
|
|
52
|
+
collect: listener.collect ?? (() => true),
|
|
53
|
+
elementValue: async (element, value, offset) => {
|
|
54
|
+
await listener.elementValue?.(element, value, offset);
|
|
55
|
+
// The schema selects both the decoder and the event ID. Keep this bridge here,
|
|
56
|
+
// rather than asserting unrelated domain types in each parser listener.
|
|
57
|
+
await listener.onElement?.({ id: element.id, element, value, offset });
|
|
58
|
+
}
|
|
59
|
+
}, listener.collect?.(root) ?? true);
|
|
60
|
+
return tree;
|
|
49
61
|
}
|
|
50
|
-
async parseContainer(dtdElement, posDone, listener) {
|
|
62
|
+
async parseContainer(dtdElement, posDone, listener, collect) {
|
|
51
63
|
const tree = {};
|
|
52
64
|
while (this.tokenizer.position < posDone) {
|
|
53
65
|
let element;
|
|
@@ -61,34 +73,24 @@ export class EbmlIterator {
|
|
|
61
73
|
}
|
|
62
74
|
throw error;
|
|
63
75
|
}
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
const
|
|
76
|
+
const definition = dtdElement.container?.[element.id];
|
|
77
|
+
if (definition) {
|
|
78
|
+
const child = { ...definition, id: element.id, parent: dtdElement };
|
|
79
|
+
const action = await listener.startNext(child);
|
|
67
80
|
switch (action) {
|
|
68
81
|
case ParseAction.ReadNext:
|
|
69
82
|
{
|
|
70
|
-
if (element.id === 0x1f43b675) {
|
|
71
|
-
// Hack to ignore remaining segment, when cluster element received
|
|
72
|
-
// await this.tokenizer.ignore(posDone - this.tokenizer.position);
|
|
73
|
-
// break;
|
|
74
|
-
}
|
|
75
83
|
debug(`Read element: name=${getElementPath(child)}{id=0x${element.id.toString(16)}, container=${!!child.container}} at position=${elementPosition}`);
|
|
76
84
|
if (child.container) {
|
|
77
85
|
const childEnd = Math.min(posDone, this.tokenizer.fileInfo.size ?? Number.POSITIVE_INFINITY, element.len >= 0 ? this.tokenizer.position + element.len : posDone);
|
|
78
|
-
const res = await this.parseContainer(child, childEnd, listener);
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
tree[child.name] = [];
|
|
82
|
-
}
|
|
83
|
-
tree[child.name].push(res);
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
tree[child.name] = res;
|
|
86
|
+
const res = await this.parseContainer(child, childEnd, listener, collect || listener.collect(child));
|
|
87
|
+
if (collect) {
|
|
88
|
+
this.collect(tree, child, res);
|
|
87
89
|
}
|
|
88
90
|
await listener.elementValue(child, res, elementPosition);
|
|
89
91
|
}
|
|
90
92
|
else {
|
|
91
|
-
const parser = this.parserMap.get(child.value);
|
|
93
|
+
const parser = child.value === undefined ? undefined : this.parserMap.get(child.value);
|
|
92
94
|
if (typeof parser === 'function') {
|
|
93
95
|
// Validate before leaf readers allocate buffers or string tokens from the declared length.
|
|
94
96
|
const end = Math.min(posDone, this.tokenizer.fileInfo.size ?? Number.POSITIVE_INFINITY);
|
|
@@ -98,9 +100,14 @@ export class EbmlIterator {
|
|
|
98
100
|
throw new EbmlContentError(`Invalid element length: ${element.len}`);
|
|
99
101
|
}
|
|
100
102
|
const value = await parser(element);
|
|
101
|
-
|
|
103
|
+
if (collect) {
|
|
104
|
+
this.collect(tree, child, value);
|
|
105
|
+
}
|
|
102
106
|
await listener.elementValue(child, value, elementPosition);
|
|
103
107
|
}
|
|
108
|
+
else {
|
|
109
|
+
await this.ignore(element.len, posDone);
|
|
110
|
+
}
|
|
104
111
|
}
|
|
105
112
|
}
|
|
106
113
|
break;
|
|
@@ -133,6 +140,17 @@ export class EbmlIterator {
|
|
|
133
140
|
}
|
|
134
141
|
return tree;
|
|
135
142
|
}
|
|
143
|
+
collect(tree, element, value) {
|
|
144
|
+
if (element.multiple) {
|
|
145
|
+
const values = (tree[element.name] ??= []);
|
|
146
|
+
if (Array.isArray(values)) {
|
|
147
|
+
values.push(value);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
tree[element.name] = value;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
136
154
|
async ignore(length, posDone) {
|
|
137
155
|
// Unknown-size EBML containers may advertise lengths above the safe integer range.
|
|
138
156
|
// Bound skips by the enclosing container and known input before passing them to the tokenizer.
|
|
@@ -160,7 +178,7 @@ export class EbmlIterator {
|
|
|
160
178
|
return id;
|
|
161
179
|
}
|
|
162
180
|
async readElement() {
|
|
163
|
-
if (this.tokenizer
|
|
181
|
+
if (isRandomAccessTokenizer(this.tokenizer) && this.elementHeader.length === 12) {
|
|
164
182
|
// RFC 8794 (July 2020), sections 4-6: an ID and size occupy at most 4 + 8 bytes.
|
|
165
183
|
// Peek both VINTs together to avoid four separate file/network reads per element.
|
|
166
184
|
// https://www.rfc-editor.org/rfc/rfc8794.html#section-4
|
|
@@ -216,6 +234,11 @@ export class EbmlIterator {
|
|
|
216
234
|
const buf = await this.readBuffer(e);
|
|
217
235
|
return readUIntBE(buf, e.len);
|
|
218
236
|
}
|
|
237
|
+
async readInt(e) {
|
|
238
|
+
const buffer = await this.readBuffer(e);
|
|
239
|
+
const value = readUIntBeAsBigInt(buffer, e.len);
|
|
240
|
+
return Number(buffer[0] & 0x80 ? value - (1n << BigInt(8 * e.len)) : value);
|
|
241
|
+
}
|
|
219
242
|
async readString(e) {
|
|
220
243
|
const buf = await this.readBuffer(e);
|
|
221
244
|
const rawString = new StringType(e.len, 'utf-8').get(buf, 0);
|
|
@@ -245,21 +268,6 @@ function readUIntBeAsBigInt(buf, len) {
|
|
|
245
268
|
return BigInt(-1);
|
|
246
269
|
}
|
|
247
270
|
}
|
|
248
|
-
function linkParents(element) {
|
|
249
|
-
if (element.container) {
|
|
250
|
-
Object.keys(element.container)
|
|
251
|
-
.map(id => {
|
|
252
|
-
const child = element.container[id];
|
|
253
|
-
child.id = Number.parseInt(id, 10);
|
|
254
|
-
return child;
|
|
255
|
-
})
|
|
256
|
-
.forEach(child => {
|
|
257
|
-
child.parent = element;
|
|
258
|
-
linkParents(child);
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
return element;
|
|
262
|
-
}
|
|
263
271
|
export function getElementPath(element) {
|
|
264
272
|
let path = '';
|
|
265
273
|
if (element.parent && element.parent.name !== 'dtd') {
|
package/lib/ebml/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
export type PrimitiveValue = string | number | boolean | Uint8Array;
|
|
1
2
|
export interface ITree {
|
|
2
|
-
[name: string]:
|
|
3
|
+
[name: string]: PrimitiveValue | ITree | (PrimitiveValue | ITree)[];
|
|
3
4
|
}
|
|
4
5
|
export declare const DataType: {
|
|
5
6
|
readonly string: 0;
|
|
@@ -8,18 +9,19 @@ export declare const DataType: {
|
|
|
8
9
|
readonly bool: 3;
|
|
9
10
|
readonly binary: 4;
|
|
10
11
|
readonly float: 5;
|
|
12
|
+
readonly int: 6;
|
|
11
13
|
};
|
|
12
14
|
export type DataType = (typeof DataType)[keyof typeof DataType];
|
|
13
|
-
export type ValueType =
|
|
15
|
+
export type ValueType = PrimitiveValue | ITree;
|
|
14
16
|
export interface IHeader {
|
|
15
17
|
id: number;
|
|
16
18
|
len: number;
|
|
17
19
|
}
|
|
18
20
|
export interface IEbmlElements {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
ebmlVersion?: number;
|
|
22
|
+
ebmlReadVersion?: number;
|
|
23
|
+
ebmlMaxIDWidth?: number;
|
|
24
|
+
ebmlMaxSizeWidth?: number;
|
|
23
25
|
docType?: string;
|
|
24
26
|
docTypeVersion?: number;
|
|
25
27
|
docTypeReadVersion?: number;
|
|
@@ -28,10 +30,36 @@ export interface IElementType {
|
|
|
28
30
|
readonly name: string;
|
|
29
31
|
readonly value?: DataType;
|
|
30
32
|
readonly container?: {
|
|
31
|
-
[id: number]: IElementType;
|
|
33
|
+
readonly [id: number]: IElementType;
|
|
32
34
|
};
|
|
33
35
|
readonly multiple?: boolean;
|
|
34
36
|
}
|
|
35
37
|
export interface IEbmlDoc {
|
|
36
|
-
ebml
|
|
38
|
+
ebml?: IEbmlElements;
|
|
37
39
|
}
|
|
40
|
+
/** Values reflect what was actually read: even required EBML elements can be absent or skipped. */
|
|
41
|
+
export type DecodedElement<S extends IElementType> = S extends {
|
|
42
|
+
readonly container: infer C extends Readonly<Record<number, IElementType>>;
|
|
43
|
+
} ? {
|
|
44
|
+
-readonly [E in C[keyof C & number] as E['name']]?: E extends {
|
|
45
|
+
readonly multiple: true;
|
|
46
|
+
} ? DecodedElement<E>[] : DecodedElement<E>;
|
|
47
|
+
} : S extends {
|
|
48
|
+
readonly value: infer V extends DataType;
|
|
49
|
+
} ? V extends typeof DataType.string ? string : V extends typeof DataType.bool ? boolean : V extends typeof DataType.binary | typeof DataType.uid ? Uint8Array : number : ITree;
|
|
50
|
+
/** Per-visit context; the schema itself never gains IDs or parent links. */
|
|
51
|
+
export interface IElementContext extends IElementType {
|
|
52
|
+
readonly id: number;
|
|
53
|
+
readonly parent: IElementContext | undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Discriminating by ID narrows the decoded value along with the element. */
|
|
56
|
+
export type ElementEvent<S extends IElementType> = S extends {
|
|
57
|
+
readonly container: infer C extends Readonly<Record<number, IElementType>>;
|
|
58
|
+
} ? {
|
|
59
|
+
[Id in keyof C & number]: {
|
|
60
|
+
id: Id;
|
|
61
|
+
element: IElementContext;
|
|
62
|
+
value: DecodedElement<C[Id]>;
|
|
63
|
+
offset: number;
|
|
64
|
+
} | ElementEvent<C[Id]>;
|
|
65
|
+
}[keyof C & number] : never;
|
package/lib/ebml/types.js
CHANGED
package/lib/flac/FlacParser.js
CHANGED
|
@@ -92,24 +92,16 @@ export class FlacParser extends AbstractID3Parser {
|
|
|
92
92
|
*/
|
|
93
93
|
async parseComment(data) {
|
|
94
94
|
const decoder = new VorbisDecoder(data, 0);
|
|
95
|
-
const vendor = decoder.
|
|
95
|
+
const { vendor, comments } = decoder.parseCommentList(message => new FlacContentError(message));
|
|
96
96
|
if (vendor.length > 0) {
|
|
97
97
|
this.metadata.setFormat('tool', vendor);
|
|
98
98
|
}
|
|
99
|
-
const
|
|
100
|
-
if (commentListLength > Math.floor(decoder.remaining / 4)) {
|
|
101
|
-
throw new FlacContentError(`Vorbis comment count ${commentListLength} exceeds remaining data`);
|
|
102
|
-
}
|
|
103
|
-
const tags = [];
|
|
104
|
-
for (let i = 0; i < commentListLength; i++) {
|
|
105
|
-
tags.push(decoder.parseUserComment());
|
|
106
|
-
}
|
|
107
|
-
await Promise.all(tags.map(tag => {
|
|
99
|
+
for (const tag of comments) {
|
|
108
100
|
if (tag.key === 'ENCODER') {
|
|
109
101
|
this.metadata.setFormat('tool', tag.value);
|
|
110
102
|
}
|
|
111
|
-
|
|
112
|
-
}
|
|
103
|
+
await this.addTag(tag.key, tag.value);
|
|
104
|
+
}
|
|
113
105
|
}
|
|
114
106
|
async parsePicture(dataLen) {
|
|
115
107
|
if (this.options.skipCovers) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type IRandomAccessTokenizer, type ITokenizer } from 'strtok3';
|
|
2
2
|
import { BasicParser } from '../common/BasicParser.js';
|
|
3
3
|
import type { INativeMetadataCollector } from '../common/MetadataCollector.js';
|
|
4
4
|
import type { IPrivateOptions } from '../type.js';
|
package/lib/id3v1/ID3v1Parser.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { textDecode } from '@borewit/text-codec';
|
|
2
2
|
import initDebug from 'debug';
|
|
3
|
+
import { isRandomAccessTokenizer } from 'strtok3';
|
|
3
4
|
import { StringType, UINT8 } from 'token-types';
|
|
4
5
|
import { APEv2Parser } from '../apev2/APEv2Parser.js';
|
|
5
6
|
import { BasicParser } from '../common/BasicParser.js';
|
|
@@ -266,7 +267,7 @@ export class ID3v1Parser extends BasicParser {
|
|
|
266
267
|
debug('Skip checking for ID3v1 because the file-size is unknown');
|
|
267
268
|
return;
|
|
268
269
|
}
|
|
269
|
-
if (this.apeHeader && this.tokenizer
|
|
270
|
+
if (this.apeHeader && isRandomAccessTokenizer(this.tokenizer)) {
|
|
270
271
|
this.tokenizer.setPosition(this.apeHeader.offset);
|
|
271
272
|
const apeParser = new APEv2Parser(this.metadata, this.tokenizer, this.options);
|
|
272
273
|
await apeParser.parseTags(this.apeHeader.footer);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ITokenizer } from 'strtok3';
|
|
2
|
+
import type { INativeMetadataCollector } from '../common/MetadataCollector.js';
|
|
3
|
+
import type { IOptions } from '../type.js';
|
|
4
|
+
/** Borrow the chunk payload; consume its remainder while leaving container padding to the caller. */
|
|
5
|
+
export declare function parseEmbeddedID3v2(metadata: INativeMetadataCollector, tokenizer: ITokenizer, options: IOptions, size: number): Promise<void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createTokenizerView, EndOfStreamError, TokenizerBoundaryError } from 'strtok3';
|
|
2
|
+
import { validateAllocation } from '../common/Allocation.js';
|
|
3
|
+
import { Id3v2ContentError } from './FrameParser.js';
|
|
4
|
+
import { ID3v2Parser } from './ID3v2Parser.js';
|
|
5
|
+
import { ID3v2Header } from './ID3v2Token.js';
|
|
6
|
+
/** Borrow the chunk payload; consume its remainder while leaving container padding to the caller. */
|
|
7
|
+
export async function parseEmbeddedID3v2(metadata, tokenizer, options, size) {
|
|
8
|
+
// Retain the metadata payload limit, including for caller-provided tokenizers.
|
|
9
|
+
validateAllocation(size, options);
|
|
10
|
+
if (size < ID3v2Header.len ||
|
|
11
|
+
(tokenizer.fileInfo.size !== undefined && size > tokenizer.fileInfo.size - tokenizer.position)) {
|
|
12
|
+
throw new EndOfStreamError();
|
|
13
|
+
}
|
|
14
|
+
const embeddedTokenizer = createTokenizerView(tokenizer, size);
|
|
15
|
+
try {
|
|
16
|
+
await new ID3v2Parser().parse(metadata, embeddedTokenizer, options);
|
|
17
|
+
await embeddedTokenizer.consumeRemainder();
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error instanceof TokenizerBoundaryError) {
|
|
21
|
+
throw new Id3v2ContentError(`Embedded ID3v2 read exceeds chunk boundary: ${error.message}`);
|
|
22
|
+
}
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
// Closing the borrowed view never closes the container's input.
|
|
27
|
+
await embeddedTokenizer.close();
|
|
28
|
+
}
|
|
29
|
+
}
|