import * as initDebug from 'debug' ; import * as strtok3 from 'strtok3/lib/core' ; import { StringType } from 'token-types' ; import * as util from '../common/Util' ; import { IOptions , IRandomReader , IApeHeader } from '../type' ; import { INativeMetadataCollector } from '../common/MetadataCollector' ; import { BasicParser } from '../common/BasicParser' ; import { DataType , DescriptorParser , Header , IDescriptor , IFooter , IHeader , ITagItemHeader , TagFooter , TagItemHeader } from './APEv2Token' ; const debug = initDebug ( 'music-metadata:parser:APEv2' ) ; const tagFormat = 'APEv2' ; interface IApeInfo { descriptor ? : IDescriptor ; header ? : IHeader ; footer ? : IFooter ; } const preamble = 'APETAGEX' ; export class APEv2Parser extends BasicParser { public static tryParseApeHeader ( metadata , tokenizer , options ) { const apeParser = new APEv2Parser ( ) ; apeParser . init ( metadata , tokenizer , options ) ; return apeParser . tryParseApeHeader ( ) ; } param ah return { number } public static calculateDuration ( ah ) { let duration = ah . totalFrames > 1 ? ah . blocksPerFrame * ( ah . totalFrames - 1 ) : 0 ; duration += ah . finalFrameBlocks ; return duration / ah . sampleRate ; } param reader param offset public static async findApeFooterOffset ( reader , offset ) { const apeBuf = Buffer . alloc ( TagFooter . len ) ; await reader . randomRead ( apeBuf , 0 , TagFooter . len , offset - TagFooter . len ) ; const tagFooter = TagFooter . get ( apeBuf , 0 ) ; if ( tagFooter . ID === 'APETAGEX' ) { debug ( `APE footer header at offset=${ offset }` ) ; return { footer : tagFooter , offset : offset - tagFooter . size } ; } } private static parseTagFooter ( metadata , buffer , options ) { const footer = TagFooter . get ( buffer , buffer . length - 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 ) ; } private ape : IApeInfo = { } ; /**
     * Parse APEv1 / APEv2 header if header signature found
     */ public async tryParseApeHeader ( ) { if ( this . tokenizer . fileInfo . size && this . tokenizer . fileInfo . size - this . tokenizer . position < TagFooter . len ) { debug ( `No APEv2 header found, end-of-file reached` ) ; return ; } const footer = await this . tokenizer . peekToken < IFooter > ( TagFooter ) ; if ( footer . ID === preamble ) { await this . tokenizer . ignore ( TagFooter . len ) ; return this . parseTags ( footer ) ; } else { debug ( `APEv2 header not found at offset=${ this . tokenizer . position }` ) ; if ( this . tokenizer . fileInfo . size ) { const remaining = this . tokenizer . fileInfo . size - this . tokenizer . position ; const buffer = Buffer . alloc ( remaining ) ; await this . tokenizer . readBuffer ( buffer ) ; return APEv2Parser . parseTagFooter ( this . metadata , buffer , this . options ) ; } } } public async parse ( ) { const descriptor = await this . tokenizer . readToken < IDescriptor > ( DescriptorParser ) ; if ( descriptor . ID !== 'MAC ' ) throw new Error ( 'Unexpected descriptor ID' ) ; this . ape . descriptor = descriptor ; const lenExp = descriptor . descriptorBytes - DescriptorParser . len ; const header = await ( lenExp > 0 ? this . parseDescriptorExpansion ( lenExp ) : this . parseHeader ( ) ) ; await this . tokenizer . ignore ( header . forwardBytes ) ; return this . tryParseApeHeader ( ) ; } public async parseTags ( footer ) { const keyBuffer = Buffer . alloc ( 256 ) ; let bytesRemaining = footer . size - TagFooter . len ; debug ( `Parse APE tags at offset=${ this . tokenizer . position }, size=${ bytesRemaining }` ) ; for ( let i = 0 ; i < footer . fields ; i ++ ) { if ( bytesRemaining < TagItemHeader . len ) { this . metadata . addWarning ( `APEv2 Tag-header: ${ footer . fields - i } items remaining, but no more tag data to read.` ) ; break ; } const tagItemHeader = await this . tokenizer . readToken < ITagItemHeader > ( TagItemHeader ) ; bytesRemaining -= 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 < string > ( new StringType ( zero , 'ascii' ) ) ; await this . tokenizer . ignore ( 1 ) ; bytesRemaining -= key . length + 1 ; switch ( tagItemHeader . flags . dataType ) { case DataType . text_utf8 : { const value = await this . tokenizer . readToken < string > ( new StringType ( tagItemHeader . size , 'utf8' ) ) ; const values = value . split ( /\x00/g ) ; for ( const val of values ) { this . metadata . addTag ( tagFormat , key , val ) ; } break ; } case DataType . binary : 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 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 }` ) ; } } } private async parseDescriptorExpansion ( lenExp ) { await this . tokenizer . ignore ( lenExp ) ; return this . parseHeader ( ) ; } private async parseHeader ( ) { const header = await this . tokenizer . readToken ( Header ) ; 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 } ; } }