Initial working version
This commit is contained in:
+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