Skip to content

Commit

Permalink
Showing 19 changed files with 59 additions and 69 deletions.
4 changes: 2 additions & 2 deletions doc/common_metadata.md
Original file line number Diff line number Diff line change
@@ -3,10 +3,10 @@
Common tags, and _native_ to _common_ tag mappings. _n_ indicates the multiplicity.
The tag mapping is strongly inspired on the [MusicBrainz Picard tag-mapping](https://picard.musicbrainz.org/docs/mappings/).

| Common tag | n | Description | ID3v1 | ID3v2.2 | ID3v2.3 | ID3v2.4 | iTunes | vorbis | APEv2 | asf | exif | EBML |
| Common tag | n | Description | ID3v1 | ID3v2.2 | ID3v2.3 | ID3v2.4 | iTunes | vorbis | APEv2 | asf | exif | matroska |
|----------------------------|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------|---------|--------------|-----------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|---------------------------------------------------------|-----------------------------------------------|-------------------------------------|-----------------------------------|------------|----------------------------|
| year | 1 | Release year | year | TYE | TYER | TYER | | | | | YEAR | |
| track | 1 | Track number on the media, e.g. `{no: 1, of: 2}` | track | TRK | TRCK | TRCK | trkn | TRACKNUMBER | TRACK | WM/TrackNumber | ITRK | |
| track | 1 | Track number on the media, e.g. `{no: 1, of: 2}` | track | TRK | TRCK | TRCK | trkn | TRACKNUMBER | TRACK | WM/TrackNumber | ITRK | TRACK:PART_NUMBER |
| disk | 1 | Disk or media number, e.g. `{no: 1, of: 2}` | | TPA | TPOS | TPOS | disk | DISCNUMBER | DISC, DISCNUMBER | WM/PartOfSet | | |
| title | 1 | Track title | title | TT2 | TIT2 | TIT2 | ©nam | TITLE | TITLE | Title | INAM, TITL | TRACK:TITLE |
| artist | 1 | Literal written track artist e.g.: `"Beth Hart & Joe Bonamassa"`. If not directly specified in a tag, this is automatically filled with `common.artists`. | artist | TP1 | TPE1 | TPE1 | ©ART | ARTIST | ARTIST | Author | IART | TRACK:ARTIST |
8 changes: 5 additions & 3 deletions lib/ParserFactory.ts
Original file line number Diff line number Diff line change
@@ -60,13 +60,15 @@ export class ParserFactory {
* @param opts - Options
* @returns Native metadata
*/
public static async parseOnContentType(tokenizer: ITokenizer, contentType: string, opts: IOptions): Promise<IAudioMetadata> {
public static async parseOnContentType(tokenizer: ITokenizer, opts: IOptions): Promise<IAudioMetadata> {

const { mimeType, path, url } = await tokenizer.fileInfo;

// Resolve parser based on MIME-type or file extension
const parserId = ParserFactory.getParserIdForMimeType(contentType) || ParserFactory.getParserIdForExtension(contentType);
const parserId = ParserFactory.getParserIdForMimeType(mimeType) || ParserFactory.getParserIdForExtension(path) || ParserFactory.getParserIdForExtension(url);

if (!parserId) {
debug('No parser found for MIME-type / extension: ' + contentType);
debug('No parser found for MIME-type / extension: ' + mimeType);
}

return this.parse(tokenizer, parserId, opts);
6 changes: 3 additions & 3 deletions lib/apev2/APEv2Parser.ts
Original file line number Diff line number Diff line change
@@ -85,7 +85,7 @@ export class APEv2Parser extends BasicParser {
*/
public async tryParseApeHeader(): Promise<void> {

if (this.tokenizer.fileSize && this.tokenizer.fileSize - this.tokenizer.position < TagFooter.len) {
if (this.tokenizer.fileInfo.size && this.tokenizer.fileInfo.size - this.tokenizer.position < TagFooter.len) {
debug(`No APEv2 header found, end-of-file reached`);
return;
}
@@ -96,9 +96,9 @@ export class APEv2Parser extends BasicParser {
return this.parseTags(footer);
} else {
debug(`APEv2 header not found at offset=${this.tokenizer.position}`);
if (this.tokenizer.fileSize) {
if (this.tokenizer.fileInfo.size) {
// Try to read the APEv2 header using just the footer-header
const remaining = this.tokenizer.fileSize - this.tokenizer.position; // ToDo: take ID3v1 into account
const remaining = this.tokenizer.fileInfo.size - this.tokenizer.position; // ToDo: take ID3v1 into account
const buffer = Buffer.alloc(remaining);
await this.tokenizer.readBuffer(buffer);
return APEv2Parser.parseTagFooter(this.metadata, buffer, this.options);
22 changes: 9 additions & 13 deletions lib/core.ts
Original file line number Diff line number Diff line change
@@ -11,43 +11,39 @@ import { getLyricsHeaderLength } from './lyrics3/Lyrics3';
/**
* Parse audio from Node Stream.Readable
* @param stream - Stream to read the audio track from
* @param mimeType - Content specification MIME-type, e.g.: 'audio/mpeg'
* @param options - Parsing options
* @param fileInfo - File information object or MIME-type string
* @returns Metadata
*/
export function parseStream(stream: Stream.Readable, mimeType?: string, options: IOptions = {}): Promise<IAudioMetadata> {
return parseFromTokenizer(strtok3.fromStream(stream), mimeType, options);
export function parseStream(stream: Stream.Readable, fileInfo?: strtok3.IFileInfo | string, options: IOptions = {}): Promise<IAudioMetadata> {
return parseFromTokenizer(strtok3.fromStream(stream, typeof fileInfo === 'string' ? {mimeType: fileInfo} : fileInfo), options);
}

/**
* Parse audio from Node Buffer
* @param buf - Buffer holding audio data
* @param mimeType - Content specification MIME-type, e.g.: 'audio/mpeg'
* @param fileInfo - File information object or MIME-type string
* @param options - Parsing options
* @returns Metadata
* Ref: https://github.com/Borewit/strtok3/blob/e6938c81ff685074d5eb3064a11c0b03ca934c1d/src/index.ts#L15
*/
export async function parseBuffer(buf: Buffer, mimeType?: string, options: IOptions = {}): Promise<IAudioMetadata> {
export async function parseBuffer(buf: Buffer, fileInfo?: strtok3.IFileInfo | string, options: IOptions = {}): Promise<IAudioMetadata> {

const bufferReader = new RandomBufferReader(buf);
await scanAppendingHeaders(bufferReader, options);

const tokenizer = strtok3.fromBuffer(buf);
return parseFromTokenizer(tokenizer, mimeType, options);
const tokenizer = strtok3.fromBuffer(buf, typeof fileInfo === 'string' ? {mimeType: fileInfo} : fileInfo);
return parseFromTokenizer(tokenizer, options);
}

/**
* Parse audio from ITokenizer source
* @param tokenizer - Audio source implementing the tokenizer interface
* @param mimeType - Content specification MIME-type, e.g.: 'audio/mpeg'
* @param options - Parsing options
* @returns Metadata
*/
export function parseFromTokenizer(tokenizer: strtok3.ITokenizer, mimeType?: string, options?: IOptions): Promise<IAudioMetadata> {
if (!tokenizer.fileSize && options && options.fileSize) {
tokenizer.fileSize = options.fileSize;
}
return ParserFactory.parseOnContentType(tokenizer, mimeType, options);
export function parseFromTokenizer(tokenizer: strtok3.ITokenizer, options?: IOptions): Promise<IAudioMetadata> {
return ParserFactory.parseOnContentType(tokenizer, options);
}

/**
4 changes: 2 additions & 2 deletions lib/flac/FlacParser.ts
Original file line number Diff line number Diff line change
@@ -64,8 +64,8 @@ export class FlacParser extends AbstractID3Parser {
}
while (!blockHeader.lastBlock);

if (this.tokenizer.fileSize && this.metadata.format.duration) {
const dataSize = this.tokenizer.fileSize - this.tokenizer.position;
if (this.tokenizer.fileInfo.size && this.metadata.format.duration) {
const dataSize = this.tokenizer.fileInfo.size - this.tokenizer.position;
this.metadata.setFormat('bitrate', 8 * dataSize / this.metadata.format.duration);
}
}
8 changes: 4 additions & 4 deletions lib/id3v1/ID3v1Parser.ts
Original file line number Diff line number Diff line change
@@ -116,7 +116,7 @@ export class ID3v1Parser extends BasicParser {

public async parse(): Promise<void> {

if (!this.tokenizer.fileSize) {
if (!this.tokenizer.fileInfo.size) {
debug('Skip checking for ID3v1 because the file-size is unknown');
return;
}
@@ -128,14 +128,14 @@ export class ID3v1Parser extends BasicParser {
await apeParser.parseTags(this.options.apeHeader.footer);
}

const offset = this.tokenizer.fileSize - Iid3v1Token.len;
const offset = this.tokenizer.fileInfo.size - Iid3v1Token.len;
if (this.tokenizer.position > offset) {
debug('Already consumed the last 128 bytes');
return;
}
const header = await this.tokenizer.readToken<IId3v1Header>(Iid3v1Token, offset);
if (header) {
debug("ID3v1 header found at: pos=%s", this.tokenizer.fileSize - Iid3v1Token.len);
debug("ID3v1 header found at: pos=%s", this.tokenizer.fileInfo.size - Iid3v1Token.len);
for (const id of ["title", "artist", "album", "comment", "track", "year"]) {
if (header[id] && header[id] !== "")
this.addTag(id, header[id]);
@@ -144,7 +144,7 @@ export class ID3v1Parser extends BasicParser {
if (genre)
this.addTag('genre', genre);
} else {
debug("ID3v1 header not found at: pos=%s", this.tokenizer.fileSize - Iid3v1Token.len);
debug("ID3v1 header not found at: pos=%s", this.tokenizer.fileInfo.size - Iid3v1Token.len);
}
}

22 changes: 6 additions & 16 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -11,30 +11,20 @@ export { IAudioMetadata, IOptions, ITag, INativeTagDict, ICommonTagsResult, IFor

const debug = _debug("music-metadata:parser");

export { parseFromTokenizer } from './core';
export { parseFromTokenizer, parseBuffer } from './core';

/**
* Parse audio from Node Stream.Readable
* @param stream - Stream to read the audio track from
* @param mimeType - Content specification MIME-type, e.g.: 'audio/mpeg'
* @param fileInfo - File information object or MIME-type, e.g.: 'audio/mpeg'
* @param options - Parsing options
* @returns Metadata
*/
export async function parseStream(stream: Stream.Readable, mimeType?: string, options: IOptions = {}): Promise<IAudioMetadata> {
const tokenizer = await strtok3.fromStream(stream);
return Core.parseFromTokenizer(tokenizer, mimeType, options);
export async function parseStream(stream: Stream.Readable, fileInfo?: strtok3.IFileInfo | string, options: IOptions = {}): Promise<IAudioMetadata> {
const tokenizer = await strtok3.fromStream(stream, typeof fileInfo === 'string' ? {mimeType: fileInfo} : fileInfo);
return Core.parseFromTokenizer(tokenizer, options);
}

/**
* Parse audio from Node Buffer
* @param stream - Audio input stream
* @param mimeType - Content specification MIME-type, e.g.: 'audio/mpeg'
* @param options - Parsing options
* @returns Metadata
* Ref: https://github.com/Borewit/strtok3/blob/e6938c81ff685074d5eb3064a11c0b03ca934c1d/src/index.ts#L15
*/
export const parseBuffer = Core.parseBuffer;

/**
* Parse audio from Node file
* @param filePath - Media file to read meta-data from
@@ -47,7 +37,7 @@ export async function parseFile(filePath: string, options: IOptions = {}): Promi

const fileTokenizer = await strtok3.fromFile(filePath);

const fileReader = new RandomFileReader(filePath, fileTokenizer.fileSize);
const fileReader = new RandomFileReader(filePath, fileTokenizer.fileInfo.size);
try {
await Core.scanAppendingHeaders(fileReader, options);
} finally {
2 changes: 1 addition & 1 deletion lib/matroska/MatroskaParser.ts
Original file line number Diff line number Diff line change
@@ -50,7 +50,7 @@ export class MatroskaParser extends BasicParser {
}

public async parse(): Promise<void> {
const matroska = await this.parseContainer(matroskaDtd.elements, this.tokenizer.fileSize, []) as any as IMatroskaDoc;
const matroska = await this.parseContainer(matroskaDtd.elements, this.tokenizer.fileInfo.size, []) as any as IMatroskaDoc;

this.metadata.setFormat('container', `EBML/${matroska.ebml.docType}`);
if (matroska.segment) {
4 changes: 2 additions & 2 deletions lib/mp4/MP4Parser.ts
Original file line number Diff line number Diff line change
@@ -144,9 +144,9 @@ export class MP4Parser extends BasicParser {

this.tracks = [];

let remainingFileSize = this.tokenizer.fileSize;
let remainingFileSize = this.tokenizer.fileInfo.size;

while (!this.tokenizer.fileSize || remainingFileSize > 0) {
while (!this.tokenizer.fileInfo.size || remainingFileSize > 0) {
try {
await this.tokenizer.peekToken<AtomToken.IAtomHeader>(AtomToken.Header);
} catch (error) {
10 changes: 5 additions & 5 deletions lib/mpeg/MpegParser.ts
Original file line number Diff line number Diff line change
@@ -340,13 +340,13 @@ export class MpegParser extends AbstractID3Parser {

const format = this.metadata.format;
const hasID3v1 = this.metadata.native.hasOwnProperty('ID3v1');
if (format.duration && this.tokenizer.fileSize) {
const mpegSize = this.tokenizer.fileSize - this.mpegOffset - (hasID3v1 ? 128 : 0);
if (format.duration && this.tokenizer.fileInfo.size) {
const mpegSize = this.tokenizer.fileInfo.size - this.mpegOffset - (hasID3v1 ? 128 : 0);
if (format.codecProfile && format.codecProfile[0] === 'V') {
this.metadata.setFormat('bitrate', mpegSize * 8 / format.duration);
}
} else if (this.tokenizer.fileSize && format.codecProfile === 'CBR') {
const mpegSize = this.tokenizer.fileSize - this.mpegOffset - (hasID3v1 ? 128 : 0);
} else if (this.tokenizer.fileInfo.size && format.codecProfile === 'CBR') {
const mpegSize = this.tokenizer.fileInfo.size - this.mpegOffset - (hasID3v1 ? 128 : 0);
const numberOfSamples = Math.round(mpegSize / this.frame_size) * this.samplesPerFrame;
this.metadata.setFormat('numberOfSamples', numberOfSamples);
const duration = numberOfSamples / format.sampleRate;
@@ -470,7 +470,7 @@ export class MpegParser extends AbstractID3Parser {
// Actual calculation will be done in finalize
this.samplesPerFrame = samples_per_frame;
this.metadata.setFormat('codecProfile', 'CBR');
if (this.tokenizer.fileSize)
if (this.tokenizer.fileInfo.size)
return true; // Will calculate duration based on the file size
} else if (this.metadata.format.duration) {
return true; // We already got the duration, stop processing MPEG stream any further
4 changes: 2 additions & 2 deletions lib/ogg/opus/OpusParser.ts
Original file line number Diff line number Diff line change
@@ -56,8 +56,8 @@ export class OpusParser extends VorbisParser {
this.metadata.setFormat('numberOfSamples', header.absoluteGranulePosition - this.idHeader.preSkip);
this.metadata.setFormat('duration', this.metadata.format.numberOfSamples / this.idHeader.inputSampleRate);

if (this.lastPos !== -1 && this.tokenizer.fileSize && this.metadata.format.duration) {
const dataSize = this.tokenizer.fileSize - this.lastPos;
if (this.lastPos !== -1 && this.tokenizer.fileInfo.size && this.metadata.format.duration) {
const dataSize = this.tokenizer.fileInfo.size - this.lastPos;
this.metadata.setFormat('bitrate', 8 * dataSize / this.metadata.format.duration);
}
}
2 changes: 1 addition & 1 deletion lib/wavpack/WavPackParser.ts
Original file line number Diff line number Diff line change
@@ -67,7 +67,7 @@ export class WavPackParser extends BasicParser {
this.audioDataSize += header.blockSize; // Count audio data for bit-rate calculation
}
}
while (!this.tokenizer.fileSize || this.tokenizer.fileSize - this.tokenizer.position >= WavPack.BlockHeaderToken.len);
while (!this.tokenizer.fileInfo.size || this.tokenizer.fileInfo.size - this.tokenizer.position >= WavPack.BlockHeaderToken.len);
this.metadata.setFormat('bitrate', this.audioDataSize * 8 / this.metadata.format.duration);
}

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -83,7 +83,7 @@
"debug": "^4.1.0",
"file-type": "^12.4.2",
"media-typer": "^1.1.0",
"strtok3": "^4.1.1",
"strtok3": "^5.0.0",
"token-types": "^2.0.0"
},
"devDependencies": {
4 changes: 2 additions & 2 deletions test/metadata-parsers.ts
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ export const Parsers: IParser[] = [
description: 'parseStream',
initParser: (filePath: string, mimeType?: string, options?: IOptions) => {
const stream = fs.createReadStream(filePath);
return mm.parseStream(stream, mimeType, options).then(metadata => {
return mm.parseStream(stream, {mimeType}, options).then(metadata => {
stream.close();
return metadata;
});
@@ -31,7 +31,7 @@ export const Parsers: IParser[] = [
description: 'parseBuffer',
initParser: (filePath: string, mimeType?: string, options?: IOptions) => {
const buffer = fs.readFileSync(filePath);
return mm.parseBuffer(buffer, mimeType, options);
return mm.parseBuffer(buffer, {mimeType}, options);
}
}
];
4 changes: 2 additions & 2 deletions test/test-file-mp4.ts
Original file line number Diff line number Diff line change
@@ -266,7 +266,7 @@ describe('Parse MPEG-4 files with iTunes metadata', () => {
let metadata: mm.IAudioMetadata;
const stream = fs.createReadStream(filePath);
try {
metadata = await mm.parseStream(stream, 'audio/mp4', {includeChapters: true});
metadata = await mm.parseStream(stream, {mimeType: 'audio/mp4'}, {includeChapters: true});
} finally {
stream.close();
}
@@ -276,7 +276,7 @@ describe('Parse MPEG-4 files with iTunes metadata', () => {
it('from a stream', async () => {

const stream = fs.createReadStream(filePath);
const metadata = await mm.parseStream(stream, 'audio/mp4', {includeChapters: true});
const metadata = await mm.parseStream(stream, {mimeType: 'audio/mp4'}, {includeChapters: true});
stream.close();

checkMetadata(metadata);
4 changes: 2 additions & 2 deletions test/test-file-mpeg.ts
Original file line number Diff line number Diff line change
@@ -56,7 +56,7 @@ describe('Parse MPEG', () => {

const streamReader = new SourceStream(buf);

await mm.parseStream(streamReader, 'audio/mpeg', {duration: true});
await mm.parseStream(streamReader, {mimeType: 'audio/mpeg'}, {duration: true});
});

it('should sync efficient, from a file', async function() {
@@ -354,7 +354,7 @@ describe('Parse MPEG', () => {
let metadata: mm.IAudioMetadata;
try {
stream.path = undefined; // disable file size based calculation
metadata = await mm.parseStream(stream, 'audio/mpeg', {duration: true});
metadata = await mm.parseStream(stream, {mimeType: 'audio/mpeg'}, {duration: true});
} finally {
stream.close();
}
2 changes: 1 addition & 1 deletion test/test-http.ts
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ describe.skip('HTTP streaming', function() {
options.fileSize = parseInt(response.headers['content-length'], 10); // Always pass this in production
}

const tags = await parseStream(response.stream, response.headers['content-type'], options);
const tags = await parseStream(response.stream, {mimeType: response.headers['content-type']}, options);
if (response.stream.destroy) {
response.stream.destroy(); // Node >= v8 only
}
8 changes: 5 additions & 3 deletions test/test-mime.ts
Original file line number Diff line number Diff line change
@@ -67,7 +67,7 @@ describe("MIME & extension mapping", () => {
audioExtension.forEach(extension => {

const streamReader = new SourceStream(buf);
const res = mm.parseStream(streamReader, extension).catch(err => {
const res = mm.parseStream(streamReader, {path: extension}).catch(err => {
handleError(extension, err);
});

@@ -81,7 +81,8 @@ describe("MIME & extension mapping", () => {
it("should be able to handle MIME-type parameter(s)", () => {

const stream = fs.createReadStream(path.join(samplePath, "MusicBrainz - Beth Hart - Sinner's Prayer [id3v2.3].wav"));
return mm.parseStream(stream, '').then(metadata => {
(stream as any).path = undefined; // Prevent type detection via path
return mm.parseStream(stream).then(metadata => {
stream.close();
assert.equal(metadata.format.container, 'WAVE');
});
@@ -112,7 +113,8 @@ describe("MIME & extension mapping", () => {
it("should throw error on recognized MIME-type which is not supported", () => {

const stream = fs.createReadStream(path.join(samplePath, 'flac.flac.jpg'));
return mm.parseStream(stream, "audio/not-existing")
(stream.path as any) = undefined;
return mm.parseStream(stream, {mimeType: "audio/not-existing"})
.then(() => {
stream.close();
assert.fail('Should throw an Error');
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -2780,10 +2780,10 @@ strip-json-comments@^3.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7"
integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==

strtok3@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-4.1.1.tgz#75043bb6175ebb22f10d48dfe9b06560345dc647"
integrity sha512-7nfDPVwCrx35LVYqEZPfrNJuoqlgOcsW2PIcru4/IbYXjtI17WtdZLtRJtpwR1Mj/alJ01FY57NsX4Gwl/ntTg==
strtok3@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-5.0.0.tgz#cf48cdede0b7641eb97e62918abce18441cff688"
integrity sha512-HpdgEUSkMqlTjO7uWEBvWHEKBYqXCbVeihlE+sa0keGsfVXspVxye1dPa4OYvnzOJsErzn6ohQU1U/ozcVAPKQ==
dependencies:
"@tokenizer/token" "^0.1.0"
debug "^4.1.1"

0 comments on commit 1ee7de2

Please sign in to comment.