mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-12 20:31:36 +00:00
306 lines
No EOL
9.7 KiB
JavaScript
306 lines
No EOL
9.7 KiB
JavaScript
const { AudioMimeType } = require('../../utils/constants')
|
|
const AudioMetaTags = require('../metadata/AudioMetaTags')
|
|
const FileMetadata = require('../metadata/FileMetadata')
|
|
|
|
/**
|
|
* @openapi
|
|
* components:
|
|
* schemas:
|
|
* audioFile:
|
|
* type: object
|
|
* properties:
|
|
* index:
|
|
* description: The index of the audio file.
|
|
* type: integer
|
|
* example: 1
|
|
* ino:
|
|
* description: The inode of the audio file.
|
|
* type: string
|
|
* example: '649644248522215260'
|
|
* metadata:
|
|
* $ref: '#/components/schemas/fileMetadata'
|
|
* addedAt:
|
|
* description: The time (in ms since POSIX epoch) when the audio file was added to the library.
|
|
* type: integer
|
|
* example: 1650621074131
|
|
* updatedAt:
|
|
* description: The time (in ms since POSIX epoch) when the audio file last updated. (Read Only)
|
|
* type: integer
|
|
* example: 1651830828023
|
|
* trackNumFromMeta:
|
|
* description: The track number of the audio file as pulled from the file's metadata. Will be null if unknown.
|
|
* type: [integer, 'null']
|
|
* example: 1
|
|
* discNumFromMeta:
|
|
* description: The disc number of the audio file as pulled from the file's metadata. Will be null if unknown.
|
|
* type: [string, 'null']
|
|
* trackNumFromFilename:
|
|
* description: The track number of the audio file as determined from the file's name. Will be null if unknown.
|
|
* type: [integer, 'null']
|
|
* example: 1
|
|
* discNumFromFilename:
|
|
* description: The track number of the audio file as determined from the file's name. Will be null if unknown.
|
|
* type: [string, 'null']
|
|
* manuallyVerified:
|
|
* description: Whether the audio file has been manually verified by a user.
|
|
* type: boolean
|
|
* example: false
|
|
* invalid:
|
|
* description: Whether the audio file is missing from the server.
|
|
* type: boolean
|
|
* example: false
|
|
* exclude:
|
|
* description: Whether the audio file has been marked for exclusion.
|
|
* type: boolean
|
|
* example: false
|
|
* error:
|
|
* description: Any error with the audio file. Will be null if there is none.
|
|
* type: [string, 'null']
|
|
* format:
|
|
* description: The format of the audio file.
|
|
* type: string
|
|
* example: MP2/3 (MPEG audio layer 2/3)
|
|
* duration:
|
|
* description: The total length (in seconds) of the audio file.
|
|
* type: number
|
|
* example: 6004.6675
|
|
* bitRate:
|
|
* description: The bit rate (in bit/s) of the audio file.
|
|
* type: integer
|
|
* example: 64000
|
|
* language:
|
|
* description: The language of the audio file.
|
|
* type: [string, 'null']
|
|
* codec:
|
|
* description: The codec of the audio file.
|
|
* type: string
|
|
* example: mp3
|
|
* timeBase:
|
|
* description: The time base of the audio file.
|
|
* type: string
|
|
* example: 1/14112000
|
|
* channels:
|
|
* description: The number of channels the audio file has.
|
|
* type: integer
|
|
* example: 2
|
|
* channelLayout:
|
|
* description: The layout of the audio file's channels.
|
|
* type: string
|
|
* example: stereo
|
|
* chapters:
|
|
* description: If the audio file is part of an audiobook, the chapters the file contains.
|
|
* type: array
|
|
* items:
|
|
* $ref: '#/components/schemas/bookChapter'
|
|
* embeddedCoverArt:
|
|
* description: The type of embedded cover art in the audio file. Will be null if none exists.
|
|
* type: [string, 'null']
|
|
* metaTags:
|
|
* $ref: '#/components/schemas/audioMetaTags'
|
|
* mimeType:
|
|
* description: The MIME type of the audio file.
|
|
* type: string
|
|
* example: audio/mpeg
|
|
*/
|
|
class AudioFile {
|
|
constructor(data) {
|
|
this.index = null
|
|
this.ino = null
|
|
/** @type {FileMetadata} */
|
|
this.metadata = null
|
|
this.addedAt = null
|
|
this.updatedAt = null
|
|
|
|
this.trackNumFromMeta = null
|
|
this.discNumFromMeta = null
|
|
this.trackNumFromFilename = null
|
|
this.discNumFromFilename = null
|
|
|
|
this.format = null
|
|
this.duration = 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
|
|
/** @type {AudioMetaTags} */
|
|
this.metaTags = null
|
|
|
|
this.manuallyVerified = false
|
|
this.invalid = false
|
|
this.exclude = false
|
|
this.error = null
|
|
|
|
if (data) {
|
|
this.construct(data)
|
|
}
|
|
}
|
|
|
|
toJSON() {
|
|
return {
|
|
index: this.index,
|
|
ino: this.ino,
|
|
metadata: this.metadata.toJSON(),
|
|
addedAt: this.addedAt,
|
|
updatedAt: this.updatedAt,
|
|
trackNumFromMeta: this.trackNumFromMeta,
|
|
discNumFromMeta: this.discNumFromMeta,
|
|
trackNumFromFilename: this.trackNumFromFilename,
|
|
discNumFromFilename: this.discNumFromFilename,
|
|
manuallyVerified: !!this.manuallyVerified,
|
|
invalid: !!this.invalid,
|
|
exclude: !!this.exclude,
|
|
error: this.error || null,
|
|
format: this.format,
|
|
duration: this.duration,
|
|
bitRate: this.bitRate,
|
|
language: this.language,
|
|
codec: this.codec,
|
|
timeBase: this.timeBase,
|
|
channels: this.channels,
|
|
channelLayout: this.channelLayout,
|
|
chapters: this.chapters,
|
|
embeddedCoverArt: this.embeddedCoverArt,
|
|
metaTags: this.metaTags?.toJSON() || {},
|
|
mimeType: this.mimeType
|
|
}
|
|
}
|
|
|
|
construct(data) {
|
|
this.index = data.index
|
|
this.ino = data.ino
|
|
this.metadata = new FileMetadata(data.metadata || {})
|
|
this.addedAt = data.addedAt
|
|
this.updatedAt = data.updatedAt
|
|
this.manuallyVerified = !!data.manuallyVerified
|
|
this.invalid = !!data.invalid
|
|
this.exclude = !!data.exclude
|
|
this.error = data.error || null
|
|
|
|
this.trackNumFromMeta = data.trackNumFromMeta
|
|
this.discNumFromMeta = data.discNumFromMeta
|
|
this.trackNumFromFilename = data.trackNumFromFilename
|
|
|
|
if (data.cdNumFromFilename !== undefined) this.discNumFromFilename = data.cdNumFromFilename // TEMP:Support old var name
|
|
else this.discNumFromFilename = data.discNumFromFilename
|
|
|
|
this.format = data.format
|
|
this.duration = data.duration
|
|
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
|
|
|
|
this.metaTags = new AudioMetaTags(data.metaTags || {})
|
|
}
|
|
|
|
get mimeType() {
|
|
const format = this.metadata.format.toUpperCase()
|
|
if (AudioMimeType[format]) {
|
|
return AudioMimeType[format]
|
|
} else {
|
|
return AudioMimeType.MP3
|
|
}
|
|
}
|
|
|
|
get isValidTrack() {
|
|
return !this.invalid && !this.exclude
|
|
}
|
|
|
|
// New scanner creates AudioFile from AudioFileScanner
|
|
setDataFromProbe(libraryFile, probeData) {
|
|
this.ino = libraryFile.ino || null
|
|
|
|
if (libraryFile.metadata instanceof FileMetadata) {
|
|
this.metadata = libraryFile.metadata.clone()
|
|
} else {
|
|
this.metadata = new FileMetadata(libraryFile.metadata)
|
|
}
|
|
|
|
this.addedAt = Date.now()
|
|
this.updatedAt = Date.now()
|
|
|
|
this.format = probeData.format
|
|
this.duration = probeData.duration
|
|
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.metaTags = probeData.audioMetaTags
|
|
this.embeddedCoverArt = probeData.embeddedCoverArt
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
let 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
|
|
}
|
|
|
|
clone() {
|
|
return new AudioFile(this.toJSON())
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {AudioFile} scannedAudioFile
|
|
* @returns {boolean} true if updates were made
|
|
*/
|
|
updateFromScan(scannedAudioFile) {
|
|
let hasUpdated = false
|
|
|
|
const newjson = scannedAudioFile.toJSON()
|
|
const ignoreKeys = ['manuallyVerified', 'ctimeMs', 'addedAt', 'updatedAt']
|
|
|
|
for (const key in newjson) {
|
|
if (key === 'metadata') {
|
|
if (this.metadata.update(newjson[key])) {
|
|
hasUpdated = true
|
|
}
|
|
} else if (key === 'metaTags') {
|
|
if (!this.metaTags || !this.metaTags.isEqual(scannedAudioFile.metaTags)) {
|
|
this.metaTags = scannedAudioFile.metaTags.clone()
|
|
hasUpdated = true
|
|
}
|
|
} else if (key === 'chapters') {
|
|
if (this.syncChapters(newjson.chapters || [])) {
|
|
hasUpdated = true
|
|
}
|
|
} else if (!ignoreKeys.includes(key) && this[key] !== newjson[key]) {
|
|
this[key] = newjson[key]
|
|
hasUpdated = true
|
|
}
|
|
}
|
|
return hasUpdated
|
|
}
|
|
}
|
|
module.exports = AudioFile |