const { isNullOrNaN } = require('../utils/index') const Logger = require('../Logger') const AudioFileMetadata = require('./AudioFileMetadata') class AudioFile { constructor(data) { this.index = null this.ino = null this.filename = null this.ext = null this.path = null this.fullPath = null this.addedAt = null this.trackNumFromMeta = null this.trackNumFromFilename = null this.cdNumFromFilename = null this.format = null this.duration = null this.size = null this.bitRate = null this.language = null this.codec = null this.timeBase = null this.channels = null this.channelLayout = null this.chapters = [] this.embeddedCoverArt = null // Tags scraped from the audio file this.metadata = null this.manuallyVerified = false this.invalid = false this.exclude = false this.error = null if (data) { this.construct(data) } } // Sort number takes cd num into account // get sortNumber() { // if (this.manuallyVerified) return this.index // var num = this.index // if (this.cdNumFromFilename && !isNaN(this.cdNumFromFilename)) { // num += (Number(this.cdNumFromFilename) * 1000) // } // return num // } toJSON() { return { index: this.index, ino: this.ino, filename: this.filename, ext: this.ext, path: this.path, fullPath: this.fullPath, addedAt: this.addedAt, trackNumFromMeta: this.trackNumFromMeta, trackNumFromFilename: this.trackNumFromFilename, cdNumFromFilename: this.cdNumFromFilename, manuallyVerified: !!this.manuallyVerified, invalid: !!this.invalid, exclude: !!this.exclude, error: this.error || null, format: this.format, duration: this.duration, size: this.size, bitRate: this.bitRate, language: this.language, codec: this.codec, timeBase: this.timeBase, channels: this.channels, channelLayout: this.channelLayout, chapters: this.chapters, embeddedCoverArt: this.embeddedCoverArt, metadata: this.metadata ? this.metadata.toJSON() : {} } } construct(data) { this.index = data.index this.ino = data.ino this.filename = data.filename this.ext = data.ext this.path = data.path this.fullPath = data.fullPath this.addedAt = data.addedAt this.manuallyVerified = !!data.manuallyVerified this.invalid = !!data.invalid this.exclude = !!data.exclude this.error = data.error || null this.trackNumFromMeta = data.trackNumFromMeta || null this.trackNumFromFilename = data.trackNumFromFilename || null this.cdNumFromFilename = data.cdNumFromFilename || null this.format = data.format this.duration = data.duration this.size = data.size this.bitRate = data.bitRate this.language = data.language this.codec = data.codec || null this.timeBase = data.timeBase this.channels = data.channels this.channelLayout = data.channelLayout this.chapters = data.chapters this.embeddedCoverArt = data.embeddedCoverArt || null // Old version of AudioFile used `tagAlbum` etc. var isOldVersion = Object.keys(data).find(key => key.startsWith('tag')) if (isOldVersion) { this.metadata = new AudioFileMetadata(data) } else { this.metadata = new AudioFileMetadata(data.metadata || {}) } } setData(data) { this.index = data.index || null this.ino = data.ino || null this.filename = data.filename this.ext = data.ext this.path = data.path this.fullPath = data.fullPath this.addedAt = Date.now() this.trackNumFromMeta = data.trackNumFromMeta || null this.trackNumFromFilename = data.trackNumFromFilename || null this.cdNumFromFilename = data.cdNumFromFilename || null this.manuallyVerified = !!data.manuallyVerified this.invalid = !!data.invalid this.exclude = !!data.exclude this.error = data.error || null this.format = data.format this.duration = data.duration this.size = data.size this.bitRate = data.bit_rate || null this.language = data.language this.codec = data.codec || null this.timeBase = data.time_base this.channels = data.channels this.channelLayout = data.channel_layout this.chapters = data.chapters || [] this.embeddedCoverArt = data.embedded_cover_art || null this.metadata = new AudioFileMetadata() this.metadata.setData(data) } // New scanner creates AudioFile from AudioFileScanner setDataFromProbe(fileData, probeData) { this.index = fileData.index || null this.ino = fileData.ino || null this.filename = fileData.filename this.ext = fileData.ext this.path = fileData.path this.fullPath = fileData.fullPath this.addedAt = Date.now() this.trackNumFromMeta = fileData.trackNumFromMeta || null this.trackNumFromFilename = fileData.trackNumFromFilename || null this.cdNumFromFilename = fileData.cdNumFromFilename || null this.format = probeData.format this.duration = probeData.duration this.size = probeData.size this.bitRate = probeData.bitRate || null this.language = probeData.language this.codec = probeData.codec || null this.timeBase = probeData.timeBase this.channels = probeData.channels this.channelLayout = probeData.channelLayout this.chapters = probeData.chapters || [] this.metadata = probeData.audioFileMetadata } validateTrackIndex(isSingleTrack) { var numFromMeta = isNullOrNaN(this.trackNumFromMeta) ? null : Number(this.trackNumFromMeta) var numFromFilename = isNullOrNaN(this.trackNumFromFilename) ? null : Number(this.trackNumFromFilename) if (isSingleTrack) { // Single audio track audiobook only use metadata tag and default to 1 return numFromMeta ? numFromMeta : 1 } if (numFromMeta !== null) return numFromMeta if (numFromFilename !== null) return numFromFilename this.invalid = true this.error = 'Failed to get track number' return null } setDuplicateTrackNumber(num) { this.invalid = true this.error = 'Duplicate track number "' + num + '"' } syncChapters(updatedChapters) { if (this.chapters.length !== updatedChapters.length) { this.chapters = updatedChapters.map(ch => ({ ...ch })) return true } else if (updatedChapters.length === 0) { if (this.chapters.length > 0) { this.chapters = [] return true } return false } var hasUpdates = false for (let i = 0; i < updatedChapters.length; i++) { if (JSON.stringify(updatedChapters[i]) !== JSON.stringify(this.chapters[i])) { hasUpdates = true } } if (hasUpdates) { this.chapters = updatedChapters.map(ch => ({ ...ch })) } return hasUpdates } // Called from audioFileScanner.js with scanData updateMetadata(data) { if (!this.metadata) this.metadata = new AudioFileMetadata() var dataMap = { format: data.format, duration: data.duration, size: data.size, bitRate: data.bit_rate || null, language: data.language, codec: data.codec || null, timeBase: data.time_base, channels: data.channels, channelLayout: data.channel_layout, chapters: data.chapters || [], embeddedCoverArt: data.embedded_cover_art || null, trackNumFromMeta: data.trackNumFromMeta, trackNumFromFilename: data.trackNumFromFilename, cdNumFromFilename: data.cdNumFromFilename } var hasUpdates = false for (const key in dataMap) { if (key === 'chapters') { var chaptersUpdated = this.syncChapters(dataMap.chapters) if (chaptersUpdated) { hasUpdates = true } } else if (dataMap[key] !== this[key]) { // Logger.debug(`[AudioFile] "${key}" from ${this[key]} => ${dataMap[key]}`) this[key] = dataMap[key] hasUpdates = true } } if (this.metadata.updateData(data)) { hasUpdates = true } return hasUpdates } clone() { return new AudioFile(this.toJSON()) } // If the file or parent directory was renamed it is synced here syncFile(newFile) { var hasUpdates = false var keysToSync = ['path', 'fullPath', 'ext', 'filename'] keysToSync.forEach((key) => { if (newFile[key] !== undefined && newFile[key] !== this[key]) { hasUpdates = true this[key] = newFile[key] } }) return hasUpdates } } module.exports = AudioFile