Initial working version

This commit is contained in:
Samuel Kent
2022-12-22 20:22:22 +11:00
parent ce9675a1cc
commit ced7fa5092
902 changed files with 150252 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
import * as strtok3 from 'strtok3/lib/core';
import { IOptions, IRandomReader, IApeHeader } from '../type';
import { INativeMetadataCollector } from '../common/MetadataCollector';
import { BasicParser } from '../common/BasicParser';
import { IFooter, IHeader } from './APEv2Token';
export declare class APEv2Parser extends BasicParser {
static tryParseApeHeader(metadata: INativeMetadataCollector, tokenizer: strtok3.ITokenizer, options: IOptions): Promise<void>;
/**
* Calculate the media file duration
* @param ah ApeHeader
* @return {number} duration in seconds
*/
static calculateDuration(ah: IHeader): number;
/**
* Calculates the APEv1 / APEv2 first field offset
* @param reader
* @param offset
*/
static findApeFooterOffset(reader: IRandomReader, offset: number): Promise<IApeHeader>;
private static parseTagFooter;
private ape;
/**
* Parse APEv1 / APEv2 header if header signature found
*/
tryParseApeHeader(): Promise<void>;
parse(): Promise<void>;
parseTags(footer: IFooter): Promise<void>;
private parseDescriptorExpansion;
private parseHeader;
}
+162
View File
@@ -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;
+4
View File
@@ -0,0 +1,4 @@
import { CaseInsensitiveTagMap } from '../common/CaseInsensitiveTagMap';
export declare class APEv2TagMapper extends CaseInsensitiveTagMap {
constructor();
}
+86
View File
@@ -0,0 +1,86 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.APEv2TagMapper = void 0;
const CaseInsensitiveTagMap_1 = require("../common/CaseInsensitiveTagMap");
/**
* ID3v2.2 tag mappings
*/
const apev2TagMap = {
Title: 'title',
Artist: 'artist',
Artists: 'artists',
'Album Artist': 'albumartist',
Album: 'album',
Year: 'date',
Originalyear: 'originalyear',
Originaldate: 'originaldate',
Comment: 'comment',
Track: 'track',
Disc: 'disk',
DISCNUMBER: 'disk',
Genre: 'genre',
'Cover Art (Front)': 'picture',
'Cover Art (Back)': 'picture',
Composer: 'composer',
Lyrics: 'lyrics',
ALBUMSORT: 'albumsort',
TITLESORT: 'titlesort',
WORK: 'work',
ARTISTSORT: 'artistsort',
ALBUMARTISTSORT: 'albumartistsort',
COMPOSERSORT: 'composersort',
Lyricist: 'lyricist',
Writer: 'writer',
Conductor: 'conductor',
// 'Performer=artist (instrument)': 'performer:instrument',
MixArtist: 'remixer',
Arranger: 'arranger',
Engineer: 'engineer',
Producer: 'producer',
DJMixer: 'djmixer',
Mixer: 'mixer',
Label: 'label',
Grouping: 'grouping',
Subtitle: 'subtitle',
DiscSubtitle: 'discsubtitle',
Compilation: 'compilation',
BPM: 'bpm',
Mood: 'mood',
Media: 'media',
CatalogNumber: 'catalognumber',
MUSICBRAINZ_ALBUMSTATUS: 'releasestatus',
MUSICBRAINZ_ALBUMTYPE: 'releasetype',
RELEASECOUNTRY: 'releasecountry',
Script: 'script',
Language: 'language',
Copyright: 'copyright',
LICENSE: 'license',
EncodedBy: 'encodedby',
EncoderSettings: 'encodersettings',
Barcode: 'barcode',
ISRC: 'isrc',
ASIN: 'asin',
musicbrainz_trackid: 'musicbrainz_recordingid',
musicbrainz_releasetrackid: 'musicbrainz_trackid',
MUSICBRAINZ_ALBUMID: 'musicbrainz_albumid',
MUSICBRAINZ_ARTISTID: 'musicbrainz_artistid',
MUSICBRAINZ_ALBUMARTISTID: 'musicbrainz_albumartistid',
MUSICBRAINZ_RELEASEGROUPID: 'musicbrainz_releasegroupid',
MUSICBRAINZ_WORKID: 'musicbrainz_workid',
MUSICBRAINZ_TRMID: 'musicbrainz_trmid',
MUSICBRAINZ_DISCID: 'musicbrainz_discid',
Acoustid_Id: 'acoustid_id',
ACOUSTID_FINGERPRINT: 'acoustid_fingerprint',
MUSICIP_PUID: 'musicip_puid',
Weblink: 'website',
REPLAYGAIN_TRACK_GAIN: 'replaygain_track_gain',
REPLAYGAIN_TRACK_PEAK: 'replaygain_track_peak',
MP3GAIN_MINMAX: 'replaygain_track_minmax',
MP3GAIN_UNDO: 'replaygain_undo'
};
class APEv2TagMapper extends CaseInsensitiveTagMap_1.CaseInsensitiveTagMap {
constructor() {
super(['APEv2'], apev2TagMap);
}
}
exports.APEv2TagMapper = APEv2TagMapper;
+100
View File
@@ -0,0 +1,100 @@
import * as Token from 'token-types';
import { IGetToken } from 'strtok3/lib/core';
/**
* APETag versionIndex history / supported formats
*
* 1.0 (1000) - Original APE tag spec. Fully supported by this code.
* 2.0 (2000) - Refined APE tag spec (better streaming support, UTF StringEncoding). Fully supported by this code.
*
* Notes:
* - also supports reading of ID3v1.1 tags
* - all saving done in the APE Tag format using CURRENT_APE_TAG_VERSION
*
* APE File Format Overview: (pieces in order -- only valid for the latest versionIndex APE files)
*
* JUNK - any amount of "junk" before the APE_DESCRIPTOR (so people that put ID3v2 tags on the files aren't hosed)
* APE_DESCRIPTOR - defines the sizes (and offsets) of all the pieces, as well as the MD5 checksum
* APE_HEADER - describes all of the necessary information about the APE file
* SEEK TABLE - the table that represents seek offsets [optional]
* HEADER DATA - the pre-audio data from the original file [optional]
* APE FRAMES - the actual compressed audio (broken into frames for seekability)
* TERMINATING DATA - the post-audio data from the original file [optional]
* TAG - describes all the properties of the file [optional]
*/
export interface IDescriptor {
ID: string;
version: number;
descriptorBytes: number;
headerBytes: number;
seekTableBytes: number;
headerDataBytes: number;
apeFrameDataBytes: number;
apeFrameDataBytesHigh: number;
terminatingDataBytes: number;
fileMD5: Uint8Array;
}
/**
* APE_HEADER: describes all of the necessary information about the APE file
*/
export interface IHeader {
compressionLevel: number;
formatFlags: number;
blocksPerFrame: number;
finalFrameBlocks: number;
totalFrames: number;
bitsPerSample: number;
channel: number;
sampleRate: number;
}
export interface IFooter {
ID: string;
version: number;
size: number;
fields: number;
flags: ITagFlags;
}
export declare enum DataType {
text_utf8 = 0,
binary = 1,
external_info = 2,
reserved = 3
}
/**
* APE_DESCRIPTOR: defines the sizes (and offsets) of all the pieces, as well as the MD5 checksum
*/
export declare const DescriptorParser: IGetToken<IDescriptor>;
/**
* APE_HEADER: describes all of the necessary information about the APE file
*/
export declare const Header: IGetToken<IHeader>;
/**
* APE Tag Header/Footer Version 2.0
* TAG: describes all the properties of the file [optional]
*/
export declare const TagFooter: IGetToken<IFooter>;
/**
* APE Tag v2.0 Item Header
*/
export interface ITagItemHeader {
size: number;
flags: ITagFlags;
}
/**
* APE Tag v2.0 Item Header
*/
export declare const TagItemHeader: IGetToken<ITagItemHeader>;
export declare const TagField: (footer: any) => Token.Uint8ArrayType;
export interface ITagFlags {
containsHeader: boolean;
containsFooter: boolean;
isHeader: boolean;
readOnly: boolean;
dataType: DataType;
}
export declare function parseTagFlags(flags: any): ITagFlags;
/**
* @param num {number}
* @param bit 0 is least significant bit (LSB)
* @return {boolean} true if bit is 1; otherwise false
*/
export declare function isBitSet(num: any, bit: any): boolean;
+126
View File
@@ -0,0 +1,126 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isBitSet = exports.parseTagFlags = exports.TagField = exports.TagItemHeader = exports.TagFooter = exports.Header = exports.DescriptorParser = exports.DataType = void 0;
const Token = require("token-types");
const FourCC_1 = require("../common/FourCC");
var DataType;
(function (DataType) {
DataType[DataType["text_utf8"] = 0] = "text_utf8";
DataType[DataType["binary"] = 1] = "binary";
DataType[DataType["external_info"] = 2] = "external_info";
DataType[DataType["reserved"] = 3] = "reserved";
})(DataType = exports.DataType || (exports.DataType = {}));
/**
* APE_DESCRIPTOR: defines the sizes (and offsets) of all the pieces, as well as the MD5 checksum
*/
exports.DescriptorParser = {
len: 52,
get: (buf, off) => {
return {
// should equal 'MAC '
ID: FourCC_1.FourCcToken.get(buf, off),
// versionIndex number * 1000 (3.81 = 3810) (remember that 4-byte alignment causes this to take 4-bytes)
version: Token.UINT32_LE.get(buf, off + 4) / 1000,
// the number of descriptor bytes (allows later expansion of this header)
descriptorBytes: Token.UINT32_LE.get(buf, off + 8),
// the number of header APE_HEADER bytes
headerBytes: Token.UINT32_LE.get(buf, off + 12),
// the number of header APE_HEADER bytes
seekTableBytes: Token.UINT32_LE.get(buf, off + 16),
// the number of header data bytes (from original file)
headerDataBytes: Token.UINT32_LE.get(buf, off + 20),
// the number of bytes of APE frame data
apeFrameDataBytes: Token.UINT32_LE.get(buf, off + 24),
// the high order number of APE frame data bytes
apeFrameDataBytesHigh: Token.UINT32_LE.get(buf, off + 28),
// the terminating data of the file (not including tag data)
terminatingDataBytes: Token.UINT32_LE.get(buf, off + 32),
// the MD5 hash of the file (see notes for usage... it's a little tricky)
fileMD5: new Token.Uint8ArrayType(16).get(buf, off + 36)
};
}
};
/**
* APE_HEADER: describes all of the necessary information about the APE file
*/
exports.Header = {
len: 24,
get: (buf, off) => {
return {
// the compression level (see defines I.E. COMPRESSION_LEVEL_FAST)
compressionLevel: Token.UINT16_LE.get(buf, off),
// any format flags (for future use)
formatFlags: Token.UINT16_LE.get(buf, off + 2),
// the number of audio blocks in one frame
blocksPerFrame: Token.UINT32_LE.get(buf, off + 4),
// the number of audio blocks in the final frame
finalFrameBlocks: Token.UINT32_LE.get(buf, off + 8),
// the total number of frames
totalFrames: Token.UINT32_LE.get(buf, off + 12),
// the bits per sample (typically 16)
bitsPerSample: Token.UINT16_LE.get(buf, off + 16),
// the number of channels (1 or 2)
channel: Token.UINT16_LE.get(buf, off + 18),
// the sample rate (typically 44100)
sampleRate: Token.UINT32_LE.get(buf, off + 20)
};
}
};
/**
* APE Tag Header/Footer Version 2.0
* TAG: describes all the properties of the file [optional]
*/
exports.TagFooter = {
len: 32,
get: (buf, off) => {
return {
// should equal 'APETAGEX'
ID: new Token.StringType(8, 'ascii').get(buf, off),
// equals CURRENT_APE_TAG_VERSION
version: Token.UINT32_LE.get(buf, off + 8),
// the complete size of the tag, including this footer (excludes header)
size: Token.UINT32_LE.get(buf, off + 12),
// the number of fields in the tag
fields: Token.UINT32_LE.get(buf, off + 16),
// reserved for later use (must be zero),
flags: parseTagFlags(Token.UINT32_LE.get(buf, off + 20))
};
}
};
/**
* APE Tag v2.0 Item Header
*/
exports.TagItemHeader = {
len: 8,
get: (buf, off) => {
return {
// Length of assigned value in bytes
size: Token.UINT32_LE.get(buf, off),
// reserved for later use (must be zero),
flags: parseTagFlags(Token.UINT32_LE.get(buf, off + 4))
};
}
};
const TagField = footer => {
return new Token.Uint8ArrayType(footer.size - exports.TagFooter.len);
};
exports.TagField = TagField;
function parseTagFlags(flags) {
return {
containsHeader: isBitSet(flags, 31),
containsFooter: isBitSet(flags, 30),
isHeader: isBitSet(flags, 31),
readOnly: isBitSet(flags, 0),
dataType: (flags & 6) >> 1
};
}
exports.parseTagFlags = parseTagFlags;
/**
* @param num {number}
* @param bit 0 is least significant bit (LSB)
* @return {boolean} true if bit is 1; otherwise false
*/
function isBitSet(num, bit) {
return (num & 1 << bit) !== 0;
}
exports.isBitSet = isBitSet;