music-metadata 10.3.1 → 10.4.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/README.md +7 -7
- package/lib/ParserFactory.d.ts +22 -20
- package/lib/ParserFactory.js +74 -131
- package/lib/aiff/AiffLoader.d.ts +2 -0
- package/lib/aiff/AiffLoader.js +8 -0
- package/lib/apev2/APEv2Parser.js +2 -4
- package/lib/apev2/Apev2Loader.d.ts +2 -0
- package/lib/apev2/Apev2Loader.js +8 -0
- package/lib/asf/AsfLoader.d.ts +2 -0
- package/lib/asf/AsfLoader.js +8 -0
- package/lib/common/BasicParser.d.ts +5 -5
- package/lib/common/BasicParser.js +1 -7
- package/lib/common/ParserLoader.d.ts +1 -0
- package/lib/common/ParserLoader.js +1 -0
- package/lib/core.d.ts +1 -1
- package/lib/core.js +4 -3
- package/lib/dsdiff/DsdiffLoader.d.ts +2 -0
- package/lib/dsdiff/DsdiffLoader.js +8 -0
- package/lib/dsf/DsfLoader.d.ts +2 -0
- package/lib/dsf/DsfLoader.js +8 -0
- package/lib/flac/FlacLoader.d.ts +2 -0
- package/lib/flac/FlacLoader.js +8 -0
- package/lib/flac/FlacParser.d.ts +0 -11
- package/lib/flac/FlacParser.js +1 -11
- package/lib/id3v1/ID3v1Parser.d.ts +5 -1
- package/lib/id3v1/ID3v1Parser.js +8 -5
- package/lib/id3v2/AbstractID3Parser.js +2 -2
- package/lib/index.js +5 -4
- package/lib/matroska/MatroskaLoader.d.ts +2 -0
- package/lib/matroska/MatroskaLoader.js +8 -0
- package/lib/matroska/MatroskaParser.d.ts +0 -11
- package/lib/matroska/MatroskaParser.js +1 -12
- package/lib/mp4/Atom.js +5 -5
- package/lib/mp4/Mp4Loader.d.ts +2 -0
- package/lib/mp4/Mp4Loader.js +8 -0
- package/lib/mpeg/MpegLoader.d.ts +2 -0
- package/lib/mpeg/MpegLoader.js +8 -0
- package/lib/musepack/MusepackLoader.d.ts +2 -0
- package/lib/musepack/MusepackLoader.js +8 -0
- package/lib/musepack/{index.d.ts → MusepackParser.d.ts} +1 -2
- package/lib/musepack/{index.js → MusepackParser.js} +4 -6
- package/lib/ogg/OggLoader.d.ts +2 -0
- package/lib/ogg/OggLoader.js +8 -0
- package/lib/wav/WaveLoader.d.ts +2 -0
- package/lib/wav/WaveLoader.js +8 -0
- package/lib/wavpack/WavPackLoader.d.ts +2 -0
- package/lib/wavpack/WavPackLoader.js +8 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[](https://github.com/Borewit/music-metadata/actions?query=branch%3Amaster)
|
|
2
2
|
[](https://ci.appveyor.com/project/Borewit/music-metadata/branch/master)
|
|
3
3
|
[](https://npmjs.org/package/music-metadata)
|
|
4
|
-
[](https://npmcharts.com/compare/music-metadata,jsmediatags,musicmetadata,node-id3,mp3-parser,id3-parser,wav-file-info?start=600)
|
|
4
|
+
[](https://npmcharts.com/compare/music-metadata,jsmediatags,musicmetadata,node-id3,mp3-parser,id3-parser,wav-file-info?start=600&interval=30)
|
|
5
5
|
[](https://coveralls.io/github/Borewit/music-metadata?branch=master)
|
|
6
6
|
[](https://app.codacy.com/app/Borewit/music-metadata?utm_source=github.com&utm_medium=referral&utm_content=Borewit/music-metadata&utm_campaign=Badge_Grade_Dashboard)
|
|
7
7
|
[](https://github.com/Borewit/music-metadata/actions/workflows/codeql-analysis.yml)
|
|
@@ -31,14 +31,14 @@ The distributed JavaScript codebase is compliant with the [ECMAScript 2020 (11th
|
|
|
31
31
|
This module requires a [Node.js ≥ 16](https://nodejs.org/en/about/previous-releases) engine.
|
|
32
32
|
It can also be used in a browser environment when bundled with a module bundler.
|
|
33
33
|
|
|
34
|
-
##
|
|
35
|
-
If you
|
|
36
|
-
Your support helps sustain ongoing development and improvements.
|
|
37
|
-
[Become a sponsor to Borewit](https://github.com/sponsors/Borewit)
|
|
34
|
+
## Support the Project
|
|
35
|
+
If you find this project useful and would like to support its development, consider sponsoring or contributing:
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
- [Become a sponsor to Borewit](https://github.com/sponsors/Borewit)
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
- Buy me a coffee:
|
|
40
|
+
|
|
41
|
+
<a href="https://www.buymeacoffee.com/borewit" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy me A coffee" height="41" width="174"></a>
|
|
42
42
|
|
|
43
43
|
## Features
|
|
44
44
|
|
package/lib/ParserFactory.d.ts
CHANGED
|
@@ -2,14 +2,18 @@ import { type MediaType } from 'media-typer';
|
|
|
2
2
|
import { type INativeMetadataCollector } from './common/MetadataCollector.js';
|
|
3
3
|
import type { IAudioMetadata, IOptions, ParserType } from './type.js';
|
|
4
4
|
import type { ITokenizer } from 'strtok3';
|
|
5
|
-
export interface
|
|
5
|
+
export interface IParserLoader {
|
|
6
|
+
/**
|
|
7
|
+
* Returns a list of supported file extensions
|
|
8
|
+
*/
|
|
9
|
+
extensions: string[];
|
|
10
|
+
parserType: ParserType;
|
|
6
11
|
/**
|
|
7
|
-
*
|
|
8
|
-
* @param metadata - Output
|
|
9
|
-
* @param tokenizer - Input
|
|
10
|
-
* @param options - Parsing options
|
|
12
|
+
* Lazy load the parser
|
|
11
13
|
*/
|
|
12
|
-
|
|
14
|
+
load(metadata: INativeMetadataCollector, tokenizer: ITokenizer, options: IOptions): Promise<ITokenParser>;
|
|
15
|
+
}
|
|
16
|
+
export interface ITokenParser {
|
|
13
17
|
/**
|
|
14
18
|
* Parse audio track.
|
|
15
19
|
* Called after init(...).
|
|
@@ -23,18 +27,16 @@ interface IContentType extends MediaType {
|
|
|
23
27
|
};
|
|
24
28
|
}
|
|
25
29
|
export declare function parseHttpContentType(contentType: string): IContentType;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export declare function getParserIdForExtension(filePath: string | undefined): ParserType | undefined;
|
|
39
|
-
export declare function loadParser(moduleName: ParserType): Promise<ITokenParser>;
|
|
30
|
+
export declare class ParserFactory {
|
|
31
|
+
parsers: IParserLoader[];
|
|
32
|
+
constructor();
|
|
33
|
+
registerParser(parser: IParserLoader): void;
|
|
34
|
+
parse(tokenizer: ITokenizer, parserLoader: IParserLoader | undefined, opts?: IOptions): Promise<IAudioMetadata>;
|
|
35
|
+
/**
|
|
36
|
+
* @param filePath - Path, filename or extension to audio file
|
|
37
|
+
* @return Parser submodule name
|
|
38
|
+
*/
|
|
39
|
+
findLoaderForExtension(filePath: string | undefined): IParserLoader | undefined;
|
|
40
|
+
findLoaderForType(moduleName: ParserType | undefined): IParserLoader | undefined;
|
|
41
|
+
}
|
|
40
42
|
export {};
|
package/lib/ParserFactory.js
CHANGED
|
@@ -3,20 +3,20 @@ 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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
6
|
+
import { mpegParserLoader } from './mpeg/MpegLoader.js';
|
|
7
|
+
import { CouldNotDetermineFileTypeError, UnsupportedFileTypeError } from './ParseError.js';
|
|
8
|
+
import { apeParserLoader } from './apev2/Apev2Loader.js';
|
|
9
|
+
import { asfParserLoader } from './asf/AsfLoader.js';
|
|
10
|
+
import { dsdiffParserLoader } from './dsdiff/DsdiffLoader.js';
|
|
11
|
+
import { aiffParserLoader } from './aiff/AiffLoader.js';
|
|
12
|
+
import { dsfParserLoader } from './dsf/DsfLoader.js';
|
|
13
|
+
import { flacParserLoader } from './flac/FlacLoader.js';
|
|
14
|
+
import { matroskaParserLoader } from './matroska/MatroskaLoader.js';
|
|
15
|
+
import { mp4ParserLoader } from './mp4/Mp4Loader.js';
|
|
16
|
+
import { musepackParserLoader } from './musepack/MusepackLoader.js';
|
|
17
|
+
import { oggParserLoader } from './ogg/OggLoader.js';
|
|
18
|
+
import { wavpackParserLoader } from './wavpack/WavPackLoader.js';
|
|
19
|
+
import { riffParserLoader } from './wav/WaveLoader.js';
|
|
20
20
|
const debug = initDebug('music-metadata:parser:factory');
|
|
21
21
|
export function parseHttpContentType(contentType) {
|
|
22
22
|
const type = ContentType.parse(contentType);
|
|
@@ -28,129 +28,72 @@ export function parseHttpContentType(contentType) {
|
|
|
28
28
|
parameters: type.parameters
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
export class ParserFactory {
|
|
32
|
+
constructor() {
|
|
33
|
+
this.parsers = [];
|
|
34
|
+
[
|
|
35
|
+
flacParserLoader,
|
|
36
|
+
mpegParserLoader,
|
|
37
|
+
apeParserLoader,
|
|
38
|
+
mp4ParserLoader,
|
|
39
|
+
matroskaParserLoader,
|
|
40
|
+
riffParserLoader,
|
|
41
|
+
oggParserLoader,
|
|
42
|
+
asfParserLoader,
|
|
43
|
+
aiffParserLoader,
|
|
44
|
+
wavpackParserLoader,
|
|
45
|
+
musepackParserLoader,
|
|
46
|
+
dsfParserLoader,
|
|
47
|
+
dsdiffParserLoader
|
|
48
|
+
].forEach(parser => this.registerParser(parser));
|
|
43
49
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
parserId = getParserIdForExtension(tokenizer.fileInfo.path);
|
|
50
|
-
}
|
|
51
|
-
if (!parserId) {
|
|
52
|
-
// Parser could not be determined on MIME-type or extension
|
|
53
|
-
debug('Guess parser on content...');
|
|
50
|
+
registerParser(parser) {
|
|
51
|
+
this.parsers.push(parser);
|
|
52
|
+
}
|
|
53
|
+
async parse(tokenizer, parserLoader, opts) {
|
|
54
|
+
if (!parserLoader) {
|
|
54
55
|
const buf = new Uint8Array(4100);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
if (tokenizer.fileInfo.mimeType) {
|
|
57
|
+
parserLoader = this.findLoaderForType(getParserIdForMimeType(tokenizer.fileInfo.mimeType));
|
|
58
|
+
}
|
|
59
|
+
if (!parserLoader && tokenizer.fileInfo.path) {
|
|
60
|
+
parserLoader = this.findLoaderForExtension(tokenizer.fileInfo.path);
|
|
59
61
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
if (!parserLoader) {
|
|
63
|
+
// Parser could not be determined on MIME-type or extension
|
|
64
|
+
debug('Guess parser on content...');
|
|
65
|
+
await tokenizer.peekBuffer(buf, { mayBeLess: true });
|
|
66
|
+
const guessedType = await fileTypeFromBuffer(buf);
|
|
67
|
+
if (!guessedType || !guessedType.mime) {
|
|
68
|
+
throw new CouldNotDetermineFileTypeError('Failed to determine audio format');
|
|
69
|
+
}
|
|
70
|
+
debug(`Guessed file type is mime=${guessedType.mime}, extension=${guessedType.ext}`);
|
|
71
|
+
parserLoader = this.findLoaderForType(getParserIdForMimeType(guessedType.mime));
|
|
72
|
+
if (!parserLoader) {
|
|
73
|
+
throw new UnsupportedFileTypeError(`Guessed MIME-type not supported: ${guessedType.mime}`);
|
|
74
|
+
}
|
|
64
75
|
}
|
|
65
76
|
}
|
|
77
|
+
// Parser found, execute parser
|
|
78
|
+
debug(`Loading ${parserLoader.parserType} parser...`);
|
|
79
|
+
const metadata = new MetadataCollector(opts);
|
|
80
|
+
const parser = await parserLoader.load(metadata, tokenizer, opts ?? {});
|
|
81
|
+
debug(`Parser ${parserLoader.parserType} loaded`);
|
|
82
|
+
await parser.parse();
|
|
83
|
+
return metadata.toCommonMetadata();
|
|
66
84
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*/
|
|
77
|
-
export function getParserIdForExtension(filePath) {
|
|
78
|
-
if (!filePath)
|
|
79
|
-
return;
|
|
80
|
-
const extension = getExtension(filePath).toLocaleLowerCase() || filePath;
|
|
81
|
-
switch (extension) {
|
|
82
|
-
case '.mp2':
|
|
83
|
-
case '.mp3':
|
|
84
|
-
case '.m2a':
|
|
85
|
-
case '.aac': // Assume it is ADTS-container
|
|
86
|
-
return 'mpeg';
|
|
87
|
-
case '.ape':
|
|
88
|
-
return 'apev2';
|
|
89
|
-
case '.mp4':
|
|
90
|
-
case '.m4a':
|
|
91
|
-
case '.m4b':
|
|
92
|
-
case '.m4pa':
|
|
93
|
-
case '.m4v':
|
|
94
|
-
case '.m4r':
|
|
95
|
-
case '.3gp':
|
|
96
|
-
return 'mp4';
|
|
97
|
-
case '.wma':
|
|
98
|
-
case '.wmv':
|
|
99
|
-
case '.asf':
|
|
100
|
-
return 'asf';
|
|
101
|
-
case '.flac':
|
|
102
|
-
return 'flac';
|
|
103
|
-
case '.ogg':
|
|
104
|
-
case '.ogv':
|
|
105
|
-
case '.oga':
|
|
106
|
-
case '.ogm':
|
|
107
|
-
case '.ogx':
|
|
108
|
-
case '.opus': // recommended filename extension for Ogg Opus
|
|
109
|
-
case '.spx': // recommended filename extension for Ogg Speex
|
|
110
|
-
return 'ogg';
|
|
111
|
-
case '.aif':
|
|
112
|
-
case '.aiff':
|
|
113
|
-
case '.aifc':
|
|
114
|
-
return 'aiff';
|
|
115
|
-
case '.wav':
|
|
116
|
-
case '.bwf': // Broadcast Wave Format
|
|
117
|
-
return 'riff';
|
|
118
|
-
case '.wv':
|
|
119
|
-
case '.wvp':
|
|
120
|
-
return 'wavpack';
|
|
121
|
-
case '.mpc':
|
|
122
|
-
return 'musepack';
|
|
123
|
-
case '.dsf':
|
|
124
|
-
return 'dsf';
|
|
125
|
-
case '.dff':
|
|
126
|
-
return 'dsdiff';
|
|
127
|
-
case '.mka':
|
|
128
|
-
case '.mkv':
|
|
129
|
-
case '.mk3d':
|
|
130
|
-
case '.mks':
|
|
131
|
-
case '.webm':
|
|
132
|
-
return 'matroska';
|
|
85
|
+
/**
|
|
86
|
+
* @param filePath - Path, filename or extension to audio file
|
|
87
|
+
* @return Parser submodule name
|
|
88
|
+
*/
|
|
89
|
+
findLoaderForExtension(filePath) {
|
|
90
|
+
if (!filePath)
|
|
91
|
+
return;
|
|
92
|
+
const extension = getExtension(filePath).toLocaleLowerCase() || filePath;
|
|
93
|
+
return this.parsers.find(parser => parser.extensions.indexOf(extension) !== -1);
|
|
133
94
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
switch (moduleName) {
|
|
137
|
-
case 'aiff': return new AIFFParser();
|
|
138
|
-
case 'adts':
|
|
139
|
-
case 'mpeg':
|
|
140
|
-
return new MpegParser();
|
|
141
|
-
case 'apev2': return new APEv2Parser();
|
|
142
|
-
case 'asf': return new AsfParser();
|
|
143
|
-
case 'dsf': return new DsfParser();
|
|
144
|
-
case 'dsdiff': return new DsdiffParser();
|
|
145
|
-
case 'flac': return new FlacParser();
|
|
146
|
-
case 'mp4': return new MP4Parser();
|
|
147
|
-
case 'musepack': return new MusepackParser();
|
|
148
|
-
case 'ogg': return new OggParser();
|
|
149
|
-
case 'riff': return new WaveParser();
|
|
150
|
-
case 'wavpack': return new WavPackParser();
|
|
151
|
-
case 'matroska': return new MatroskaParser();
|
|
152
|
-
default:
|
|
153
|
-
throw new InternalParserError(`Unknown parser type: ${moduleName}`);
|
|
95
|
+
findLoaderForType(moduleName) {
|
|
96
|
+
return moduleName ? this.parsers.find(parser => parser.parserType === moduleName) : undefined;
|
|
154
97
|
}
|
|
155
98
|
}
|
|
156
99
|
function getExtension(fname) {
|
|
@@ -181,7 +124,7 @@ function getParserIdForMimeType(httpContentType) {
|
|
|
181
124
|
return 'mpeg';
|
|
182
125
|
case 'aac':
|
|
183
126
|
case 'aacp':
|
|
184
|
-
return '
|
|
127
|
+
return 'mpeg'; // adts
|
|
185
128
|
case 'flac':
|
|
186
129
|
return 'flac';
|
|
187
130
|
case 'ape':
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const aiffParserLoader = {
|
|
2
|
+
parserType: 'aiff',
|
|
3
|
+
extensions: ['.aif', 'aiff', 'aifc'],
|
|
4
|
+
async load(metadata, tokenizer, options) {
|
|
5
|
+
return new (await import('./AiffParser.js')).AIFFParser(metadata, tokenizer, options);
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=AiffLoader.js.map
|
package/lib/apev2/APEv2Parser.js
CHANGED
|
@@ -17,8 +17,7 @@ export class APEv2Parser extends BasicParser {
|
|
|
17
17
|
this.ape = {};
|
|
18
18
|
}
|
|
19
19
|
static tryParseApeHeader(metadata, tokenizer, options) {
|
|
20
|
-
const apeParser = new APEv2Parser();
|
|
21
|
-
apeParser.init(metadata, tokenizer, options);
|
|
20
|
+
const apeParser = new APEv2Parser(metadata, tokenizer, options);
|
|
22
21
|
return apeParser.tryParseApeHeader();
|
|
23
22
|
}
|
|
24
23
|
/**
|
|
@@ -57,8 +56,7 @@ export class APEv2Parser extends BasicParser {
|
|
|
57
56
|
if (footer.ID !== preamble)
|
|
58
57
|
throw new ApeContentError('Unexpected APEv2 Footer ID preamble value');
|
|
59
58
|
strtok3.fromBuffer(buffer);
|
|
60
|
-
const apeParser = new APEv2Parser();
|
|
61
|
-
apeParser.init(metadata, strtok3.fromBuffer(buffer), options);
|
|
59
|
+
const apeParser = new APEv2Parser(metadata, strtok3.fromBuffer(buffer), options);
|
|
62
60
|
return apeParser.parseTags(footer);
|
|
63
61
|
}
|
|
64
62
|
/**
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const apeParserLoader = {
|
|
2
|
+
parserType: 'apev2',
|
|
3
|
+
extensions: ['.ape'],
|
|
4
|
+
async load(metadata, tokenizer, options) {
|
|
5
|
+
return new (await import('./APEv2Parser.js')).APEv2Parser(metadata, tokenizer, options);
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=Apev2Loader.js.map
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { ITokenizer } from 'strtok3';
|
|
2
2
|
import type { ITokenParser } from '../ParserFactory.js';
|
|
3
|
-
import type { IOptions
|
|
3
|
+
import type { IOptions } from '../type.js';
|
|
4
4
|
import type { INativeMetadataCollector } from './MetadataCollector.js';
|
|
5
5
|
export declare abstract class BasicParser implements ITokenParser {
|
|
6
|
-
protected metadata: INativeMetadataCollector;
|
|
7
|
-
protected tokenizer: ITokenizer;
|
|
8
|
-
protected options:
|
|
6
|
+
protected readonly metadata: INativeMetadataCollector;
|
|
7
|
+
protected readonly tokenizer: ITokenizer;
|
|
8
|
+
protected readonly options: IOptions;
|
|
9
9
|
/**
|
|
10
10
|
* Initialize parser with output (metadata), input (tokenizer) & parsing options (options).
|
|
11
11
|
* @param {INativeMetadataCollector} metadata Output
|
|
12
12
|
* @param {ITokenizer} tokenizer Input
|
|
13
13
|
* @param {IOptions} options Parsing options
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
constructor(metadata: INativeMetadataCollector, tokenizer: ITokenizer, options: IOptions);
|
|
16
16
|
abstract parse(): Promise<void>;
|
|
17
17
|
}
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
export class BasicParser {
|
|
2
|
-
constructor() {
|
|
3
|
-
this.metadata = undefined;
|
|
4
|
-
this.tokenizer = undefined;
|
|
5
|
-
this.options = undefined;
|
|
6
|
-
}
|
|
7
2
|
/**
|
|
8
3
|
* Initialize parser with output (metadata), input (tokenizer) & parsing options (options).
|
|
9
4
|
* @param {INativeMetadataCollector} metadata Output
|
|
10
5
|
* @param {ITokenizer} tokenizer Input
|
|
11
6
|
* @param {IOptions} options Parsing options
|
|
12
7
|
*/
|
|
13
|
-
|
|
8
|
+
constructor(metadata, tokenizer, options) {
|
|
14
9
|
this.metadata = metadata;
|
|
15
10
|
this.tokenizer = tokenizer;
|
|
16
11
|
this.options = options;
|
|
17
|
-
return this;
|
|
18
12
|
}
|
|
19
13
|
}
|
|
20
14
|
//# sourceMappingURL=BasicParser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/core.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Primary entry point, Node.js specific entry point is
|
|
2
|
+
* Primary entry point, Node.js specific entry point is MusepackParser.ts
|
|
3
3
|
*/
|
|
4
4
|
import { type AnyWebByteStream, type IFileInfo, type ITokenizer } from 'strtok3';
|
|
5
5
|
import type { IAudioMetadata, INativeTagDict, IOptions, IPicture, IPrivateOptions, IRandomReader, ITag } from './type.js';
|
package/lib/core.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Primary entry point, Node.js specific entry point is
|
|
2
|
+
* Primary entry point, Node.js specific entry point is MusepackParser.ts
|
|
3
3
|
*/
|
|
4
4
|
import { fromWebStream, fromBuffer } from 'strtok3';
|
|
5
|
-
import {
|
|
5
|
+
import { ParserFactory } from './ParserFactory.js';
|
|
6
6
|
import { RandomUint8ArrayReader } from './common/RandomUint8ArrayReader.js';
|
|
7
7
|
import { APEv2Parser } from './apev2/APEv2Parser.js';
|
|
8
8
|
import { hasID3v1Header } from './id3v1/ID3v1Parser.js';
|
|
@@ -53,7 +53,8 @@ export async function parseBuffer(uint8Array, fileInfo, options = {}) {
|
|
|
53
53
|
* @returns Metadata
|
|
54
54
|
*/
|
|
55
55
|
export function parseFromTokenizer(tokenizer, options) {
|
|
56
|
-
|
|
56
|
+
const parserFactory = new ParserFactory();
|
|
57
|
+
return parserFactory.parse(tokenizer, undefined, options);
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
59
60
|
* Create a dictionary ordered by their tag id (key)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const dsdiffParserLoader = {
|
|
2
|
+
parserType: 'dsdiff',
|
|
3
|
+
extensions: ['.dff'],
|
|
4
|
+
async load(metadata, tokenizer, options) {
|
|
5
|
+
return new (await import('./DsdiffParser.js')).DsdiffParser(metadata, tokenizer, options);
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=DsdiffLoader.js.map
|
package/lib/flac/FlacParser.d.ts
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
|
-
import type { ITokenizer } from 'strtok3';
|
|
2
1
|
import { AbstractID3Parser } from '../id3v2/AbstractID3Parser.js';
|
|
3
|
-
import type { INativeMetadataCollector } from '../common/MetadataCollector.js';
|
|
4
|
-
import type { IOptions } from '../type.js';
|
|
5
|
-
import type { ITokenParser } from '../ParserFactory.js';
|
|
6
2
|
export declare class FlacParser extends AbstractID3Parser {
|
|
7
3
|
private vorbisParser;
|
|
8
4
|
private padding;
|
|
9
|
-
/**
|
|
10
|
-
* Initialize parser with output (metadata), input (tokenizer) & parsing options (options).
|
|
11
|
-
* @param {INativeMetadataCollector} metadata Output
|
|
12
|
-
* @param {ITokenizer} tokenizer Input
|
|
13
|
-
* @param {IOptions} options Parsing options
|
|
14
|
-
*/
|
|
15
|
-
init(metadata: INativeMetadataCollector, tokenizer: ITokenizer, options: IOptions): ITokenParser;
|
|
16
5
|
postId3v2Parse(): Promise<void>;
|
|
17
6
|
private parseDataBlock;
|
|
18
7
|
/**
|
package/lib/flac/FlacParser.js
CHANGED
|
@@ -27,19 +27,9 @@ var BlockType;
|
|
|
27
27
|
export class FlacParser extends AbstractID3Parser {
|
|
28
28
|
constructor() {
|
|
29
29
|
super(...arguments);
|
|
30
|
+
this.vorbisParser = new VorbisParser(this.metadata, this.options);
|
|
30
31
|
this.padding = 0;
|
|
31
32
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Initialize parser with output (metadata), input (tokenizer) & parsing options (options).
|
|
34
|
-
* @param {INativeMetadataCollector} metadata Output
|
|
35
|
-
* @param {ITokenizer} tokenizer Input
|
|
36
|
-
* @param {IOptions} options Parsing options
|
|
37
|
-
*/
|
|
38
|
-
init(metadata, tokenizer, options) {
|
|
39
|
-
super.init(metadata, tokenizer, options);
|
|
40
|
-
this.vorbisParser = new VorbisParser(metadata, options);
|
|
41
|
-
return this;
|
|
42
|
-
}
|
|
43
33
|
async postId3v2Parse() {
|
|
44
34
|
const fourCC = await this.tokenizer.readToken(FourCcToken);
|
|
45
35
|
if (fourCC.toString() !== 'fLaC') {
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import type { ITokenizer } from 'strtok3';
|
|
1
2
|
import { BasicParser } from '../common/BasicParser.js';
|
|
2
|
-
import type { IRandomReader } from '../type.js';
|
|
3
|
+
import type { IPrivateOptions, IRandomReader } from '../type.js';
|
|
4
|
+
import type { INativeMetadataCollector } from '../common/MetadataCollector.js';
|
|
3
5
|
/**
|
|
4
6
|
* ID3v1 Genre mappings
|
|
5
7
|
* Ref: https://de.wikipedia.org/wiki/Liste_der_ID3v1-Genres
|
|
6
8
|
*/
|
|
7
9
|
export declare const Genres: string[];
|
|
8
10
|
export declare class ID3v1Parser extends BasicParser {
|
|
11
|
+
private apeHeader;
|
|
12
|
+
constructor(metadata: INativeMetadataCollector, tokenizer: ITokenizer, options: IPrivateOptions);
|
|
9
13
|
private static getGenre;
|
|
10
14
|
parse(): Promise<void>;
|
|
11
15
|
private addTag;
|
package/lib/id3v1/ID3v1Parser.js
CHANGED
|
@@ -79,6 +79,10 @@ class Id3v1StringType {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
export class ID3v1Parser extends BasicParser {
|
|
82
|
+
constructor(metadata, tokenizer, options) {
|
|
83
|
+
super(metadata, tokenizer, options);
|
|
84
|
+
this.apeHeader = options.apeHeader;
|
|
85
|
+
}
|
|
82
86
|
static getGenre(genreIndex) {
|
|
83
87
|
if (genreIndex < Genres.length) {
|
|
84
88
|
return Genres[genreIndex];
|
|
@@ -90,11 +94,10 @@ export class ID3v1Parser extends BasicParser {
|
|
|
90
94
|
debug('Skip checking for ID3v1 because the file-size is unknown');
|
|
91
95
|
return;
|
|
92
96
|
}
|
|
93
|
-
if (this.
|
|
94
|
-
this.tokenizer.ignore(this.
|
|
95
|
-
const apeParser = new APEv2Parser();
|
|
96
|
-
apeParser.
|
|
97
|
-
await apeParser.parseTags(this.options.apeHeader.footer);
|
|
97
|
+
if (this.apeHeader) {
|
|
98
|
+
this.tokenizer.ignore(this.apeHeader.offset - this.tokenizer.position);
|
|
99
|
+
const apeParser = new APEv2Parser(this.metadata, this.tokenizer, this.options);
|
|
100
|
+
await apeParser.parseTags(this.apeHeader.footer);
|
|
98
101
|
}
|
|
99
102
|
const offset = this.tokenizer.fileInfo.size - Iid3v1Token.len;
|
|
100
103
|
if (this.tokenizer.position > offset) {
|
|
@@ -40,8 +40,8 @@ export class AbstractID3Parser extends BasicParser {
|
|
|
40
40
|
this.finalize();
|
|
41
41
|
}
|
|
42
42
|
else {
|
|
43
|
-
const id3v1parser = new ID3v1Parser();
|
|
44
|
-
await id3v1parser.
|
|
43
|
+
const id3v1parser = new ID3v1Parser(this.metadata, this.tokenizer, this.options);
|
|
44
|
+
await id3v1parser.parse();
|
|
45
45
|
this.finalize();
|
|
46
46
|
}
|
|
47
47
|
}
|
package/lib/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { fromFile, fromStream } from 'strtok3';
|
|
5
5
|
import initDebug from 'debug';
|
|
6
6
|
import { parseFromTokenizer, scanAppendingHeaders } from './core.js';
|
|
7
|
-
import {
|
|
7
|
+
import { ParserFactory } from './ParserFactory.js';
|
|
8
8
|
import { RandomFileReader } from './common/RandomFileReader.js';
|
|
9
9
|
export * from './core.js';
|
|
10
10
|
const debug = initDebug('music-metadata:parser');
|
|
@@ -35,11 +35,12 @@ export async function parseFile(filePath, options = {}) {
|
|
|
35
35
|
finally {
|
|
36
36
|
await fileReader.close();
|
|
37
37
|
}
|
|
38
|
+
const parserFactory = new ParserFactory();
|
|
38
39
|
try {
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
40
|
+
const parserLoader = parserFactory.findLoaderForExtension(filePath);
|
|
41
|
+
if (!parserLoader)
|
|
41
42
|
debug(' Parser could not be determined by file extension');
|
|
42
|
-
return await parse(fileTokenizer,
|
|
43
|
+
return await parserFactory.parse(fileTokenizer, parserLoader, options);
|
|
43
44
|
}
|
|
44
45
|
finally {
|
|
45
46
|
await fileTokenizer.close();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const matroskaParserLoader = {
|
|
2
|
+
parserType: 'matroska',
|
|
3
|
+
extensions: ['.mka', '.mkv', '.mk3d', '.mks', 'webm'],
|
|
4
|
+
async load(metadata, tokenizer, options) {
|
|
5
|
+
return new (await import('./MatroskaParser.js')).MatroskaParser(metadata, tokenizer, options);
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=MatroskaLoader.js.map
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type { ITokenizer } from 'strtok3';
|
|
2
|
-
import type { INativeMetadataCollector } from '../common/MetadataCollector.js';
|
|
3
1
|
import { BasicParser } from '../common/BasicParser.js';
|
|
4
|
-
import type { IOptions } from '../type.js';
|
|
5
|
-
import type { ITokenParser } from '../ParserFactory.js';
|
|
6
2
|
/**
|
|
7
3
|
* Extensible Binary Meta Language (EBML) parser
|
|
8
4
|
* https://en.wikipedia.org/wiki/Extensible_Binary_Meta_Language
|
|
@@ -18,13 +14,6 @@ export declare class MatroskaParser extends BasicParser {
|
|
|
18
14
|
* Significant performance impact
|
|
19
15
|
*/
|
|
20
16
|
private flagUseIndexToSkipClusters;
|
|
21
|
-
/**
|
|
22
|
-
* Initialize parser with output (metadata), input (tokenizer) & parsing options (options).
|
|
23
|
-
* @param {INativeMetadataCollector} metadata Output
|
|
24
|
-
* @param {ITokenizer} tokenizer Input
|
|
25
|
-
* @param {IOptions} options Parsing options
|
|
26
|
-
*/
|
|
27
|
-
init(metadata: INativeMetadataCollector, tokenizer: ITokenizer, options: IOptions): ITokenParser;
|
|
28
17
|
parse(): Promise<void>;
|
|
29
18
|
private addTag;
|
|
30
19
|
}
|
|
@@ -19,18 +19,7 @@ export class MatroskaParser extends BasicParser {
|
|
|
19
19
|
* Use index to skip multiple segment/cluster elements at once.
|
|
20
20
|
* Significant performance impact
|
|
21
21
|
*/
|
|
22
|
-
this.flagUseIndexToSkipClusters = false;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Initialize parser with output (metadata), input (tokenizer) & parsing options (options).
|
|
26
|
-
* @param {INativeMetadataCollector} metadata Output
|
|
27
|
-
* @param {ITokenizer} tokenizer Input
|
|
28
|
-
* @param {IOptions} options Parsing options
|
|
29
|
-
*/
|
|
30
|
-
init(metadata, tokenizer, options) {
|
|
31
|
-
super.init(metadata, tokenizer, options);
|
|
32
|
-
this.flagUseIndexToSkipClusters = options.mkvUseIndex ?? false;
|
|
33
|
-
return this;
|
|
22
|
+
this.flagUseIndexToSkipClusters = this.options.mkvUseIndex ?? false;
|
|
34
23
|
}
|
|
35
24
|
async parse() {
|
|
36
25
|
const containerSize = this.tokenizer.fileInfo.size ?? Number.MAX_SAFE_INTEGER;
|
package/lib/mp4/Atom.js
CHANGED
|
@@ -6,13 +6,13 @@ export class Atom {
|
|
|
6
6
|
static async readAtom(tokenizer, dataHandler, parent, remaining) {
|
|
7
7
|
// Parse atom header
|
|
8
8
|
const offset = tokenizer.position;
|
|
9
|
-
|
|
9
|
+
debug(`Reading next token on offset=${offset}...`); // buf.toString('ascii')
|
|
10
10
|
const header = await tokenizer.readToken(AtomToken.Header);
|
|
11
|
-
const extended = header.length ===
|
|
11
|
+
const extended = header.length === 1n;
|
|
12
12
|
if (extended) {
|
|
13
13
|
header.length = await tokenizer.readToken(AtomToken.ExtendedSize);
|
|
14
14
|
}
|
|
15
|
-
const atomBean = new Atom(header,
|
|
15
|
+
const atomBean = new Atom(header, extended, parent);
|
|
16
16
|
const payloadLength = atomBean.getPayloadLength(remaining);
|
|
17
17
|
debug(`parse atom name=${atomBean.atomPath}, extended=${atomBean.extended}, offset=${offset}, len=${atomBean.header.length}`); // buf.toString('ascii')
|
|
18
18
|
await atomBean.readData(tokenizer, dataHandler, payloadLength);
|
|
@@ -29,13 +29,13 @@ export class Atom {
|
|
|
29
29
|
return this.extended ? 16 : 8;
|
|
30
30
|
}
|
|
31
31
|
getPayloadLength(remaining) {
|
|
32
|
-
return (this.header.length ===
|
|
32
|
+
return (this.header.length === 0n ? remaining : Number(this.header.length)) - this.getHeaderLength();
|
|
33
33
|
}
|
|
34
34
|
async readAtoms(tokenizer, dataHandler, size) {
|
|
35
35
|
while (size > 0) {
|
|
36
36
|
const atomBean = await Atom.readAtom(tokenizer, dataHandler, this, size);
|
|
37
37
|
this.children.push(atomBean);
|
|
38
|
-
size -= atomBean.header.length ===
|
|
38
|
+
size -= atomBean.header.length === 0n ? size : Number(atomBean.header.length);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
async readData(tokenizer, dataHandler, remaining) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const mp4ParserLoader = {
|
|
2
|
+
parserType: 'mp4',
|
|
3
|
+
extensions: ['.mp4', '.m4a', '.m4b', '.m4pa', 'm4v', 'm4r', '3gp'],
|
|
4
|
+
async load(metadata, tokenizer, options) {
|
|
5
|
+
return new (await import('./MP4Parser.js')).MP4Parser(metadata, tokenizer, options);
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=Mp4Loader.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const mpegParserLoader = {
|
|
2
|
+
parserType: 'mpeg',
|
|
3
|
+
extensions: ['.mp2', '.mp3', '.m2a', '.aac', 'aacp'],
|
|
4
|
+
async load(metadata, tokenizer, options) {
|
|
5
|
+
return new (await import('./MpegParser.js')).MpegParser(metadata, tokenizer, options);
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=MpegLoader.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const musepackParserLoader = {
|
|
2
|
+
parserType: 'musepack',
|
|
3
|
+
extensions: ['.mpc'],
|
|
4
|
+
async load(metadata, tokenizer, options) {
|
|
5
|
+
return new (await import('./MusepackParser.js')).MusepackParser(metadata, tokenizer, options);
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=MusepackLoader.js.map
|
|
@@ -5,28 +5,26 @@ import { MpcSv8Parser } from './sv8/MpcSv8Parser.js';
|
|
|
5
5
|
import { MpcSv7Parser } from './sv7/MpcSv7Parser.js';
|
|
6
6
|
import { MusepackContentError } from './MusepackConentError.js';
|
|
7
7
|
const debug = initDebug('music-metadata:parser:musepack');
|
|
8
|
-
class MusepackParser extends AbstractID3Parser {
|
|
8
|
+
export class MusepackParser extends AbstractID3Parser {
|
|
9
9
|
async postId3v2Parse() {
|
|
10
10
|
const signature = await this.tokenizer.peekToken(new Token.StringType(3, 'latin1'));
|
|
11
11
|
let mpcParser;
|
|
12
12
|
switch (signature) {
|
|
13
13
|
case 'MP+': {
|
|
14
14
|
debug('Stream-version 7');
|
|
15
|
-
mpcParser = new MpcSv7Parser();
|
|
15
|
+
mpcParser = new MpcSv7Parser(this.metadata, this.tokenizer, this.options);
|
|
16
16
|
break;
|
|
17
17
|
}
|
|
18
18
|
case 'MPC': {
|
|
19
19
|
debug('Stream-version 8');
|
|
20
|
-
mpcParser = new MpcSv8Parser();
|
|
20
|
+
mpcParser = new MpcSv8Parser(this.metadata, this.tokenizer, this.options);
|
|
21
21
|
break;
|
|
22
22
|
}
|
|
23
23
|
default: {
|
|
24
24
|
throw new MusepackContentError('Invalid signature prefix');
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
mpcParser.init(this.metadata, this.tokenizer, this.options);
|
|
28
27
|
return mpcParser.parse();
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
|
-
|
|
32
|
-
//# sourceMappingURL=index.js.map
|
|
30
|
+
//# sourceMappingURL=MusepackParser.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const oggParserLoader = {
|
|
2
|
+
parserType: 'ogg',
|
|
3
|
+
extensions: ['.ogg', '.ogv', '.oga', '.ogm', '.ogx', '.opus', '.spx'],
|
|
4
|
+
async load(metadata, tokenizer, options) {
|
|
5
|
+
return new (await import('./OggParser.js')).OggParser(metadata, tokenizer, options);
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=OggLoader.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const riffParserLoader = {
|
|
2
|
+
parserType: 'riff',
|
|
3
|
+
extensions: ['.wav', 'wave', '.bwf'],
|
|
4
|
+
async load(metadata, tokenizer, options) {
|
|
5
|
+
return new (await import('./WaveParser.js')).WaveParser(metadata, tokenizer, options);
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=WaveLoader.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const wavpackParserLoader = {
|
|
2
|
+
parserType: 'wavpack',
|
|
3
|
+
extensions: ['.wv', '.wvp'],
|
|
4
|
+
async load(metadata, tokenizer, options) {
|
|
5
|
+
return new (await import('./WavPackParser.js')).WavPackParser(metadata, tokenizer, options);
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=WavPackLoader.js.map
|
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": "10.
|
|
4
|
+
"version": "10.4.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Borewit",
|
|
7
7
|
"url": "https://github.com/Borewit"
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"debug": "^4.3.4",
|
|
109
109
|
"file-type": "^19.4.1",
|
|
110
110
|
"media-typer": "^1.1.0",
|
|
111
|
-
"strtok3": "^
|
|
111
|
+
"strtok3": "^9.0.0",
|
|
112
112
|
"token-types": "^6.0.0",
|
|
113
113
|
"uint8array-extras": "^1.4.0"
|
|
114
114
|
},
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"@types/debug": "^4.1.12",
|
|
121
121
|
"@types/media-typer": "^1.1.3",
|
|
122
122
|
"@types/mocha": "^10.0.7",
|
|
123
|
-
"@types/node": "^22.5.
|
|
123
|
+
"@types/node": "^22.5.4",
|
|
124
124
|
"c8": "^10.1.2",
|
|
125
125
|
"chai": "^5.1.1",
|
|
126
126
|
"chai-as-promised": "^8.0.0",
|