Initial working version
This commit is contained in:
+162
@@ -0,0 +1,162 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.APEv2Parser = void 0;
|
||||
const initDebug = require("debug");
|
||||
const strtok3 = require("strtok3/lib/core");
|
||||
const util = require("../common/Util");
|
||||
const BasicParser_1 = require("../common/BasicParser");
|
||||
const APEv2Token_1 = require("./APEv2Token");
|
||||
const token_types_1 = require("token-types");
|
||||
const debug = initDebug('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 textstring
|
||||
const value = await this.tokenizer.readToken(new token_types_1.StringType(tagItemHeader.size, 'utf8'));
|
||||
const values = value.split(/\x00/g);
|
||||
/*jshint loopfunc:true */
|
||||
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;
|
||||
default:
|
||||
throw new Error(`Unexpected data-type: ${tagItemHeader.flags.dataType}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
Reference in New Issue
Block a user