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.
- package/README.md +10 -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/DsdiffLoader.js +1 -1
- package/lib/dsdiff/DsdiffParser.js +2 -6
- package/lib/dsf/DsfParser.js +1 -1
- package/lib/ebml/EbmlIterator.d.ts +20 -15
- package/lib/ebml/EbmlIterator.js +75 -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/index.d.ts +2 -0
- package/lib/index.js +6 -1
- 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 +13 -3
- 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 +5 -5
package/README.md
CHANGED
|
@@ -177,6 +177,8 @@ parseFile(filePath: string, options?: IOptions): Promise<IAudioMetadata>
|
|
|
177
177
|
|
|
178
178
|
- This function is **Node.js-only** and relies on Node.js-specific APIs to access the filesystem.
|
|
179
179
|
|
|
180
|
+
- The path must resolve to a regular file. Symlinks to regular files are supported. Directories, FIFOs (named pipes), and other non-regular filesystem objects are rejected with `NotRegularFileError` from `strtok3`. FIFOs are rejected without waiting for a writer. To parse streamed audio, use [`parseStream()`](#parsestream-function).
|
|
181
|
+
|
|
180
182
|
- For browser environments, consider using the [parseBlob](#parseblob-function) to parse [File object](https://developer.mozilla.org/en-US/docs/Web/API/File) objects.
|
|
181
183
|
|
|
182
184
|
##### Example:
|
|
@@ -618,6 +620,7 @@ Returns a list of supported MIME-types. This may include some MIME-types which a
|
|
|
618
620
|
Maximum bytes buffered for a single metadata payload, accumulated Ogg packet, or tokenizer stream lookahead.
|
|
619
621
|
Must be a positive safe integer. Parsing rejects payloads above this limit before allocating them.
|
|
620
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.
|
|
621
624
|
Oversized tokenizer allocations or stream lookahead reject with `strtok3.BufferSizeError`.
|
|
622
625
|
When using `parseFromTokenizer()`, also configure `maxBufferSize` on the supplied tokenizer at creation.
|
|
623
626
|
Increase it for files with unusually large metadata or cover art, or lower it for constrained environments.
|
|
@@ -631,10 +634,17 @@ Returns a list of supported MIME-types. This may include some MIME-types which a
|
|
|
631
634
|
- Metadata not listed in the SeekHead may be skipped.
|
|
632
635
|
- If the SeekHead is missing, this option has no effect.
|
|
633
636
|
|
|
637
|
+
For large MKV files on network shares, use `parseFile(filePath, { mkvUseIndex: true })`
|
|
638
|
+
when the file's SeekHead index is reliable. The default traversal reads every cluster header,
|
|
639
|
+
so network latency can dominate even though the audio/video payloads are skipped.
|
|
640
|
+
|
|
634
641
|
- `observer`: `(update: IMetadataEvent) => void`:
|
|
635
642
|
|
|
636
643
|
Callback function triggered when common tags or format properties are updated during parsing.
|
|
637
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.
|
|
638
648
|
|
|
639
649
|
- `skipCovers`: `boolean` (default: `false`)
|
|
640
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
|
|
@@ -51,19 +53,22 @@ export declare class EbmlIterator {
|
|
|
51
53
|
private ebmlMaxIDLength;
|
|
52
54
|
private ebmlMaxSizeLength;
|
|
53
55
|
private tokenizer;
|
|
56
|
+
private readonly elementHeader;
|
|
54
57
|
/**
|
|
55
58
|
* @param {ITokenizer} tokenizer Input
|
|
56
59
|
* @param tokenizer
|
|
57
60
|
*/
|
|
58
61
|
constructor(tokenizer: ITokenizer, options?: IOptions);
|
|
59
|
-
iterate(dtdElement:
|
|
62
|
+
iterate<const S extends IElementType>(dtdElement: S, posDone: number, listener: IElementListener<S>): Promise<DecodedElement<S>>;
|
|
60
63
|
private parseContainer;
|
|
64
|
+
private collect;
|
|
61
65
|
private ignore;
|
|
62
66
|
private readVintData;
|
|
63
67
|
private readElement;
|
|
64
68
|
private readFloat;
|
|
65
69
|
private readFlag;
|
|
66
70
|
private readUint;
|
|
71
|
+
private readInt;
|
|
67
72
|
private readString;
|
|
68
73
|
private readBuffer;
|
|
69
74
|
}
|
package/lib/ebml/EbmlIterator.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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
|
-
import { readBuffer } from '../common/Allocation.js';
|
|
5
|
+
import { getMaxMemoryAllocation, readBuffer } from '../common/Allocation.js';
|
|
6
6
|
import { makeUnexpectedFileContentError } from '../ParseError.js';
|
|
7
7
|
import { DataType } from './types.js';
|
|
8
8
|
const debug = initDebug('music-metadata:parser:ebml');
|
|
@@ -28,6 +28,7 @@ export class EbmlIterator {
|
|
|
28
28
|
ebmlMaxIDLength = 4;
|
|
29
29
|
ebmlMaxSizeLength = 8;
|
|
30
30
|
tokenizer;
|
|
31
|
+
elementHeader;
|
|
31
32
|
/**
|
|
32
33
|
* @param {ITokenizer} tokenizer Input
|
|
33
34
|
* @param tokenizer
|
|
@@ -35,7 +36,9 @@ export class EbmlIterator {
|
|
|
35
36
|
constructor(tokenizer, options = {}) {
|
|
36
37
|
this.options = options;
|
|
37
38
|
this.tokenizer = tokenizer;
|
|
39
|
+
this.elementHeader = new Uint8Array(Math.min(12, getMaxMemoryAllocation(options)));
|
|
38
40
|
this.parserMap.set(DataType.uint, e => this.readUint(e));
|
|
41
|
+
this.parserMap.set(DataType.int, e => this.readInt(e));
|
|
39
42
|
this.parserMap.set(DataType.string, e => this.readString(e));
|
|
40
43
|
this.parserMap.set(DataType.binary, e => this.readBuffer(e));
|
|
41
44
|
this.parserMap.set(DataType.uid, async (e) => this.readBuffer(e));
|
|
@@ -43,9 +46,20 @@ export class EbmlIterator {
|
|
|
43
46
|
this.parserMap.set(DataType.float, e => this.readFloat(e));
|
|
44
47
|
}
|
|
45
48
|
async iterate(dtdElement, posDone, listener) {
|
|
46
|
-
|
|
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;
|
|
47
61
|
}
|
|
48
|
-
async parseContainer(dtdElement, posDone, listener) {
|
|
62
|
+
async parseContainer(dtdElement, posDone, listener, collect) {
|
|
49
63
|
const tree = {};
|
|
50
64
|
while (this.tokenizer.position < posDone) {
|
|
51
65
|
let element;
|
|
@@ -59,34 +73,24 @@ export class EbmlIterator {
|
|
|
59
73
|
}
|
|
60
74
|
throw error;
|
|
61
75
|
}
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
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);
|
|
65
80
|
switch (action) {
|
|
66
81
|
case ParseAction.ReadNext:
|
|
67
82
|
{
|
|
68
|
-
if (element.id === 0x1f43b675) {
|
|
69
|
-
// Hack to ignore remaining segment, when cluster element received
|
|
70
|
-
// await this.tokenizer.ignore(posDone - this.tokenizer.position);
|
|
71
|
-
// break;
|
|
72
|
-
}
|
|
73
83
|
debug(`Read element: name=${getElementPath(child)}{id=0x${element.id.toString(16)}, container=${!!child.container}} at position=${elementPosition}`);
|
|
74
84
|
if (child.container) {
|
|
75
85
|
const childEnd = Math.min(posDone, this.tokenizer.fileInfo.size ?? Number.POSITIVE_INFINITY, element.len >= 0 ? this.tokenizer.position + element.len : posDone);
|
|
76
|
-
const res = await this.parseContainer(child, childEnd, listener);
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
tree[child.name] = [];
|
|
80
|
-
}
|
|
81
|
-
tree[child.name].push(res);
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
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);
|
|
85
89
|
}
|
|
86
90
|
await listener.elementValue(child, res, elementPosition);
|
|
87
91
|
}
|
|
88
92
|
else {
|
|
89
|
-
const parser = this.parserMap.get(child.value);
|
|
93
|
+
const parser = child.value === undefined ? undefined : this.parserMap.get(child.value);
|
|
90
94
|
if (typeof parser === 'function') {
|
|
91
95
|
// Validate before leaf readers allocate buffers or string tokens from the declared length.
|
|
92
96
|
const end = Math.min(posDone, this.tokenizer.fileInfo.size ?? Number.POSITIVE_INFINITY);
|
|
@@ -96,9 +100,14 @@ export class EbmlIterator {
|
|
|
96
100
|
throw new EbmlContentError(`Invalid element length: ${element.len}`);
|
|
97
101
|
}
|
|
98
102
|
const value = await parser(element);
|
|
99
|
-
|
|
103
|
+
if (collect) {
|
|
104
|
+
this.collect(tree, child, value);
|
|
105
|
+
}
|
|
100
106
|
await listener.elementValue(child, value, elementPosition);
|
|
101
107
|
}
|
|
108
|
+
else {
|
|
109
|
+
await this.ignore(element.len, posDone);
|
|
110
|
+
}
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
113
|
break;
|
|
@@ -131,6 +140,17 @@ export class EbmlIterator {
|
|
|
131
140
|
}
|
|
132
141
|
return tree;
|
|
133
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
|
+
}
|
|
134
154
|
async ignore(length, posDone) {
|
|
135
155
|
// Unknown-size EBML containers may advertise lengths above the safe integer range.
|
|
136
156
|
// Bound skips by the enclosing container and known input before passing them to the tokenizer.
|
|
@@ -158,6 +178,33 @@ export class EbmlIterator {
|
|
|
158
178
|
return id;
|
|
159
179
|
}
|
|
160
180
|
async readElement() {
|
|
181
|
+
if (isRandomAccessTokenizer(this.tokenizer) && this.elementHeader.length === 12) {
|
|
182
|
+
// RFC 8794 (July 2020), sections 4-6: an ID and size occupy at most 4 + 8 bytes.
|
|
183
|
+
// Peek both VINTs together to avoid four separate file/network reads per element.
|
|
184
|
+
// https://www.rfc-editor.org/rfc/rfc8794.html#section-4
|
|
185
|
+
const available = await this.tokenizer.peekBuffer(this.elementHeader, { mayBeLess: true });
|
|
186
|
+
const width = (offset, maximum) => {
|
|
187
|
+
if (offset >= available) {
|
|
188
|
+
throw new EndOfStreamError();
|
|
189
|
+
}
|
|
190
|
+
for (let length = 1; length <= maximum; ++length) {
|
|
191
|
+
if ((this.elementHeader[offset] & (0x80 >> (length - 1))) !== 0) {
|
|
192
|
+
if (offset + length > available) {
|
|
193
|
+
throw new EndOfStreamError();
|
|
194
|
+
}
|
|
195
|
+
return length;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
throw new EbmlContentError('VINT value exceeding maximum size');
|
|
199
|
+
};
|
|
200
|
+
const idLength = width(0, this.ebmlMaxIDLength);
|
|
201
|
+
const sizeLength = width(idLength, this.ebmlMaxSizeLength);
|
|
202
|
+
const id = readUIntBE(this.elementHeader.subarray(0, idLength), idLength);
|
|
203
|
+
const lenField = this.elementHeader.subarray(idLength, idLength + sizeLength);
|
|
204
|
+
lenField[0] ^= 0x80 >> (sizeLength - 1);
|
|
205
|
+
this.tokenizer.setPosition(this.tokenizer.position + idLength + sizeLength);
|
|
206
|
+
return { id, len: readUIntBE(lenField, sizeLength) };
|
|
207
|
+
}
|
|
161
208
|
const id = await this.readVintData(this.ebmlMaxIDLength);
|
|
162
209
|
const lenField = await this.readVintData(this.ebmlMaxSizeLength);
|
|
163
210
|
lenField[0] ^= 0x80 >> (lenField.length - 1);
|
|
@@ -187,6 +234,11 @@ export class EbmlIterator {
|
|
|
187
234
|
const buf = await this.readBuffer(e);
|
|
188
235
|
return readUIntBE(buf, e.len);
|
|
189
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
|
+
}
|
|
190
242
|
async readString(e) {
|
|
191
243
|
const buf = await this.readBuffer(e);
|
|
192
244
|
const rawString = new StringType(e.len, 'utf-8').get(buf, 0);
|
|
@@ -216,21 +268,6 @@ function readUIntBeAsBigInt(buf, len) {
|
|
|
216
268
|
return BigInt(-1);
|
|
217
269
|
}
|
|
218
270
|
}
|
|
219
|
-
function linkParents(element) {
|
|
220
|
-
if (element.container) {
|
|
221
|
-
Object.keys(element.container)
|
|
222
|
-
.map(id => {
|
|
223
|
-
const child = element.container[id];
|
|
224
|
-
child.id = Number.parseInt(id, 10);
|
|
225
|
-
return child;
|
|
226
|
-
})
|
|
227
|
-
.forEach(child => {
|
|
228
|
-
child.parent = element;
|
|
229
|
-
linkParents(child);
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
return element;
|
|
233
|
-
}
|
|
234
271
|
export function getElementPath(element) {
|
|
235
272
|
let path = '';
|
|
236
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);
|