mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-04-25 01:16:40 +02:00
Updates to LibraryItemController to use new model
This commit is contained in:
parent
dd0ebdf2d8
commit
4787e7fdb5
@ -32,7 +32,7 @@ const ShareManager = require('../managers/ShareManager')
|
|||||||
* @typedef {RequestWithUser & RequestEntityObject} LibraryItemControllerRequest
|
* @typedef {RequestWithUser & RequestEntityObject} LibraryItemControllerRequest
|
||||||
*
|
*
|
||||||
* @typedef RequestLibraryFileObject
|
* @typedef RequestLibraryFileObject
|
||||||
* @property {import('../models/LibraryItem').LibraryFileObject} libraryFile
|
* @property {import('../objects/files/LibraryFile')} libraryFile
|
||||||
*
|
*
|
||||||
* @typedef {RequestWithUser & RequestEntityObject & RequestLibraryFileObject} LibraryItemControllerRequestWithFile
|
* @typedef {RequestWithUser & RequestEntityObject & RequestLibraryFileObject} LibraryItemControllerRequestWithFile
|
||||||
*/
|
*/
|
||||||
@ -83,6 +83,10 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* PATCH: /api/items/:id
|
||||||
|
*
|
||||||
|
* @deprecated
|
||||||
|
* Use the updateMedia /api/items/:id/media endpoint instead or updateCover /api/items/:id/cover
|
||||||
*
|
*
|
||||||
* @param {LibraryItemControllerRequest} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
@ -288,10 +292,10 @@ class LibraryItemController {
|
|||||||
let result = null
|
let result = null
|
||||||
if (req.body?.url) {
|
if (req.body?.url) {
|
||||||
Logger.debug(`[LibraryItemController] Requesting download cover from url "${req.body.url}"`)
|
Logger.debug(`[LibraryItemController] Requesting download cover from url "${req.body.url}"`)
|
||||||
result = await CoverManager.downloadCoverFromUrl(req.oldLibraryItem, req.body.url)
|
result = await CoverManager.downloadCoverFromUrlNew(req.body.url, req.libraryItem.id, req.libraryItem.isFile ? null : req.libraryItem.path)
|
||||||
} else if (req.files?.cover) {
|
} else if (req.files?.cover) {
|
||||||
Logger.debug(`[LibraryItemController] Handling uploaded cover`)
|
Logger.debug(`[LibraryItemController] Handling uploaded cover`)
|
||||||
result = await CoverManager.uploadCover(req.oldLibraryItem, req.files.cover)
|
result = await CoverManager.uploadCover(req.libraryItem, req.files.cover)
|
||||||
} else {
|
} else {
|
||||||
return res.status(400).send('Invalid request no file or url')
|
return res.status(400).send('Invalid request no file or url')
|
||||||
}
|
}
|
||||||
@ -303,8 +307,15 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updateAndReturnJson) {
|
if (updateAndReturnJson) {
|
||||||
await Database.updateLibraryItem(req.oldLibraryItem)
|
req.libraryItem.media.coverPath = result.cover
|
||||||
SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
|
req.libraryItem.media.changed('coverPath', true)
|
||||||
|
await req.libraryItem.media.save()
|
||||||
|
|
||||||
|
// client uses updatedAt timestamp in URL to force refresh cover
|
||||||
|
req.libraryItem.changed('updatedAt', true)
|
||||||
|
await req.libraryItem.save()
|
||||||
|
|
||||||
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
cover: result.cover
|
cover: result.cover
|
||||||
@ -323,13 +334,20 @@ class LibraryItemController {
|
|||||||
return res.status(400).send('Invalid request no cover path')
|
return res.status(400).send('Invalid request no cover path')
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationResult = await CoverManager.validateCoverPath(req.body.cover, req.oldLibraryItem)
|
const validationResult = await CoverManager.validateCoverPath(req.body.cover, req.libraryItem)
|
||||||
if (validationResult.error) {
|
if (validationResult.error) {
|
||||||
return res.status(500).send(validationResult.error)
|
return res.status(500).send(validationResult.error)
|
||||||
}
|
}
|
||||||
if (validationResult.updated) {
|
if (validationResult.updated) {
|
||||||
await Database.updateLibraryItem(req.oldLibraryItem)
|
req.libraryItem.media.coverPath = validationResult.cover
|
||||||
SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
|
req.libraryItem.media.changed('coverPath', true)
|
||||||
|
await req.libraryItem.media.save()
|
||||||
|
|
||||||
|
// client uses updatedAt timestamp in URL to force refresh cover
|
||||||
|
req.libraryItem.changed('updatedAt', true)
|
||||||
|
await req.libraryItem.save()
|
||||||
|
|
||||||
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
}
|
}
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@ -345,10 +363,17 @@ class LibraryItemController {
|
|||||||
*/
|
*/
|
||||||
async removeCover(req, res) {
|
async removeCover(req, res) {
|
||||||
if (req.libraryItem.media.coverPath) {
|
if (req.libraryItem.media.coverPath) {
|
||||||
req.oldLibraryItem.updateMediaCover('')
|
req.libraryItem.media.coverPath = null
|
||||||
|
req.libraryItem.media.changed('coverPath', true)
|
||||||
|
await req.libraryItem.media.save()
|
||||||
|
|
||||||
|
// client uses updatedAt timestamp in URL to force refresh cover
|
||||||
|
req.libraryItem.changed('updatedAt', true)
|
||||||
|
await req.libraryItem.save()
|
||||||
|
|
||||||
await CacheManager.purgeCoverCache(req.libraryItem.id)
|
await CacheManager.purgeCoverCache(req.libraryItem.id)
|
||||||
await Database.updateLibraryItem(req.oldLibraryItem)
|
|
||||||
SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
}
|
}
|
||||||
|
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
@ -451,11 +476,32 @@ class LibraryItemController {
|
|||||||
Logger.error(`[LibraryItemController] updateTracks invalid orderedFileData ${req.libraryItem.id}`)
|
Logger.error(`[LibraryItemController] updateTracks invalid orderedFileData ${req.libraryItem.id}`)
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
|
// Ensure that each orderedFileData has a valid ino and is in the book audioFiles
|
||||||
|
if (orderedFileData.some((fileData) => !fileData?.ino || !req.libraryItem.media.audioFiles.some((af) => af.ino === fileData.ino))) {
|
||||||
|
Logger.error(`[LibraryItemController] updateTracks invalid orderedFileData ${req.libraryItem.id}`)
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
req.oldLibraryItem.media.updateAudioTracks(orderedFileData)
|
let index = 1
|
||||||
await Database.updateLibraryItem(req.oldLibraryItem)
|
const updatedAudioFiles = orderedFileData.map((fileData) => {
|
||||||
SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
|
const audioFile = req.libraryItem.media.audioFiles.find((af) => af.ino === fileData.ino)
|
||||||
res.json(req.oldLibraryItem.toJSON())
|
audioFile.manuallyVerified = true
|
||||||
|
audioFile.exclude = !!fileData.exclude
|
||||||
|
if (audioFile.exclude) {
|
||||||
|
audioFile.index = -1
|
||||||
|
} else {
|
||||||
|
audioFile.index = index++
|
||||||
|
}
|
||||||
|
return audioFile
|
||||||
|
})
|
||||||
|
updatedAudioFiles.sort((a, b) => a.index - b.index)
|
||||||
|
|
||||||
|
req.libraryItem.media.audioFiles = updatedAudioFiles
|
||||||
|
req.libraryItem.media.changed('audioFiles', true)
|
||||||
|
await req.libraryItem.media.save()
|
||||||
|
|
||||||
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
|
res.json(req.libraryItem.toOldJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -787,7 +833,7 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(this.audioMetadataManager.getMetadataObjectForApi(req.oldLibraryItem))
|
res.json(this.audioMetadataManager.getMetadataObjectForApi(req.libraryItem))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -802,26 +848,51 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.libraryItem.isMissing || !req.libraryItem.isBook || !req.libraryItem.media.includedAudioFiles.length) {
|
if (req.libraryItem.isMissing || !req.libraryItem.isBook || !req.libraryItem.media.hasAudioTracks) {
|
||||||
Logger.error(`[LibraryItemController] Invalid library item`)
|
Logger.error(`[LibraryItemController] Invalid library item`)
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.body.chapters) {
|
if (!Array.isArray(req.body.chapters) || req.body.chapters.some((c) => !c.title || typeof c.title !== 'string' || c.start === undefined || typeof c.start !== 'number' || c.end === undefined || typeof c.end !== 'number')) {
|
||||||
Logger.error(`[LibraryItemController] Invalid payload`)
|
Logger.error(`[LibraryItemController] Invalid payload`)
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const chapters = req.body.chapters || []
|
const chapters = req.body.chapters || []
|
||||||
const wasUpdated = req.oldLibraryItem.media.updateChapters(chapters)
|
|
||||||
if (wasUpdated) {
|
let hasUpdates = false
|
||||||
await Database.updateLibraryItem(req.oldLibraryItem)
|
if (chapters.length !== req.libraryItem.media.chapters.length) {
|
||||||
SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
|
req.libraryItem.media.chapters = chapters.map((c, index) => {
|
||||||
|
return {
|
||||||
|
id: index,
|
||||||
|
title: c.title,
|
||||||
|
start: c.start,
|
||||||
|
end: c.end
|
||||||
|
}
|
||||||
|
})
|
||||||
|
hasUpdates = true
|
||||||
|
} else {
|
||||||
|
for (const [index, chapter] of chapters.entries()) {
|
||||||
|
const currentChapter = req.libraryItem.media.chapters[index]
|
||||||
|
if (currentChapter.title !== chapter.title || currentChapter.start !== chapter.start || currentChapter.end !== chapter.end) {
|
||||||
|
currentChapter.title = chapter.title
|
||||||
|
currentChapter.start = chapter.start
|
||||||
|
currentChapter.end = chapter.end
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasUpdates) {
|
||||||
|
req.libraryItem.media.changed('chapters', true)
|
||||||
|
await req.libraryItem.media.save()
|
||||||
|
|
||||||
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
updated: wasUpdated
|
updated: hasUpdates
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -829,7 +900,7 @@ class LibraryItemController {
|
|||||||
* GET: /api/items/:id/ffprobe/:fileid
|
* GET: /api/items/:id/ffprobe/:fileid
|
||||||
* FFProbe JSON result from audio file
|
* FFProbe JSON result from audio file
|
||||||
*
|
*
|
||||||
* @param {LibraryItemControllerRequestWithFile} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async getFFprobeData(req, res) {
|
async getFFprobeData(req, res) {
|
||||||
@ -837,18 +908,14 @@ class LibraryItemController {
|
|||||||
Logger.error(`[LibraryItemController] Non-admin user "${req.user.username}" attempted to get ffprobe data`)
|
Logger.error(`[LibraryItemController] Non-admin user "${req.user.username}" attempted to get ffprobe data`)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
if (req.libraryFile.fileType !== 'audio') {
|
|
||||||
Logger.error(`[LibraryItemController] Invalid filetype "${req.libraryFile.fileType}" for fileid "${req.params.fileid}". Expected audio file`)
|
|
||||||
return res.sendStatus(400)
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioFile = req.oldLibraryItem.media.findFileWithInode(req.params.fileid)
|
const audioFile = req.libraryItem.getAudioFileWithIno(req.params.fileid)
|
||||||
if (!audioFile) {
|
if (!audioFile) {
|
||||||
Logger.error(`[LibraryItemController] Audio file not found with inode value ${req.params.fileid}`)
|
Logger.error(`[LibraryItemController] Audio file not found with inode value ${req.params.fileid}`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ffprobeData = await AudioFileScanner.probeAudioFile(audioFile)
|
const ffprobeData = await AudioFileScanner.probeAudioFile(audioFile.metadata.path)
|
||||||
res.json(ffprobeData)
|
res.json(ffprobeData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -889,17 +956,35 @@ class LibraryItemController {
|
|||||||
await fs.remove(libraryFile.metadata.path).catch((error) => {
|
await fs.remove(libraryFile.metadata.path).catch((error) => {
|
||||||
Logger.error(`[LibraryItemController] Failed to delete library file at "${libraryFile.metadata.path}"`, error)
|
Logger.error(`[LibraryItemController] Failed to delete library file at "${libraryFile.metadata.path}"`, error)
|
||||||
})
|
})
|
||||||
req.oldLibraryItem.removeLibraryFile(req.params.fileid)
|
|
||||||
|
|
||||||
if (req.oldLibraryItem.media.removeFileWithInode(req.params.fileid)) {
|
req.libraryItem.libraryFiles = req.libraryItem.libraryFiles.filter((lf) => lf.ino !== req.params.fileid)
|
||||||
// If book has no more media files then mark it as missing
|
req.libraryItem.changed('libraryFiles', true)
|
||||||
if (req.libraryItem.mediaType === 'book' && !req.libraryItem.media.hasMediaFiles) {
|
|
||||||
req.oldLibraryItem.setMissing()
|
if (req.libraryItem.isBook) {
|
||||||
|
if (req.libraryItem.media.audioFiles.some((af) => af.ino === req.params.fileid)) {
|
||||||
|
req.libraryItem.media.audioFiles = req.libraryItem.media.audioFiles.filter((af) => af.ino !== req.params.fileid)
|
||||||
|
req.libraryItem.media.changed('audioFiles', true)
|
||||||
|
} else if (req.libraryItem.media.ebookFile?.ino === req.params.fileid) {
|
||||||
|
req.libraryItem.media.ebookFile = null
|
||||||
|
req.libraryItem.media.changed('ebookFile', true)
|
||||||
}
|
}
|
||||||
|
if (!req.libraryItem.media.hasMediaFiles) {
|
||||||
|
req.libraryItem.isMissing = true
|
||||||
|
}
|
||||||
|
} else if (req.libraryItem.media.podcastEpisodes.some((ep) => ep.audioFile.ino === req.params.fileid)) {
|
||||||
|
const episodeToRemove = req.libraryItem.media.podcastEpisodes.find((ep) => ep.audioFile.ino === req.params.fileid)
|
||||||
|
await episodeToRemove.destroy()
|
||||||
|
|
||||||
|
req.libraryItem.media.podcastEpisodes = req.libraryItem.media.podcastEpisodes.filter((ep) => ep.audioFile.ino !== req.params.fileid)
|
||||||
}
|
}
|
||||||
req.oldLibraryItem.updatedAt = Date.now()
|
|
||||||
await Database.updateLibraryItem(req.oldLibraryItem)
|
if (req.libraryItem.media.changed()) {
|
||||||
SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
|
await req.libraryItem.media.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
await req.libraryItem.save()
|
||||||
|
|
||||||
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -961,13 +1046,13 @@ class LibraryItemController {
|
|||||||
async getEBookFile(req, res) {
|
async getEBookFile(req, res) {
|
||||||
let ebookFile = null
|
let ebookFile = null
|
||||||
if (req.params.fileid) {
|
if (req.params.fileid) {
|
||||||
ebookFile = req.oldLibraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid)
|
ebookFile = req.libraryItem.getLibraryFileWithIno(req.params.fileid)
|
||||||
if (!ebookFile?.isEBookFile) {
|
if (!ebookFile?.isEBookFile) {
|
||||||
Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`)
|
Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`)
|
||||||
return res.status(400).send('Invalid ebook file id')
|
return res.status(400).send('Invalid ebook file id')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ebookFile = req.oldLibraryItem.media.ebookFile
|
ebookFile = req.libraryItem.media.ebookFile
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ebookFile) {
|
if (!ebookFile) {
|
||||||
@ -999,28 +1084,55 @@ class LibraryItemController {
|
|||||||
* if an ebook file is the primary ebook, then it will be changed to supplementary
|
* if an ebook file is the primary ebook, then it will be changed to supplementary
|
||||||
* if an ebook file is supplementary, then it will be changed to primary
|
* if an ebook file is supplementary, then it will be changed to primary
|
||||||
*
|
*
|
||||||
* @param {LibraryItemControllerRequest} req
|
* @param {LibraryItemControllerRequestWithFile} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async updateEbookFileStatus(req, res) {
|
async updateEbookFileStatus(req, res) {
|
||||||
const ebookLibraryFile = req.oldLibraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid)
|
if (!req.libraryItem.isBook) {
|
||||||
if (!ebookLibraryFile?.isEBookFile) {
|
Logger.error(`[LibraryItemController] Invalid media type for ebook file status update`)
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
if (!req.libraryFile?.isEBookFile) {
|
||||||
Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`)
|
Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`)
|
||||||
return res.status(400).send('Invalid ebook file id')
|
return res.status(400).send('Invalid ebook file id')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ebookLibraryFile = req.libraryFile
|
||||||
|
let primaryEbookFile = null
|
||||||
|
|
||||||
|
const ebookLibraryFileInos = req.libraryItem
|
||||||
|
.getLibraryFiles()
|
||||||
|
.filter((lf) => lf.isEBookFile)
|
||||||
|
.map((lf) => lf.ino)
|
||||||
|
|
||||||
if (ebookLibraryFile.isSupplementary) {
|
if (ebookLibraryFile.isSupplementary) {
|
||||||
Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to primary`)
|
Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to primary`)
|
||||||
req.oldLibraryItem.setPrimaryEbook(ebookLibraryFile)
|
|
||||||
|
primaryEbookFile = ebookLibraryFile.toJSON()
|
||||||
|
delete primaryEbookFile.isSupplementary
|
||||||
|
delete primaryEbookFile.fileType
|
||||||
|
primaryEbookFile.ebookFormat = ebookLibraryFile.metadata.format
|
||||||
} else {
|
} else {
|
||||||
Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to supplementary`)
|
Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to supplementary`)
|
||||||
ebookLibraryFile.isSupplementary = true
|
|
||||||
req.oldLibraryItem.setPrimaryEbook(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
req.oldLibraryItem.updatedAt = Date.now()
|
req.libraryItem.media.ebookFile = primaryEbookFile
|
||||||
await Database.updateLibraryItem(req.oldLibraryItem)
|
req.libraryItem.media.changed('ebookFile', true)
|
||||||
SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
|
await req.libraryItem.media.save()
|
||||||
|
|
||||||
|
req.libraryItem.libraryFiles = req.libraryItem.libraryFiles.map((lf) => {
|
||||||
|
if (ebookLibraryFileInos.includes(lf.ino)) {
|
||||||
|
lf.isSupplementary = lf.ino !== primaryEbookFile?.ino
|
||||||
|
}
|
||||||
|
return lf
|
||||||
|
})
|
||||||
|
req.libraryItem.changed('libraryFiles', true)
|
||||||
|
|
||||||
|
req.libraryItem.isMissing = !req.libraryItem.media.hasMediaFiles
|
||||||
|
|
||||||
|
await req.libraryItem.save()
|
||||||
|
|
||||||
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1042,7 +1154,7 @@ class LibraryItemController {
|
|||||||
|
|
||||||
// For library file routes, get the library file
|
// For library file routes, get the library file
|
||||||
if (req.params.fileid) {
|
if (req.params.fileid) {
|
||||||
req.libraryFile = req.oldLibraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid)
|
req.libraryFile = req.libraryItem.getLibraryFileWithIno(req.params.fileid)
|
||||||
if (!req.libraryFile) {
|
if (!req.libraryFile) {
|
||||||
Logger.error(`[LibraryItemController] Library file "${req.params.fileid}" does not exist for library item`)
|
Logger.error(`[LibraryItemController] Library file "${req.params.fileid}" does not exist for library item`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
|
@ -34,8 +34,13 @@ class AudioMetadataMangaer {
|
|||||||
return this.tasksQueued.some((t) => t.data.libraryItemId === libraryItemId) || this.tasksRunning.some((t) => t.data.libraryItemId === libraryItemId)
|
return this.tasksQueued.some((t) => t.data.libraryItemId === libraryItemId) || this.tasksRunning.some((t) => t.data.libraryItemId === libraryItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('../models/LibraryItem')} libraryItem
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
getMetadataObjectForApi(libraryItem) {
|
getMetadataObjectForApi(libraryItem) {
|
||||||
return ffmpegHelpers.getFFMetadataObject(libraryItem, libraryItem.media.includedAudioFiles.length)
|
return ffmpegHelpers.getFFMetadataObject(libraryItem.toOldJSONExpanded(), libraryItem.media.includedAudioFiles.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -79,6 +79,12 @@ class CoverManager {
|
|||||||
return imgType
|
return imgType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('../models/LibraryItem')} libraryItem
|
||||||
|
* @param {*} coverFile - file object from req.files
|
||||||
|
* @returns {Promise<{error:string}|{cover:string}>}
|
||||||
|
*/
|
||||||
async uploadCover(libraryItem, coverFile) {
|
async uploadCover(libraryItem, coverFile) {
|
||||||
const extname = Path.extname(coverFile.name.toLowerCase())
|
const extname = Path.extname(coverFile.name.toLowerCase())
|
||||||
if (!extname || !globals.SupportedImageTypes.includes(extname.slice(1))) {
|
if (!extname || !globals.SupportedImageTypes.includes(extname.slice(1))) {
|
||||||
@ -110,14 +116,20 @@ class CoverManager {
|
|||||||
await this.removeOldCovers(coverDirPath, extname)
|
await this.removeOldCovers(coverDirPath, extname)
|
||||||
await CacheManager.purgeCoverCache(libraryItem.id)
|
await CacheManager.purgeCoverCache(libraryItem.id)
|
||||||
|
|
||||||
Logger.info(`[CoverManager] Uploaded libraryItem cover "${coverFullPath}" for "${libraryItem.media.metadata.title}"`)
|
Logger.info(`[CoverManager] Uploaded libraryItem cover "${coverFullPath}" for "${libraryItem.media.title}"`)
|
||||||
|
|
||||||
libraryItem.updateMediaCover(coverFullPath)
|
|
||||||
return {
|
return {
|
||||||
cover: coverFullPath
|
cover: coverFullPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} libraryItem - old library item
|
||||||
|
* @param {string} url
|
||||||
|
* @param {boolean} [forceLibraryItemFolder=false]
|
||||||
|
* @returns {Promise<{error:string}|{cover:string}>}
|
||||||
|
*/
|
||||||
async downloadCoverFromUrl(libraryItem, url, forceLibraryItemFolder = false) {
|
async downloadCoverFromUrl(libraryItem, url, forceLibraryItemFolder = false) {
|
||||||
try {
|
try {
|
||||||
// Force save cover with library item is used for adding new podcasts
|
// Force save cover with library item is used for adding new podcasts
|
||||||
@ -166,6 +178,12 @@ class CoverManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} coverPath
|
||||||
|
* @param {import('../models/LibraryItem')} libraryItem
|
||||||
|
* @returns {Promise<{error:string}|{cover:string,updated:boolean}>}
|
||||||
|
*/
|
||||||
async validateCoverPath(coverPath, libraryItem) {
|
async validateCoverPath(coverPath, libraryItem) {
|
||||||
// Invalid cover path
|
// Invalid cover path
|
||||||
if (!coverPath || coverPath.startsWith('http:') || coverPath.startsWith('https:')) {
|
if (!coverPath || coverPath.startsWith('http:') || coverPath.startsWith('https:')) {
|
||||||
@ -235,7 +253,6 @@ class CoverManager {
|
|||||||
|
|
||||||
await CacheManager.purgeCoverCache(libraryItem.id)
|
await CacheManager.purgeCoverCache(libraryItem.id)
|
||||||
|
|
||||||
libraryItem.updateMediaCover(coverPath)
|
|
||||||
return {
|
return {
|
||||||
cover: coverPath,
|
cover: coverPath,
|
||||||
updated: true
|
updated: true
|
||||||
|
@ -1152,13 +1152,49 @@ class LibraryItem extends Model {
|
|||||||
Logger.error(`[LibraryItem] hasAudioTracks: Library item "${this.id}" does not have media`)
|
Logger.error(`[LibraryItem] hasAudioTracks: Library item "${this.id}" does not have media`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (this.mediaType === 'book') {
|
if (this.isBook) {
|
||||||
return this.media.audioFiles?.length > 0
|
return this.media.audioFiles?.length > 0
|
||||||
} else {
|
} else {
|
||||||
return this.media.podcastEpisodes?.length > 0
|
return this.media.podcastEpisodes?.length > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} ino
|
||||||
|
* @returns {import('./Book').AudioFileObject}
|
||||||
|
*/
|
||||||
|
getAudioFileWithIno(ino) {
|
||||||
|
if (!this.media) {
|
||||||
|
Logger.error(`[LibraryItem] getAudioFileWithIno: Library item "${this.id}" does not have media`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (this.isBook) {
|
||||||
|
return this.media.audioFiles.find((af) => af.ino === ino)
|
||||||
|
} else {
|
||||||
|
return this.media.podcastEpisodes.find((pe) => pe.audioFile?.ino === ino)?.audioFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} ino
|
||||||
|
* @returns {LibraryFile}
|
||||||
|
*/
|
||||||
|
getLibraryFileWithIno(ino) {
|
||||||
|
const libraryFile = this.libraryFiles.find((lf) => lf.ino === ino)
|
||||||
|
if (!libraryFile) return null
|
||||||
|
return new LibraryFile(libraryFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
getLibraryFiles() {
|
||||||
|
return this.libraryFiles.map((lf) => new LibraryFile(lf))
|
||||||
|
}
|
||||||
|
|
||||||
|
getLibraryFilesJson() {
|
||||||
|
return this.libraryFiles.map((lf) => new LibraryFile(lf).toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
toOldJSON() {
|
toOldJSON() {
|
||||||
if (!this.media) {
|
if (!this.media) {
|
||||||
throw new Error(`[LibraryItem] Cannot convert to old JSON without media for library item "${this.id}"`)
|
throw new Error(`[LibraryItem] Cannot convert to old JSON without media for library item "${this.id}"`)
|
||||||
@ -1184,7 +1220,8 @@ class LibraryItem extends Model {
|
|||||||
isInvalid: !!this.isInvalid,
|
isInvalid: !!this.isInvalid,
|
||||||
mediaType: this.mediaType,
|
mediaType: this.mediaType,
|
||||||
media: this.media.toOldJSON(this.id),
|
media: this.media.toOldJSON(this.id),
|
||||||
libraryFiles: structuredClone(this.libraryFiles)
|
// LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database
|
||||||
|
libraryFiles: this.getLibraryFilesJson()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1237,7 +1274,8 @@ class LibraryItem extends Model {
|
|||||||
isInvalid: !!this.isInvalid,
|
isInvalid: !!this.isInvalid,
|
||||||
mediaType: this.mediaType,
|
mediaType: this.mediaType,
|
||||||
media: this.media.toOldJSONExpanded(this.id),
|
media: this.media.toOldJSONExpanded(this.id),
|
||||||
libraryFiles: structuredClone(this.libraryFiles),
|
// LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database
|
||||||
|
libraryFiles: this.getLibraryFilesJson(),
|
||||||
size: this.size
|
size: this.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -327,20 +327,5 @@ class LibraryItem {
|
|||||||
}
|
}
|
||||||
return false
|
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
|
|
||||||
*
|
|
||||||
* @param {LibraryFile} [libraryFile]
|
|
||||||
*/
|
|
||||||
setPrimaryEbook(ebookLibraryFile = null) {
|
|
||||||
const ebookLibraryFiles = this.libraryFiles.filter((lf) => lf.isEBookFile)
|
|
||||||
for (const libraryFile of ebookLibraryFiles) {
|
|
||||||
libraryFile.isSupplementary = ebookLibraryFile?.ino !== libraryFile.ino
|
|
||||||
}
|
|
||||||
this.media.setEbookFile(ebookLibraryFile)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
module.exports = LibraryItem
|
module.exports = LibraryItem
|
||||||
|
@ -33,8 +33,8 @@ class Book {
|
|||||||
this.metadata = new BookMetadata(book.metadata)
|
this.metadata = new BookMetadata(book.metadata)
|
||||||
this.coverPath = book.coverPath
|
this.coverPath = book.coverPath
|
||||||
this.tags = [...book.tags]
|
this.tags = [...book.tags]
|
||||||
this.audioFiles = book.audioFiles.map(f => new AudioFile(f))
|
this.audioFiles = book.audioFiles.map((f) => new AudioFile(f))
|
||||||
this.chapters = book.chapters.map(c => ({ ...c }))
|
this.chapters = book.chapters.map((c) => ({ ...c }))
|
||||||
this.ebookFile = book.ebookFile ? new EBookFile(book.ebookFile) : null
|
this.ebookFile = book.ebookFile ? new EBookFile(book.ebookFile) : null
|
||||||
this.lastCoverSearch = book.lastCoverSearch || null
|
this.lastCoverSearch = book.lastCoverSearch || null
|
||||||
this.lastCoverSearchQuery = book.lastCoverSearchQuery || null
|
this.lastCoverSearchQuery = book.lastCoverSearchQuery || null
|
||||||
@ -47,8 +47,8 @@ class Book {
|
|||||||
metadata: this.metadata.toJSON(),
|
metadata: this.metadata.toJSON(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
audioFiles: this.audioFiles.map(f => f.toJSON()),
|
audioFiles: this.audioFiles.map((f) => f.toJSON()),
|
||||||
chapters: this.chapters.map(c => ({ ...c })),
|
chapters: this.chapters.map((c) => ({ ...c })),
|
||||||
ebookFile: this.ebookFile ? this.ebookFile.toJSON() : null
|
ebookFile: this.ebookFile ? this.ebookFile.toJSON() : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,11 +75,11 @@ class Book {
|
|||||||
metadata: this.metadata.toJSONExpanded(),
|
metadata: this.metadata.toJSONExpanded(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
audioFiles: this.audioFiles.map(f => f.toJSON()),
|
audioFiles: this.audioFiles.map((f) => f.toJSON()),
|
||||||
chapters: this.chapters.map(c => ({ ...c })),
|
chapters: this.chapters.map((c) => ({ ...c })),
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
size: this.size,
|
size: this.size,
|
||||||
tracks: this.tracks.map(t => t.toJSON()),
|
tracks: this.tracks.map((t) => t.toJSON()),
|
||||||
ebookFile: this.ebookFile?.toJSON() || null
|
ebookFile: this.ebookFile?.toJSON() || null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,14 +87,14 @@ class Book {
|
|||||||
toJSONForMetadataFile() {
|
toJSONForMetadataFile() {
|
||||||
return {
|
return {
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
chapters: this.chapters.map(c => ({ ...c })),
|
chapters: this.chapters.map((c) => ({ ...c })),
|
||||||
...this.metadata.toJSONForMetadataFile()
|
...this.metadata.toJSONForMetadataFile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get size() {
|
get size() {
|
||||||
var total = 0
|
var total = 0
|
||||||
this.audioFiles.forEach((af) => total += af.metadata.size)
|
this.audioFiles.forEach((af) => (total += af.metadata.size))
|
||||||
if (this.ebookFile) {
|
if (this.ebookFile) {
|
||||||
total += this.ebookFile.metadata.size
|
total += this.ebookFile.metadata.size
|
||||||
}
|
}
|
||||||
@ -104,7 +104,7 @@ class Book {
|
|||||||
return !!this.tracks.length || this.ebookFile
|
return !!this.tracks.length || this.ebookFile
|
||||||
}
|
}
|
||||||
get includedAudioFiles() {
|
get includedAudioFiles() {
|
||||||
return this.audioFiles.filter(af => !af.exclude)
|
return this.audioFiles.filter((af) => !af.exclude)
|
||||||
}
|
}
|
||||||
get tracks() {
|
get tracks() {
|
||||||
let startOffset = 0
|
let startOffset = 0
|
||||||
@ -117,7 +117,7 @@ class Book {
|
|||||||
}
|
}
|
||||||
get duration() {
|
get duration() {
|
||||||
let total = 0
|
let total = 0
|
||||||
this.tracks.forEach((track) => total += track.duration)
|
this.tracks.forEach((track) => (total += track.duration))
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
get numTracks() {
|
get numTracks() {
|
||||||
@ -149,30 +149,6 @@ class Book {
|
|||||||
return hasUpdates
|
return hasUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
updateChapters(chapters) {
|
|
||||||
var hasUpdates = this.chapters.length !== chapters.length
|
|
||||||
if (hasUpdates) {
|
|
||||||
this.chapters = chapters.map(ch => ({
|
|
||||||
id: ch.id,
|
|
||||||
start: ch.start,
|
|
||||||
end: ch.end,
|
|
||||||
title: ch.title
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < this.chapters.length; i++) {
|
|
||||||
const currChapter = this.chapters[i]
|
|
||||||
const newChapter = chapters[i]
|
|
||||||
if (!hasUpdates && (currChapter.title !== newChapter.title || currChapter.start !== newChapter.start || currChapter.end !== newChapter.end)) {
|
|
||||||
hasUpdates = true
|
|
||||||
}
|
|
||||||
this.chapters[i].title = newChapter.title
|
|
||||||
this.chapters[i].start = newChapter.start
|
|
||||||
this.chapters[i].end = newChapter.end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hasUpdates
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCover(coverPath) {
|
updateCover(coverPath) {
|
||||||
coverPath = filePathToPOSIX(coverPath)
|
coverPath = filePathToPOSIX(coverPath)
|
||||||
if (this.coverPath === coverPath) return false
|
if (this.coverPath === coverPath) return false
|
||||||
@ -180,75 +156,6 @@ class Book {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFileWithInode(inode) {
|
|
||||||
if (this.audioFiles.some(af => af.ino === inode)) {
|
|
||||||
this.audioFiles = this.audioFiles.filter(af => af.ino !== inode)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (this.ebookFile && this.ebookFile.ino === inode) {
|
|
||||||
this.ebookFile = null
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get audio file or ebook file from inode
|
|
||||||
* @param {string} inode
|
|
||||||
* @returns {(AudioFile|EBookFile|null)}
|
|
||||||
*/
|
|
||||||
findFileWithInode(inode) {
|
|
||||||
const audioFile = this.audioFiles.find(af => af.ino === inode)
|
|
||||||
if (audioFile) return audioFile
|
|
||||||
if (this.ebookFile && this.ebookFile.ino === inode) return this.ebookFile
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the EBookFile from a LibraryFile
|
|
||||||
* If null then ebookFile will be removed from the book
|
|
||||||
*
|
|
||||||
* @param {LibraryFile} [libraryFile]
|
|
||||||
*/
|
|
||||||
setEbookFile(libraryFile = null) {
|
|
||||||
if (!libraryFile) {
|
|
||||||
this.ebookFile = null
|
|
||||||
} else {
|
|
||||||
const ebookFile = new EBookFile()
|
|
||||||
ebookFile.setData(libraryFile)
|
|
||||||
this.ebookFile = ebookFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addAudioFile(audioFile) {
|
|
||||||
this.audioFiles.push(audioFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAudioTracks(orderedFileData) {
|
|
||||||
let index = 1
|
|
||||||
this.audioFiles = orderedFileData.map((fileData) => {
|
|
||||||
const audioFile = this.audioFiles.find(af => af.ino === fileData.ino)
|
|
||||||
audioFile.manuallyVerified = true
|
|
||||||
audioFile.error = null
|
|
||||||
if (fileData.exclude !== undefined) {
|
|
||||||
audioFile.exclude = !!fileData.exclude
|
|
||||||
}
|
|
||||||
if (audioFile.exclude) {
|
|
||||||
audioFile.index = -1
|
|
||||||
} else {
|
|
||||||
audioFile.index = index++
|
|
||||||
}
|
|
||||||
return audioFile
|
|
||||||
})
|
|
||||||
|
|
||||||
this.rebuildTracks()
|
|
||||||
}
|
|
||||||
|
|
||||||
rebuildTracks() {
|
|
||||||
Logger.debug(`[Book] Tracks being rebuilt...!`)
|
|
||||||
this.audioFiles.sort((a, b) => a.index - b.index)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only checks container format
|
// Only checks container format
|
||||||
checkCanDirectPlay(payload) {
|
checkCanDirectPlay(payload) {
|
||||||
var supportedMimeTypes = payload.supportedMimeTypes || []
|
var supportedMimeTypes = payload.supportedMimeTypes || []
|
||||||
@ -268,7 +175,7 @@ class Book {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getChapters() {
|
getChapters() {
|
||||||
return this.chapters?.map(ch => ({ ...ch })) || []
|
return this.chapters?.map((ch) => ({ ...ch })) || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = Book
|
module.exports = Book
|
||||||
|
@ -181,20 +181,6 @@ class Podcast {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFileWithInode(inode) {
|
|
||||||
const hasEpisode = this.episodes.some((ep) => ep.audioFile.ino === inode)
|
|
||||||
if (hasEpisode) {
|
|
||||||
this.episodes = this.episodes.filter((ep) => ep.audioFile.ino !== inode)
|
|
||||||
}
|
|
||||||
return hasEpisode
|
|
||||||
}
|
|
||||||
|
|
||||||
findFileWithInode(inode) {
|
|
||||||
var episode = this.episodes.find((ep) => ep.audioFile.ino === inode)
|
|
||||||
if (episode) return episode.audioFile
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
setData(mediaData) {
|
setData(mediaData) {
|
||||||
this.metadata = new PodcastMetadata()
|
this.metadata = new PodcastMetadata()
|
||||||
if (mediaData.metadata) {
|
if (mediaData.metadata) {
|
||||||
|
@ -202,12 +202,12 @@ class AudioFileScanner {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {AudioFile} audioFile
|
* @param {string} audioFilePath
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
probeAudioFile(audioFile) {
|
probeAudioFile(audioFilePath) {
|
||||||
Logger.debug(`[AudioFileScanner] Running ffprobe for audio file at "${audioFile.metadata.path}"`)
|
Logger.debug(`[AudioFileScanner] Running ffprobe for audio file at "${audioFilePath}"`)
|
||||||
return prober.rawProbe(audioFile.metadata.path)
|
return prober.rawProbe(audioFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user