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