Initial working version
This commit is contained in:
+17
@@ -0,0 +1,17 @@
|
||||
import { ITokenizer } from 'strtok3/lib/core';
|
||||
import { BasicParser } from '../common/BasicParser';
|
||||
/**
|
||||
* Abstract parser which tries take ID3v2 and ID3v1 headers.
|
||||
*/
|
||||
export declare abstract class AbstractID3Parser extends BasicParser {
|
||||
static startsWithID3v2Header(tokenizer: ITokenizer): Promise<boolean>;
|
||||
private id3parser;
|
||||
parse(): Promise<void>;
|
||||
/**
|
||||
* Called after ID3v2 headers are parsed
|
||||
*/
|
||||
abstract _parse(): Promise<void>;
|
||||
protected finalize(): void;
|
||||
private parseID3v2;
|
||||
private tryReadId3v2Headers;
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AbstractID3Parser = void 0;
|
||||
const core_1 = require("strtok3/lib/core");
|
||||
const ID3v2Token_1 = require("./ID3v2Token");
|
||||
const ID3v2Parser_1 = require("./ID3v2Parser");
|
||||
const ID3v1Parser_1 = require("../id3v1/ID3v1Parser");
|
||||
const _debug = require("debug");
|
||||
const BasicParser_1 = require("../common/BasicParser");
|
||||
const debug = _debug('music-metadata:parser:ID3');
|
||||
/**
|
||||
* Abstract parser which tries take ID3v2 and ID3v1 headers.
|
||||
*/
|
||||
class AbstractID3Parser extends BasicParser_1.BasicParser {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.id3parser = new ID3v2Parser_1.ID3v2Parser();
|
||||
}
|
||||
static async startsWithID3v2Header(tokenizer) {
|
||||
return (await tokenizer.peekToken(ID3v2Token_1.ID3v2Header)).fileIdentifier === 'ID3';
|
||||
}
|
||||
async parse() {
|
||||
try {
|
||||
await this.parseID3v2();
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof core_1.EndOfStreamError) {
|
||||
debug(`End-of-stream`);
|
||||
}
|
||||
else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
finalize() {
|
||||
return;
|
||||
}
|
||||
async parseID3v2() {
|
||||
await this.tryReadId3v2Headers();
|
||||
debug("End of ID3v2 header, go to MPEG-parser: pos=%s", this.tokenizer.position);
|
||||
await this._parse();
|
||||
if (this.options.skipPostHeaders && this.metadata.hasAny()) {
|
||||
this.finalize();
|
||||
}
|
||||
else {
|
||||
const id3v1parser = new ID3v1Parser_1.ID3v1Parser();
|
||||
await id3v1parser.init(this.metadata, this.tokenizer, this.options).parse();
|
||||
this.finalize();
|
||||
}
|
||||
}
|
||||
async tryReadId3v2Headers() {
|
||||
const id3Header = await this.tokenizer.peekToken(ID3v2Token_1.ID3v2Header);
|
||||
if (id3Header.fileIdentifier === "ID3") {
|
||||
debug("Found ID3v2 header, pos=%s", this.tokenizer.position);
|
||||
await this.id3parser.parse(this.metadata, this.tokenizer, this.options);
|
||||
return this.tryReadId3v2Headers();
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.AbstractID3Parser = AbstractID3Parser;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/// <reference types="node" />
|
||||
import { ID3v2MajorVersion } from './ID3v2Token';
|
||||
import { IWarningCollector } from '../common/MetadataCollector';
|
||||
export declare function parseGenre(origVal: string): string[];
|
||||
export declare class FrameParser {
|
||||
private major;
|
||||
private warningCollector;
|
||||
/**
|
||||
* Create id3v2 frame parser
|
||||
* @param major - Major version, e.g. (4) for id3v2.4
|
||||
* @param warningCollector - Used to collect decode issue
|
||||
*/
|
||||
constructor(major: ID3v2MajorVersion, warningCollector: IWarningCollector);
|
||||
readData(b: Buffer, type: string, includeCovers: boolean): any;
|
||||
protected static fixPictureMimeType(pictureType: string): string;
|
||||
/**
|
||||
* Converts TMCL (Musician credits list) or TIPL (Involved people list)
|
||||
* @param entries
|
||||
*/
|
||||
private static functionList;
|
||||
/**
|
||||
* id3v2.4 defines that multiple T* values are separated by 0x00
|
||||
* id3v2.3 defines that TCOM, TEXT, TOLY, TOPE & TPE1 values are separated by /
|
||||
* @param tag - Tag name
|
||||
* @param text - Concatenated tag value
|
||||
* @returns Split tag value
|
||||
*/
|
||||
private splitValue;
|
||||
private static trimArray;
|
||||
private static readIdentifierAndData;
|
||||
private static getNullTerminatorLength;
|
||||
}
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.FrameParser = exports.parseGenre = void 0;
|
||||
const initDebug = require("debug");
|
||||
const Token = require("token-types");
|
||||
const util = require("../common/Util");
|
||||
const ID3v2Token_1 = require("./ID3v2Token");
|
||||
const ID3v1Parser_1 = require("../id3v1/ID3v1Parser");
|
||||
const debug = initDebug('music-metadata:id3v2:frame-parser');
|
||||
const defaultEnc = 'latin1'; // latin1 == iso-8859-1;
|
||||
function parseGenre(origVal) {
|
||||
// match everything inside parentheses
|
||||
const genres = [];
|
||||
let code;
|
||||
let word = '';
|
||||
for (const c of origVal) {
|
||||
if (typeof code === 'string') {
|
||||
if (c === '(' && code === '') {
|
||||
word += '(';
|
||||
code = undefined;
|
||||
}
|
||||
else if (c === ')') {
|
||||
if (word !== '') {
|
||||
genres.push(word);
|
||||
word = '';
|
||||
}
|
||||
const genre = parseGenreCode(code);
|
||||
if (genre) {
|
||||
genres.push(genre);
|
||||
}
|
||||
code = undefined;
|
||||
}
|
||||
else
|
||||
code += c;
|
||||
}
|
||||
else if (c === '(') {
|
||||
code = '';
|
||||
}
|
||||
else {
|
||||
word += c;
|
||||
}
|
||||
}
|
||||
if (word) {
|
||||
if (genres.length === 0 && word.match(/^\d*$/)) {
|
||||
word = ID3v1Parser_1.Genres[word];
|
||||
}
|
||||
genres.push(word);
|
||||
}
|
||||
return genres;
|
||||
}
|
||||
exports.parseGenre = parseGenre;
|
||||
function parseGenreCode(code) {
|
||||
if (code === 'RX')
|
||||
return 'Remix';
|
||||
if (code === 'CR')
|
||||
return 'Cover';
|
||||
if (code.match(/^\d*$/)) {
|
||||
return ID3v1Parser_1.Genres[code];
|
||||
}
|
||||
}
|
||||
class FrameParser {
|
||||
/**
|
||||
* Create id3v2 frame parser
|
||||
* @param major - Major version, e.g. (4) for id3v2.4
|
||||
* @param warningCollector - Used to collect decode issue
|
||||
*/
|
||||
constructor(major, warningCollector) {
|
||||
this.major = major;
|
||||
this.warningCollector = warningCollector;
|
||||
}
|
||||
readData(b, type, includeCovers) {
|
||||
if (b.length === 0) {
|
||||
this.warningCollector.addWarning(`id3v2.${this.major} header has empty tag type=${type}`);
|
||||
return;
|
||||
}
|
||||
const { encoding, bom } = ID3v2Token_1.TextEncodingToken.get(b, 0);
|
||||
const length = b.length;
|
||||
let offset = 0;
|
||||
let output = []; // ToDo
|
||||
const nullTerminatorLength = FrameParser.getNullTerminatorLength(encoding);
|
||||
let fzero;
|
||||
const out = {};
|
||||
debug(`Parsing tag type=${type}, encoding=${encoding}, bom=${bom}`);
|
||||
switch (type !== 'TXXX' && type[0] === 'T' ? 'T*' : type) {
|
||||
case 'T*': // 4.2.1. Text information frames - details
|
||||
case 'IPLS': // v2.3: Involved people list
|
||||
case 'MVIN':
|
||||
case 'MVNM':
|
||||
case 'PCS':
|
||||
case 'PCST':
|
||||
const text = util.decodeString(b.slice(1), encoding).replace(/\x00+$/, '');
|
||||
switch (type) {
|
||||
case 'TMCL': // Musician credits list
|
||||
case 'TIPL': // Involved people list
|
||||
case 'IPLS': // Involved people list
|
||||
output = this.splitValue(type, text);
|
||||
output = FrameParser.functionList(output);
|
||||
break;
|
||||
case 'TRK':
|
||||
case 'TRCK':
|
||||
case 'TPOS':
|
||||
output = text;
|
||||
break;
|
||||
case 'TCOM':
|
||||
case 'TEXT':
|
||||
case 'TOLY':
|
||||
case 'TOPE':
|
||||
case 'TPE1':
|
||||
case 'TSRC':
|
||||
// id3v2.3 defines that TCOM, TEXT, TOLY, TOPE & TPE1 values are separated by /
|
||||
output = this.splitValue(type, text);
|
||||
break;
|
||||
case 'TCO':
|
||||
case 'TCON':
|
||||
output = this.splitValue(type, text).map(v => parseGenre(v)).reduce((acc, val) => acc.concat(val), []);
|
||||
break;
|
||||
case 'PCS':
|
||||
case 'PCST':
|
||||
// TODO: Why `default` not results `1` but `''`?
|
||||
output = this.major >= 4 ? this.splitValue(type, text) : [text];
|
||||
output = (Array.isArray(output) && output[0] === '') ? 1 : 0;
|
||||
break;
|
||||
default:
|
||||
output = this.major >= 4 ? this.splitValue(type, text) : [text];
|
||||
}
|
||||
break;
|
||||
case 'TXXX':
|
||||
output = FrameParser.readIdentifierAndData(b, offset + 1, length, encoding);
|
||||
output = {
|
||||
description: output.id,
|
||||
text: this.splitValue(type, util.decodeString(output.data, encoding).replace(/\x00+$/, ''))
|
||||
};
|
||||
break;
|
||||
case 'PIC':
|
||||
case 'APIC':
|
||||
if (includeCovers) {
|
||||
const pic = {};
|
||||
offset += 1;
|
||||
switch (this.major) {
|
||||
case 2:
|
||||
pic.format = util.decodeString(b.slice(offset, offset + 3), 'latin1'); // 'latin1'; // latin1 == iso-8859-1;
|
||||
offset += 3;
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
fzero = util.findZero(b, offset, length, defaultEnc);
|
||||
pic.format = util.decodeString(b.slice(offset, fzero), defaultEnc);
|
||||
offset = fzero + 1;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Warning: unexpected major versionIndex: ' + this.major);
|
||||
}
|
||||
pic.format = FrameParser.fixPictureMimeType(pic.format);
|
||||
pic.type = ID3v2Token_1.AttachedPictureType[b[offset]];
|
||||
offset += 1;
|
||||
fzero = util.findZero(b, offset, length, encoding);
|
||||
pic.description = util.decodeString(b.slice(offset, fzero), encoding);
|
||||
offset = fzero + nullTerminatorLength;
|
||||
pic.data = Buffer.from(b.slice(offset, length));
|
||||
output = pic;
|
||||
}
|
||||
break;
|
||||
case 'CNT':
|
||||
case 'PCNT':
|
||||
output = Token.UINT32_BE.get(b, 0);
|
||||
break;
|
||||
case 'SYLT':
|
||||
// skip text encoding (1 byte),
|
||||
// language (3 bytes),
|
||||
// time stamp format (1 byte),
|
||||
// content tagTypes (1 byte),
|
||||
// content descriptor (1 byte)
|
||||
offset += 7;
|
||||
output = [];
|
||||
while (offset < length) {
|
||||
const txt = b.slice(offset, offset = util.findZero(b, offset, length, encoding));
|
||||
offset += 5; // push offset forward one + 4 byte timestamp
|
||||
output.push(util.decodeString(txt, encoding));
|
||||
}
|
||||
break;
|
||||
case 'ULT':
|
||||
case 'USLT':
|
||||
case 'COM':
|
||||
case 'COMM':
|
||||
offset += 1;
|
||||
out.language = util.decodeString(b.slice(offset, offset + 3), defaultEnc);
|
||||
offset += 3;
|
||||
fzero = util.findZero(b, offset, length, encoding);
|
||||
out.description = util.decodeString(b.slice(offset, fzero), encoding);
|
||||
offset = fzero + nullTerminatorLength;
|
||||
out.text = util.decodeString(b.slice(offset, length), encoding).replace(/\x00+$/, '');
|
||||
output = [out];
|
||||
break;
|
||||
case 'UFID':
|
||||
output = FrameParser.readIdentifierAndData(b, offset, length, defaultEnc);
|
||||
output = { owner_identifier: output.id, identifier: output.data };
|
||||
break;
|
||||
case 'PRIV': // private frame
|
||||
output = FrameParser.readIdentifierAndData(b, offset, length, defaultEnc);
|
||||
output = { owner_identifier: output.id, data: output.data };
|
||||
break;
|
||||
case 'POPM': // Popularimeter
|
||||
fzero = util.findZero(b, offset, length, defaultEnc);
|
||||
const email = util.decodeString(b.slice(offset, fzero), defaultEnc);
|
||||
offset = fzero + 1;
|
||||
const dataLen = length - offset;
|
||||
output = {
|
||||
email,
|
||||
rating: b.readUInt8(offset),
|
||||
counter: dataLen >= 5 ? b.readUInt32BE(offset + 1) : undefined
|
||||
};
|
||||
break;
|
||||
case 'GEOB': { // General encapsulated object
|
||||
fzero = util.findZero(b, offset + 1, length, encoding);
|
||||
const mimeType = util.decodeString(b.slice(offset + 1, fzero), defaultEnc);
|
||||
offset = fzero + 1;
|
||||
fzero = util.findZero(b, offset, length - offset, encoding);
|
||||
const filename = util.decodeString(b.slice(offset, fzero), defaultEnc);
|
||||
offset = fzero + 1;
|
||||
fzero = util.findZero(b, offset, length - offset, encoding);
|
||||
const description = util.decodeString(b.slice(offset, fzero), defaultEnc);
|
||||
output = {
|
||||
type: mimeType,
|
||||
filename,
|
||||
description,
|
||||
data: b.slice(offset + 1, length)
|
||||
};
|
||||
break;
|
||||
}
|
||||
// W-Frames:
|
||||
case 'WCOM':
|
||||
case 'WCOP':
|
||||
case 'WOAF':
|
||||
case 'WOAR':
|
||||
case 'WOAS':
|
||||
case 'WORS':
|
||||
case 'WPAY':
|
||||
case 'WPUB':
|
||||
// Decode URL
|
||||
output = util.decodeString(b.slice(offset, fzero), defaultEnc);
|
||||
break;
|
||||
case 'WXXX': {
|
||||
// Decode URL
|
||||
fzero = util.findZero(b, offset + 1, length, encoding);
|
||||
const description = util.decodeString(b.slice(offset + 1, fzero), encoding);
|
||||
offset = fzero + (encoding === 'utf16le' ? 2 : 1);
|
||||
output = { description, url: util.decodeString(b.slice(offset, length), defaultEnc) };
|
||||
break;
|
||||
}
|
||||
case 'WFD':
|
||||
case 'WFED':
|
||||
output = util.decodeString(b.slice(offset + 1, util.findZero(b, offset + 1, length, encoding)), encoding);
|
||||
break;
|
||||
case 'MCDI': {
|
||||
// Music CD identifier
|
||||
output = b.slice(0, length);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
debug('Warning: unsupported id3v2-tag-type: ' + type);
|
||||
break;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
static fixPictureMimeType(pictureType) {
|
||||
pictureType = pictureType.toLocaleLowerCase();
|
||||
switch (pictureType) {
|
||||
case 'jpg':
|
||||
return 'image/jpeg';
|
||||
case 'png':
|
||||
return 'image/png';
|
||||
}
|
||||
return pictureType;
|
||||
}
|
||||
/**
|
||||
* Converts TMCL (Musician credits list) or TIPL (Involved people list)
|
||||
* @param entries
|
||||
*/
|
||||
static functionList(entries) {
|
||||
const res = {};
|
||||
for (let i = 0; i + 1 < entries.length; i += 2) {
|
||||
const names = entries[i + 1].split(',');
|
||||
res[entries[i]] = res.hasOwnProperty(entries[i]) ? res[entries[i]].concat(names) : names;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* id3v2.4 defines that multiple T* values are separated by 0x00
|
||||
* id3v2.3 defines that TCOM, TEXT, TOLY, TOPE & TPE1 values are separated by /
|
||||
* @param tag - Tag name
|
||||
* @param text - Concatenated tag value
|
||||
* @returns Split tag value
|
||||
*/
|
||||
splitValue(tag, text) {
|
||||
let values;
|
||||
if (this.major < 4) {
|
||||
values = text.split(/\x00/g);
|
||||
if (values.length > 1) {
|
||||
this.warningCollector.addWarning(`ID3v2.${this.major} ${tag} uses non standard null-separator.`);
|
||||
}
|
||||
else {
|
||||
values = text.split(/\//g);
|
||||
}
|
||||
}
|
||||
else {
|
||||
values = text.split(/\x00/g);
|
||||
}
|
||||
return FrameParser.trimArray(values);
|
||||
}
|
||||
static trimArray(values) {
|
||||
return values.map(value => value.replace(/\x00+$/, '').trim());
|
||||
}
|
||||
static readIdentifierAndData(b, offset, length, encoding) {
|
||||
const fzero = util.findZero(b, offset, length, encoding);
|
||||
const id = util.decodeString(b.slice(offset, fzero), encoding);
|
||||
offset = fzero + FrameParser.getNullTerminatorLength(encoding);
|
||||
return { id, data: b.slice(offset, length) };
|
||||
}
|
||||
static getNullTerminatorLength(enc) {
|
||||
return enc === 'utf16le' ? 2 : 1;
|
||||
}
|
||||
}
|
||||
exports.FrameParser = FrameParser;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { INativeTagMap } from '../common/GenericTagTypes';
|
||||
import { CaseInsensitiveTagMap } from '../common/CaseInsensitiveTagMap';
|
||||
/**
|
||||
* ID3v2.2 tag mappings
|
||||
*/
|
||||
export declare const id3v22TagMap: INativeTagMap;
|
||||
export declare class ID3v22TagMapper extends CaseInsensitiveTagMap {
|
||||
constructor();
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ID3v22TagMapper = exports.id3v22TagMap = void 0;
|
||||
const CaseInsensitiveTagMap_1 = require("../common/CaseInsensitiveTagMap");
|
||||
/**
|
||||
* ID3v2.2 tag mappings
|
||||
*/
|
||||
exports.id3v22TagMap = {
|
||||
TT2: 'title',
|
||||
TP1: 'artist',
|
||||
TP2: 'albumartist',
|
||||
TAL: 'album',
|
||||
TYE: 'year',
|
||||
COM: 'comment',
|
||||
TRK: 'track',
|
||||
TPA: 'disk',
|
||||
TCO: 'genre',
|
||||
PIC: 'picture',
|
||||
TCM: 'composer',
|
||||
TOR: 'originaldate',
|
||||
TOT: 'originalalbum',
|
||||
TXT: 'lyricist',
|
||||
TP3: 'conductor',
|
||||
TPB: 'label',
|
||||
TT1: 'grouping',
|
||||
TT3: 'subtitle',
|
||||
TLA: 'language',
|
||||
TCR: 'copyright',
|
||||
WCP: 'license',
|
||||
TEN: 'encodedby',
|
||||
TSS: 'encodersettings',
|
||||
WAR: 'website',
|
||||
'COM:iTunPGAP': 'gapless'
|
||||
/* ToDo: iTunes tags:
|
||||
'COM:iTunNORM': ,
|
||||
'COM:iTunSMPB': 'encoder delay',
|
||||
'COM:iTunes_CDDB_IDs'
|
||||
*/ ,
|
||||
PCS: 'podcast',
|
||||
TCP: "compilation",
|
||||
TDR: 'date',
|
||||
TS2: 'albumartistsort',
|
||||
TSA: 'albumsort',
|
||||
TSC: 'composersort',
|
||||
TSP: 'artistsort',
|
||||
TST: 'titlesort',
|
||||
WFD: 'podcasturl'
|
||||
};
|
||||
class ID3v22TagMapper extends CaseInsensitiveTagMap_1.CaseInsensitiveTagMap {
|
||||
constructor() {
|
||||
super(['ID3v2.2'], exports.id3v22TagMap);
|
||||
}
|
||||
}
|
||||
exports.ID3v22TagMapper = ID3v22TagMapper;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { IRating, ITag } from '../type';
|
||||
import { INativeMetadataCollector } from '../common/MetadataCollector';
|
||||
import { CaseInsensitiveTagMap } from '../common/CaseInsensitiveTagMap';
|
||||
export declare class ID3v24TagMapper extends CaseInsensitiveTagMap {
|
||||
static toRating(popm: any): IRating;
|
||||
constructor();
|
||||
/**
|
||||
* Handle post mapping exceptions / correction
|
||||
* @param {string} tag to post map
|
||||
* @param warnings USed to register warnings
|
||||
* @return Common value e.g. "Buena Vista Social Club"
|
||||
*/
|
||||
protected postMap(tag: ITag, warnings: INativeMetadataCollector): void;
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ID3v24TagMapper = void 0;
|
||||
const GenericTagMapper_1 = require("../common/GenericTagMapper");
|
||||
const util = require("../common/Util");
|
||||
const CaseInsensitiveTagMap_1 = require("../common/CaseInsensitiveTagMap");
|
||||
/**
|
||||
* ID3v2.3/ID3v2.4 tag mappings
|
||||
*/
|
||||
const id3v24TagMap = {
|
||||
// id3v2.3
|
||||
TIT2: 'title',
|
||||
TPE1: 'artist',
|
||||
'TXXX:Artists': 'artists',
|
||||
TPE2: 'albumartist',
|
||||
TALB: 'album',
|
||||
TDRV: 'date',
|
||||
/**
|
||||
* Original release year
|
||||
*/
|
||||
TORY: 'originalyear',
|
||||
TPOS: 'disk',
|
||||
TCON: 'genre',
|
||||
APIC: 'picture',
|
||||
TCOM: 'composer',
|
||||
'USLT:description': 'lyrics',
|
||||
TSOA: 'albumsort',
|
||||
TSOT: 'titlesort',
|
||||
TOAL: 'originalalbum',
|
||||
TSOP: 'artistsort',
|
||||
TSO2: 'albumartistsort',
|
||||
TSOC: 'composersort',
|
||||
TEXT: 'lyricist',
|
||||
'TXXX:Writer': 'writer',
|
||||
TPE3: 'conductor',
|
||||
// 'IPLS:instrument': 'performer:instrument', // ToDo
|
||||
TPE4: 'remixer',
|
||||
'IPLS:arranger': 'arranger',
|
||||
'IPLS:engineer': 'engineer',
|
||||
'IPLS:producer': 'producer',
|
||||
'IPLS:DJ-mix': 'djmixer',
|
||||
'IPLS:mix': 'mixer',
|
||||
TPUB: 'label',
|
||||
TIT1: 'grouping',
|
||||
TIT3: 'subtitle',
|
||||
TRCK: 'track',
|
||||
TCMP: 'compilation',
|
||||
POPM: 'rating',
|
||||
TBPM: 'bpm',
|
||||
TMED: 'media',
|
||||
'TXXX:CATALOGNUMBER': 'catalognumber',
|
||||
'TXXX:MusicBrainz Album Status': 'releasestatus',
|
||||
'TXXX:MusicBrainz Album Type': 'releasetype',
|
||||
/**
|
||||
* Release country as documented: https://picard.musicbrainz.org/docs/mappings/#cite_note-0
|
||||
*/
|
||||
'TXXX:MusicBrainz Album Release Country': 'releasecountry',
|
||||
/**
|
||||
* Release country as implemented // ToDo: report
|
||||
*/
|
||||
'TXXX:RELEASECOUNTRY': 'releasecountry',
|
||||
'TXXX:SCRIPT': 'script',
|
||||
TLAN: 'language',
|
||||
TCOP: 'copyright',
|
||||
WCOP: 'license',
|
||||
TENC: 'encodedby',
|
||||
TSSE: 'encodersettings',
|
||||
'TXXX:BARCODE': 'barcode',
|
||||
'TXXX:ISRC': 'isrc',
|
||||
TSRC: 'isrc',
|
||||
'TXXX:ASIN': 'asin',
|
||||
'TXXX:originalyear': 'originalyear',
|
||||
'UFID:http://musicbrainz.org': 'musicbrainz_recordingid',
|
||||
'TXXX:MusicBrainz Release Track Id': 'musicbrainz_trackid',
|
||||
'TXXX:MusicBrainz Album Id': 'musicbrainz_albumid',
|
||||
'TXXX:MusicBrainz Artist Id': 'musicbrainz_artistid',
|
||||
'TXXX:MusicBrainz Album Artist Id': 'musicbrainz_albumartistid',
|
||||
'TXXX:MusicBrainz Release Group Id': 'musicbrainz_releasegroupid',
|
||||
'TXXX:MusicBrainz Work Id': 'musicbrainz_workid',
|
||||
'TXXX:MusicBrainz TRM Id': 'musicbrainz_trmid',
|
||||
'TXXX:MusicBrainz Disc Id': 'musicbrainz_discid',
|
||||
'TXXX:ACOUSTID_ID': 'acoustid_id',
|
||||
'TXXX:Acoustid Id': 'acoustid_id',
|
||||
'TXXX:Acoustid Fingerprint': 'acoustid_fingerprint',
|
||||
'TXXX:MusicIP PUID': 'musicip_puid',
|
||||
'TXXX:MusicMagic Fingerprint': 'musicip_fingerprint',
|
||||
WOAR: 'website',
|
||||
// id3v2.4
|
||||
// ToDo: In same sequence as defined at http://id3.org/id3v2.4.0-frames
|
||||
TDRC: 'date',
|
||||
TYER: 'year',
|
||||
TDOR: 'originaldate',
|
||||
// 'TMCL:instrument': 'performer:instrument',
|
||||
'TIPL:arranger': 'arranger',
|
||||
'TIPL:engineer': 'engineer',
|
||||
'TIPL:producer': 'producer',
|
||||
'TIPL:DJ-mix': 'djmixer',
|
||||
'TIPL:mix': 'mixer',
|
||||
TMOO: 'mood',
|
||||
// additional mappings:
|
||||
SYLT: 'lyrics',
|
||||
TSST: 'discsubtitle',
|
||||
TKEY: 'key',
|
||||
COMM: 'comment',
|
||||
TOPE: 'originalartist',
|
||||
// Windows Media Player
|
||||
'PRIV:AverageLevel': 'averageLevel',
|
||||
'PRIV:PeakLevel': 'peakLevel',
|
||||
// Discogs
|
||||
'TXXX:DISCOGS_ARTIST_ID': 'discogs_artist_id',
|
||||
'TXXX:DISCOGS_ARTISTS': 'artists',
|
||||
'TXXX:DISCOGS_ARTIST_NAME': 'artists',
|
||||
'TXXX:DISCOGS_ALBUM_ARTISTS': 'albumartist',
|
||||
'TXXX:DISCOGS_CATALOG': 'catalognumber',
|
||||
'TXXX:DISCOGS_COUNTRY': 'releasecountry',
|
||||
'TXXX:DISCOGS_DATE': 'originaldate',
|
||||
'TXXX:DISCOGS_LABEL': 'label',
|
||||
'TXXX:DISCOGS_LABEL_ID': 'discogs_label_id',
|
||||
'TXXX:DISCOGS_MASTER_RELEASE_ID': 'discogs_master_release_id',
|
||||
'TXXX:DISCOGS_RATING': 'discogs_rating',
|
||||
'TXXX:DISCOGS_RELEASED': 'date',
|
||||
'TXXX:DISCOGS_RELEASE_ID': 'discogs_release_id',
|
||||
'TXXX:DISCOGS_VOTES': 'discogs_votes',
|
||||
'TXXX:CATALOGID': 'catalognumber',
|
||||
'TXXX:STYLE': 'genre',
|
||||
'TXXX:REPLAYGAIN_TRACK_PEAK': 'replaygain_track_peak',
|
||||
'TXXX:REPLAYGAIN_TRACK_GAIN': 'replaygain_track_gain',
|
||||
'TXXX:REPLAYGAIN_ALBUM_PEAK': 'replaygain_album_peak',
|
||||
'TXXX:REPLAYGAIN_ALBUM_GAIN': 'replaygain_album_gain',
|
||||
'TXXX:MP3GAIN_MINMAX': 'replaygain_track_minmax',
|
||||
'TXXX:MP3GAIN_ALBUM_MINMAX': 'replaygain_album_minmax',
|
||||
'TXXX:MP3GAIN_UNDO': 'replaygain_undo',
|
||||
MVNM: 'movement',
|
||||
MVIN: 'movementIndex',
|
||||
PCST: 'podcast',
|
||||
TCAT: 'category',
|
||||
TDES: 'description',
|
||||
TDRL: 'date',
|
||||
TGID: 'podcastId',
|
||||
TKWD: 'keywords',
|
||||
WFED: 'podcasturl'
|
||||
};
|
||||
class ID3v24TagMapper extends CaseInsensitiveTagMap_1.CaseInsensitiveTagMap {
|
||||
static toRating(popm) {
|
||||
return {
|
||||
source: popm.email,
|
||||
rating: popm.rating > 0 ? (popm.rating - 1) / 254 * GenericTagMapper_1.CommonTagMapper.maxRatingScore : undefined
|
||||
};
|
||||
}
|
||||
constructor() {
|
||||
super(['ID3v2.3', 'ID3v2.4'], id3v24TagMap);
|
||||
}
|
||||
/**
|
||||
* Handle post mapping exceptions / correction
|
||||
* @param {string} tag to post map
|
||||
* @param warnings USed to register warnings
|
||||
* @return Common value e.g. "Buena Vista Social Club"
|
||||
*/
|
||||
postMap(tag, warnings) {
|
||||
switch (tag.id) {
|
||||
case 'UFID': // decode MusicBrainz Recording Id
|
||||
if (tag.value.owner_identifier === 'http://musicbrainz.org') {
|
||||
tag.id += ':' + tag.value.owner_identifier;
|
||||
tag.value = util.decodeString(tag.value.identifier, 'latin1'); // latin1 == iso-8859-1
|
||||
}
|
||||
break;
|
||||
case 'PRIV':
|
||||
switch (tag.value.owner_identifier) {
|
||||
// decode Windows Media Player
|
||||
case 'AverageLevel':
|
||||
case 'PeakValue':
|
||||
tag.id += ':' + tag.value.owner_identifier;
|
||||
tag.value = tag.value.data.length === 4 ? tag.value.data.readUInt32LE(0) : null;
|
||||
if (tag.value === null) {
|
||||
warnings.addWarning(`Failed to parse PRIV:PeakValue`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
warnings.addWarning(`Unknown PRIV owner-identifier: ${tag.value.owner_identifier}`);
|
||||
}
|
||||
break;
|
||||
case 'COMM':
|
||||
tag.value = tag.value ? tag.value.text : null;
|
||||
break;
|
||||
case 'POPM':
|
||||
tag.value = ID3v24TagMapper.toRating(tag.value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.ID3v24TagMapper = ID3v24TagMapper;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/// <reference types="node" />
|
||||
import { ITokenizer } from 'strtok3/lib/core';
|
||||
import { IOptions } from '../type';
|
||||
import { INativeMetadataCollector } from '../common/MetadataCollector';
|
||||
export declare class ID3v2Parser {
|
||||
static removeUnsyncBytes(buffer: Buffer): Buffer;
|
||||
private static getFrameHeaderLength;
|
||||
private static readFrameFlags;
|
||||
private static readFrameData;
|
||||
/**
|
||||
* Create a combined tag key, of tag & description
|
||||
* @param {string} tag e.g.: COM
|
||||
* @param {string} description e.g. iTunPGAP
|
||||
* @returns {string} e.g. COM:iTunPGAP
|
||||
*/
|
||||
private static makeDescriptionTagName;
|
||||
private tokenizer;
|
||||
private id3Header;
|
||||
private metadata;
|
||||
private headerType;
|
||||
private options;
|
||||
parse(metadata: INativeMetadataCollector, tokenizer: ITokenizer, options: IOptions): Promise<void>;
|
||||
parseExtendedHeader(): Promise<void>;
|
||||
parseExtendedHeaderData(dataRemaining: number, extendedHeaderSize: number): Promise<void>;
|
||||
parseId3Data(dataLen: number): Promise<void>;
|
||||
private addTag;
|
||||
private parseMetadata;
|
||||
private readFrameHeader;
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ID3v2Parser = void 0;
|
||||
const Token = require("token-types");
|
||||
const util = require("../common/Util");
|
||||
const FrameParser_1 = require("./FrameParser");
|
||||
const ID3v2Token_1 = require("./ID3v2Token");
|
||||
class ID3v2Parser {
|
||||
static removeUnsyncBytes(buffer) {
|
||||
let readI = 0;
|
||||
let writeI = 0;
|
||||
while (readI < buffer.length - 1) {
|
||||
if (readI !== writeI) {
|
||||
buffer[writeI] = buffer[readI];
|
||||
}
|
||||
readI += (buffer[readI] === 0xFF && buffer[readI + 1] === 0) ? 2 : 1;
|
||||
writeI++;
|
||||
}
|
||||
if (readI < buffer.length) {
|
||||
buffer[writeI++] = buffer[readI];
|
||||
}
|
||||
return buffer.slice(0, writeI);
|
||||
}
|
||||
static getFrameHeaderLength(majorVer) {
|
||||
switch (majorVer) {
|
||||
case 2:
|
||||
return 6;
|
||||
case 3:
|
||||
case 4:
|
||||
return 10;
|
||||
default:
|
||||
throw new Error('header versionIndex is incorrect');
|
||||
}
|
||||
}
|
||||
static readFrameFlags(b) {
|
||||
return {
|
||||
status: {
|
||||
tag_alter_preservation: util.getBit(b, 0, 6),
|
||||
file_alter_preservation: util.getBit(b, 0, 5),
|
||||
read_only: util.getBit(b, 0, 4)
|
||||
},
|
||||
format: {
|
||||
grouping_identity: util.getBit(b, 1, 7),
|
||||
compression: util.getBit(b, 1, 3),
|
||||
encryption: util.getBit(b, 1, 2),
|
||||
unsynchronisation: util.getBit(b, 1, 1),
|
||||
data_length_indicator: util.getBit(b, 1, 0)
|
||||
}
|
||||
};
|
||||
}
|
||||
static readFrameData(buf, frameHeader, majorVer, includeCovers, warningCollector) {
|
||||
const frameParser = new FrameParser_1.FrameParser(majorVer, warningCollector);
|
||||
switch (majorVer) {
|
||||
case 2:
|
||||
return frameParser.readData(buf, frameHeader.id, includeCovers);
|
||||
case 3:
|
||||
case 4:
|
||||
if (frameHeader.flags.format.unsynchronisation) {
|
||||
buf = ID3v2Parser.removeUnsyncBytes(buf);
|
||||
}
|
||||
if (frameHeader.flags.format.data_length_indicator) {
|
||||
buf = buf.slice(4, buf.length);
|
||||
}
|
||||
return frameParser.readData(buf, frameHeader.id, includeCovers);
|
||||
default:
|
||||
throw new Error('Unexpected majorVer: ' + majorVer);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Create a combined tag key, of tag & description
|
||||
* @param {string} tag e.g.: COM
|
||||
* @param {string} description e.g. iTunPGAP
|
||||
* @returns {string} e.g. COM:iTunPGAP
|
||||
*/
|
||||
static makeDescriptionTagName(tag, description) {
|
||||
return tag + (description ? ':' + description : '');
|
||||
}
|
||||
async parse(metadata, tokenizer, options) {
|
||||
this.tokenizer = tokenizer;
|
||||
this.metadata = metadata;
|
||||
this.options = options;
|
||||
const id3Header = await this.tokenizer.readToken(ID3v2Token_1.ID3v2Header);
|
||||
if (id3Header.fileIdentifier !== 'ID3') {
|
||||
throw new Error('expected ID3-header file-identifier \'ID3\' was not found');
|
||||
}
|
||||
this.id3Header = id3Header;
|
||||
this.headerType = ('ID3v2.' + id3Header.version.major);
|
||||
if (id3Header.flags.isExtendedHeader) {
|
||||
return this.parseExtendedHeader();
|
||||
}
|
||||
else {
|
||||
return this.parseId3Data(id3Header.size);
|
||||
}
|
||||
}
|
||||
async parseExtendedHeader() {
|
||||
const extendedHeader = await this.tokenizer.readToken(ID3v2Token_1.ExtendedHeader);
|
||||
const dataRemaining = extendedHeader.size - ID3v2Token_1.ExtendedHeader.len;
|
||||
if (dataRemaining > 0) {
|
||||
return this.parseExtendedHeaderData(dataRemaining, extendedHeader.size);
|
||||
}
|
||||
else {
|
||||
return this.parseId3Data(this.id3Header.size - extendedHeader.size);
|
||||
}
|
||||
}
|
||||
async parseExtendedHeaderData(dataRemaining, extendedHeaderSize) {
|
||||
const buffer = Buffer.alloc(dataRemaining);
|
||||
await this.tokenizer.readBuffer(buffer, { length: dataRemaining });
|
||||
return this.parseId3Data(this.id3Header.size - extendedHeaderSize);
|
||||
}
|
||||
async parseId3Data(dataLen) {
|
||||
const buffer = Buffer.alloc(dataLen);
|
||||
await this.tokenizer.readBuffer(buffer, { length: dataLen });
|
||||
for (const tag of this.parseMetadata(buffer)) {
|
||||
if (tag.id === 'TXXX') {
|
||||
if (tag.value) {
|
||||
for (const text of tag.value.text) {
|
||||
this.addTag(ID3v2Parser.makeDescriptionTagName(tag.id, tag.value.description), text);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (tag.id === 'COM') {
|
||||
for (const value of tag.value) {
|
||||
this.addTag(ID3v2Parser.makeDescriptionTagName(tag.id, value.description), value.text);
|
||||
}
|
||||
}
|
||||
else if (tag.id === 'COMM') {
|
||||
for (const value of tag.value) {
|
||||
this.addTag(ID3v2Parser.makeDescriptionTagName(tag.id, value.description), value);
|
||||
}
|
||||
}
|
||||
else if (Array.isArray(tag.value)) {
|
||||
for (const value of tag.value) {
|
||||
this.addTag(tag.id, value);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.addTag(tag.id, tag.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
addTag(id, value) {
|
||||
this.metadata.addTag(this.headerType, id, value);
|
||||
}
|
||||
parseMetadata(data) {
|
||||
let offset = 0;
|
||||
const tags = [];
|
||||
while (true) {
|
||||
if (offset === data.length)
|
||||
break;
|
||||
const frameHeaderLength = ID3v2Parser.getFrameHeaderLength(this.id3Header.version.major);
|
||||
if (offset + frameHeaderLength > data.length) {
|
||||
this.metadata.addWarning('Illegal ID3v2 tag length');
|
||||
break;
|
||||
}
|
||||
const frameHeaderBytes = data.slice(offset, offset += frameHeaderLength);
|
||||
const frameHeader = this.readFrameHeader(frameHeaderBytes, this.id3Header.version.major);
|
||||
const frameDataBytes = data.slice(offset, offset += frameHeader.length);
|
||||
const values = ID3v2Parser.readFrameData(frameDataBytes, frameHeader, this.id3Header.version.major, !this.options.skipCovers, this.metadata);
|
||||
if (values) {
|
||||
tags.push({ id: frameHeader.id, value: values });
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
readFrameHeader(v, majorVer) {
|
||||
let header;
|
||||
switch (majorVer) {
|
||||
case 2:
|
||||
header = {
|
||||
id: v.toString('ascii', 0, 3),
|
||||
length: Token.UINT24_BE.get(v, 3)
|
||||
};
|
||||
if (!header.id.match(/[A-Z0-9]{3}/g)) {
|
||||
this.metadata.addWarning(`Invalid ID3v2.${this.id3Header.version.major} frame-header-ID: ${header.id}`);
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
header = {
|
||||
id: v.toString('ascii', 0, 4),
|
||||
length: (majorVer === 4 ? ID3v2Token_1.UINT32SYNCSAFE : Token.UINT32_BE).get(v, 4),
|
||||
flags: ID3v2Parser.readFrameFlags(v.slice(8, 10))
|
||||
};
|
||||
if (!header.id.match(/[A-Z0-9]{4}/g)) {
|
||||
this.metadata.addWarning(`Invalid ID3v2.${this.id3Header.version.major} frame-header-ID: ${header.id}`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unexpected majorVer: ' + majorVer);
|
||||
}
|
||||
return header;
|
||||
}
|
||||
}
|
||||
exports.ID3v2Parser = ID3v2Parser;
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import * as util from '../common/Util';
|
||||
import { IGetToken } from 'strtok3/lib/core';
|
||||
/**
|
||||
* The picture type according to the ID3v2 APIC frame
|
||||
* Ref: http://id3.org/id3v2.3.0#Attached_picture
|
||||
*/
|
||||
export declare enum AttachedPictureType {
|
||||
'Other' = 0,
|
||||
"32x32 pixels 'file icon' (PNG only)" = 1,
|
||||
'Other file icon' = 2,
|
||||
'Cover (front)' = 3,
|
||||
'Cover (back)' = 4,
|
||||
'Leaflet page' = 5,
|
||||
'Media (e.g. label side of CD)' = 6,
|
||||
'Lead artist/lead performer/soloist' = 7,
|
||||
'Artist/performer' = 8,
|
||||
'Conductor' = 9,
|
||||
'Band/Orchestra' = 10,
|
||||
'Composer' = 11,
|
||||
'Lyricist/text writer' = 12,
|
||||
'Recording Location' = 13,
|
||||
'During recording' = 14,
|
||||
'During performance' = 15,
|
||||
'Movie/video screen capture' = 16,
|
||||
'A bright coloured fish' = 17,
|
||||
'Illustration' = 18,
|
||||
'Band/artist logotype' = 19,
|
||||
'Publisher/Studio logotype' = 20
|
||||
}
|
||||
export declare type ID3v2MajorVersion = 2 | 3 | 4;
|
||||
export interface IExtendedHeader {
|
||||
size: number;
|
||||
extendedFlags: number;
|
||||
sizeOfPadding: number;
|
||||
crcDataPresent: boolean;
|
||||
}
|
||||
/**
|
||||
* 28 bits (representing up to 256MB) integer, the msb is 0 to avoid 'false syncsignals'.
|
||||
* 4 * %0xxxxxxx
|
||||
*/
|
||||
export declare const UINT32SYNCSAFE: {
|
||||
get: (buf: Uint8Array, off: number) => number;
|
||||
len: number;
|
||||
};
|
||||
/**
|
||||
* ID3v2 tag header
|
||||
*/
|
||||
export interface IID3v2header {
|
||||
fileIdentifier: string;
|
||||
version: {
|
||||
major: ID3v2MajorVersion;
|
||||
revision: number;
|
||||
};
|
||||
flags: {
|
||||
unsynchronisation: boolean;
|
||||
isExtendedHeader: boolean;
|
||||
expIndicator: boolean;
|
||||
footer: boolean;
|
||||
};
|
||||
size: number;
|
||||
}
|
||||
/**
|
||||
* ID3v2 header
|
||||
* Ref: http://id3.org/id3v2.3.0#ID3v2_header
|
||||
* ToDo
|
||||
*/
|
||||
export declare const ID3v2Header: IGetToken<IID3v2header>;
|
||||
export declare const ExtendedHeader: IGetToken<IExtendedHeader>;
|
||||
export interface ITextEncoding {
|
||||
encoding: util.StringEncoding;
|
||||
bom?: boolean;
|
||||
}
|
||||
export declare const TextEncodingToken: IGetToken<ITextEncoding>;
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.TextEncodingToken = exports.ExtendedHeader = exports.ID3v2Header = exports.UINT32SYNCSAFE = exports.AttachedPictureType = void 0;
|
||||
const Token = require("token-types");
|
||||
const util = require("../common/Util");
|
||||
/**
|
||||
* The picture type according to the ID3v2 APIC frame
|
||||
* Ref: http://id3.org/id3v2.3.0#Attached_picture
|
||||
*/
|
||||
var AttachedPictureType;
|
||||
(function (AttachedPictureType) {
|
||||
AttachedPictureType[AttachedPictureType["Other"] = 0] = "Other";
|
||||
AttachedPictureType[AttachedPictureType["32x32 pixels 'file icon' (PNG only)"] = 1] = "32x32 pixels 'file icon' (PNG only)";
|
||||
AttachedPictureType[AttachedPictureType["Other file icon"] = 2] = "Other file icon";
|
||||
AttachedPictureType[AttachedPictureType["Cover (front)"] = 3] = "Cover (front)";
|
||||
AttachedPictureType[AttachedPictureType["Cover (back)"] = 4] = "Cover (back)";
|
||||
AttachedPictureType[AttachedPictureType["Leaflet page"] = 5] = "Leaflet page";
|
||||
AttachedPictureType[AttachedPictureType["Media (e.g. label side of CD)"] = 6] = "Media (e.g. label side of CD)";
|
||||
AttachedPictureType[AttachedPictureType["Lead artist/lead performer/soloist"] = 7] = "Lead artist/lead performer/soloist";
|
||||
AttachedPictureType[AttachedPictureType["Artist/performer"] = 8] = "Artist/performer";
|
||||
AttachedPictureType[AttachedPictureType["Conductor"] = 9] = "Conductor";
|
||||
AttachedPictureType[AttachedPictureType["Band/Orchestra"] = 10] = "Band/Orchestra";
|
||||
AttachedPictureType[AttachedPictureType["Composer"] = 11] = "Composer";
|
||||
AttachedPictureType[AttachedPictureType["Lyricist/text writer"] = 12] = "Lyricist/text writer";
|
||||
AttachedPictureType[AttachedPictureType["Recording Location"] = 13] = "Recording Location";
|
||||
AttachedPictureType[AttachedPictureType["During recording"] = 14] = "During recording";
|
||||
AttachedPictureType[AttachedPictureType["During performance"] = 15] = "During performance";
|
||||
AttachedPictureType[AttachedPictureType["Movie/video screen capture"] = 16] = "Movie/video screen capture";
|
||||
AttachedPictureType[AttachedPictureType["A bright coloured fish"] = 17] = "A bright coloured fish";
|
||||
AttachedPictureType[AttachedPictureType["Illustration"] = 18] = "Illustration";
|
||||
AttachedPictureType[AttachedPictureType["Band/artist logotype"] = 19] = "Band/artist logotype";
|
||||
AttachedPictureType[AttachedPictureType["Publisher/Studio logotype"] = 20] = "Publisher/Studio logotype";
|
||||
})(AttachedPictureType = exports.AttachedPictureType || (exports.AttachedPictureType = {}));
|
||||
/**
|
||||
* 28 bits (representing up to 256MB) integer, the msb is 0 to avoid 'false syncsignals'.
|
||||
* 4 * %0xxxxxxx
|
||||
*/
|
||||
exports.UINT32SYNCSAFE = {
|
||||
get: (buf, off) => {
|
||||
return buf[off + 3] & 0x7f | ((buf[off + 2]) << 7) |
|
||||
((buf[off + 1]) << 14) | ((buf[off]) << 21);
|
||||
},
|
||||
len: 4
|
||||
};
|
||||
/**
|
||||
* ID3v2 header
|
||||
* Ref: http://id3.org/id3v2.3.0#ID3v2_header
|
||||
* ToDo
|
||||
*/
|
||||
exports.ID3v2Header = {
|
||||
len: 10,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
// ID3v2/file identifier "ID3"
|
||||
fileIdentifier: new Token.StringType(3, 'ascii').get(buf, off),
|
||||
// ID3v2 versionIndex
|
||||
version: {
|
||||
major: Token.INT8.get(buf, off + 3),
|
||||
revision: Token.INT8.get(buf, off + 4)
|
||||
},
|
||||
// ID3v2 flags
|
||||
flags: {
|
||||
// Unsynchronisation
|
||||
unsynchronisation: util.getBit(buf, off + 5, 7),
|
||||
// Extended header
|
||||
isExtendedHeader: util.getBit(buf, off + 5, 6),
|
||||
// Experimental indicator
|
||||
expIndicator: util.getBit(buf, off + 5, 5),
|
||||
footer: util.getBit(buf, off + 5, 4)
|
||||
},
|
||||
size: exports.UINT32SYNCSAFE.get(buf, off + 6)
|
||||
};
|
||||
}
|
||||
};
|
||||
exports.ExtendedHeader = {
|
||||
len: 10,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
// Extended header size
|
||||
size: Token.UINT32_BE.get(buf, off),
|
||||
// Extended Flags
|
||||
extendedFlags: Token.UINT16_BE.get(buf, off + 4),
|
||||
// Size of padding
|
||||
sizeOfPadding: Token.UINT32_BE.get(buf, off + 6),
|
||||
// CRC data present
|
||||
crcDataPresent: util.getBit(buf, off + 4, 31)
|
||||
};
|
||||
}
|
||||
};
|
||||
exports.TextEncodingToken = {
|
||||
len: 1,
|
||||
get: (buf, off) => {
|
||||
switch (buf.readUInt8(off)) {
|
||||
case 0x00:
|
||||
return { encoding: 'latin1' }; // binary
|
||||
case 0x01:
|
||||
return { encoding: 'utf16le', bom: true };
|
||||
case 0x02:
|
||||
return { encoding: 'utf16le', bom: false };
|
||||
case 0x03:
|
||||
return { encoding: 'utf8', bom: false };
|
||||
default:
|
||||
return { encoding: 'utf8', bom: false };
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user