mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01: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 RequestLibraryFileObject | ||||
|  * @property {import('../models/LibraryItem').LibraryFileObject} libraryFile | ||||
|  * @property {import('../objects/files/LibraryFile')} libraryFile | ||||
|  * | ||||
|  * @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 {Response} res | ||||
| @ -288,10 +292,10 @@ class LibraryItemController { | ||||
|     let result = null | ||||
|     if (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) { | ||||
|       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 { | ||||
|       return res.status(400).send('Invalid request no file or url') | ||||
|     } | ||||
| @ -303,8 +307,15 @@ class LibraryItemController { | ||||
|     } | ||||
| 
 | ||||
|     if (updateAndReturnJson) { | ||||
|       await Database.updateLibraryItem(req.oldLibraryItem) | ||||
|       SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) | ||||
|       req.libraryItem.media.coverPath = result.cover | ||||
|       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({ | ||||
|         success: true, | ||||
|         cover: result.cover | ||||
| @ -323,13 +334,20 @@ class LibraryItemController { | ||||
|       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) { | ||||
|       return res.status(500).send(validationResult.error) | ||||
|     } | ||||
|     if (validationResult.updated) { | ||||
|       await Database.updateLibraryItem(req.oldLibraryItem) | ||||
|       SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) | ||||
|       req.libraryItem.media.coverPath = validationResult.cover | ||||
|       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({ | ||||
|       success: true, | ||||
| @ -345,10 +363,17 @@ class LibraryItemController { | ||||
|    */ | ||||
|   async removeCover(req, res) { | ||||
|     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 Database.updateLibraryItem(req.oldLibraryItem) | ||||
|       SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) | ||||
| 
 | ||||
|       SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded()) | ||||
|     } | ||||
| 
 | ||||
|     res.sendStatus(200) | ||||
| @ -451,11 +476,32 @@ class LibraryItemController { | ||||
|       Logger.error(`[LibraryItemController] updateTracks invalid orderedFileData ${req.libraryItem.id}`) | ||||
|       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) | ||||
|     await Database.updateLibraryItem(req.oldLibraryItem) | ||||
|     SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) | ||||
|     res.json(req.oldLibraryItem.toJSON()) | ||||
|     let index = 1 | ||||
|     const updatedAudioFiles = orderedFileData.map((fileData) => { | ||||
|       const audioFile = req.libraryItem.media.audioFiles.find((af) => af.ino === fileData.ino) | ||||
|       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) | ||||
|     } | ||||
| 
 | ||||
|     res.json(this.audioMetadataManager.getMetadataObjectForApi(req.oldLibraryItem)) | ||||
|     res.json(this.audioMetadataManager.getMetadataObjectForApi(req.libraryItem)) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
| @ -802,26 +848,51 @@ class LibraryItemController { | ||||
|       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`) | ||||
|       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`) | ||||
|       return res.sendStatus(400) | ||||
|     } | ||||
| 
 | ||||
|     const chapters = req.body.chapters || [] | ||||
|     const wasUpdated = req.oldLibraryItem.media.updateChapters(chapters) | ||||
|     if (wasUpdated) { | ||||
|       await Database.updateLibraryItem(req.oldLibraryItem) | ||||
|       SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) | ||||
| 
 | ||||
|     let hasUpdates = false | ||||
|     if (chapters.length !== req.libraryItem.media.chapters.length) { | ||||
|       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({ | ||||
|       success: true, | ||||
|       updated: wasUpdated | ||||
|       updated: hasUpdates | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
| @ -829,7 +900,7 @@ class LibraryItemController { | ||||
|    * GET: /api/items/:id/ffprobe/:fileid | ||||
|    * FFProbe JSON result from audio file | ||||
|    * | ||||
|    * @param {LibraryItemControllerRequestWithFile} req | ||||
|    * @param {LibraryItemControllerRequest} req | ||||
|    * @param {Response} 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`) | ||||
|       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) { | ||||
|       Logger.error(`[LibraryItemController] Audio file not found with inode value ${req.params.fileid}`) | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
| 
 | ||||
|     const ffprobeData = await AudioFileScanner.probeAudioFile(audioFile) | ||||
|     const ffprobeData = await AudioFileScanner.probeAudioFile(audioFile.metadata.path) | ||||
|     res.json(ffprobeData) | ||||
|   } | ||||
| 
 | ||||
| @ -889,17 +956,35 @@ class LibraryItemController { | ||||
|     await fs.remove(libraryFile.metadata.path).catch((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)) { | ||||
|       // If book has no more media files then mark it as missing
 | ||||
|       if (req.libraryItem.mediaType === 'book' && !req.libraryItem.media.hasMediaFiles) { | ||||
|         req.oldLibraryItem.setMissing() | ||||
|     req.libraryItem.libraryFiles = req.libraryItem.libraryFiles.filter((lf) => lf.ino !== req.params.fileid) | ||||
|     req.libraryItem.changed('libraryFiles', true) | ||||
| 
 | ||||
|     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) | ||||
|     SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) | ||||
| 
 | ||||
|     if (req.libraryItem.media.changed()) { | ||||
|       await req.libraryItem.media.save() | ||||
|     } | ||||
| 
 | ||||
|     await req.libraryItem.save() | ||||
| 
 | ||||
|     SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded()) | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
| @ -961,13 +1046,13 @@ class LibraryItemController { | ||||
|   async getEBookFile(req, res) { | ||||
|     let ebookFile = null | ||||
|     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) { | ||||
|         Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`) | ||||
|         return res.status(400).send('Invalid ebook file id') | ||||
|       } | ||||
|     } else { | ||||
|       ebookFile = req.oldLibraryItem.media.ebookFile | ||||
|       ebookFile = req.libraryItem.media.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 supplementary, then it will be changed to primary | ||||
|    * | ||||
|    * @param {LibraryItemControllerRequest} req | ||||
|    * @param {LibraryItemControllerRequestWithFile} req | ||||
|    * @param {Response} res | ||||
|    */ | ||||
|   async updateEbookFileStatus(req, res) { | ||||
|     const ebookLibraryFile = req.oldLibraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid) | ||||
|     if (!ebookLibraryFile?.isEBookFile) { | ||||
|     if (!req.libraryItem.isBook) { | ||||
|       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}"`) | ||||
|       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) { | ||||
|       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 { | ||||
|       Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to supplementary`) | ||||
|       ebookLibraryFile.isSupplementary = true | ||||
|       req.oldLibraryItem.setPrimaryEbook(null) | ||||
|     } | ||||
| 
 | ||||
|     req.oldLibraryItem.updatedAt = Date.now() | ||||
|     await Database.updateLibraryItem(req.oldLibraryItem) | ||||
|     SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) | ||||
|     req.libraryItem.media.ebookFile = primaryEbookFile | ||||
|     req.libraryItem.media.changed('ebookFile', true) | ||||
|     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) | ||||
|   } | ||||
| 
 | ||||
| @ -1042,7 +1154,7 @@ class LibraryItemController { | ||||
| 
 | ||||
|     // For library file routes, get the library file
 | ||||
|     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) { | ||||
|         Logger.error(`[LibraryItemController] Library file "${req.params.fileid}" does not exist for library item`) | ||||
|         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) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * @param {import('../models/LibraryItem')} libraryItem | ||||
|    * @returns | ||||
|    */ | ||||
|   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 | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * @param {import('../models/LibraryItem')} libraryItem | ||||
|    * @param {*} coverFile - file object from req.files | ||||
|    * @returns {Promise<{error:string}|{cover:string}>} | ||||
|    */ | ||||
|   async uploadCover(libraryItem, coverFile) { | ||||
|     const extname = Path.extname(coverFile.name.toLowerCase()) | ||||
|     if (!extname || !globals.SupportedImageTypes.includes(extname.slice(1))) { | ||||
| @ -110,14 +116,20 @@ class CoverManager { | ||||
|     await this.removeOldCovers(coverDirPath, extname) | ||||
|     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 { | ||||
|       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) { | ||||
|     try { | ||||
|       // 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) { | ||||
|     // Invalid cover path
 | ||||
|     if (!coverPath || coverPath.startsWith('http:') || coverPath.startsWith('https:')) { | ||||
| @ -235,7 +253,6 @@ class CoverManager { | ||||
| 
 | ||||
|     await CacheManager.purgeCoverCache(libraryItem.id) | ||||
| 
 | ||||
|     libraryItem.updateMediaCover(coverPath) | ||||
|     return { | ||||
|       cover: coverPath, | ||||
|       updated: true | ||||
|  | ||||
| @ -1152,13 +1152,49 @@ class LibraryItem extends Model { | ||||
|       Logger.error(`[LibraryItem] hasAudioTracks: Library item "${this.id}" does not have media`) | ||||
|       return false | ||||
|     } | ||||
|     if (this.mediaType === 'book') { | ||||
|     if (this.isBook) { | ||||
|       return this.media.audioFiles?.length > 0 | ||||
|     } else { | ||||
|       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() { | ||||
|     if (!this.media) { | ||||
|       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, | ||||
|       mediaType: this.mediaType, | ||||
|       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, | ||||
|       mediaType: this.mediaType, | ||||
|       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 | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -327,20 +327,5 @@ class LibraryItem { | ||||
|     } | ||||
|     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 | ||||
|  | ||||
| @ -33,8 +33,8 @@ class Book { | ||||
|     this.metadata = new BookMetadata(book.metadata) | ||||
|     this.coverPath = book.coverPath | ||||
|     this.tags = [...book.tags] | ||||
|     this.audioFiles = book.audioFiles.map(f => new AudioFile(f)) | ||||
|     this.chapters = book.chapters.map(c => ({ ...c })) | ||||
|     this.audioFiles = book.audioFiles.map((f) => new AudioFile(f)) | ||||
|     this.chapters = book.chapters.map((c) => ({ ...c })) | ||||
|     this.ebookFile = book.ebookFile ? new EBookFile(book.ebookFile) : null | ||||
|     this.lastCoverSearch = book.lastCoverSearch || null | ||||
|     this.lastCoverSearchQuery = book.lastCoverSearchQuery || null | ||||
| @ -47,8 +47,8 @@ class Book { | ||||
|       metadata: this.metadata.toJSON(), | ||||
|       coverPath: this.coverPath, | ||||
|       tags: [...this.tags], | ||||
|       audioFiles: this.audioFiles.map(f => f.toJSON()), | ||||
|       chapters: this.chapters.map(c => ({ ...c })), | ||||
|       audioFiles: this.audioFiles.map((f) => f.toJSON()), | ||||
|       chapters: this.chapters.map((c) => ({ ...c })), | ||||
|       ebookFile: this.ebookFile ? this.ebookFile.toJSON() : null | ||||
|     } | ||||
|   } | ||||
| @ -75,11 +75,11 @@ class Book { | ||||
|       metadata: this.metadata.toJSONExpanded(), | ||||
|       coverPath: this.coverPath, | ||||
|       tags: [...this.tags], | ||||
|       audioFiles: this.audioFiles.map(f => f.toJSON()), | ||||
|       chapters: this.chapters.map(c => ({ ...c })), | ||||
|       audioFiles: this.audioFiles.map((f) => f.toJSON()), | ||||
|       chapters: this.chapters.map((c) => ({ ...c })), | ||||
|       duration: this.duration, | ||||
|       size: this.size, | ||||
|       tracks: this.tracks.map(t => t.toJSON()), | ||||
|       tracks: this.tracks.map((t) => t.toJSON()), | ||||
|       ebookFile: this.ebookFile?.toJSON() || null | ||||
|     } | ||||
|   } | ||||
| @ -87,14 +87,14 @@ class Book { | ||||
|   toJSONForMetadataFile() { | ||||
|     return { | ||||
|       tags: [...this.tags], | ||||
|       chapters: this.chapters.map(c => ({ ...c })), | ||||
|       chapters: this.chapters.map((c) => ({ ...c })), | ||||
|       ...this.metadata.toJSONForMetadataFile() | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   get size() { | ||||
|     var total = 0 | ||||
|     this.audioFiles.forEach((af) => total += af.metadata.size) | ||||
|     this.audioFiles.forEach((af) => (total += af.metadata.size)) | ||||
|     if (this.ebookFile) { | ||||
|       total += this.ebookFile.metadata.size | ||||
|     } | ||||
| @ -104,7 +104,7 @@ class Book { | ||||
|     return !!this.tracks.length || this.ebookFile | ||||
|   } | ||||
|   get includedAudioFiles() { | ||||
|     return this.audioFiles.filter(af => !af.exclude) | ||||
|     return this.audioFiles.filter((af) => !af.exclude) | ||||
|   } | ||||
|   get tracks() { | ||||
|     let startOffset = 0 | ||||
| @ -117,7 +117,7 @@ class Book { | ||||
|   } | ||||
|   get duration() { | ||||
|     let total = 0 | ||||
|     this.tracks.forEach((track) => total += track.duration) | ||||
|     this.tracks.forEach((track) => (total += track.duration)) | ||||
|     return total | ||||
|   } | ||||
|   get numTracks() { | ||||
| @ -149,30 +149,6 @@ class Book { | ||||
|     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) { | ||||
|     coverPath = filePathToPOSIX(coverPath) | ||||
|     if (this.coverPath === coverPath) return false | ||||
| @ -180,75 +156,6 @@ class Book { | ||||
|     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
 | ||||
|   checkCanDirectPlay(payload) { | ||||
|     var supportedMimeTypes = payload.supportedMimeTypes || [] | ||||
| @ -268,7 +175,7 @@ class Book { | ||||
|   } | ||||
| 
 | ||||
|   getChapters() { | ||||
|     return this.chapters?.map(ch => ({ ...ch })) || [] | ||||
|     return this.chapters?.map((ch) => ({ ...ch })) || [] | ||||
|   } | ||||
| } | ||||
| module.exports = Book | ||||
|  | ||||
| @ -181,20 +181,6 @@ class Podcast { | ||||
|     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) { | ||||
|     this.metadata = new PodcastMetadata() | ||||
|     if (mediaData.metadata) { | ||||
|  | ||||
| @ -202,12 +202,12 @@ class AudioFileScanner { | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * @param {AudioFile} audioFile | ||||
|    * @param {string} audioFilePath | ||||
|    * @returns {object} | ||||
|    */ | ||||
|   probeAudioFile(audioFile) { | ||||
|     Logger.debug(`[AudioFileScanner] Running ffprobe for audio file at "${audioFile.metadata.path}"`) | ||||
|     return prober.rawProbe(audioFile.metadata.path) | ||||
|   probeAudioFile(audioFilePath) { | ||||
|     Logger.debug(`[AudioFileScanner] Running ffprobe for audio file at "${audioFilePath}"`) | ||||
|     return prober.rawProbe(audioFilePath) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user