diff --git a/server/managers/CoverManager.js b/server/managers/CoverManager.js index 49b2d79b..3f11fd66 100644 --- a/server/managers/CoverManager.js +++ b/server/managers/CoverManager.js @@ -19,7 +19,7 @@ class CoverManager { } getCoverDirectory(libraryItem) { - if (this.db.serverSettings.storeCoverWithItem && !libraryItem.isFile) { + if (this.db.serverSettings.storeCoverWithItem && !libraryItem.isFile && !libraryItem.isMusic) { return libraryItem.path } else { return Path.posix.join(this.ItemMetadataPath, libraryItem.id) diff --git a/server/objects/files/AudioFile.js b/server/objects/files/AudioFile.js index 46d6666a..be16cf92 100644 --- a/server/objects/files/AudioFile.js +++ b/server/objects/files/AudioFile.js @@ -131,7 +131,7 @@ class AudioFile { this.channels = probeData.channels this.channelLayout = probeData.channelLayout this.chapters = probeData.chapters || [] - this.metaTags = probeData.audioFileMetadata + this.metaTags = probeData.audioMetaTags this.embeddedCoverArt = probeData.embeddedCoverArt } @@ -167,9 +167,7 @@ class AudioFile { let hasUpdated = false const newjson = scannedAudioFile.toJSON() - if (this.manuallyVerified) newjson.manuallyVerified = true - if (this.exclude) newjson.exclude = true - newjson.addedAt = this.addedAt + const ignoreKeys = ['manuallyVerified', 'exclude', 'addedAt', 'updatedAt'] for (const key in newjson) { if (key === 'metadata') { @@ -185,7 +183,7 @@ class AudioFile { if (this.syncChapters(newjson.chapters || [])) { hasUpdated = true } - } else if (this[key] !== newjson[key]) { + } else if (!ignoreKeys.includes(key) && this[key] !== newjson[key]) { this[key] = newjson[key] hasUpdated = true } diff --git a/server/objects/mediaTypes/Music.js b/server/objects/mediaTypes/Music.js index f7517dbe..c685f567 100644 --- a/server/objects/mediaTypes/Music.js +++ b/server/objects/mediaTypes/Music.js @@ -131,6 +131,12 @@ class Music { this.audioFile = audioFile } + setMetadataFromAudioFile(overrideExistingDetails = false) { + if (!this.audioFile) return false + if (!this.audioFile.metaTags) return false + return this.metadata.setDataFromAudioMetaTags(this.audioFile.metaTags, overrideExistingDetails) + } + syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) { return false } diff --git a/server/objects/metadata/AudioMetaTags.js b/server/objects/metadata/AudioMetaTags.js index 7f1486b7..caeb2ce6 100644 --- a/server/objects/metadata/AudioMetaTags.js +++ b/server/objects/metadata/AudioMetaTags.js @@ -21,6 +21,15 @@ class AudioMetaTags { this.tagLanguage = null this.tagASIN = null this.tagOverdriveMediaMarker = null + this.tagOriginalYear = null + this.tagReleaseCountry = null + this.tagReleaseType = null + this.tagReleaseStatus = null + this.tagISRC = null + this.tagMusicBrainzTrackId = null + this.tagMusicBrainzAlbumId = null + this.tagMusicBrainzAlbumArtistId = null + this.tagMusicBrainzArtistId = null if (metadata) { this.construct(metadata) @@ -29,7 +38,7 @@ class AudioMetaTags { toJSON() { // Only return the tags that are actually set - var json = {} + const json = {} for (const key in this) { if (key.startsWith('tag') && this[key]) { json[key] = this[key] @@ -38,6 +47,51 @@ class AudioMetaTags { return json } + get trackNumAndTotal() { + const data = { + number: null, + total: null + } + + // Track ID3 tag might be "3/10" or just "3" + if (this.tagTrack) { + const trackParts = this.tagTrack.split('/').map(part => Number(part)) + if (trackParts.length > 0) { + // Fractional track numbers not supported + data.number = !isNaN(trackParts[0]) ? Math.trunc(trackParts[0]) : null + } + if (trackParts.length > 1) { + data.total = !isNaN(trackParts[1]) ? trackParts[1] : null + } + } + + return data + } + + get discNumAndTotal() { + const data = { + number: null, + total: null + } + + if (this.tagDisc) { + const discParts = this.tagDisc.split('/').map(p => Number(p)) + if (discParts.length > 0) { + data.number = !isNaN(discParts[0]) ? Math.trunc(discParts[0]) : null + } + if (discParts.length > 1) { + data.total = !isNaN(discParts[1]) ? discParts[1] : null + } + } + + return data + } + + get discNumber() { return this.discNumAndTotal.number } + get discTotal() { return this.discNumAndTotal.total } + get trackNumber() { return this.trackNumAndTotal.number } + get trackTotal() { return this.trackNumAndTotal.total } + construct(metadata) { this.tagAlbum = metadata.tagAlbum || null this.tagArtist = metadata.tagArtist || null @@ -60,6 +114,15 @@ class AudioMetaTags { this.tagLanguage = metadata.tagLanguage || null this.tagASIN = metadata.tagASIN || null this.tagOverdriveMediaMarker = metadata.tagOverdriveMediaMarker || null + this.tagOriginalYear = metadata.tagOriginalYear || null + this.tagReleaseCountry = metadata.tagReleaseCountry || null + this.tagReleaseType = metadata.tagReleaseType || null + this.tagReleaseStatus = metadata.tagReleaseStatus || null + this.tagISRC = metadata.tagISRC || null + this.tagMusicBrainzTrackId = metadata.tagMusicBrainzTrackId || null + this.tagMusicBrainzAlbumId = metadata.tagMusicBrainzAlbumId || null + this.tagMusicBrainzAlbumArtistId = metadata.tagMusicBrainzAlbumArtistId || null + this.tagMusicBrainzArtistId = metadata.tagMusicBrainzArtistId || null } // Data parsed in prober.js @@ -85,6 +148,15 @@ class AudioMetaTags { this.tagLanguage = payload.file_tag_language || null this.tagASIN = payload.file_tag_asin || null this.tagOverdriveMediaMarker = payload.file_tag_overdrive_media_marker || null + this.tagOriginalYear = payload.file_tag_originalyear || null + this.tagReleaseCountry = payload.file_tag_releasecountry || null + this.tagReleaseType = payload.file_tag_releasetype || null + this.tagReleaseStatus = payload.file_tag_releasestatus || null + this.tagISRC = payload.file_tag_isrc || null + this.tagMusicBrainzTrackId = payload.file_tag_musicbrainz_trackid || null + this.tagMusicBrainzAlbumId = payload.file_tag_musicbrainz_albumid || null + this.tagMusicBrainzAlbumArtistId = payload.file_tag_musicbrainz_albumartistid || null + this.tagMusicBrainzArtistId = payload.file_tag_musicbrainz_artistid || null } setDataFromTone(tags) { @@ -114,9 +186,18 @@ class AudioMetaTags { tagLanguage: payload.file_tag_language || null, tagASIN: payload.file_tag_asin || null, tagOverdriveMediaMarker: payload.file_tag_overdrive_media_marker || null, + tagOriginalYear: payload.file_tag_originalyear || null, + tagReleaseCountry: payload.file_tag_releasecountry || null, + tagReleaseType: payload.file_tag_releasetype || null, + tagReleaseStatus: payload.file_tag_releasestatus || null, + tagISRC: payload.file_tag_isrc || null, + tagMusicBrainzTrackId: payload.file_tag_musicbrainz_trackid || null, + tagMusicBrainzAlbumId: payload.file_tag_musicbrainz_albumid || null, + tagMusicBrainzAlbumArtistId: payload.file_tag_musicbrainz_albumartistid || null, + tagMusicBrainzArtistId: payload.file_tag_musicbrainz_artistid || null } - var hasUpdates = false + let hasUpdates = false for (const key in dataMap) { if (dataMap[key] !== this[key]) { this[key] = dataMap[key] diff --git a/server/objects/metadata/BookMetadata.js b/server/objects/metadata/BookMetadata.js index 7e488c36..dc3f1558 100644 --- a/server/objects/metadata/BookMetadata.js +++ b/server/objects/metadata/BookMetadata.js @@ -306,11 +306,9 @@ class BookMetadata { // tagToUse = mapping.altTag } - if (value && typeof value === 'string') { // Trim whitespace - value = value.trim() - } + if (value && typeof value === 'string') { + value = value.trim() // Trim whitespace - if (value) { if (mapping.key === 'narrators' && (!this.narrators.length || overrideExistingDetails)) { updatePayload.narrators = this.parseNarratorsTag(value) } else if (mapping.key === 'authors' && (!this.authors.length || overrideExistingDetails)) { @@ -335,13 +333,13 @@ class BookMetadata { // Returns array of names in First Last format parseNarratorsTag(narratorsTag) { - var parsed = parseNameString.parse(narratorsTag) + const parsed = parseNameString.parse(narratorsTag) return parsed ? parsed.names : [] } // Return array of authors minified with placeholder id parseAuthorsTag(authorsTag) { - var parsed = parseNameString.parse(authorsTag) + const parsed = parseNameString.parse(authorsTag) if (!parsed) return [] return (parsed.names || []).map((au) => { return { @@ -353,7 +351,7 @@ class BookMetadata { parseGenresTag(genreTag) { if (!genreTag || !genreTag.length) return [] - var separators = ['/', '//', ';'] + 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) diff --git a/server/objects/metadata/MusicMetadata.js b/server/objects/metadata/MusicMetadata.js index c55c6494..b0202c6b 100644 --- a/server/objects/metadata/MusicMetadata.js +++ b/server/objects/metadata/MusicMetadata.js @@ -4,13 +4,31 @@ const { areEquivalent, copyValue, cleanStringForSearch, getTitleIgnorePrefix, ge class MusicMetadata { constructor(metadata) { this.title = null - this.artist = 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) } @@ -18,23 +36,55 @@ class MusicMetadata { construct(metadata) { this.title = metadata.title - this.artist = metadata.artist + 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.language = metadata.language + 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, - artist: this.artist, + 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 + 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 } } @@ -42,12 +92,28 @@ class MusicMetadata { return { title: this.title, titleIgnorePrefix: this.titlePrefixAtEnd, - artist: this.artist, + 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 + 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 } } @@ -68,7 +134,7 @@ class MusicMetadata { } searchQuery(query) { // Returns key if match is found - const keysToCheck = ['title', 'artist', 'album'] + const keysToCheck = ['title', 'album'] for (const key of keysToCheck) { if (this[key] && cleanStringForSearch(String(this[key])).includes(query)) { return { @@ -100,5 +166,154 @@ class MusicMetadata { } 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') { + value = value.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 \ No newline at end of file diff --git a/server/scanner/MediaFileScanner.js b/server/scanner/MediaFileScanner.js index 5a678549..b572e935 100644 --- a/server/scanner/MediaFileScanner.js +++ b/server/scanner/MediaFileScanner.js @@ -93,8 +93,8 @@ class MediaFileScanner { } const audioFile = new AudioFile() - audioFile.trackNumFromMeta = probeData.trackNumber - audioFile.discNumFromMeta = probeData.discNumber + audioFile.trackNumFromMeta = probeData.audioMetaTags.trackNumber + audioFile.discNumFromMeta = probeData.audioMetaTags.discNumber if (mediaType === 'book') { const { trackNumber, discNumber } = this.getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, libraryFile) audioFile.trackNumFromFilename = trackNumber @@ -303,6 +303,18 @@ class MediaFileScanner { hasUpdated = true } else if (libraryItem.media.audioFile && libraryItem.media.audioFile.updateFromScan(mediaScanResult.audioFiles[0])) { hasUpdated = true + console.log('Updated from scan') + } + + if (libraryItem.media.setMetadataFromAudioFile()) { + hasUpdated = true + } + + // If the audio track has no title meta tag then use the audio file name + if (!libraryItem.media.metadata.title && libraryItem.media.audioFile) { + const audioFileName = libraryItem.media.audioFile.metadata.filename + libraryItem.media.metadata.title = Path.basename(audioFileName, Path.extname(audioFileName)) + hasUpdated = true } } } diff --git a/server/scanner/MediaProbeData.js b/server/scanner/MediaProbeData.js index f4ac2b59..43906781 100644 --- a/server/scanner/MediaProbeData.js +++ b/server/scanner/MediaProbeData.js @@ -1,4 +1,4 @@ -const AudioFileMetadata = require('../objects/metadata/AudioMetaTags') +const AudioMetaTags = require('../objects/metadata/AudioMetaTags') class MediaProbeData { constructor(probeData) { @@ -19,7 +19,7 @@ class MediaProbeData { this.sampleRate = null this.chapters = [] - this.audioFileMetadata = null + this.audioMetaTags = null this.trackNumber = null this.trackTotal = null @@ -34,8 +34,8 @@ class MediaProbeData { construct(probeData) { for (const key in probeData) { - if (key === 'audioFileMetadata' && probeData[key]) { - this[key] = new AudioFileMetadata(probeData[key]) + if (key === 'audioMetaTags' && probeData[key]) { + this[key] = new AudioMetaTags(probeData[key]) } else if (this[key] !== undefined) { this[key] = probeData[key] } @@ -48,61 +48,35 @@ class MediaProbeData { } setData(data) { - var audioStream = data.audio_stream this.embeddedCoverArt = data.video_stream ? this.getEmbeddedCoverArt(data.video_stream) : null this.format = data.format this.duration = data.duration this.size = data.size - this.audioStream = audioStream + this.audioStream = data.audio_stream this.videoStream = this.embeddedCoverArt ? null : data.video_stream || null - this.bitRate = audioStream.bit_rate || data.bit_rate - this.codec = audioStream.codec - this.timeBase = audioStream.time_base - this.language = audioStream.language - this.channelLayout = audioStream.channel_layout - this.channels = audioStream.channels - this.sampleRate = audioStream.sample_rate + this.bitRate = this.audioStream.bit_rate || data.bit_rate + this.codec = this.audioStream.codec + this.timeBase = this.audioStream.time_base + this.language = this.audioStream.language + this.channelLayout = this.audioStream.channel_layout + this.channels = this.audioStream.channels + this.sampleRate = this.audioStream.sample_rate this.chapters = data.chapters || [] - if (data.tags) { // New for tone library data (toneProber.js) - this.audioFileMetadata = new AudioFileMetadata() - this.audioFileMetadata.setDataFromTone(data.tags) - } else { // Data from ffprobe (prober.js) - var metatags = {} - for (const key in data) { - if (data[key] && key.startsWith('file_tag')) { - metatags[key] = data[key] - } - } + this.audioMetaTags = new AudioMetaTags() + this.audioMetaTags.setData(data.tags) + } - this.audioFileMetadata = new AudioFileMetadata() - this.audioFileMetadata.setData(metatags) - } + setDataFromTone(data) { + // TODO: Implement - // Track ID3 tag might be "3/10" or just "3" - if (this.audioFileMetadata.tagTrack) { - var trackParts = this.audioFileMetadata.tagTrack.split('/').map(part => Number(part)) - if (trackParts.length > 0) { - // Fractional track numbers not supported - this.trackNumber = !isNaN(trackParts[0]) ? Math.trunc(trackParts[0]) : null - } - if (trackParts.length > 1) { - this.trackTotal = !isNaN(trackParts[1]) ? trackParts[1] : null - } - } - - // Parse disc tag - if (this.audioFileMetadata.tagDisc) { - var discParts = this.audioFileMetadata.tagDisc.split('/').map(p => Number(p)) - if (discParts.length > 0) { - this.discNumber = !isNaN(discParts[0]) ? Math.trunc(discParts[0]) : null - } - if (discParts.length > 1) { - this.discTotal = !isNaN(discParts[1]) ? discParts[1] : null - } - } + this.format = data.format + this.duration = data.duration + this.size = data.size + this.audioMetaTags = new AudioMetaTags() + this.audioMetaTags.setDataFromTone(data.tags) } } module.exports = MediaProbeData \ No newline at end of file diff --git a/server/utils/prober.js b/server/utils/prober.js index 4ac57a4b..552a2304 100644 --- a/server/utils/prober.js +++ b/server/utils/prober.js @@ -73,7 +73,7 @@ function tryGrabChannelLayout(stream) { function tryGrabTags(stream, ...tags) { if (!stream.tags) return null for (let i = 0; i < tags.length; i++) { - var value = stream.tags[tags[i]] || stream.tags[tags[i].toUpperCase()] + const value = stream.tags[tags[i]] || stream.tags[tags[i].toUpperCase()] if (value && value.trim()) return value.trim() } return null @@ -182,6 +182,15 @@ function parseTags(format, verbose) { file_tag_isbn: tryGrabTags(format, 'isbn'), file_tag_language: tryGrabTags(format, 'language', 'lang'), file_tag_asin: tryGrabTags(format, 'asin'), + file_tag_originalyear: tryGrabTags(format, 'originalyear'), + file_tag_releasecountry: tryGrabTags(format, 'MusicBrainz Album Release Country', 'releasecountry'), + file_tag_releasestatus: tryGrabTags(format, 'MusicBrainz Album Status', 'releasestatus', 'musicbrainz_albumstatus'), + file_tag_releasetype: tryGrabTags(format, 'MusicBrainz Album Type', 'releasetype', 'musicbrainz_albumtype'), + file_tag_isrc: tryGrabTags(format, 'tsrc', 'isrc'), + file_tag_musicbrainz_trackid: tryGrabTags(format, 'MusicBrainz Release Track Id', 'musicbrainz_releasetrackid'), + file_tag_musicbrainz_albumid: tryGrabTags(format, 'MusicBrainz Album Id', 'musicbrainz_albumid'), + file_tag_musicbrainz_albumartistid: tryGrabTags(format, 'MusicBrainz Album Artist Id', 'musicbrainz_albumartistid'), + file_tag_musicbrainz_artistid: tryGrabTags(format, 'MusicBrainz Artist Id', 'musicbrainz_artistid'), // Not sure if these are actually used yet or not file_tag_creation_time: tryGrabTags(format, 'creation_time'), @@ -213,20 +222,18 @@ function getDefaultAudioStream(audioStreams) { function parseProbeData(data, verbose = false) { try { - var { format, streams, chapters } = data + const { format, streams, chapters } = data - var sizeBytes = !isNaN(format.size) ? Number(format.size) : null - var sizeMb = sizeBytes !== null ? Number((sizeBytes / (1024 * 1024)).toFixed(2)) : null + const sizeBytes = !isNaN(format.size) ? Number(format.size) : null + const sizeMb = sizeBytes !== null ? Number((sizeBytes / (1024 * 1024)).toFixed(2)) : null - // Logger.debug('Parsing Data for', Path.basename(format.filename)) - var tags = parseTags(format, verbose) - var cleanedData = { + let cleanedData = { format: format.format_long_name || format.name || 'Unknown', duration: !isNaN(format.duration) ? Number(format.duration) : null, size: sizeBytes, sizeMb, bit_rate: !isNaN(format.bit_rate) ? Number(format.bit_rate) : null, - ...tags + tags: parseTags(format, verbose) } if (verbose && format.tags) { cleanedData.rawTags = format.tags @@ -234,11 +241,11 @@ function parseProbeData(data, verbose = false) { const cleaned_streams = streams.map(s => parseMediaStreamInfo(s, streams, cleanedData.bit_rate)) cleanedData.video_stream = cleaned_streams.find(s => s.type === 'video') - var audioStreams = cleaned_streams.filter(s => s.type === 'audio') + const audioStreams = cleaned_streams.filter(s => s.type === 'audio') cleanedData.audio_stream = getDefaultAudioStream(audioStreams) if (cleanedData.audio_stream && cleanedData.video_stream) { - var videoBitrate = cleanedData.video_stream.bit_rate + const videoBitrate = cleanedData.video_stream.bit_rate // If audio stream bitrate larger then video, most likely incorrect if (cleanedData.audio_stream.bit_rate > videoBitrate) { cleanedData.video_stream.bit_rate = cleanedData.bit_rate @@ -247,10 +254,9 @@ function parseProbeData(data, verbose = false) { // If format does not have tags, check audio stream (https://github.com/advplyr/audiobookshelf/issues/256) if (!format.tags && cleanedData.audio_stream && cleanedData.audio_stream.tags) { - var tags = parseTags(cleanedData.audio_stream) cleanedData = { ...cleanedData, - ...tags + tags: parseTags(cleanedData.audio_stream, verbose) } } @@ -277,13 +283,13 @@ function probe(filepath, verbose = false) { } } - var rawProbeData = parseProbeData(raw, verbose) + const rawProbeData = parseProbeData(raw, verbose) if (!rawProbeData || (!rawProbeData.audio_stream && !rawProbeData.video_stream)) { return { error: rawProbeData ? 'Invalid media file: no audio or video streams found' : 'Probe Failed' } } else { - var probeData = new MediaProbeData() + const probeData = new MediaProbeData() probeData.setData(rawProbeData) return probeData } diff --git a/server/utils/scandir.js b/server/utils/scandir.js index 16dbbc54..fbbc1a8a 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -95,11 +95,7 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) { if (mediaType === 'music') { const audioFileGroup = {} fileItems.filter(i => isMediaFile(mediaType, i.extension)).forEach((item) => { - if (!item.reldirpath) { - audioFileGroup[item.name] = item.name - } else { - audioFileGroup[item.reldirpath] = [item.name] - } + audioFileGroup[item.path] = item.path }) return audioFileGroup } @@ -201,7 +197,14 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) { let isFile = false // item is not in a folder let libraryItemData = null let fileObjs = [] - if (libraryItemPath === libraryItemGrouping[libraryItemPath]) { + if (libraryMediaType === 'music') { + libraryItemData = { + path: Path.posix.join(folderPath, libraryItemPath), + relPath: libraryItemPath + } + fileObjs = await cleanFileObjects(folderPath, [libraryItemPath]) + isFile = true + } else if (libraryItemPath === libraryItemGrouping[libraryItemPath]) { // Media file in root only get title libraryItemData = { mediaMetadata: { @@ -344,23 +347,8 @@ function getPodcastDataFromDir(folderPath, relPath) { } } -function getMusicDataFromDir(folderPath, relPath, fileNames) { - relPath = relPath.replace(/\\/g, '/') - - const firstFileName = fileNames.length ? fileNames[0] : '' - return { - mediaMetadata: { - title: Path.basename(firstFileName, Path.extname(firstFileName)) - }, - relPath: relPath, // relative music audio file path i.e. /Some Folder/.. - path: Path.posix.join(folderPath, relPath) // i.e. /music/Some Folder/.. - } -} - function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettings, fileNames) { - if (libraryMediaType === 'music') { - return getMusicDataFromDir(folderPath, relPath, fileNames) - } else if (libraryMediaType === 'podcast') { + if (libraryMediaType === 'podcast') { return getPodcastDataFromDir(folderPath, relPath) } else if (libraryMediaType === 'book') { var parseSubtitle = !!serverSettings.scannerParseSubtitle diff --git a/server/utils/toneProber.js b/server/utils/toneProber.js index c1d8de9d..d415941b 100644 --- a/server/utils/toneProber.js +++ b/server/utils/toneProber.js @@ -147,7 +147,7 @@ module.exports.probe = (filepath, verbose = false) => { } const rawProbeData = parseProbeDump(dumpPayload) const probeData = new MediaProbeData() - probeData.setData(rawProbeData) + probeData.setDataFromTone(rawProbeData) return probeData }).catch((error) => { Logger.error(`[toneProber] Failed to probe file at path "${filepath}"`, error)