Initial working version
This commit is contained in:
+72
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Page header
|
||||
* Ref: https://www.xiph.org/ogg/doc/framing.html#page_header
|
||||
*/
|
||||
export interface IPageHeader {
|
||||
/**
|
||||
* capture_pattern
|
||||
* A header begins with a capture pattern that simplifies identifying pages;
|
||||
* once the decoder has found the capture pattern it can do a more intensive job of verifying that it has in fact found a page boundary (as opposed to an inadvertent coincidence in the byte stream).
|
||||
*/
|
||||
capturePattern: string;
|
||||
/**
|
||||
* stream_structure_version
|
||||
*/
|
||||
version: number;
|
||||
/**
|
||||
* header_type_flag
|
||||
*/
|
||||
headerType: {
|
||||
/**
|
||||
* True: continued packet;
|
||||
* False: fresh packet
|
||||
*/
|
||||
continued: boolean;
|
||||
/**
|
||||
* True: first page of logical bitstream (bos)
|
||||
* False: not first page of logical bitstream
|
||||
*/
|
||||
firstPage: boolean;
|
||||
/**
|
||||
* True: last page of logical bitstream (eos)
|
||||
* False: not last page of logical bitstream
|
||||
*/
|
||||
lastPage: boolean;
|
||||
};
|
||||
/**
|
||||
* The total samples encoded after including all packets finished on this page
|
||||
* The position specified in the frame header of the last page tells how long the data coded by the bitstream is.
|
||||
*/
|
||||
absoluteGranulePosition: number;
|
||||
streamSerialNumber: number;
|
||||
pageSequenceNo: number;
|
||||
pageChecksum: number;
|
||||
/**
|
||||
* The number of segment entries to appear in the segment table.
|
||||
* The maximum number of 255 segments (255 bytes each) sets the maximum possible physical page size at 65307 bytes or
|
||||
* just under 64kB (thus we know that a header corrupted so as destroy sizing/alignment information will not cause a
|
||||
* runaway bitstream. We'll read in the page according to the corrupted size information that's guaranteed to be a
|
||||
* reasonable size regardless, notice the checksum mismatch, drop sync and then look for recapture).
|
||||
*/
|
||||
page_segments: number;
|
||||
}
|
||||
export interface ISegmentTable {
|
||||
totalPageSize: number;
|
||||
}
|
||||
export interface IPageConsumer {
|
||||
/**
|
||||
* Parse Ogg page
|
||||
* @param {IPageHeader} header Ogg page header
|
||||
* @param {Buffer} pageData Ogg page data
|
||||
*/
|
||||
parsePage(header: IPageHeader, pageData: Uint8Array): any;
|
||||
/**
|
||||
* Calculate duration of provided header
|
||||
* @param header Ogg header
|
||||
*/
|
||||
calculateDuration(header: IPageHeader): any;
|
||||
/**
|
||||
* Force to parse pending segments
|
||||
*/
|
||||
flush(): any;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import * as Ogg from './Ogg';
|
||||
import { BasicParser } from '../common/BasicParser';
|
||||
import { IGetToken } from 'strtok3/lib/core';
|
||||
export declare class SegmentTable implements IGetToken<Ogg.ISegmentTable> {
|
||||
private static sum;
|
||||
len: number;
|
||||
constructor(header: Ogg.IPageHeader);
|
||||
get(buf: any, off: any): Ogg.ISegmentTable;
|
||||
}
|
||||
/**
|
||||
* Parser for Ogg logical bitstream framing
|
||||
*/
|
||||
export declare class OggParser extends BasicParser {
|
||||
private static Header;
|
||||
private header;
|
||||
private pageNumber;
|
||||
private pageConsumer;
|
||||
/**
|
||||
* Parse page
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
parse(): Promise<void>;
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.OggParser = exports.SegmentTable = void 0;
|
||||
const Token = require("token-types");
|
||||
const initDebug = require("debug");
|
||||
const util = require("../common/Util");
|
||||
const FourCC_1 = require("../common/FourCC");
|
||||
const VorbisParser_1 = require("./vorbis/VorbisParser");
|
||||
const OpusParser_1 = require("./opus/OpusParser");
|
||||
const SpeexParser_1 = require("./speex/SpeexParser");
|
||||
const BasicParser_1 = require("../common/BasicParser");
|
||||
const TheoraParser_1 = require("./theora/TheoraParser");
|
||||
const core_1 = require("strtok3/lib/core");
|
||||
const debug = initDebug('music-metadata:parser:ogg');
|
||||
class SegmentTable {
|
||||
constructor(header) {
|
||||
this.len = header.page_segments;
|
||||
}
|
||||
static sum(buf, off, len) {
|
||||
let s = 0;
|
||||
for (let i = off; i < off + len; ++i) {
|
||||
s += buf[i];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
get(buf, off) {
|
||||
return {
|
||||
totalPageSize: SegmentTable.sum(buf, off, this.len)
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.SegmentTable = SegmentTable;
|
||||
/**
|
||||
* Parser for Ogg logical bitstream framing
|
||||
*/
|
||||
class OggParser extends BasicParser_1.BasicParser {
|
||||
/**
|
||||
* Parse page
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async parse() {
|
||||
debug('pos=%s, parsePage()', this.tokenizer.position);
|
||||
try {
|
||||
let header;
|
||||
do {
|
||||
header = await this.tokenizer.readToken(OggParser.Header);
|
||||
if (header.capturePattern !== 'OggS')
|
||||
throw new Error('Invalid Ogg capture pattern');
|
||||
this.metadata.setFormat('container', 'Ogg');
|
||||
this.header = header;
|
||||
this.pageNumber = header.pageSequenceNo;
|
||||
debug('page#=%s, Ogg.id=%s', header.pageSequenceNo, header.capturePattern);
|
||||
const segmentTable = await this.tokenizer.readToken(new SegmentTable(header));
|
||||
debug('totalPageSize=%s', segmentTable.totalPageSize);
|
||||
const pageData = await this.tokenizer.readToken(new Token.Uint8ArrayType(segmentTable.totalPageSize));
|
||||
debug('firstPage=%s, lastPage=%s, continued=%s', header.headerType.firstPage, header.headerType.lastPage, header.headerType.continued);
|
||||
if (header.headerType.firstPage) {
|
||||
const id = new Token.StringType(7, 'ascii').get(Buffer.from(pageData), 0);
|
||||
switch (id) {
|
||||
case '\x01vorbis': // Ogg/Vorbis
|
||||
debug('Set page consumer to Ogg/Vorbis');
|
||||
this.pageConsumer = new VorbisParser_1.VorbisParser(this.metadata, this.options);
|
||||
break;
|
||||
case 'OpusHea': // Ogg/Opus
|
||||
debug('Set page consumer to Ogg/Opus');
|
||||
this.pageConsumer = new OpusParser_1.OpusParser(this.metadata, this.options, this.tokenizer);
|
||||
break;
|
||||
case 'Speex ': // Ogg/Speex
|
||||
debug('Set page consumer to Ogg/Speex');
|
||||
this.pageConsumer = new SpeexParser_1.SpeexParser(this.metadata, this.options, this.tokenizer);
|
||||
break;
|
||||
case 'fishead':
|
||||
case '\x00theora': // Ogg/Theora
|
||||
debug('Set page consumer to Ogg/Theora');
|
||||
this.pageConsumer = new TheoraParser_1.TheoraParser(this.metadata, this.options, this.tokenizer);
|
||||
break;
|
||||
default:
|
||||
throw new Error('gg audio-codec not recognized (id=' + id + ')');
|
||||
}
|
||||
}
|
||||
this.pageConsumer.parsePage(header, pageData);
|
||||
} while (!header.headerType.lastPage);
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof core_1.EndOfStreamError) {
|
||||
this.metadata.addWarning('Last OGG-page is not marked with last-page flag');
|
||||
debug(`End-of-stream`);
|
||||
this.metadata.addWarning('Last OGG-page is not marked with last-page flag');
|
||||
if (this.header) {
|
||||
this.pageConsumer.calculateDuration(this.header);
|
||||
}
|
||||
}
|
||||
else if (err.message.startsWith('FourCC')) {
|
||||
if (this.pageNumber > 0) {
|
||||
// ignore this error: work-around if last OGG-page is not marked with last-page flag
|
||||
this.metadata.addWarning('Invalid FourCC ID, maybe last OGG-page is not marked with last-page flag');
|
||||
this.pageConsumer.flush();
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.OggParser = OggParser;
|
||||
OggParser.Header = {
|
||||
len: 27,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
capturePattern: FourCC_1.FourCcToken.get(buf, off),
|
||||
version: Token.UINT8.get(buf, off + 4),
|
||||
headerType: {
|
||||
continued: util.getBit(buf, off + 5, 0),
|
||||
firstPage: util.getBit(buf, off + 5, 1),
|
||||
lastPage: util.getBit(buf, off + 5, 2)
|
||||
},
|
||||
// packet_flag: buf.readUInt8(off + 5),
|
||||
absoluteGranulePosition: Number(Token.UINT64_LE.get(buf, off + 6)),
|
||||
streamSerialNumber: Token.UINT32_LE.get(buf, off + 14),
|
||||
pageSequenceNo: Token.UINT32_LE.get(buf, off + 18),
|
||||
pageChecksum: Token.UINT32_LE.get(buf, off + 22),
|
||||
page_segments: Token.UINT8.get(buf, off + 26)
|
||||
};
|
||||
}
|
||||
};
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { IGetToken } from 'strtok3/lib/core';
|
||||
/**
|
||||
* Opus ID Header interface
|
||||
* Ref: https://wiki.xiph.org/OggOpus#ID_Header
|
||||
*/
|
||||
export interface IIdHeader {
|
||||
/**
|
||||
* Magic signature: "OpusHead" (64 bits)
|
||||
*/
|
||||
magicSignature: string;
|
||||
/**
|
||||
* Version number (8 bits unsigned): 0x01 for this spec
|
||||
*/
|
||||
version: number;
|
||||
/**
|
||||
* Channel count 'c' (8 bits unsigned): MUST be > 0
|
||||
*/
|
||||
channelCount: number;
|
||||
/**
|
||||
* Pre-skip (16 bits unsigned, little endian)
|
||||
*/
|
||||
preSkip: number;
|
||||
/**
|
||||
* Input sample rate (32 bits unsigned, little endian): informational only
|
||||
*/
|
||||
inputSampleRate: number;
|
||||
/**
|
||||
* Output gain (16 bits, little endian, signed Q7.8 in dB) to apply when decoding
|
||||
*/
|
||||
outputGain: number;
|
||||
/**
|
||||
* Channel mapping family (8 bits unsigned)
|
||||
* - 0 = one stream: mono or L,R stereo
|
||||
* - 1 = channels in vorbis spec order: mono or L,R stereo or ... or FL,C,FR,RL,RR,LFE, ...
|
||||
* - 2..254 = reserved (treat as 255)
|
||||
* - 255 = no defined channel meaning
|
||||
*/
|
||||
channelMapping: number;
|
||||
}
|
||||
/**
|
||||
* Opus ID Header parser
|
||||
* Ref: https://wiki.xiph.org/OggOpus#ID_Header
|
||||
*/
|
||||
export declare class IdHeader implements IGetToken<IIdHeader> {
|
||||
len: number;
|
||||
constructor(len: number);
|
||||
get(buf: any, off: any): IIdHeader;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.IdHeader = void 0;
|
||||
const Token = require("token-types");
|
||||
/**
|
||||
* Opus ID Header parser
|
||||
* Ref: https://wiki.xiph.org/OggOpus#ID_Header
|
||||
*/
|
||||
class IdHeader {
|
||||
constructor(len) {
|
||||
this.len = len;
|
||||
if (len < 19) {
|
||||
throw new Error("ID-header-page 0 should be at least 19 bytes long");
|
||||
}
|
||||
}
|
||||
get(buf, off) {
|
||||
return {
|
||||
magicSignature: new Token.StringType(8, 'ascii').get(buf, off + 0),
|
||||
version: buf.readUInt8(off + 8),
|
||||
channelCount: buf.readUInt8(off + 9),
|
||||
preSkip: buf.readInt16LE(off + 10),
|
||||
inputSampleRate: buf.readInt32LE(off + 12),
|
||||
outputGain: buf.readInt16LE(off + 16),
|
||||
channelMapping: buf.readUInt8(off + 18)
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.IdHeader = IdHeader;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/// <reference types="node" />
|
||||
import { ITokenizer } from 'strtok3/lib/core';
|
||||
import { IPageHeader } from '../Ogg';
|
||||
import { VorbisParser } from '../vorbis/VorbisParser';
|
||||
import { IOptions } from '../../type';
|
||||
import { INativeMetadataCollector } from '../../common/MetadataCollector';
|
||||
/**
|
||||
* Opus parser
|
||||
* Internet Engineering Task Force (IETF) - RFC 6716
|
||||
* Used by OggParser
|
||||
*/
|
||||
export declare class OpusParser extends VorbisParser {
|
||||
private tokenizer;
|
||||
private idHeader;
|
||||
private lastPos;
|
||||
constructor(metadata: INativeMetadataCollector, options: IOptions, tokenizer: ITokenizer);
|
||||
/**
|
||||
* Parse first Opus Ogg page
|
||||
* @param {IPageHeader} header
|
||||
* @param {Buffer} pageData
|
||||
*/
|
||||
protected parseFirstPage(header: IPageHeader, pageData: Buffer): void;
|
||||
protected parseFullPage(pageData: Buffer): void;
|
||||
calculateDuration(header: IPageHeader): void;
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.OpusParser = void 0;
|
||||
const Token = require("token-types");
|
||||
const Opus = require("./Opus");
|
||||
const VorbisParser_1 = require("../vorbis/VorbisParser");
|
||||
/**
|
||||
* Opus parser
|
||||
* Internet Engineering Task Force (IETF) - RFC 6716
|
||||
* Used by OggParser
|
||||
*/
|
||||
class OpusParser extends VorbisParser_1.VorbisParser {
|
||||
constructor(metadata, options, tokenizer) {
|
||||
super(metadata, options);
|
||||
this.tokenizer = tokenizer;
|
||||
this.lastPos = -1;
|
||||
}
|
||||
/**
|
||||
* Parse first Opus Ogg page
|
||||
* @param {IPageHeader} header
|
||||
* @param {Buffer} pageData
|
||||
*/
|
||||
parseFirstPage(header, pageData) {
|
||||
this.metadata.setFormat('codec', 'Opus');
|
||||
// Parse Opus ID Header
|
||||
this.idHeader = new Opus.IdHeader(pageData.length).get(pageData, 0);
|
||||
if (this.idHeader.magicSignature !== "OpusHead")
|
||||
throw new Error("Illegal ogg/Opus magic-signature");
|
||||
this.metadata.setFormat('sampleRate', this.idHeader.inputSampleRate);
|
||||
this.metadata.setFormat('numberOfChannels', this.idHeader.channelCount);
|
||||
}
|
||||
parseFullPage(pageData) {
|
||||
const magicSignature = new Token.StringType(8, 'ascii').get(pageData, 0);
|
||||
switch (magicSignature) {
|
||||
case 'OpusTags':
|
||||
this.parseUserCommentList(pageData, 8);
|
||||
this.lastPos = this.tokenizer.position - pageData.length;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
calculateDuration(header) {
|
||||
if (this.metadata.format.sampleRate && header.absoluteGranulePosition >= 0) {
|
||||
// Calculate duration
|
||||
const pos_48bit = header.absoluteGranulePosition - this.idHeader.preSkip;
|
||||
this.metadata.setFormat('numberOfSamples', pos_48bit);
|
||||
this.metadata.setFormat('duration', pos_48bit / 48000);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.OpusParser = OpusParser;
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { IGetToken } from 'strtok3/lib/core';
|
||||
/**
|
||||
* Speex Header Packet
|
||||
* Ref: https://www.speex.org/docs/manual/speex-manual/node8.html#SECTION00830000000000000000
|
||||
*/
|
||||
export interface IHeader {
|
||||
/**
|
||||
* speex_string, char[] 8
|
||||
*/
|
||||
speex: string;
|
||||
/**
|
||||
* speex_version, char[] 20
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Version id
|
||||
*/
|
||||
version_id: number;
|
||||
header_size: number;
|
||||
rate: number;
|
||||
mode: number;
|
||||
mode_bitstream_version: number;
|
||||
nb_channels: number;
|
||||
bitrate: number;
|
||||
frame_size: number;
|
||||
vbr: number;
|
||||
frames_per_packet: number;
|
||||
extra_headers: number;
|
||||
reserved1: number;
|
||||
reserved2: number;
|
||||
}
|
||||
/**
|
||||
* Speex Header Packet
|
||||
* Ref: https://www.speex.org/docs/manual/speex-manual/node8.html#SECTION00830000000000000000
|
||||
*/
|
||||
export declare const Header: IGetToken<IHeader>;
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Header = void 0;
|
||||
const Token = require("token-types");
|
||||
const util = require("../../common/Util");
|
||||
/**
|
||||
* Speex Header Packet
|
||||
* Ref: https://www.speex.org/docs/manual/speex-manual/node8.html#SECTION00830000000000000000
|
||||
*/
|
||||
exports.Header = {
|
||||
len: 80,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
speex: new Token.StringType(8, 'ascii').get(buf, off + 0),
|
||||
version: util.trimRightNull(new Token.StringType(20, 'ascii').get(buf, off + 8)),
|
||||
version_id: buf.readInt32LE(off + 28),
|
||||
header_size: buf.readInt32LE(off + 32),
|
||||
rate: buf.readInt32LE(off + 36),
|
||||
mode: buf.readInt32LE(off + 40),
|
||||
mode_bitstream_version: buf.readInt32LE(off + 44),
|
||||
nb_channels: buf.readInt32LE(off + 48),
|
||||
bitrate: buf.readInt32LE(off + 52),
|
||||
frame_size: buf.readInt32LE(off + 56),
|
||||
vbr: buf.readInt32LE(off + 60),
|
||||
frames_per_packet: buf.readInt32LE(off + 64),
|
||||
extra_headers: buf.readInt32LE(off + 68),
|
||||
reserved1: buf.readInt32LE(off + 72),
|
||||
reserved2: buf.readInt32LE(off + 76)
|
||||
};
|
||||
}
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/// <reference types="node" />
|
||||
import { ITokenizer } from 'strtok3/lib/core';
|
||||
import { IPageHeader } from '../Ogg';
|
||||
import { VorbisParser } from '../vorbis/VorbisParser';
|
||||
import { IOptions } from '../../type';
|
||||
import { INativeMetadataCollector } from '../../common/MetadataCollector';
|
||||
/**
|
||||
* Speex, RFC 5574
|
||||
* Ref:
|
||||
* https://www.speex.org/docs/manual/speex-manual/
|
||||
* https://tools.ietf.org/html/rfc5574
|
||||
*/
|
||||
export declare class SpeexParser extends VorbisParser {
|
||||
private tokenizer;
|
||||
constructor(metadata: INativeMetadataCollector, options: IOptions, tokenizer: ITokenizer);
|
||||
/**
|
||||
* Parse first Speex Ogg page
|
||||
* @param {IPageHeader} header
|
||||
* @param {Buffer} pageData
|
||||
*/
|
||||
protected parseFirstPage(header: IPageHeader, pageData: Buffer): void;
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.SpeexParser = void 0;
|
||||
const initDebug = require("debug");
|
||||
const Speex = require("./Speex");
|
||||
const VorbisParser_1 = require("../vorbis/VorbisParser");
|
||||
const debug = initDebug('music-metadata:parser:ogg:speex');
|
||||
/**
|
||||
* Speex, RFC 5574
|
||||
* Ref:
|
||||
* https://www.speex.org/docs/manual/speex-manual/
|
||||
* https://tools.ietf.org/html/rfc5574
|
||||
*/
|
||||
class SpeexParser extends VorbisParser_1.VorbisParser {
|
||||
constructor(metadata, options, tokenizer) {
|
||||
super(metadata, options);
|
||||
this.tokenizer = tokenizer;
|
||||
}
|
||||
/**
|
||||
* Parse first Speex Ogg page
|
||||
* @param {IPageHeader} header
|
||||
* @param {Buffer} pageData
|
||||
*/
|
||||
parseFirstPage(header, pageData) {
|
||||
debug('First Ogg/Speex page');
|
||||
const speexHeader = Speex.Header.get(pageData, 0);
|
||||
this.metadata.setFormat('codec', `Speex ${speexHeader.version}`);
|
||||
this.metadata.setFormat('numberOfChannels', speexHeader.nb_channels);
|
||||
this.metadata.setFormat('sampleRate', speexHeader.rate);
|
||||
if (speexHeader.bitrate !== -1) {
|
||||
this.metadata.setFormat('bitrate', speexHeader.bitrate);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.SpeexParser = SpeexParser;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { IGetToken } from 'strtok3/lib/core';
|
||||
/**
|
||||
* 6.2 Identification Header
|
||||
* Ref: https://theora.org/doc/Theora.pdf: 6.2 Identification Header Decode
|
||||
*/
|
||||
export interface IIdentificationHeader {
|
||||
id: string;
|
||||
vmaj: number;
|
||||
vmin: number;
|
||||
vrev: number;
|
||||
vmbw: number;
|
||||
vmbh: number;
|
||||
nombr: number;
|
||||
nqual: number;
|
||||
}
|
||||
/**
|
||||
* 6.2 Identification Header
|
||||
* Ref: https://theora.org/doc/Theora.pdf: 6.2 Identification Header Decode
|
||||
*/
|
||||
export declare const IdentificationHeader: IGetToken<IIdentificationHeader>;
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.IdentificationHeader = void 0;
|
||||
const Token = require("token-types");
|
||||
/**
|
||||
* 6.2 Identification Header
|
||||
* Ref: https://theora.org/doc/Theora.pdf: 6.2 Identification Header Decode
|
||||
*/
|
||||
exports.IdentificationHeader = {
|
||||
len: 42,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
id: new Token.StringType(7, 'ascii').get(buf, off),
|
||||
vmaj: buf.readUInt8(off + 7),
|
||||
vmin: buf.readUInt8(off + 8),
|
||||
vrev: buf.readUInt8(off + 9),
|
||||
vmbw: buf.readUInt16BE(off + 10),
|
||||
vmbh: buf.readUInt16BE(off + 17),
|
||||
nombr: Token.UINT24_BE.get(buf, off + 37),
|
||||
nqual: buf.readUInt8(off + 40)
|
||||
};
|
||||
}
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/// <reference types="node" />
|
||||
import { ITokenizer } from 'strtok3/lib/core';
|
||||
import { IOptions } from '../../type';
|
||||
import { INativeMetadataCollector } from '../../common/MetadataCollector';
|
||||
import * as Ogg from '../Ogg';
|
||||
/**
|
||||
* Ref:
|
||||
* https://theora.org/doc/Theora.pdf
|
||||
*/
|
||||
export declare class TheoraParser implements Ogg.IPageConsumer {
|
||||
private metadata;
|
||||
private tokenizer;
|
||||
constructor(metadata: INativeMetadataCollector, options: IOptions, tokenizer: ITokenizer);
|
||||
/**
|
||||
* Vorbis 1 parser
|
||||
* @param header Ogg Page Header
|
||||
* @param pageData Page data
|
||||
*/
|
||||
parsePage(header: Ogg.IPageHeader, pageData: Buffer): void;
|
||||
flush(): void;
|
||||
calculateDuration(header: Ogg.IPageHeader): void;
|
||||
/**
|
||||
* Parse first Theora Ogg page. the initial identification header packet
|
||||
* @param {IPageHeader} header
|
||||
* @param {Buffer} pageData
|
||||
*/
|
||||
protected parseFirstPage(header: Ogg.IPageHeader, pageData: Buffer): void;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.TheoraParser = void 0;
|
||||
const initDebug = require("debug");
|
||||
const Theora_1 = require("./Theora");
|
||||
const debug = initDebug('music-metadata:parser:ogg:theora');
|
||||
/**
|
||||
* Ref:
|
||||
* https://theora.org/doc/Theora.pdf
|
||||
*/
|
||||
class TheoraParser {
|
||||
constructor(metadata, options, tokenizer) {
|
||||
this.metadata = metadata;
|
||||
this.tokenizer = tokenizer;
|
||||
}
|
||||
/**
|
||||
* Vorbis 1 parser
|
||||
* @param header Ogg Page Header
|
||||
* @param pageData Page data
|
||||
*/
|
||||
parsePage(header, pageData) {
|
||||
if (header.headerType.firstPage) {
|
||||
this.parseFirstPage(header, pageData);
|
||||
}
|
||||
}
|
||||
flush() {
|
||||
debug('flush');
|
||||
}
|
||||
calculateDuration(header) {
|
||||
debug('duration calculation not implemented');
|
||||
}
|
||||
/**
|
||||
* Parse first Theora Ogg page. the initial identification header packet
|
||||
* @param {IPageHeader} header
|
||||
* @param {Buffer} pageData
|
||||
*/
|
||||
parseFirstPage(header, pageData) {
|
||||
debug('First Ogg/Theora page');
|
||||
this.metadata.setFormat('codec', 'Theora');
|
||||
const idHeader = Theora_1.IdentificationHeader.get(pageData, 0);
|
||||
this.metadata.setFormat('bitrate', idHeader.nombr);
|
||||
}
|
||||
}
|
||||
exports.TheoraParser = TheoraParser;
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/// <reference types="node" />
|
||||
import { IPicture } from '../../type';
|
||||
import { IGetToken } from 'strtok3/lib/core';
|
||||
export interface IVorbisPicture extends IPicture {
|
||||
type: string;
|
||||
description: string;
|
||||
width: number;
|
||||
height: number;
|
||||
colour_depth: number;
|
||||
indexed_color: number;
|
||||
}
|
||||
/**
|
||||
* Interface to parsed result of METADATA_BLOCK_PICTURE
|
||||
* Ref: https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
|
||||
* Ref: https://xiph.org/flac/format.html#metadata_block_picture
|
||||
*/
|
||||
export interface IVorbisPicture {
|
||||
type: string;
|
||||
format: string;
|
||||
description: string;
|
||||
width: number;
|
||||
height: number;
|
||||
colour_depth: number;
|
||||
indexed_color: number;
|
||||
data: Buffer;
|
||||
}
|
||||
/**
|
||||
* Parse the METADATA_BLOCK_PICTURE
|
||||
* Ref: https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
|
||||
* Ref: https://xiph.org/flac/format.html#metadata_block_picture
|
||||
* // ToDo: move to ID3 / APIC?
|
||||
*/
|
||||
export declare class VorbisPictureToken implements IGetToken<IVorbisPicture> {
|
||||
len: any;
|
||||
static fromBase64(base64str: string): IVorbisPicture;
|
||||
static fromBuffer(buffer: Buffer): IVorbisPicture;
|
||||
constructor(len: any);
|
||||
get(buffer: Buffer, offset: number): IVorbisPicture;
|
||||
}
|
||||
/**
|
||||
* Vorbis 1 decoding tokens
|
||||
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-620004.2.1
|
||||
*/
|
||||
/**
|
||||
* Comment header interface
|
||||
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-620004.2.1
|
||||
*/
|
||||
export interface ICommonHeader {
|
||||
/**
|
||||
* Packet Type
|
||||
*/
|
||||
packetType: number;
|
||||
/**
|
||||
* Should be 'vorbis'
|
||||
*/
|
||||
vorbis: string;
|
||||
}
|
||||
/**
|
||||
* Comment header decoder
|
||||
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-620004.2.1
|
||||
*/
|
||||
export declare const CommonHeader: IGetToken<ICommonHeader>;
|
||||
/**
|
||||
* Identification header interface
|
||||
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2
|
||||
*/
|
||||
export interface IFormatInfo {
|
||||
version: number;
|
||||
channelMode: number;
|
||||
sampleRate: number;
|
||||
bitrateMax: number;
|
||||
bitrateNominal: number;
|
||||
bitrateMin: number;
|
||||
}
|
||||
/**
|
||||
* Identification header decoder
|
||||
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2
|
||||
*/
|
||||
export declare const IdentificationHeader: IGetToken<IFormatInfo>;
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.IdentificationHeader = exports.CommonHeader = exports.VorbisPictureToken = void 0;
|
||||
const Token = require("token-types");
|
||||
const ID3v2Token_1 = require("../../id3v2/ID3v2Token");
|
||||
/**
|
||||
* Parse the METADATA_BLOCK_PICTURE
|
||||
* Ref: https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
|
||||
* Ref: https://xiph.org/flac/format.html#metadata_block_picture
|
||||
* // ToDo: move to ID3 / APIC?
|
||||
*/
|
||||
class VorbisPictureToken {
|
||||
constructor(len) {
|
||||
this.len = len;
|
||||
}
|
||||
static fromBase64(base64str) {
|
||||
return this.fromBuffer(Buffer.from(base64str, 'base64'));
|
||||
}
|
||||
static fromBuffer(buffer) {
|
||||
const pic = new VorbisPictureToken(buffer.length);
|
||||
return pic.get(buffer, 0);
|
||||
}
|
||||
get(buffer, offset) {
|
||||
const type = ID3v2Token_1.AttachedPictureType[Token.UINT32_BE.get(buffer, offset)];
|
||||
const mimeLen = Token.UINT32_BE.get(buffer, offset += 4);
|
||||
const format = buffer.toString('utf-8', offset += 4, offset + mimeLen);
|
||||
const descLen = Token.UINT32_BE.get(buffer, offset += mimeLen);
|
||||
const description = buffer.toString('utf-8', offset += 4, offset + descLen);
|
||||
const width = Token.UINT32_BE.get(buffer, offset += descLen);
|
||||
const height = Token.UINT32_BE.get(buffer, offset += 4);
|
||||
const colour_depth = Token.UINT32_BE.get(buffer, offset += 4);
|
||||
const indexed_color = Token.UINT32_BE.get(buffer, offset += 4);
|
||||
const picDataLen = Token.UINT32_BE.get(buffer, offset += 4);
|
||||
const data = Buffer.from(buffer.slice(offset += 4, offset + picDataLen));
|
||||
return {
|
||||
type,
|
||||
format,
|
||||
description,
|
||||
width,
|
||||
height,
|
||||
colour_depth,
|
||||
indexed_color,
|
||||
data
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.VorbisPictureToken = VorbisPictureToken;
|
||||
/**
|
||||
* Comment header decoder
|
||||
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-620004.2.1
|
||||
*/
|
||||
exports.CommonHeader = {
|
||||
len: 7,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
packetType: buf.readUInt8(off),
|
||||
vorbis: new Token.StringType(6, 'ascii').get(buf, off + 1)
|
||||
};
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Identification header decoder
|
||||
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2
|
||||
*/
|
||||
exports.IdentificationHeader = {
|
||||
len: 23,
|
||||
get: (uint8Array, off) => {
|
||||
const dataView = new DataView(uint8Array.buffer, uint8Array.byteOffset);
|
||||
return {
|
||||
version: dataView.getUint32(off + 0, true),
|
||||
channelMode: dataView.getUint8(off + 4),
|
||||
sampleRate: dataView.getUint32(off + 5, true),
|
||||
bitrateMax: dataView.getUint32(off + 9, true),
|
||||
bitrateNominal: dataView.getUint32(off + 13, true),
|
||||
bitrateMin: dataView.getUint32(off + 17, true)
|
||||
};
|
||||
}
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
export declare class VorbisDecoder {
|
||||
private readonly data;
|
||||
private offset;
|
||||
constructor(data: Uint8Array, offset: any);
|
||||
readInt32(): number;
|
||||
readStringUtf8(): string;
|
||||
parseUserComment(): {
|
||||
key: string;
|
||||
value: string;
|
||||
len: number;
|
||||
};
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.VorbisDecoder = void 0;
|
||||
const Token = require("token-types");
|
||||
class VorbisDecoder {
|
||||
constructor(data, offset) {
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
}
|
||||
readInt32() {
|
||||
const value = Token.UINT32_LE.get(this.data, this.offset);
|
||||
this.offset += 4;
|
||||
return value;
|
||||
}
|
||||
readStringUtf8() {
|
||||
const len = this.readInt32();
|
||||
const value = Buffer.from(this.data).toString('utf-8', this.offset, this.offset + len);
|
||||
this.offset += len;
|
||||
return value;
|
||||
}
|
||||
parseUserComment() {
|
||||
const offset0 = this.offset;
|
||||
const v = this.readStringUtf8();
|
||||
const idx = v.indexOf('=');
|
||||
return {
|
||||
key: v.slice(0, idx).toUpperCase(),
|
||||
value: v.slice(idx + 1),
|
||||
len: this.offset - offset0
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.VorbisDecoder = VorbisDecoder;
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/// <reference types="node" />
|
||||
import { IOptions } from '../../type';
|
||||
import { INativeMetadataCollector } from '../../common/MetadataCollector';
|
||||
import * as Ogg from '../Ogg';
|
||||
import { IVorbisPicture } from './Vorbis';
|
||||
/**
|
||||
* Vorbis 1 Parser.
|
||||
* Used by OggParser
|
||||
*/
|
||||
export declare class VorbisParser implements Ogg.IPageConsumer {
|
||||
protected metadata: INativeMetadataCollector;
|
||||
protected options: IOptions;
|
||||
private pageSegments;
|
||||
constructor(metadata: INativeMetadataCollector, options: IOptions);
|
||||
/**
|
||||
* Vorbis 1 parser
|
||||
* @param header Ogg Page Header
|
||||
* @param pageData Page data
|
||||
*/
|
||||
parsePage(header: Ogg.IPageHeader, pageData: Buffer): void;
|
||||
flush(): void;
|
||||
parseUserComment(pageData: Buffer, offset: number): number;
|
||||
addTag(id: string, value: string | IVorbisPicture): void;
|
||||
calculateDuration(header: Ogg.IPageHeader): void;
|
||||
/**
|
||||
* Parse first Ogg/Vorbis page
|
||||
* @param {IPageHeader} header
|
||||
* @param {Buffer} pageData
|
||||
*/
|
||||
protected parseFirstPage(header: Ogg.IPageHeader, pageData: Buffer): void;
|
||||
protected parseFullPage(pageData: Buffer): void;
|
||||
/**
|
||||
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-840005.2
|
||||
*/
|
||||
protected parseUserCommentList(pageData: Buffer, offset: number): void;
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.VorbisParser = void 0;
|
||||
const Token = require("token-types");
|
||||
const _debug = require("debug");
|
||||
const VorbisDecoder_1 = require("./VorbisDecoder");
|
||||
const Vorbis_1 = require("./Vorbis");
|
||||
const debug = _debug('music-metadata:parser:ogg:vorbis1');
|
||||
/**
|
||||
* Vorbis 1 Parser.
|
||||
* Used by OggParser
|
||||
*/
|
||||
class VorbisParser {
|
||||
constructor(metadata, options) {
|
||||
this.metadata = metadata;
|
||||
this.options = options;
|
||||
this.pageSegments = [];
|
||||
}
|
||||
/**
|
||||
* Vorbis 1 parser
|
||||
* @param header Ogg Page Header
|
||||
* @param pageData Page data
|
||||
*/
|
||||
parsePage(header, pageData) {
|
||||
if (header.headerType.firstPage) {
|
||||
this.parseFirstPage(header, pageData);
|
||||
}
|
||||
else {
|
||||
if (header.headerType.continued) {
|
||||
if (this.pageSegments.length === 0) {
|
||||
throw new Error("Cannot continue on previous page");
|
||||
}
|
||||
this.pageSegments.push(pageData);
|
||||
}
|
||||
if (header.headerType.lastPage || !header.headerType.continued) {
|
||||
// Flush page segments
|
||||
if (this.pageSegments.length > 0) {
|
||||
const fullPage = Buffer.concat(this.pageSegments);
|
||||
this.parseFullPage(fullPage);
|
||||
}
|
||||
// Reset page segments
|
||||
this.pageSegments = header.headerType.lastPage ? [] : [pageData];
|
||||
}
|
||||
}
|
||||
if (header.headerType.lastPage) {
|
||||
this.calculateDuration(header);
|
||||
}
|
||||
}
|
||||
flush() {
|
||||
this.parseFullPage(Buffer.concat(this.pageSegments));
|
||||
}
|
||||
parseUserComment(pageData, offset) {
|
||||
const decoder = new VorbisDecoder_1.VorbisDecoder(pageData, offset);
|
||||
const tag = decoder.parseUserComment();
|
||||
this.addTag(tag.key, tag.value);
|
||||
return tag.len;
|
||||
}
|
||||
addTag(id, value) {
|
||||
if (id === 'METADATA_BLOCK_PICTURE' && (typeof value === 'string')) {
|
||||
if (this.options.skipCovers) {
|
||||
debug(`Ignore picture`);
|
||||
return;
|
||||
}
|
||||
value = Vorbis_1.VorbisPictureToken.fromBase64(value);
|
||||
debug(`Push picture: id=${id}, format=${value.format}`);
|
||||
}
|
||||
else {
|
||||
debug(`Push tag: id=${id}, value=${value}`);
|
||||
}
|
||||
this.metadata.addTag('vorbis', id, value);
|
||||
}
|
||||
calculateDuration(header) {
|
||||
if (this.metadata.format.sampleRate && header.absoluteGranulePosition >= 0) {
|
||||
// Calculate duration
|
||||
this.metadata.setFormat('numberOfSamples', header.absoluteGranulePosition);
|
||||
this.metadata.setFormat('duration', this.metadata.format.numberOfSamples / this.metadata.format.sampleRate);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Parse first Ogg/Vorbis page
|
||||
* @param {IPageHeader} header
|
||||
* @param {Buffer} pageData
|
||||
*/
|
||||
parseFirstPage(header, pageData) {
|
||||
this.metadata.setFormat('codec', 'Vorbis I');
|
||||
debug("Parse first page");
|
||||
// Parse Vorbis common header
|
||||
const commonHeader = Vorbis_1.CommonHeader.get(pageData, 0);
|
||||
if (commonHeader.vorbis !== 'vorbis')
|
||||
throw new Error('Metadata does not look like Vorbis');
|
||||
if (commonHeader.packetType === 1) {
|
||||
const idHeader = Vorbis_1.IdentificationHeader.get(pageData, Vorbis_1.CommonHeader.len);
|
||||
this.metadata.setFormat('sampleRate', idHeader.sampleRate);
|
||||
this.metadata.setFormat('bitrate', idHeader.bitrateNominal);
|
||||
this.metadata.setFormat('numberOfChannels', idHeader.channelMode);
|
||||
debug("sample-rate=%s[hz], bitrate=%s[b/s], channel-mode=%s", idHeader.sampleRate, idHeader.bitrateNominal, idHeader.channelMode);
|
||||
}
|
||||
else
|
||||
throw new Error('First Ogg page should be type 1: the identification header');
|
||||
}
|
||||
parseFullPage(pageData) {
|
||||
// New page
|
||||
const commonHeader = Vorbis_1.CommonHeader.get(pageData, 0);
|
||||
debug("Parse full page: type=%s, byteLength=%s", commonHeader.packetType, pageData.byteLength);
|
||||
switch (commonHeader.packetType) {
|
||||
case 3: // type 3: comment header
|
||||
return this.parseUserCommentList(pageData, Vorbis_1.CommonHeader.len);
|
||||
case 1: // type 1: the identification header
|
||||
case 5: // type 5: setup header type
|
||||
break; // ignore
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-840005.2
|
||||
*/
|
||||
parseUserCommentList(pageData, offset) {
|
||||
const strLen = Token.UINT32_LE.get(pageData, offset);
|
||||
offset += 4;
|
||||
// const vendorString = new Token.StringType(strLen, 'utf-8').get(pageData, offset);
|
||||
offset += strLen;
|
||||
let userCommentListLength = Token.UINT32_LE.get(pageData, offset);
|
||||
offset += 4;
|
||||
while (userCommentListLength-- > 0) {
|
||||
offset += this.parseUserComment(pageData, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.VorbisParser = VorbisParser;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { CommonTagMapper } from '../../common/GenericTagMapper';
|
||||
import { IRating, ITag } from '../../type';
|
||||
export declare class VorbisTagMapper extends CommonTagMapper {
|
||||
static toRating(email: string, rating: string): IRating;
|
||||
constructor();
|
||||
protected postMap(tag: ITag): void;
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.VorbisTagMapper = void 0;
|
||||
const GenericTagMapper_1 = require("../../common/GenericTagMapper");
|
||||
/**
|
||||
* Vorbis tag mappings
|
||||
*
|
||||
* Mapping from native header format to one or possibly more 'common' entries
|
||||
* The common entries aim to read the same information from different media files
|
||||
* independent of the underlying format
|
||||
*/
|
||||
const vorbisTagMap = {
|
||||
TITLE: 'title',
|
||||
ARTIST: 'artist',
|
||||
ARTISTS: 'artists',
|
||||
ALBUMARTIST: 'albumartist',
|
||||
'ALBUM ARTIST': 'albumartist',
|
||||
ALBUM: 'album',
|
||||
DATE: 'date',
|
||||
ORIGINALDATE: 'originaldate',
|
||||
ORIGINALYEAR: 'originalyear',
|
||||
COMMENT: 'comment',
|
||||
TRACKNUMBER: 'track',
|
||||
DISCNUMBER: 'disk',
|
||||
GENRE: 'genre',
|
||||
METADATA_BLOCK_PICTURE: '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', // ToDo
|
||||
REMIXER: 'remixer',
|
||||
ARRANGER: 'arranger',
|
||||
ENGINEER: 'engineer',
|
||||
PRODUCER: 'producer',
|
||||
DJMIXER: 'djmixer',
|
||||
MIXER: 'mixer',
|
||||
LABEL: 'label',
|
||||
GROUPING: 'grouping',
|
||||
SUBTITLE: 'subtitle',
|
||||
DISCSUBTITLE: 'discsubtitle',
|
||||
TRACKTOTAL: 'totaltracks',
|
||||
DISCTOTAL: 'totaldiscs',
|
||||
COMPILATION: 'compilation',
|
||||
RATING: 'rating',
|
||||
BPM: 'bpm',
|
||||
KEY: 'key',
|
||||
MOOD: 'mood',
|
||||
MEDIA: 'media',
|
||||
CATALOGNUMBER: 'catalognumber',
|
||||
RELEASESTATUS: 'releasestatus',
|
||||
RELEASETYPE: '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_ID_FINGERPRINT: 'acoustid_fingerprint',
|
||||
MUSICIP_PUID: 'musicip_puid',
|
||||
// 'FINGERPRINT=MusicMagic Fingerprint {fingerprint}': 'musicip_fingerprint', // ToDo
|
||||
WEBSITE: 'website',
|
||||
NOTES: 'notes',
|
||||
TOTALTRACKS: 'totaltracks',
|
||||
TOTALDISCS: 'totaldiscs',
|
||||
// Discogs
|
||||
DISCOGS_ARTIST_ID: 'discogs_artist_id',
|
||||
DISCOGS_ARTISTS: 'artists',
|
||||
DISCOGS_ARTIST_NAME: 'artists',
|
||||
DISCOGS_ALBUM_ARTISTS: 'albumartist',
|
||||
DISCOGS_CATALOG: 'catalognumber',
|
||||
DISCOGS_COUNTRY: 'releasecountry',
|
||||
DISCOGS_DATE: 'originaldate',
|
||||
DISCOGS_LABEL: 'label',
|
||||
DISCOGS_LABEL_ID: 'discogs_label_id',
|
||||
DISCOGS_MASTER_RELEASE_ID: 'discogs_master_release_id',
|
||||
DISCOGS_RATING: 'discogs_rating',
|
||||
DISCOGS_RELEASED: 'date',
|
||||
DISCOGS_RELEASE_ID: 'discogs_release_id',
|
||||
DISCOGS_VOTES: 'discogs_votes',
|
||||
CATALOGID: 'catalognumber',
|
||||
STYLE: 'genre',
|
||||
//
|
||||
REPLAYGAIN_TRACK_GAIN: 'replaygain_track_gain',
|
||||
REPLAYGAIN_TRACK_PEAK: 'replaygain_track_peak',
|
||||
REPLAYGAIN_ALBUM_GAIN: 'replaygain_album_gain',
|
||||
REPLAYGAIN_ALBUM_PEAK: 'replaygain_album_peak',
|
||||
// To Sure if these (REPLAYGAIN_MINMAX, REPLAYGAIN_ALBUM_MINMAX & REPLAYGAIN_UNDO) are used for Vorbis:
|
||||
REPLAYGAIN_MINMAX: 'replaygain_track_minmax',
|
||||
REPLAYGAIN_ALBUM_MINMAX: 'replaygain_album_minmax',
|
||||
REPLAYGAIN_UNDO: 'replaygain_undo'
|
||||
};
|
||||
class VorbisTagMapper extends GenericTagMapper_1.CommonTagMapper {
|
||||
static toRating(email, rating) {
|
||||
return {
|
||||
source: email ? email.toLowerCase() : email,
|
||||
rating: parseFloat(rating) * GenericTagMapper_1.CommonTagMapper.maxRatingScore
|
||||
};
|
||||
}
|
||||
constructor() {
|
||||
super(['vorbis'], vorbisTagMap);
|
||||
}
|
||||
postMap(tag) {
|
||||
if (tag.id.indexOf('RATING:') === 0) {
|
||||
const keys = tag.id.split(':');
|
||||
tag.value = VorbisTagMapper.toRating(keys[1], tag.value);
|
||||
tag.id = keys[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.VorbisTagMapper = VorbisTagMapper;
|
||||
Reference in New Issue
Block a user