const Logger = require('../../Logger') const { areEquivalent, copyValue, cleanStringForSearch, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index') class MusicMetadata { constructor(metadata) { this.title = null this.artists = [] // Array of strings this.album = null this.albumArtist = null this.genres = [] // Array of strings this.composer = null this.originalYear = null this.releaseDate = null this.releaseCountry = null this.releaseType = null this.releaseStatus = null this.recordLabel = null this.language = null this.explicit = false this.discNumber = null this.discTotal = null this.trackNumber = null this.trackTotal = null this.isrc = null this.musicBrainzTrackId = null this.musicBrainzAlbumId = null this.musicBrainzAlbumArtistId = null this.musicBrainzArtistId = null if (metadata) { this.construct(metadata) } } construct(metadata) { this.title = metadata.title this.artists = metadata.artists ? [...metadata.artists] : [] this.album = metadata.album this.albumArtist = metadata.albumArtist this.genres = metadata.genres ? [...metadata.genres] : [] this.composer = metadata.composer || null this.originalYear = metadata.originalYear || null this.releaseDate = metadata.releaseDate || null this.releaseCountry = metadata.releaseCountry || null this.releaseType = metadata.releaseType || null this.releaseStatus = metadata.releaseStatus || null this.recordLabel = metadata.recordLabel || null this.language = metadata.language || null this.explicit = !!metadata.explicit this.discNumber = metadata.discNumber || null this.discTotal = metadata.discTotal || null this.trackNumber = metadata.trackNumber || null this.trackTotal = metadata.trackTotal || null this.isrc = metadata.isrc || null this.musicBrainzTrackId = metadata.musicBrainzTrackId || null this.musicBrainzAlbumId = metadata.musicBrainzAlbumId || null this.musicBrainzAlbumArtistId = metadata.musicBrainzAlbumArtistId || null this.musicBrainzArtistId = metadata.musicBrainzArtistId || null } toJSON() { return { title: this.title, artists: [...this.artists], album: this.album, albumArtist: this.albumArtist, genres: [...this.genres], composer: this.composer, originalYear: this.originalYear, releaseDate: this.releaseDate, releaseCountry: this.releaseCountry, releaseType: this.releaseType, releaseStatus: this.releaseStatus, recordLabel: this.recordLabel, language: this.language, explicit: this.explicit, discNumber: this.discNumber, discTotal: this.discTotal, trackNumber: this.trackNumber, trackTotal: this.trackTotal, isrc: this.isrc, musicBrainzTrackId: this.musicBrainzTrackId, musicBrainzAlbumId: this.musicBrainzAlbumId, musicBrainzAlbumArtistId: this.musicBrainzAlbumArtistId, musicBrainzArtistId: this.musicBrainzArtistId } } toJSONMinified() { return { title: this.title, titleIgnorePrefix: this.titlePrefixAtEnd, artists: [...this.artists], album: this.album, albumArtist: this.albumArtist, genres: [...this.genres], composer: this.composer, originalYear: this.originalYear, releaseDate: this.releaseDate, releaseCountry: this.releaseCountry, releaseType: this.releaseType, releaseStatus: this.releaseStatus, recordLabel: this.recordLabel, language: this.language, explicit: this.explicit, discNumber: this.discNumber, discTotal: this.discTotal, trackNumber: this.trackNumber, trackTotal: this.trackTotal, isrc: this.isrc, musicBrainzTrackId: this.musicBrainzTrackId, musicBrainzAlbumId: this.musicBrainzAlbumId, musicBrainzAlbumArtistId: this.musicBrainzAlbumArtistId, musicBrainzArtistId: this.musicBrainzArtistId } } toJSONExpanded() { return this.toJSONMinified() } clone() { return new MusicMetadata(this.toJSON()) } get titleIgnorePrefix() { return getTitleIgnorePrefix(this.title) } get titlePrefixAtEnd() { return getTitlePrefixAtEnd(this.title) } searchQuery(query) { // Returns key if match is found const keysToCheck = ['title', 'album'] for (const key of keysToCheck) { if (this[key] && cleanStringForSearch(String(this[key])).includes(query)) { return { matchKey: key, matchText: this[key] } } } return null } setData(mediaMetadata = {}) { this.title = mediaMetadata.title || null this.artist = mediaMetadata.artist || null this.album = mediaMetadata.album || null } update(payload) { const json = this.toJSON() let hasUpdates = false for (const key in json) { if (payload[key] !== undefined) { if (!areEquivalent(payload[key], json[key])) { this[key] = copyValue(payload[key]) Logger.debug('[MusicMetadata] Key updated', key, this[key]) hasUpdates = true } } } return hasUpdates } parseArtistsTag(artistsTag) { if (!artistsTag || !artistsTag.length) return [] const separators = ['/', '//', ';'] for (let i = 0; i < separators.length; i++) { if (artistsTag.includes(separators[i])) { return artistsTag.split(separators[i]).map(artist => artist.trim()).filter(a => !!a) } } return [artistsTag] } parseGenresTag(genreTag) { if (!genreTag || !genreTag.length) return [] const separators = ['/', '//', ';'] for (let i = 0; i < separators.length; i++) { if (genreTag.includes(separators[i])) { return genreTag.split(separators[i]).map(genre => genre.trim()).filter(g => !!g) } } return [genreTag] } setDataFromAudioMetaTags(audioFileMetaTags, overrideExistingDetails = false) { const MetadataMapArray = [ { tag: 'tagTitle', key: 'title', }, { tag: 'tagArtist', key: 'artists' }, { tag: 'tagAlbumArtist', key: 'albumArtist' }, { tag: 'tagAlbum', key: 'album', }, { tag: 'tagPublisher', key: 'recordLabel' }, { tag: 'tagComposer', key: 'composer' }, { tag: 'tagDate', key: 'releaseDate' }, { tag: 'tagReleaseCountry', key: 'releaseCountry' }, { tag: 'tagReleaseType', key: 'releaseType' }, { tag: 'tagReleaseStatus', key: 'releaseStatus' }, { tag: 'tagOriginalYear', key: 'originalYear' }, { tag: 'tagGenre', key: 'genres' }, { tag: 'tagLanguage', key: 'language' }, { tag: 'tagLanguage', key: 'language' }, { tag: 'tagISRC', key: 'isrc' }, { tag: 'tagMusicBrainzTrackId', key: 'musicBrainzTrackId' }, { tag: 'tagMusicBrainzAlbumId', key: 'musicBrainzAlbumId' }, { tag: 'tagMusicBrainzAlbumArtistId', key: 'musicBrainzAlbumArtistId' }, { tag: 'tagMusicBrainzArtistId', key: 'musicBrainzArtistId' }, { tag: 'trackNumber', key: 'trackNumber' }, { tag: 'trackTotal', key: 'trackTotal' }, { tag: 'discNumber', key: 'discNumber' }, { tag: 'discTotal', key: 'discTotal' } ] const updatePayload = {} // Metadata is only mapped to the music track if it is empty MetadataMapArray.forEach((mapping) => { let value = audioFileMetaTags[mapping.tag] // let tagToUse = mapping.tag if (!value && mapping.altTag) { value = audioFileMetaTags[mapping.altTag] // tagToUse = mapping.altTag } if (value && (typeof value === 'string' || typeof value === 'number')) { value = value.toString().trim() // Trim whitespace if (mapping.key === 'artists' && (!this.artists.length || overrideExistingDetails)) { updatePayload.artists = this.parseArtistsTag(value) } else if (mapping.key === 'genres' && (!this.genres.length || overrideExistingDetails)) { updatePayload.genres = this.parseGenresTag(value) } else if (!this[mapping.key] || overrideExistingDetails) { updatePayload[mapping.key] = value // Logger.debug(`[Book] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${updatePayload[mapping.key]}`) } } }) if (Object.keys(updatePayload).length) { return this.update(updatePayload) } return false } } module.exports = MusicMetadata