165 lines
7.8 KiB
JavaScript
165 lines
7.8 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.APEv2Parser = void 0;
|
|
const debug_1 = require("debug");
|
|
const strtok3 = require("strtok3/lib/core");
|
|
const token_types_1 = require("token-types");
|
|
const util = require("../common/Util");
|
|
const BasicParser_1 = require("../common/BasicParser");
|
|
const APEv2Token_1 = require("./APEv2Token");
|
|
const debug = (0, debug_1.default)('music-metadata:parser:APEv2');
|
|
const tagFormat = 'APEv2';
|
|
const preamble = 'APETAGEX';
|
|
class APEv2Parser extends BasicParser_1.BasicParser {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.ape = {};
|
|
}
|
|
static tryParseApeHeader(metadata, tokenizer, options) {
|
|
const apeParser = new APEv2Parser();
|
|
apeParser.init(metadata, tokenizer, options);
|
|
return apeParser.tryParseApeHeader();
|
|
}
|
|
/**
|
|
* Calculate the media file duration
|
|
* @param ah ApeHeader
|
|
* @return {number} duration in seconds
|
|
*/
|
|
static calculateDuration(ah) {
|
|
let duration = ah.totalFrames > 1 ? ah.blocksPerFrame * (ah.totalFrames - 1) : 0;
|
|
duration += ah.finalFrameBlocks;
|
|
return duration / ah.sampleRate;
|
|
}
|
|
/**
|
|
* Calculates the APEv1 / APEv2 first field offset
|
|
* @param reader
|
|
* @param offset
|
|
*/
|
|
static async findApeFooterOffset(reader, offset) {
|
|
// Search for APE footer header at the end of the file
|
|
const apeBuf = Buffer.alloc(APEv2Token_1.TagFooter.len);
|
|
await reader.randomRead(apeBuf, 0, APEv2Token_1.TagFooter.len, offset - APEv2Token_1.TagFooter.len);
|
|
const tagFooter = APEv2Token_1.TagFooter.get(apeBuf, 0);
|
|
if (tagFooter.ID === 'APETAGEX') {
|
|
debug(`APE footer header at offset=${offset}`);
|
|
return { footer: tagFooter, offset: offset - tagFooter.size };
|
|
}
|
|
}
|
|
static parseTagFooter(metadata, buffer, options) {
|
|
const footer = APEv2Token_1.TagFooter.get(buffer, buffer.length - APEv2Token_1.TagFooter.len);
|
|
if (footer.ID !== preamble)
|
|
throw new Error('Unexpected APEv2 Footer ID preamble value.');
|
|
strtok3.fromBuffer(buffer);
|
|
const apeParser = new APEv2Parser();
|
|
apeParser.init(metadata, strtok3.fromBuffer(buffer), options);
|
|
return apeParser.parseTags(footer);
|
|
}
|
|
/**
|
|
* Parse APEv1 / APEv2 header if header signature found
|
|
*/
|
|
async tryParseApeHeader() {
|
|
if (this.tokenizer.fileInfo.size && this.tokenizer.fileInfo.size - this.tokenizer.position < APEv2Token_1.TagFooter.len) {
|
|
debug(`No APEv2 header found, end-of-file reached`);
|
|
return;
|
|
}
|
|
const footer = await this.tokenizer.peekToken(APEv2Token_1.TagFooter);
|
|
if (footer.ID === preamble) {
|
|
await this.tokenizer.ignore(APEv2Token_1.TagFooter.len);
|
|
return this.parseTags(footer);
|
|
}
|
|
else {
|
|
debug(`APEv2 header not found at offset=${this.tokenizer.position}`);
|
|
if (this.tokenizer.fileInfo.size) {
|
|
// Try to read the APEv2 header using just the footer-header
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
async parse() {
|
|
const descriptor = await this.tokenizer.readToken(APEv2Token_1.DescriptorParser);
|
|
if (descriptor.ID !== 'MAC ')
|
|
throw new Error('Unexpected descriptor ID');
|
|
this.ape.descriptor = descriptor;
|
|
const lenExp = descriptor.descriptorBytes - APEv2Token_1.DescriptorParser.len;
|
|
const header = await (lenExp > 0 ? this.parseDescriptorExpansion(lenExp) : this.parseHeader());
|
|
await this.tokenizer.ignore(header.forwardBytes);
|
|
return this.tryParseApeHeader();
|
|
}
|
|
async parseTags(footer) {
|
|
const keyBuffer = Buffer.alloc(256); // maximum tag key length
|
|
let bytesRemaining = footer.size - APEv2Token_1.TagFooter.len;
|
|
debug(`Parse APE tags at offset=${this.tokenizer.position}, size=${bytesRemaining}`);
|
|
for (let i = 0; i < footer.fields; i++) {
|
|
if (bytesRemaining < APEv2Token_1.TagItemHeader.len) {
|
|
this.metadata.addWarning(`APEv2 Tag-header: ${footer.fields - i} items remaining, but no more tag data to read.`);
|
|
break;
|
|
}
|
|
// Only APEv2 tag has tag item headers
|
|
const tagItemHeader = await this.tokenizer.readToken(APEv2Token_1.TagItemHeader);
|
|
bytesRemaining -= APEv2Token_1.TagItemHeader.len + tagItemHeader.size;
|
|
await this.tokenizer.peekBuffer(keyBuffer, { length: Math.min(keyBuffer.length, bytesRemaining) });
|
|
let zero = util.findZero(keyBuffer, 0, keyBuffer.length);
|
|
const key = await this.tokenizer.readToken(new token_types_1.StringType(zero, 'ascii'));
|
|
await this.tokenizer.ignore(1);
|
|
bytesRemaining -= key.length + 1;
|
|
switch (tagItemHeader.flags.dataType) {
|
|
case APEv2Token_1.DataType.text_utf8: { // utf-8 text-string
|
|
const value = await this.tokenizer.readToken(new token_types_1.StringType(tagItemHeader.size, 'utf8'));
|
|
const values = value.split(/\x00/g);
|
|
for (const val of values) {
|
|
this.metadata.addTag(tagFormat, key, val);
|
|
}
|
|
break;
|
|
}
|
|
case APEv2Token_1.DataType.binary: // binary (probably artwork)
|
|
if (this.options.skipCovers) {
|
|
await this.tokenizer.ignore(tagItemHeader.size);
|
|
}
|
|
else {
|
|
const picData = Buffer.alloc(tagItemHeader.size);
|
|
await this.tokenizer.readBuffer(picData);
|
|
zero = util.findZero(picData, 0, picData.length);
|
|
const description = picData.toString('utf8', 0, zero);
|
|
const data = Buffer.from(picData.slice(zero + 1));
|
|
this.metadata.addTag(tagFormat, key, {
|
|
description,
|
|
data
|
|
});
|
|
}
|
|
break;
|
|
case APEv2Token_1.DataType.external_info:
|
|
debug(`Ignore external info ${key}`);
|
|
await this.tokenizer.ignore(tagItemHeader.size);
|
|
break;
|
|
case APEv2Token_1.DataType.reserved:
|
|
debug(`Ignore external info ${key}`);
|
|
this.metadata.addWarning(`APEv2 header declares a reserved datatype for "${key}"`);
|
|
await this.tokenizer.ignore(tagItemHeader.size);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
async parseDescriptorExpansion(lenExp) {
|
|
await this.tokenizer.ignore(lenExp);
|
|
return this.parseHeader();
|
|
}
|
|
async parseHeader() {
|
|
const header = await this.tokenizer.readToken(APEv2Token_1.Header);
|
|
// ToDo before
|
|
this.metadata.setFormat('lossless', true);
|
|
this.metadata.setFormat('container', 'Monkey\'s Audio');
|
|
this.metadata.setFormat('bitsPerSample', header.bitsPerSample);
|
|
this.metadata.setFormat('sampleRate', header.sampleRate);
|
|
this.metadata.setFormat('numberOfChannels', header.channel);
|
|
this.metadata.setFormat('duration', APEv2Parser.calculateDuration(header));
|
|
return {
|
|
forwardBytes: this.ape.descriptor.seekTableBytes + this.ape.descriptor.headerDataBytes +
|
|
this.ape.descriptor.apeFrameDataBytes + this.ape.descriptor.terminatingDataBytes
|
|
};
|
|
}
|
|
}
|
|
exports.APEv2Parser = APEv2Parser;
|