audiobookshelf/server/objects/LibraryItem.js

347 lines
10 KiB
JavaScript
Raw Normal View History

2024-09-04 00:04:58 +02:00
const uuidv4 = require('uuid').v4
2022-07-06 02:53:01 +02:00
const fs = require('../libs/fsExtra')
const Path = require('path')
2022-03-09 02:31:44 +01:00
const Logger = require('../Logger')
const LibraryFile = require('./files/LibraryFile')
const Book = require('./mediaTypes/Book')
const Podcast = require('./mediaTypes/Podcast')
2023-10-20 00:20:12 +02:00
const { areEquivalent, copyValue } = require('../utils/index')
const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils')
2022-03-09 02:31:44 +01:00
class LibraryItem {
constructor(libraryItem = null) {
this.id = null
this.ino = null // Inode
this.oldLibraryItemId = null
2022-03-09 02:31:44 +01:00
this.libraryId = null
this.folderId = null
this.path = null
this.relPath = null
this.isFile = false
2022-03-09 02:31:44 +01:00
this.mtimeMs = null
this.ctimeMs = null
this.birthtimeMs = null
this.addedAt = null
this.updatedAt = null
2022-03-09 02:31:44 +01:00
this.lastScan = null
this.scanVersion = null
// Was scanned and no longer exists
2022-03-09 02:31:44 +01:00
this.isMissing = false
// Was scanned and no longer has media files
this.isInvalid = false
2022-03-09 02:31:44 +01:00
this.mediaType = null
this.media = null
2022-03-09 02:31:44 +01:00
/** @type {LibraryFile[]} */
2022-03-09 02:31:44 +01:00
this.libraryFiles = []
if (libraryItem) {
this.construct(libraryItem)
}
// Temporary attributes
this.isSavingMetadata = false
2022-03-09 02:31:44 +01:00
}
construct(libraryItem) {
this.id = libraryItem.id
this.ino = libraryItem.ino || null
this.oldLibraryItemId = libraryItem.oldLibraryItemId
2022-03-09 02:31:44 +01:00
this.libraryId = libraryItem.libraryId
this.folderId = libraryItem.folderId
this.path = libraryItem.path
this.relPath = libraryItem.relPath
this.isFile = !!libraryItem.isFile
2022-03-09 02:31:44 +01:00
this.mtimeMs = libraryItem.mtimeMs || 0
this.ctimeMs = libraryItem.ctimeMs || 0
this.birthtimeMs = libraryItem.birthtimeMs || 0
this.addedAt = libraryItem.addedAt
this.updatedAt = libraryItem.updatedAt || this.addedAt
2022-03-09 02:31:44 +01:00
this.lastScan = libraryItem.lastScan || null
this.scanVersion = libraryItem.scanVersion || null
this.isMissing = !!libraryItem.isMissing
this.isInvalid = !!libraryItem.isInvalid
2022-03-09 02:31:44 +01:00
this.mediaType = libraryItem.mediaType
if (this.mediaType === 'book') {
this.media = new Book(libraryItem.media)
} else if (this.mediaType === 'podcast') {
this.media = new Podcast(libraryItem.media)
2022-03-09 02:31:44 +01:00
}
2022-05-31 02:26:53 +02:00
this.media.libraryItemId = this.id
2022-03-09 02:31:44 +01:00
2024-09-04 00:04:58 +02:00
this.libraryFiles = libraryItem.libraryFiles.map((f) => new LibraryFile(f))
// Migration for v2.2.23 to set ebook library files as supplementary
if (this.isBook && this.media.ebookFile) {
for (const libraryFile of this.libraryFiles) {
if (libraryFile.isEBookFile && libraryFile.isSupplementary === null) {
libraryFile.isSupplementary = this.media.ebookFile.ino !== libraryFile.ino
}
}
}
2022-03-09 02:31:44 +01:00
}
toJSON() {
return {
id: this.id,
ino: this.ino,
oldLibraryItemId: this.oldLibraryItemId,
2022-03-09 02:31:44 +01:00
libraryId: this.libraryId,
folderId: this.folderId,
path: this.path,
relPath: this.relPath,
isFile: this.isFile,
2022-03-09 02:31:44 +01:00
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
updatedAt: this.updatedAt,
2022-03-09 02:31:44 +01:00
lastScan: this.lastScan,
scanVersion: this.scanVersion,
isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid,
mediaType: this.mediaType,
media: this.media.toJSON(),
2024-09-04 00:04:58 +02:00
libraryFiles: this.libraryFiles.map((f) => f.toJSON())
2022-03-09 02:31:44 +01:00
}
}
toJSONMinified() {
return {
id: this.id,
ino: this.ino,
oldLibraryItemId: this.oldLibraryItemId,
libraryId: this.libraryId,
folderId: this.folderId,
path: this.path,
relPath: this.relPath,
isFile: this.isFile,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
updatedAt: this.updatedAt,
isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid,
mediaType: this.mediaType,
media: this.media.toJSONMinified(),
numFiles: this.libraryFiles.length,
size: this.size
}
}
// Adds additional helpful fields like media duration, tracks, etc.
toJSONExpanded() {
return {
id: this.id,
ino: this.ino,
oldLibraryItemId: this.oldLibraryItemId,
libraryId: this.libraryId,
folderId: this.folderId,
path: this.path,
relPath: this.relPath,
isFile: this.isFile,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
updatedAt: this.updatedAt,
lastScan: this.lastScan,
scanVersion: this.scanVersion,
isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid,
mediaType: this.mediaType,
media: this.media.toJSONExpanded(),
2024-09-04 00:04:58 +02:00
libraryFiles: this.libraryFiles.map((f) => f.toJSON()),
size: this.size
}
}
2024-09-04 00:04:58 +02:00
get isPodcast() {
return this.mediaType === 'podcast'
}
get isBook() {
return this.mediaType === 'book'
}
get size() {
let total = 0
2024-09-04 00:04:58 +02:00
this.libraryFiles.forEach((lf) => (total += lf.metadata.size))
return total
}
get hasAudioFiles() {
2024-09-04 00:04:58 +02:00
return this.libraryFiles.some((lf) => lf.fileType === 'audio')
}
get hasMediaEntities() {
return this.media.hasMediaEntities
}
// Data comes from scandir library item data
2023-10-20 00:20:12 +02:00
// TODO: Remove this function. Only used when creating a new podcast now
setData(libraryMediaType, payload) {
2023-07-05 01:14:44 +02:00
this.id = uuidv4()
2022-05-31 02:26:53 +02:00
this.mediaType = libraryMediaType
2023-10-20 00:20:12 +02:00
if (libraryMediaType === 'podcast') {
this.media = new Podcast()
2023-10-20 00:20:12 +02:00
} else {
Logger.error(`[LibraryItem] setData called with unsupported media type "${libraryMediaType}"`)
return
}
2023-07-05 01:14:44 +02:00
this.media.id = uuidv4()
2022-05-31 02:26:53 +02:00
this.media.libraryItemId = this.id
for (const key in payload) {
if (key === 'libraryFiles') {
2024-09-04 00:04:58 +02:00
this.libraryFiles = payload.libraryFiles.map((lf) => lf.clone())
2023-02-24 00:55:11 +01:00
// Set cover image
2024-09-04 00:04:58 +02:00
const imageFiles = this.libraryFiles.filter((lf) => lf.fileType === 'image')
const coverMatch = imageFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
2023-02-24 00:55:11 +01:00
if (coverMatch) {
this.media.coverPath = coverMatch.metadata.path
} else if (imageFiles.length) {
this.media.coverPath = imageFiles[0].metadata.path
}
} else if (this[key] !== undefined && key !== 'media') {
this[key] = payload[key]
}
}
if (payload.media) {
this.media.setData(payload.media)
}
this.addedAt = Date.now()
this.updatedAt = Date.now()
}
update(payload) {
2022-12-26 23:08:53 +01:00
const json = this.toJSON()
let hasUpdates = false
for (const key in json) {
if (payload[key] !== undefined) {
if (key === 'media') {
if (this.media.update(payload[key])) {
hasUpdates = true
}
} else if (!areEquivalent(payload[key], json[key])) {
this[key] = copyValue(payload[key])
hasUpdates = true
}
}
}
if (hasUpdates) {
this.updatedAt = Date.now()
}
return hasUpdates
}
updateMediaCover(coverPath) {
this.media.updateCover(coverPath)
this.updatedAt = Date.now()
return true
}
setMissing() {
this.isMissing = true
this.updatedAt = Date.now()
}
getDirectPlayTracklist(episodeId) {
return this.media.getDirectPlayTracklist(episodeId)
}
/**
* Save metadata.json file
2023-10-20 00:20:12 +02:00
* TODO: Move to new LibraryItem model
* @returns {Promise<LibraryFile>} null if not saved
*/
async saveMetadata() {
if (this.isSavingMetadata || !global.MetadataPath) return null
2022-05-31 02:26:53 +02:00
this.isSavingMetadata = true
let metadataPath = Path.join(global.MetadataPath, 'items', this.id)
let storeMetadataWithItem = global.ServerSettings.storeMetadataWithItem
if (storeMetadataWithItem && !this.isFile) {
metadataPath = this.path
} else {
// Make sure metadata book dir exists
storeMetadataWithItem = false
await fs.ensureDir(metadataPath)
}
const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`)
2024-09-04 00:04:58 +02:00
return fs
.writeFile(metadataFilePath, JSON.stringify(this.media.toJSONForMetadataFile(), null, 2))
.then(async () => {
// Add metadata.json to libraryFiles array if it is new
let metadataLibraryFile = this.libraryFiles.find((lf) => lf.metadata.path === filePathToPOSIX(metadataFilePath))
if (storeMetadataWithItem) {
if (!metadataLibraryFile) {
metadataLibraryFile = new LibraryFile()
await metadataLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
this.libraryFiles.push(metadataLibraryFile)
} else {
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
if (fileTimestamps) {
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
metadataLibraryFile.metadata.size = fileTimestamps.size
metadataLibraryFile.ino = fileTimestamps.ino
}
}
const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path)
if (libraryItemDirTimestamps) {
this.mtimeMs = libraryItemDirTimestamps.mtimeMs
this.ctimeMs = libraryItemDirTimestamps.ctimeMs
}
}
2024-09-04 00:04:58 +02:00
Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`)
return metadataLibraryFile
})
.catch((error) => {
Logger.error(`[LibraryItem] Failed to save json file at "${metadataFilePath}"`, error)
return null
})
.finally(() => {
this.isSavingMetadata = false
})
}
removeLibraryFile(ino) {
if (!ino) return false
2024-09-04 00:04:58 +02:00
const libraryFile = this.libraryFiles.find((lf) => lf.ino === ino)
if (libraryFile) {
2024-09-04 00:04:58 +02:00
this.libraryFiles = this.libraryFiles.filter((lf) => lf.ino !== ino)
this.updatedAt = Date.now()
return true
}
return false
}
/**
* Set the EBookFile from a LibraryFile
* If null then ebookFile will be removed from the book
* all ebook library files that are not primary are marked as supplementary
2024-09-04 00:04:58 +02:00
*
* @param {LibraryFile} [libraryFile]
*/
setPrimaryEbook(ebookLibraryFile = null) {
2024-09-04 00:04:58 +02:00
const ebookLibraryFiles = this.libraryFiles.filter((lf) => lf.isEBookFile)
for (const libraryFile of ebookLibraryFiles) {
libraryFile.isSupplementary = ebookLibraryFile?.ino !== libraryFile.ino
}
this.media.setEbookFile(ebookLibraryFile)
}
2022-03-09 02:31:44 +01:00
}
2024-09-04 00:04:58 +02:00
module.exports = LibraryItem