Buckets:
dieumercimvemba/mediastore / scripts /whatsapp-bridge /node_modules /music-metadata /lib /common /MetadataCollector.js
| import { TrackTypeValueToKeyMap, } from '../type.js'; | |
| import initDebug from 'debug'; | |
| import { isSingleton, isUnique } from './GenericTagTypes.js'; | |
| import { CombinedTagMapper } from './CombinedTagMapper.js'; | |
| import { CommonTagMapper } from './GenericTagMapper.js'; | |
| import { toRatio } from './Util.js'; | |
| import { fileTypeFromBuffer } from 'file-type'; | |
| import { parseLyrics } from '../lrc/LyricsParser.js'; | |
| const debug = initDebug('music-metadata:collector'); | |
| const TagPriority = ['matroska', 'APEv2', 'vorbis', 'ID3v2.4', 'ID3v2.3', 'ID3v2.2', 'exif', 'asf', 'iTunes', 'AIFF', 'ID3v1']; | |
| /** | |
| * Provided to the parser to uodate the metadata result. | |
| * Responsible for triggering async updates | |
| */ | |
| export class MetadataCollector { | |
| constructor(opts) { | |
| this.format = { | |
| tagTypes: [], | |
| trackInfo: [] | |
| }; | |
| this.native = {}; | |
| this.common = { | |
| track: { no: null, of: null }, | |
| disk: { no: null, of: null }, | |
| movementIndex: { no: null, of: null } | |
| }; | |
| 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(); | |
| this.opts = opts; | |
| 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 as the worst because of the 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=${streamInfo.type ? TrackTypeValueToKeyMap[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 } }); | |
| } | |
| } | |
| setAudioOnly() { | |
| this.setFormat('hasAudio', true); | |
| this.setFormat('hasVideo', false); | |
| } | |
| async 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 }); | |
| await this.toCommon(tagType, tagId, value); | |
| } | |
| addWarning(warning) { | |
| this.quality.warnings.push({ message: warning }); | |
| } | |
| async 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': | |
| return this.handleSingularArtistTag(tagType, tag, 'artist', 'artists'); | |
| case 'albumartist': | |
| return this.handleSingularArtistTag(tagType, tag, 'albumartist', 'albumartists'); | |
| case 'artists': | |
| return this.handlePluralArtistTag(tagType, tag, 'artist', 'artists'); | |
| case 'albumartists': | |
| return this.handlePluralArtistTag(tagType, tag, 'albumartist', 'albumartists'); | |
| case 'picture': | |
| return this.postFixPicture(tag.value).then(picture => { | |
| if (picture !== null) { | |
| tag.value = picture; | |
| this.setGenericTag(tagType, tag); | |
| } | |
| }); | |
| case 'totaltracks': | |
| this.common.track.of = CommonTagMapper.toIntOrNull(tag.value); | |
| return; | |
| case 'totaldiscs': | |
| this.common.disk.of = CommonTagMapper.toIntOrNull(tag.value); | |
| return; | |
| case 'movementTotal': | |
| this.common.movementIndex.of = 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] = CommonTagMapper.normalizeTrack(tag.value); | |
| this.common[tag.id].of = of != null ? of : this.common[tag.id].of; | |
| return; | |
| } | |
| case 'bpm': | |
| case 'year': | |
| case 'originalyear': | |
| tag.value = Number.parseInt(tag.value, 10); | |
| break; | |
| case 'date': { | |
| // ToDo: be more strict on 'YYYY...' | |
| const year = Number.parseInt(tag.value.substr(0, 4), 10); | |
| if (!Number.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' ? Number.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 = toRatio(tag.value); | |
| break; | |
| case 'replaygain_track_minmax': | |
| tag.value = tag.value.split(',').map(v => Number.parseInt(v, 10)); | |
| break; | |
| case 'replaygain_undo': { | |
| const minMix = tag.value.split(',').map(v => Number.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 | |
| const commonTag = this.common[tag.id]; | |
| if (commonTag && commonTag.indexOf(tag.value) !== -1) | |
| return; | |
| break; | |
| } | |
| case 'comment': | |
| if (typeof tag.value === 'string') { | |
| tag.value = { text: tag.value }; | |
| } | |
| if (tag.value.descriptor === 'iTunPGAP') { | |
| this.setGenericTag(tagType, { id: 'gapless', value: tag.value.text === '1' }); | |
| } | |
| break; | |
| case 'lyrics': | |
| if (typeof tag.value === 'string') { | |
| tag.value = parseLyrics(tag.value); | |
| } | |
| 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 | |
| }; | |
| } | |
| /** | |
| * Handle singular artist tags (artist, albumartist) and cross-populate to plural form | |
| */ | |
| handleSingularArtistTag(tagType, tag, singularId, pluralId) { | |
| if (this.commonOrigin[singularId] === this.originPriority[tagType]) { | |
| // Assume the singular field is used as plural (multiple values from same source) | |
| return this.postMap('artificial', { id: pluralId, value: tag.value }); | |
| } | |
| if (!this.common[pluralId]) { | |
| // Fill plural using singular source | |
| this.setGenericTag('artificial', { id: pluralId, value: tag.value }); | |
| } | |
| this.setGenericTag(tagType, tag); | |
| } | |
| /** | |
| * Handle plural artist tags (artists, albumartists) and cross-populate to singular form | |
| */ | |
| handlePluralArtistTag(tagType, tag, singularId, pluralId) { | |
| if (!this.common[singularId] || this.commonOrigin[singularId] === this.originPriority.artificial) { | |
| if (!this.common[pluralId] || this.common[pluralId].indexOf(tag.value) === -1) { | |
| // Fill singular using plural source | |
| const values = (this.common[pluralId] || []).concat([tag.value]); | |
| const value = joinArtists(values); | |
| this.setGenericTag('artificial', { id: singularId, value }); | |
| } | |
| } | |
| this.setGenericTag(tagType, tag); | |
| } | |
| /** | |
| * Fix some common issues with picture object | |
| * @param picture Picture | |
| */ | |
| async postFixPicture(picture) { | |
| if (picture.data && picture.data.length > 0) { | |
| if (!picture.format) { | |
| const fileType = await fileTypeFromBuffer(Uint8Array.from(picture.data)); // ToDO: remove Buffer | |
| 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 | |
| */ | |
| async toCommon(tagType, tagId, value) { | |
| const tag = { id: tagId, value }; | |
| const genericTag = this.tagMapper.mapTag(tagType, tag, this); | |
| if (genericTag) { | |
| await 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 (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 (!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 | |
| } | |
| } | |
| export function joinArtists(artists) { | |
| if (artists.length > 2) { | |
| return `${artists.slice(0, artists.length - 1).join(', ')} & ${artists[artists.length - 1]}`; | |
| } | |
| return artists.join(' & '); | |
| } | |
Xet Storage Details
- Size:
- 12 kB
- Xet hash:
- a087f744dc73957a5f82f7433e7b47b21fa8d41c2223ec573882a19013ac87e5
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.