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
+17
View File
@@ -0,0 +1,17 @@
import { ITokenizer } from 'strtok3/lib/core';
import { ITokenParser } from '../ParserFactory';
import { IOptions, IPrivateOptions } from '../type';
import { INativeMetadataCollector } from './MetadataCollector';
export declare abstract class BasicParser implements ITokenParser {
protected metadata: INativeMetadataCollector;
protected tokenizer: ITokenizer;
protected options: IPrivateOptions;
/**
* Initialize parser with output (metadata), input (tokenizer) & parsing options (options).
* @param {INativeMetadataCollector} metadata Output
* @param {ITokenizer} tokenizer Input
* @param {IOptions} options Parsing options
*/
init(metadata: INativeMetadataCollector, tokenizer: ITokenizer, options: IOptions): ITokenParser;
abstract parse(): any;
}
+18
View File
@@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BasicParser = void 0;
class BasicParser {
/**
* Initialize parser with output (metadata), input (tokenizer) & parsing options (options).
* @param {INativeMetadataCollector} metadata Output
* @param {ITokenizer} tokenizer Input
* @param {IOptions} options Parsing options
*/
init(metadata, tokenizer, options) {
this.metadata = metadata;
this.tokenizer = tokenizer;
this.options = options;
return this;
}
}
exports.BasicParser = BasicParser;
+10
View File
@@ -0,0 +1,10 @@
import { INativeTagMap, TagType } from './GenericTagTypes';
import { CommonTagMapper } from './GenericTagMapper';
export declare class CaseInsensitiveTagMap extends CommonTagMapper {
constructor(tagTypes: TagType[], tagMap: INativeTagMap);
/**
* @tag Native header tag
* @return common tag name (alias)
*/
protected getCommonName(tag: string): import("./GenericTagTypes").GenericTagId;
}
+21
View File
@@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CaseInsensitiveTagMap = void 0;
const GenericTagMapper_1 = require("./GenericTagMapper");
class CaseInsensitiveTagMap extends GenericTagMapper_1.CommonTagMapper {
constructor(tagTypes, tagMap) {
const upperCaseMap = {};
for (const tag of Object.keys(tagMap)) {
upperCaseMap[tag.toUpperCase()] = tagMap[tag];
}
super(tagTypes, upperCaseMap);
}
/**
* @tag Native header tag
* @return common tag name (alias)
*/
getCommonName(tag) {
return this.tagMap[tag.toUpperCase()];
}
}
exports.CaseInsensitiveTagMap = CaseInsensitiveTagMap;
+19
View File
@@ -0,0 +1,19 @@
import { IGenericTag, TagType } from "./GenericTagTypes";
import { IGenericTagMapper } from "./GenericTagMapper";
import { ITag } from "../type";
import { INativeMetadataCollector } from './MetadataCollector';
export declare class CombinedTagMapper {
tagMappers: {
[index: string]: IGenericTagMapper;
};
constructor();
/**
* Convert native to generic (common) tags
* @param tagType Originating tag format
* @param tag Native tag to map to a generic tag id
* @param warnings
* @return Generic tag result (output of this function)
*/
mapTag(tagType: TagType, tag: ITag, warnings: INativeMetadataCollector): IGenericTag;
private registerTagMapper;
}
+51
View File
@@ -0,0 +1,51 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CombinedTagMapper = void 0;
const ID3v1TagMap_1 = require("../id3v1/ID3v1TagMap");
const ID3v24TagMapper_1 = require("../id3v2/ID3v24TagMapper");
const AsfTagMapper_1 = require("../asf/AsfTagMapper");
const ID3v22TagMapper_1 = require("../id3v2/ID3v22TagMapper");
const APEv2TagMapper_1 = require("../apev2/APEv2TagMapper");
const MP4TagMapper_1 = require("../mp4/MP4TagMapper");
const VorbisTagMapper_1 = require("../ogg/vorbis/VorbisTagMapper");
const RiffInfoTagMap_1 = require("../riff/RiffInfoTagMap");
const MatroskaTagMapper_1 = require("../matroska/MatroskaTagMapper");
class CombinedTagMapper {
constructor() {
this.tagMappers = {};
[
new ID3v1TagMap_1.ID3v1TagMapper(),
new ID3v22TagMapper_1.ID3v22TagMapper(),
new ID3v24TagMapper_1.ID3v24TagMapper(),
new MP4TagMapper_1.MP4TagMapper(),
new MP4TagMapper_1.MP4TagMapper(),
new VorbisTagMapper_1.VorbisTagMapper(),
new APEv2TagMapper_1.APEv2TagMapper(),
new AsfTagMapper_1.AsfTagMapper(),
new RiffInfoTagMap_1.RiffInfoTagMapper(),
new MatroskaTagMapper_1.MatroskaTagMapper()
].forEach(mapper => {
this.registerTagMapper(mapper);
});
}
/**
* Convert native to generic (common) tags
* @param tagType Originating tag format
* @param tag Native tag to map to a generic tag id
* @param warnings
* @return Generic tag result (output of this function)
*/
mapTag(tagType, tag, warnings) {
const tagMapper = this.tagMappers[tagType];
if (tagMapper) {
return this.tagMappers[tagType].mapGenericTag(tag, warnings);
}
throw new Error("No generic tag mapper defined for tag-format: " + tagType);
}
registerTagMapper(genericTagMapper) {
for (const tagType of genericTagMapper.tagTypes) {
this.tagMappers[tagType] = genericTagMapper;
}
}
}
exports.CombinedTagMapper = CombinedTagMapper;
+6
View File
@@ -0,0 +1,6 @@
import { IToken } from "strtok3/lib/core";
/**
* Token for read FourCC
* Ref: https://en.wikipedia.org/wiki/FourCC
*/
export declare const FourCcToken: IToken<string>;
+28
View File
@@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FourCcToken = void 0;
const util = require("./Util");
const validFourCC = /^[\x21-\x7e©][\x20-\x7e\x00()]{3}/;
/**
* Token for read FourCC
* Ref: https://en.wikipedia.org/wiki/FourCC
*/
exports.FourCcToken = {
len: 4,
get: (buf, off) => {
const id = buf.toString('binary', off, off + exports.FourCcToken.len);
switch (id) {
default:
if (!id.match(validFourCC)) {
throw new Error(`FourCC contains invalid characters: ${util.a2hex(id)} "${id}"`);
}
}
return id;
},
put: (buffer, offset, id) => {
const str = Buffer.from(id, 'binary');
if (str.length !== 4)
throw new Error("Invalid length");
return str.copy(buffer, offset);
}
};
+51
View File
@@ -0,0 +1,51 @@
import * as generic from './GenericTagTypes';
import { ITag } from '../type';
import { INativeMetadataCollector, IWarningCollector } from './MetadataCollector';
export interface IGenericTagMapper {
/**
* Which tagType it able to map to the generic mapping format
*/
tagTypes: generic.TagType[];
/**
* Basic tag map
*/
tagMap: generic.INativeTagMap;
/**
* Map native tag to generic tag
* @param tag Native tag
* @param warnings Register warnings
* @return Generic tag, if native tag could be mapped
*/
mapGenericTag(tag: ITag, warnings: INativeMetadataCollector): generic.IGenericTag;
}
export declare class CommonTagMapper implements IGenericTagMapper {
tagTypes: generic.TagType[];
tagMap: generic.INativeTagMap;
static maxRatingScore: number;
static toIntOrNull(str: string): number;
static normalizeTrack(origVal: number | string): {
no: number;
of: number;
};
constructor(tagTypes: generic.TagType[], tagMap: generic.INativeTagMap);
/**
* Process and set common tags
* write common tags to
* @param tag Native tag
* @param warnings Register warnings
* @return common name
*/
mapGenericTag(tag: ITag, warnings: IWarningCollector): generic.IGenericTag;
/**
* Convert native tag key to common tag key
* @tag Native header tag
* @return common tag name (alias)
*/
protected getCommonName(tag: string): generic.GenericTagId;
/**
* Handle post mapping exceptions / correction
* @param {string} tag Tag e.g. {"©alb", "Buena Vista Social Club")
* @param {warnings} Used to register warnings
*/
protected postMap(tag: ITag, warnings: IWarningCollector): void;
}
+55
View File
@@ -0,0 +1,55 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CommonTagMapper = void 0;
class CommonTagMapper {
constructor(tagTypes, tagMap) {
this.tagTypes = tagTypes;
this.tagMap = tagMap;
}
static toIntOrNull(str) {
const cleaned = parseInt(str, 10);
return isNaN(cleaned) ? null : cleaned;
}
// TODO: a string of 1of1 would fail to be converted
// converts 1/10 to no : 1, of : 10
// or 1 to no : 1, of : 0
static normalizeTrack(origVal) {
const split = origVal.toString().split('/');
return {
no: parseInt(split[0], 10) || null,
of: parseInt(split[1], 10) || null
};
}
/**
* Process and set common tags
* write common tags to
* @param tag Native tag
* @param warnings Register warnings
* @return common name
*/
mapGenericTag(tag, warnings) {
tag = { id: tag.id, value: tag.value }; // clone object
this.postMap(tag, warnings);
// Convert native tag event to generic 'alias' tag
const id = this.getCommonName(tag.id);
return id ? { id, value: tag.value } : null;
}
/**
* Convert native tag key to common tag key
* @tag Native header tag
* @return common tag name (alias)
*/
getCommonName(tag) {
return this.tagMap[tag];
}
/**
* Handle post mapping exceptions / correction
* @param {string} tag Tag e.g. {"©alb", "Buena Vista Social Club")
* @param {warnings} Used to register warnings
*/
postMap(tag, warnings) {
return;
}
}
exports.CommonTagMapper = CommonTagMapper;
CommonTagMapper.maxRatingScore = 1;
+33
View File
@@ -0,0 +1,33 @@
export declare type TagType = 'vorbis' | 'ID3v1' | 'ID3v2.2' | 'ID3v2.3' | 'ID3v2.4' | 'APEv2' | 'asf' | 'iTunes' | 'exif' | 'matroska';
export interface IGenericTag {
id: GenericTagId;
value: any;
}
export declare type GenericTagId = 'track' | 'disk' | 'year' | 'title' | 'artist' | 'artists' | 'albumartist' | 'album' | 'date' | 'originaldate' | 'originalyear' | 'comment' | 'genre' | 'picture' | 'composer' | 'lyrics' | 'albumsort' | 'titlesort' | 'work' | 'artistsort' | 'albumartistsort' | 'composersort' | 'lyricist' | 'writer' | 'conductor' | 'remixer' | 'arranger' | 'engineer' | 'technician' | 'producer' | 'djmixer' | 'mixer' | 'publisher' | 'label' | 'grouping' | 'subtitle' | 'discsubtitle' | 'totaltracks' | 'totaldiscs' | 'compilation' | 'rating' | 'bpm' | 'mood' | 'media' | 'catalognumber' | 'tvShow' | 'tvShowSort' | 'tvEpisode' | 'tvEpisodeId' | 'tvNetwork' | 'tvSeason' | 'podcast' | 'podcasturl' | 'releasestatus' | 'releasetype' | 'releasecountry' | 'script' | 'language' | 'copyright' | 'license' | 'encodedby' | 'encodersettings' | 'gapless' | 'barcode' | 'isrc' | 'asin' | 'musicbrainz_recordingid' | 'musicbrainz_trackid' | 'musicbrainz_albumid' | 'musicbrainz_artistid' | 'musicbrainz_albumartistid' | 'musicbrainz_releasegroupid' | 'musicbrainz_workid' | 'musicbrainz_trmid' | 'musicbrainz_discid' | 'acoustid_id' | 'acoustid_fingerprint' | 'musicip_puid' | 'musicip_fingerprint' | 'website' | 'performer:instrument' | 'peakLevel' | 'averageLevel' | 'notes' | 'key' | 'originalalbum' | 'originalartist' | 'discogs_artist_id' | 'discogs_label_id' | 'discogs_master_release_id' | 'discogs_rating' | 'discogs_release_id' | 'discogs_votes' | 'replaygain_track_gain' | 'replaygain_track_peak' | 'replaygain_album_gain' | 'replaygain_album_peak' | 'replaygain_track_minmax' | 'replaygain_album_minmax' | 'replaygain_undo' | 'description' | 'longDescription' | 'category' | 'hdVideo' | 'keywords' | 'movement' | 'movementIndex' | 'movementTotal' | 'podcastId' | 'showMovement' | 'stik';
export interface INativeTagMap {
[index: string]: GenericTagId;
}
export interface ITagInfo {
/**
* True if result is an array
*/
multiple: boolean;
/**
* True if the result is an array and each value in the array should be unique
*/
unique?: boolean;
}
export interface ITagInfoMap {
[index: string]: ITagInfo;
}
export declare const commonTags: ITagInfoMap;
/**
* @param alias Name of common tag
* @returns {boolean|*} true if given alias is mapped as a singleton', otherwise false
*/
export declare function isSingleton(alias: GenericTagId): boolean;
/**
* @param alias Common (generic) tag
* @returns {boolean|*} true if given alias is a singleton or explicitly marked as unique
*/
export declare function isUnique(alias: GenericTagId): boolean;
+131
View File
@@ -0,0 +1,131 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isUnique = exports.isSingleton = exports.commonTags = void 0;
exports.commonTags = {
year: { multiple: false },
track: { multiple: false },
disk: { multiple: false },
title: { multiple: false },
artist: { multiple: false },
artists: { multiple: true, unique: true },
albumartist: { multiple: false },
album: { multiple: false },
date: { multiple: false },
originaldate: { multiple: false },
originalyear: { multiple: false },
comment: { multiple: true, unique: false },
genre: { multiple: true, unique: true },
picture: { multiple: true, unique: true },
composer: { multiple: true, unique: true },
lyrics: { multiple: true, unique: false },
albumsort: { multiple: false, unique: true },
titlesort: { multiple: false, unique: true },
work: { multiple: false, unique: true },
artistsort: { multiple: false, unique: true },
albumartistsort: { multiple: false, unique: true },
composersort: { multiple: false, unique: true },
lyricist: { multiple: true, unique: true },
writer: { multiple: true, unique: true },
conductor: { multiple: true, unique: true },
remixer: { multiple: true, unique: true },
arranger: { multiple: true, unique: true },
engineer: { multiple: true, unique: true },
producer: { multiple: true, unique: true },
technician: { multiple: true, unique: true },
djmixer: { multiple: true, unique: true },
mixer: { multiple: true, unique: true },
label: { multiple: true, unique: true },
grouping: { multiple: false },
subtitle: { multiple: true },
discsubtitle: { multiple: false },
totaltracks: { multiple: false },
totaldiscs: { multiple: false },
compilation: { multiple: false },
rating: { multiple: true },
bpm: { multiple: false },
mood: { multiple: false },
media: { multiple: false },
catalognumber: { multiple: true, unique: true },
tvShow: { multiple: false },
tvShowSort: { multiple: false },
tvSeason: { multiple: false },
tvEpisode: { multiple: false },
tvEpisodeId: { multiple: false },
tvNetwork: { multiple: false },
podcast: { multiple: false },
podcasturl: { multiple: false },
releasestatus: { multiple: false },
releasetype: { multiple: true },
releasecountry: { multiple: false },
script: { multiple: false },
language: { multiple: false },
copyright: { multiple: false },
license: { multiple: false },
encodedby: { multiple: false },
encodersettings: { multiple: false },
gapless: { multiple: false },
barcode: { multiple: false },
isrc: { multiple: true },
asin: { multiple: false },
musicbrainz_recordingid: { multiple: false },
musicbrainz_trackid: { multiple: false },
musicbrainz_albumid: { multiple: false },
musicbrainz_artistid: { multiple: true },
musicbrainz_albumartistid: { multiple: true },
musicbrainz_releasegroupid: { multiple: false },
musicbrainz_workid: { multiple: false },
musicbrainz_trmid: { multiple: false },
musicbrainz_discid: { multiple: false },
acoustid_id: { multiple: false },
acoustid_fingerprint: { multiple: false },
musicip_puid: { multiple: false },
musicip_fingerprint: { multiple: false },
website: { multiple: false },
'performer:instrument': { multiple: true, unique: true },
averageLevel: { multiple: false },
peakLevel: { multiple: false },
notes: { multiple: true, unique: false },
key: { multiple: false },
originalalbum: { multiple: false },
originalartist: { multiple: false },
discogs_artist_id: { multiple: true, unique: true },
discogs_release_id: { multiple: false },
discogs_label_id: { multiple: false },
discogs_master_release_id: { multiple: false },
discogs_votes: { multiple: false },
discogs_rating: { multiple: false },
replaygain_track_peak: { multiple: false },
replaygain_track_gain: { multiple: false },
replaygain_album_peak: { multiple: false },
replaygain_album_gain: { multiple: false },
replaygain_track_minmax: { multiple: false },
replaygain_album_minmax: { multiple: false },
replaygain_undo: { multiple: false },
description: { multiple: true },
longDescription: { multiple: false },
category: { multiple: true },
hdVideo: { multiple: false },
keywords: { multiple: true },
movement: { multiple: false },
movementIndex: { multiple: false },
movementTotal: { multiple: false },
podcastId: { multiple: false },
showMovement: { multiple: false },
stik: { multiple: false }
};
/**
* @param alias Name of common tag
* @returns {boolean|*} true if given alias is mapped as a singleton', otherwise false
*/
function isSingleton(alias) {
return exports.commonTags.hasOwnProperty(alias) && !exports.commonTags[alias].multiple;
}
exports.isSingleton = isSingleton;
/**
* @param alias Common (generic) tag
* @returns {boolean|*} true if given alias is a singleton or explicitly marked as unique
*/
function isUnique(alias) {
return !exports.commonTags[alias].multiple || exports.commonTags[alias].unique;
}
exports.isUnique = isUnique;
+76
View File
@@ -0,0 +1,76 @@
import { FormatId, IAudioMetadata, ICommonTagsResult, IFormat, INativeTags, IOptions, IQualityInformation, ITrackInfo } from '../type';
import { IGenericTag, TagType } from './GenericTagTypes';
/**
* Combines all generic-tag-mappers for each tag type
*/
export interface IWarningCollector {
/**
* Register parser warning
* @param warning
*/
addWarning(warning: string): any;
}
export interface INativeMetadataCollector extends IWarningCollector {
/**
* Only use this for reading
*/
readonly format: IFormat;
readonly native: INativeTags;
readonly quality: IQualityInformation;
/**
* @returns {boolean} true if one or more tags have been found
*/
hasAny(): boolean;
setFormat(key: FormatId, value: any): any;
addTag(tagType: TagType, tagId: string, value: any): any;
addStreamInfo(streamInfo: ITrackInfo): any;
}
/**
* Provided to the parser to uodate the metadata result.
* Responsible for triggering async updates
*/
export declare class MetadataCollector implements INativeMetadataCollector {
private opts;
readonly format: IFormat;
readonly native: INativeTags;
readonly common: ICommonTagsResult;
readonly quality: IQualityInformation;
/**
* Keeps track of origin priority for each mapped id
*/
private readonly commonOrigin;
/**
* Maps a tag type to a priority
*/
private readonly originPriority;
private tagMapper;
constructor(opts: IOptions);
/**
* @returns {boolean} true if one or more tags have been found
*/
hasAny(): boolean;
addStreamInfo(streamInfo: ITrackInfo): void;
setFormat(key: FormatId, value: any): void;
addTag(tagType: TagType, tagId: string, value: any): void;
addWarning(warning: string): void;
postMap(tagType: TagType | 'artificial', tag: IGenericTag): any;
/**
* Convert native tags to common tags
* @returns {IAudioMetadata} Native + common tags
*/
toCommonMetadata(): IAudioMetadata;
/**
* Fix some common issues with picture object
* @param pictureType
*/
private postFixPicture;
/**
* Convert native tag to common tags
*/
private toCommon;
/**
* Set generic tag
*/
private setGenericTag;
}
export declare function joinArtists(artists: string[]): string;
+274
View File
@@ -0,0 +1,274 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.joinArtists = exports.MetadataCollector = void 0;
const type_1 = require("../type");
const _debug = require("debug");
const GenericTagTypes_1 = require("./GenericTagTypes");
const CombinedTagMapper_1 = require("./CombinedTagMapper");
const GenericTagMapper_1 = require("./GenericTagMapper");
const Util_1 = require("./Util");
const FileType = require("file-type/core");
const debug = _debug('music-metadata:collector');
const TagPriority = ['matroska', 'APEv2', 'vorbis', 'ID3v2.4', 'ID3v2.3', 'ID3v2.2', 'exif', 'asf', 'iTunes', 'ID3v1'];
/**
* Provided to the parser to uodate the metadata result.
* Responsible for triggering async updates
*/
class MetadataCollector {
constructor(opts) {
this.opts = opts;
this.format = {
tagTypes: [],
trackInfo: []
};
this.native = {};
this.common = {
track: { no: null, of: null },
disk: { no: null, of: null },
movementIndex: {}
};
this.quality = {
warnings: []
};
/**
* Keeps track of origin priority for each mapped id
*/
this.commonOrigin = {};
/**
* Maps a tag type to a priority
*/
this.originPriority = {};
this.tagMapper = new CombinedTagMapper_1.CombinedTagMapper();
let priority = 1;
for (const tagType of TagPriority) {
this.originPriority[tagType] = priority++;
}
this.originPriority.artificial = 500; // Filled using alternative tags
this.originPriority.id3v1 = 600; // Consider worst due to field length limit
}
/**
* @returns {boolean} true if one or more tags have been found
*/
hasAny() {
return Object.keys(this.native).length > 0;
}
addStreamInfo(streamInfo) {
debug(`streamInfo: type=${type_1.TrackType[streamInfo.type]}, codec=${streamInfo.codecName}`);
this.format.trackInfo.push(streamInfo);
}
setFormat(key, value) {
debug(`format: ${key} = ${value}`);
this.format[key] = value; // as any to override readonly
if (this.opts.observer) {
this.opts.observer({ metadata: this, tag: { type: 'format', id: key, value } });
}
}
addTag(tagType, tagId, value) {
debug(`tag ${tagType}.${tagId} = ${value}`);
if (!this.native[tagType]) {
this.format.tagTypes.push(tagType);
this.native[tagType] = [];
}
this.native[tagType].push({ id: tagId, value });
this.toCommon(tagType, tagId, value);
}
addWarning(warning) {
this.quality.warnings.push({ message: warning });
}
postMap(tagType, tag) {
// Common tag (alias) found
// check if we need to do something special with common tag
// if the event has been aliased then we need to clean it before
// it is emitted to the user. e.g. genre (20) -> Electronic
switch (tag.id) {
case 'artist':
if (this.commonOrigin.artist === this.originPriority[tagType]) {
// Assume the artist field is used as artists
return this.postMap('artificial', { id: 'artists', value: tag.value });
}
if (!this.common.artists) {
// Fill artists using artist source
this.setGenericTag('artificial', { id: 'artists', value: tag.value });
}
break;
case 'artists':
if (!this.common.artist || this.commonOrigin.artist === this.originPriority.artificial) {
if (!this.common.artists || this.common.artists.indexOf(tag.value) === -1) {
// Fill artist using artists source
const artists = (this.common.artists || []).concat([tag.value]);
const value = joinArtists(artists);
const artistTag = { id: 'artist', value };
this.setGenericTag('artificial', artistTag);
}
}
break;
case 'picture':
this.postFixPicture(tag.value).then(picture => {
if (picture !== null) {
tag.value = picture;
this.setGenericTag(tagType, tag);
}
});
return;
case 'totaltracks':
this.common.track.of = GenericTagMapper_1.CommonTagMapper.toIntOrNull(tag.value);
return;
case 'totaldiscs':
this.common.disk.of = GenericTagMapper_1.CommonTagMapper.toIntOrNull(tag.value);
return;
case 'movementTotal':
this.common.movementIndex.of = GenericTagMapper_1.CommonTagMapper.toIntOrNull(tag.value);
return;
case 'track':
case 'disk':
case 'movementIndex':
const of = this.common[tag.id].of; // store of value, maybe maybe overwritten
this.common[tag.id] = GenericTagMapper_1.CommonTagMapper.normalizeTrack(tag.value);
this.common[tag.id].of = of != null ? of : this.common[tag.id].of;
return;
case 'year':
case 'originalyear':
tag.value = parseInt(tag.value, 10);
break;
case 'date':
// ToDo: be more strict on 'YYYY...'
const year = parseInt(tag.value.substr(0, 4), 10);
if (!isNaN(year)) {
this.common.year = year;
}
break;
case 'discogs_label_id':
case 'discogs_release_id':
case 'discogs_master_release_id':
case 'discogs_artist_id':
case 'discogs_votes':
tag.value = typeof tag.value === 'string' ? parseInt(tag.value, 10) : tag.value;
break;
case 'replaygain_track_gain':
case 'replaygain_track_peak':
case 'replaygain_album_gain':
case 'replaygain_album_peak':
tag.value = (0, Util_1.toRatio)(tag.value);
break;
case 'replaygain_track_minmax':
tag.value = tag.value.split(',').map(v => parseInt(v, 10));
break;
case 'replaygain_undo':
const minMix = tag.value.split(',').map(v => parseInt(v, 10));
tag.value = {
leftChannel: minMix[0],
rightChannel: minMix[1]
};
break;
case 'gapless': // iTunes gap-less flag
case 'compilation':
case 'podcast':
case 'showMovement':
tag.value = tag.value === '1' || tag.value === 1; // boolean
break;
case 'isrc': // Only keep unique values
if (this.common[tag.id] && this.common[tag.id].indexOf(tag.value) !== -1)
return;
break;
default:
// nothing to do
}
if (tag.value !== null) {
this.setGenericTag(tagType, tag);
}
}
/**
* Convert native tags to common tags
* @returns {IAudioMetadata} Native + common tags
*/
toCommonMetadata() {
return {
format: this.format,
native: this.native,
quality: this.quality,
common: this.common
};
}
/**
* Fix some common issues with picture object
* @param pictureType
*/
async postFixPicture(picture) {
if (picture.data && picture.data.length > 0) {
if (!picture.format) {
const fileType = await FileType.fromBuffer(picture.data);
if (fileType) {
picture.format = fileType.mime;
}
else {
return null;
}
}
picture.format = picture.format.toLocaleLowerCase();
switch (picture.format) {
case 'image/jpg':
picture.format = 'image/jpeg'; // ToDo: register warning
}
return picture;
}
this.addWarning(`Empty picture tag found`);
return null;
}
/**
* Convert native tag to common tags
*/
toCommon(tagType, tagId, value) {
const tag = { id: tagId, value };
const genericTag = this.tagMapper.mapTag(tagType, tag, this);
if (genericTag) {
this.postMap(tagType, genericTag);
}
}
/**
* Set generic tag
*/
setGenericTag(tagType, tag) {
debug(`common.${tag.id} = ${tag.value}`);
const prio0 = this.commonOrigin[tag.id] || 1000;
const prio1 = this.originPriority[tagType];
if ((0, GenericTagTypes_1.isSingleton)(tag.id)) {
if (prio1 <= prio0) {
this.common[tag.id] = tag.value;
this.commonOrigin[tag.id] = prio1;
}
else {
return debug(`Ignore native tag (singleton): ${tagType}.${tag.id} = ${tag.value}`);
}
}
else {
if (prio1 === prio0) {
if (!(0, GenericTagTypes_1.isUnique)(tag.id) || this.common[tag.id].indexOf(tag.value) === -1) {
this.common[tag.id].push(tag.value);
}
else {
debug(`Ignore duplicate value: ${tagType}.${tag.id} = ${tag.value}`);
}
// no effect? this.commonOrigin[tag.id] = prio1;
}
else if (prio1 < prio0) {
this.common[tag.id] = [tag.value];
this.commonOrigin[tag.id] = prio1;
}
else {
return debug(`Ignore native tag (list): ${tagType}.${tag.id} = ${tag.value}`);
}
}
if (this.opts.observer) {
this.opts.observer({ metadata: this, tag: { type: 'common', id: tag.id, value: tag.value } });
}
// ToDo: trigger metadata event
}
}
exports.MetadataCollector = MetadataCollector;
function joinArtists(artists) {
if (artists.length > 2) {
return artists.slice(0, artists.length - 1).join(', ') + ' & ' + artists[artists.length - 1];
}
return artists.join(' & ');
}
exports.joinArtists = joinArtists;
+20
View File
@@ -0,0 +1,20 @@
/// <reference types="node" />
import { IRandomReader } from '../type';
/**
* Provides abstract file access via the IRandomRead interface
*/
export declare class RandomFileReader implements IRandomReader {
fileSize: number;
private readonly fd;
constructor(filePath: string, fileSize: number);
/**
* Read from a given position of an abstracted file or buffer.
* @param buffer {Buffer} is the buffer that the data will be written to.
* @param offset {number} is the offset in the buffer to start writing at.
* @param length {number}is an integer specifying the number of bytes to read.
* @param position {number} is an argument specifying where to begin reading from in the file.
* @return {Promise<number>} bytes read
*/
randomRead(buffer: Buffer, offset: number, length: number, position: number): Promise<number>;
close(): void;
}
+37
View File
@@ -0,0 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RandomFileReader = void 0;
const fs = require("fs");
/**
* Provides abstract file access via the IRandomRead interface
*/
class RandomFileReader {
constructor(filePath, fileSize) {
this.fileSize = fileSize;
this.fd = fs.openSync(filePath, 'r');
}
/**
* Read from a given position of an abstracted file or buffer.
* @param buffer {Buffer} is the buffer that the data will be written to.
* @param offset {number} is the offset in the buffer to start writing at.
* @param length {number}is an integer specifying the number of bytes to read.
* @param position {number} is an argument specifying where to begin reading from in the file.
* @return {Promise<number>} bytes read
*/
randomRead(buffer, offset, length, position) {
return new Promise((resolve, reject) => {
fs.read(this.fd, buffer, offset, length, position, (err, bytesRead) => {
if (err) {
reject(err);
}
else {
resolve(bytesRead);
}
});
});
}
close() {
fs.closeSync(this.fd);
}
}
exports.RandomFileReader = RandomFileReader;
+18
View File
@@ -0,0 +1,18 @@
import { IRandomReader } from '../type';
/**
* Provides abstract Uint8Array access via the IRandomRead interface
*/
export declare class RandomUint8ArrayReader implements IRandomReader {
private readonly uint8Array;
readonly fileSize: number;
constructor(uint8Array: Uint8Array);
/**
* Read from a given position of an abstracted file or buffer.
* @param uint8Array - Uint8Array that the data will be written to.
* @param offset - Offset in the buffer to start writing at.
* @param length - Integer specifying the number of bytes to read.
* @param position - Specifies where to begin reading from in the file.
* @return Promise providing bytes read
*/
randomRead(uint8Array: Uint8Array, offset: number, length: number, position: number): Promise<number>;
}
+25
View File
@@ -0,0 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RandomUint8ArrayReader = void 0;
/**
* Provides abstract Uint8Array access via the IRandomRead interface
*/
class RandomUint8ArrayReader {
constructor(uint8Array) {
this.uint8Array = uint8Array;
this.fileSize = uint8Array.length;
}
/**
* Read from a given position of an abstracted file or buffer.
* @param uint8Array - Uint8Array that the data will be written to.
* @param offset - Offset in the buffer to start writing at.
* @param length - Integer specifying the number of bytes to read.
* @param position - Specifies where to begin reading from in the file.
* @return Promise providing bytes read
*/
async randomRead(uint8Array, offset, length, position) {
uint8Array.set(this.uint8Array.subarray(position, position + length), offset);
return length;
}
}
exports.RandomUint8ArrayReader = RandomUint8ArrayReader;
+57
View File
@@ -0,0 +1,57 @@
/// <reference types="node" />
import { IRatio } from '../type';
export declare type StringEncoding = 'ascii' | 'utf8' | 'utf16le' | 'ucs2' | 'base64url' | 'latin1' | 'hex';
export declare function getBit(buf: Uint8Array, off: number, bit: number): boolean;
/**
* Found delimiting zero in uint8Array
* @param uint8Array Uint8Array to find the zero delimiter in
* @param start Offset in uint8Array
* @param end Last position to parse in uint8Array
* @param encoding The string encoding used
* @return Absolute position on uint8Array where zero found
*/
export declare function findZero(uint8Array: Uint8Array, start: number, end: number, encoding?: StringEncoding): number;
export declare function trimRightNull(x: string): string;
/**
*
* @param buffer Decoder input data
* @param encoding 'utf16le' | 'utf16' | 'utf8' | 'iso-8859-1'
* @return {string}
*/
export declare function decodeString(buffer: Buffer, encoding: StringEncoding): string;
export declare function stripNulls(str: string): string;
/**
* Read bit-aligned number start from buffer
* Total offset in bits = byteOffset * 8 + bitOffset
* @param buf Byte buffer
* @param byteOffset Starting offset in bytes
* @param bitOffset Starting offset in bits: 0 = lsb
* @param len Length of number in bits
* @return {number} decoded bit aligned number
*/
export declare function getBitAllignedNumber(buf: Uint8Array, byteOffset: number, bitOffset: number, len: number): number;
/**
* Read bit-aligned number start from buffer
* Total offset in bits = byteOffset * 8 + bitOffset
* @param buf Byte buffer
* @param byteOffset Starting offset in bytes
* @param bitOffset Starting offset in bits: 0 = most significant bit, 7 is least significant bit
* @return {number} decoded bit aligned number
*/
export declare function isBitSet(buf: Uint8Array, byteOffset: number, bitOffset: number): boolean;
export declare function a2hex(str: string): string;
/**
* Convert power ratio to DB
* ratio: [0..1]
*/
export declare function ratioToDb(ratio: number): number;
/**
* Convert dB to ratio
* db Decibels
*/
export declare function dbToRatio(dB: number): number;
/**
* Convert replay gain to ratio and Decibel
* @param value string holding a ratio like '0.034' or '-7.54 dB'
*/
export declare function toRatio(value: string): IRatio;
+169
View File
@@ -0,0 +1,169 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.toRatio = exports.dbToRatio = exports.ratioToDb = exports.a2hex = exports.isBitSet = exports.getBitAllignedNumber = exports.stripNulls = exports.decodeString = exports.trimRightNull = exports.findZero = exports.getBit = void 0;
function getBit(buf, off, bit) {
return (buf[off] & (1 << bit)) !== 0;
}
exports.getBit = getBit;
/**
* Found delimiting zero in uint8Array
* @param uint8Array Uint8Array to find the zero delimiter in
* @param start Offset in uint8Array
* @param end Last position to parse in uint8Array
* @param encoding The string encoding used
* @return Absolute position on uint8Array where zero found
*/
function findZero(uint8Array, start, end, encoding) {
let i = start;
if (encoding === 'utf16le') {
while (uint8Array[i] !== 0 || uint8Array[i + 1] !== 0) {
if (i >= end)
return end;
i += 2;
}
return i;
}
else {
while (uint8Array[i] !== 0) {
if (i >= end)
return end;
i++;
}
return i;
}
}
exports.findZero = findZero;
function trimRightNull(x) {
const pos0 = x.indexOf('\0');
return pos0 === -1 ? x : x.substr(0, pos0);
}
exports.trimRightNull = trimRightNull;
function swapBytes(uint8Array) {
const l = uint8Array.length;
if ((l & 1) !== 0)
throw new Error('Buffer length must be even');
for (let i = 0; i < l; i += 2) {
const a = uint8Array[i];
uint8Array[i] = uint8Array[i + 1];
uint8Array[i + 1] = a;
}
return uint8Array;
}
/**
*
* @param buffer Decoder input data
* @param encoding 'utf16le' | 'utf16' | 'utf8' | 'iso-8859-1'
* @return {string}
*/
function decodeString(buffer, encoding) {
// annoying workaround for a double BOM issue
// https://github.com/leetreveil/musicmetadata/issues/84
let offset = 0;
if (buffer[0] === 0xFF && buffer[1] === 0xFE) { // little endian
if (encoding === 'utf16le') {
offset = 2;
}
else if (buffer[2] === 0xFE && buffer[3] === 0xFF) {
offset = 2; // Clear double BOM
}
}
else if (encoding === 'utf16le' && buffer[0] === 0xFE && buffer[1] === 0xFF) {
// BOM, indicating big endian decoding
return decodeString(swapBytes(buffer), encoding);
}
return buffer.toString(encoding, offset);
}
exports.decodeString = decodeString;
function stripNulls(str) {
str = str.replace(/^\x00+/g, '');
str = str.replace(/\x00+$/g, '');
return str;
}
exports.stripNulls = stripNulls;
/**
* Read bit-aligned number start from buffer
* Total offset in bits = byteOffset * 8 + bitOffset
* @param buf Byte buffer
* @param byteOffset Starting offset in bytes
* @param bitOffset Starting offset in bits: 0 = lsb
* @param len Length of number in bits
* @return {number} decoded bit aligned number
*/
function getBitAllignedNumber(buf, byteOffset, bitOffset, len) {
const byteOff = byteOffset + ~~(bitOffset / 8);
const bitOff = bitOffset % 8;
let value = buf[byteOff];
value &= 0xff >> bitOff;
const bitsRead = 8 - bitOff;
const bitsLeft = len - bitsRead;
if (bitsLeft < 0) {
value >>= (8 - bitOff - len);
}
else if (bitsLeft > 0) {
value <<= bitsLeft;
value |= getBitAllignedNumber(buf, byteOffset, bitOffset + bitsRead, bitsLeft);
}
return value;
}
exports.getBitAllignedNumber = getBitAllignedNumber;
/**
* Read bit-aligned number start from buffer
* Total offset in bits = byteOffset * 8 + bitOffset
* @param buf Byte buffer
* @param byteOffset Starting offset in bytes
* @param bitOffset Starting offset in bits: 0 = most significant bit, 7 is least significant bit
* @return {number} decoded bit aligned number
*/
function isBitSet(buf, byteOffset, bitOffset) {
return getBitAllignedNumber(buf, byteOffset, bitOffset, 1) === 1;
}
exports.isBitSet = isBitSet;
function a2hex(str) {
const arr = [];
for (let i = 0, l = str.length; i < l; i++) {
const hex = Number(str.charCodeAt(i)).toString(16);
arr.push(hex.length === 1 ? '0' + hex : hex);
}
return arr.join(' ');
}
exports.a2hex = a2hex;
/**
* Convert power ratio to DB
* ratio: [0..1]
*/
function ratioToDb(ratio) {
return 10 * Math.log10(ratio);
}
exports.ratioToDb = ratioToDb;
/**
* Convert dB to ratio
* db Decibels
*/
function dbToRatio(dB) {
return Math.pow(10, dB / 10);
}
exports.dbToRatio = dbToRatio;
/**
* Convert replay gain to ratio and Decibel
* @param value string holding a ratio like '0.034' or '-7.54 dB'
*/
function toRatio(value) {
const ps = value.split(' ').map(p => p.trim().toLowerCase());
// @ts-ignore
if (ps.length >= 1) {
const v = parseFloat(ps[0]);
if (ps.length === 2 && ps[1] === 'db') {
return {
dB: v,
ratio: dbToRatio(v)
};
}
else {
return {
dB: ratioToDb(v),
ratio: v
};
}
}
}
exports.toRatio = toRatio;