mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-04 03:17:00 -05:00 
			
		
		
		
	Implementing toOld functions for LibraryItem/Book/Podcast
This commit is contained in:
		
							parent
							
								
									de8b0abc3a
								
							
						
					
					
						commit
						dd0ebdf2d8
					
				@ -24,6 +24,17 @@ const ShareManager = require('../managers/ShareManager')
 | 
				
			|||||||
 * @property {import('../models/User')} user
 | 
					 * @property {import('../models/User')} user
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @typedef {Request & RequestUserObject} RequestWithUser
 | 
					 * @typedef {Request & RequestUserObject} RequestWithUser
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @typedef RequestEntityObject
 | 
				
			||||||
 | 
					 * @property {import('../models/LibraryItem')} libraryItem
 | 
				
			||||||
 | 
					 * @property {Object} oldLibraryItem - To be removed
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @typedef {RequestWithUser & RequestEntityObject} LibraryItemControllerRequest
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @typedef RequestLibraryFileObject
 | 
				
			||||||
 | 
					 * @property {import('../models/LibraryItem').LibraryFileObject} libraryFile
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @typedef {RequestWithUser & RequestEntityObject & RequestLibraryFileObject} LibraryItemControllerRequestWithFile
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LibraryItemController {
 | 
					class LibraryItemController {
 | 
				
			||||||
@ -35,17 +46,17 @@ class LibraryItemController {
 | 
				
			|||||||
   * ?include=progress,rssfeed,downloads,share
 | 
					   * ?include=progress,rssfeed,downloads,share
 | 
				
			||||||
   * ?expanded=1
 | 
					   * ?expanded=1
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async findOne(req, res) {
 | 
					  async findOne(req, res) {
 | 
				
			||||||
    const includeEntities = (req.query.include || '').split(',')
 | 
					    const includeEntities = (req.query.include || '').split(',')
 | 
				
			||||||
    if (req.query.expanded == 1) {
 | 
					    if (req.query.expanded == 1) {
 | 
				
			||||||
      var item = req.libraryItem.toJSONExpanded()
 | 
					      const item = req.libraryItem.toOldJSONExpanded()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Include users media progress
 | 
					      // Include users media progress
 | 
				
			||||||
      if (includeEntities.includes('progress')) {
 | 
					      if (includeEntities.includes('progress')) {
 | 
				
			||||||
        var episodeId = req.query.episode || null
 | 
					        const episodeId = req.query.episode || null
 | 
				
			||||||
        item.userMediaProgress = req.user.getOldMediaProgress(item.id, episodeId)
 | 
					        item.userMediaProgress = req.user.getOldMediaProgress(item.id, episodeId)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -68,28 +79,27 @@ class LibraryItemController {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      return res.json(item)
 | 
					      return res.json(item)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    res.json(req.libraryItem)
 | 
					    res.json(req.libraryItem.toOldJSON())
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async update(req, res) {
 | 
					  async update(req, res) {
 | 
				
			||||||
    var libraryItem = req.libraryItem
 | 
					 | 
				
			||||||
    // Item has cover and update is removing cover so purge it from cache
 | 
					    // Item has cover and update is removing cover so purge it from cache
 | 
				
			||||||
    if (libraryItem.media.coverPath && req.body.media && (req.body.media.coverPath === '' || req.body.media.coverPath === null)) {
 | 
					    if (req.libraryItem.media.coverPath && req.body.media && (req.body.media.coverPath === '' || req.body.media.coverPath === null)) {
 | 
				
			||||||
      await CacheManager.purgeCoverCache(libraryItem.id)
 | 
					      await CacheManager.purgeCoverCache(req.libraryItem.id)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const hasUpdates = libraryItem.update(req.body)
 | 
					    const hasUpdates = req.oldLibraryItem.update(req.body)
 | 
				
			||||||
    if (hasUpdates) {
 | 
					    if (hasUpdates) {
 | 
				
			||||||
      Logger.debug(`[LibraryItemController] Updated now saving`)
 | 
					      Logger.debug(`[LibraryItemController] Updated now saving`)
 | 
				
			||||||
      await Database.updateLibraryItem(libraryItem)
 | 
					      await Database.updateLibraryItem(req.oldLibraryItem)
 | 
				
			||||||
      SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
 | 
					      SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    res.json(libraryItem.toJSON())
 | 
					    res.json(req.oldLibraryItem.toJSON())
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@ -100,7 +110,7 @@ class LibraryItemController {
 | 
				
			|||||||
   *
 | 
					   *
 | 
				
			||||||
   * @this {import('../routers/ApiRouter')}
 | 
					   * @this {import('../routers/ApiRouter')}
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async delete(req, res) {
 | 
					  async delete(req, res) {
 | 
				
			||||||
@ -111,14 +121,14 @@ class LibraryItemController {
 | 
				
			|||||||
    const authorIds = []
 | 
					    const authorIds = []
 | 
				
			||||||
    const seriesIds = []
 | 
					    const seriesIds = []
 | 
				
			||||||
    if (req.libraryItem.isPodcast) {
 | 
					    if (req.libraryItem.isPodcast) {
 | 
				
			||||||
      mediaItemIds.push(...req.libraryItem.media.episodes.map((ep) => ep.id))
 | 
					      mediaItemIds.push(...req.libraryItem.media.podcastEpisodes.map((ep) => ep.id))
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      mediaItemIds.push(req.libraryItem.media.id)
 | 
					      mediaItemIds.push(req.libraryItem.media.id)
 | 
				
			||||||
      if (req.libraryItem.media.metadata.authors?.length) {
 | 
					      if (req.libraryItem.media.authors?.length) {
 | 
				
			||||||
        authorIds.push(...req.libraryItem.media.metadata.authors.map((au) => au.id))
 | 
					        authorIds.push(...req.libraryItem.media.authors.map((au) => au.id))
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (req.libraryItem.media.metadata.series?.length) {
 | 
					      if (req.libraryItem.media.series?.length) {
 | 
				
			||||||
        seriesIds.push(...req.libraryItem.media.metadata.series.map((se) => se.id))
 | 
					        seriesIds.push(...req.libraryItem.media.series.map((se) => se.id))
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -155,7 +165,7 @@ class LibraryItemController {
 | 
				
			|||||||
   * GET: /api/items/:id/download
 | 
					   * GET: /api/items/:id/download
 | 
				
			||||||
   * Download library item. Zip file if multiple files.
 | 
					   * Download library item. Zip file if multiple files.
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async download(req, res) {
 | 
					  async download(req, res) {
 | 
				
			||||||
@ -164,7 +174,7 @@ class LibraryItemController {
 | 
				
			|||||||
      return res.sendStatus(403)
 | 
					      return res.sendStatus(403)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const libraryItemPath = req.libraryItem.path
 | 
					    const libraryItemPath = req.libraryItem.path
 | 
				
			||||||
    const itemTitle = req.libraryItem.media.metadata.title
 | 
					    const itemTitle = req.libraryItem.media.title
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
 | 
					    Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -194,11 +204,10 @@ class LibraryItemController {
 | 
				
			|||||||
   *
 | 
					   *
 | 
				
			||||||
   * @this {import('../routers/ApiRouter')}
 | 
					   * @this {import('../routers/ApiRouter')}
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async updateMedia(req, res) {
 | 
					  async updateMedia(req, res) {
 | 
				
			||||||
    const libraryItem = req.libraryItem
 | 
					 | 
				
			||||||
    const mediaPayload = req.body
 | 
					    const mediaPayload = req.body
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (mediaPayload.url) {
 | 
					    if (mediaPayload.url) {
 | 
				
			||||||
@ -207,44 +216,44 @@ class LibraryItemController {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Book specific
 | 
					    // Book specific
 | 
				
			||||||
    if (libraryItem.isBook) {
 | 
					    if (req.libraryItem.isBook) {
 | 
				
			||||||
      await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId)
 | 
					      await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, req.libraryItem.libraryId)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Podcast specific
 | 
					    // Podcast specific
 | 
				
			||||||
    let isPodcastAutoDownloadUpdated = false
 | 
					    let isPodcastAutoDownloadUpdated = false
 | 
				
			||||||
    if (libraryItem.isPodcast) {
 | 
					    if (req.libraryItem.isPodcast) {
 | 
				
			||||||
      if (mediaPayload.autoDownloadEpisodes !== undefined && libraryItem.media.autoDownloadEpisodes !== mediaPayload.autoDownloadEpisodes) {
 | 
					      if (mediaPayload.autoDownloadEpisodes !== undefined && req.libraryItem.media.autoDownloadEpisodes !== mediaPayload.autoDownloadEpisodes) {
 | 
				
			||||||
        isPodcastAutoDownloadUpdated = true
 | 
					        isPodcastAutoDownloadUpdated = true
 | 
				
			||||||
      } else if (mediaPayload.autoDownloadSchedule !== undefined && libraryItem.media.autoDownloadSchedule !== mediaPayload.autoDownloadSchedule) {
 | 
					      } else if (mediaPayload.autoDownloadSchedule !== undefined && req.libraryItem.media.autoDownloadSchedule !== mediaPayload.autoDownloadSchedule) {
 | 
				
			||||||
        isPodcastAutoDownloadUpdated = true
 | 
					        isPodcastAutoDownloadUpdated = true
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Book specific - Get all series being removed from this item
 | 
					    // Book specific - Get all series being removed from this item
 | 
				
			||||||
    let seriesRemoved = []
 | 
					    let seriesRemoved = []
 | 
				
			||||||
    if (libraryItem.isBook && mediaPayload.metadata?.series) {
 | 
					    if (req.libraryItem.isBook && mediaPayload.metadata?.series) {
 | 
				
			||||||
      const seriesIdsInUpdate = mediaPayload.metadata.series?.map((se) => se.id) || []
 | 
					      const seriesIdsInUpdate = mediaPayload.metadata.series?.map((se) => se.id) || []
 | 
				
			||||||
      seriesRemoved = libraryItem.media.metadata.series.filter((se) => !seriesIdsInUpdate.includes(se.id))
 | 
					      seriesRemoved = req.oldLibraryItem.media.metadata.series.filter((se) => !seriesIdsInUpdate.includes(se.id))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let authorsRemoved = []
 | 
					    let authorsRemoved = []
 | 
				
			||||||
    if (libraryItem.isBook && mediaPayload.metadata?.authors) {
 | 
					    if (req.libraryItem.isBook && mediaPayload.metadata?.authors) {
 | 
				
			||||||
      const authorIdsInUpdate = mediaPayload.metadata.authors.map((au) => au.id)
 | 
					      const authorIdsInUpdate = mediaPayload.metadata.authors.map((au) => au.id)
 | 
				
			||||||
      authorsRemoved = libraryItem.media.metadata.authors.filter((au) => !authorIdsInUpdate.includes(au.id))
 | 
					      authorsRemoved = req.oldLibraryItem.media.metadata.authors.filter((au) => !authorIdsInUpdate.includes(au.id))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const hasUpdates = libraryItem.media.update(mediaPayload) || mediaPayload.url
 | 
					    const hasUpdates = req.oldLibraryItem.media.update(mediaPayload) || mediaPayload.url
 | 
				
			||||||
    if (hasUpdates) {
 | 
					    if (hasUpdates) {
 | 
				
			||||||
      libraryItem.updatedAt = Date.now()
 | 
					      req.oldLibraryItem.updatedAt = Date.now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (isPodcastAutoDownloadUpdated) {
 | 
					      if (isPodcastAutoDownloadUpdated) {
 | 
				
			||||||
        this.cronManager.checkUpdatePodcastCron(libraryItem)
 | 
					        this.cronManager.checkUpdatePodcastCron(req.oldLibraryItem)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
 | 
					      Logger.debug(`[LibraryItemController] Updated library item media ${req.oldLibraryItem.media.metadata.title}`)
 | 
				
			||||||
      await Database.updateLibraryItem(libraryItem)
 | 
					      await Database.updateLibraryItem(req.oldLibraryItem)
 | 
				
			||||||
      SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
 | 
					      SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (authorsRemoved.length) {
 | 
					      if (authorsRemoved.length) {
 | 
				
			||||||
        // Check remove empty authors
 | 
					        // Check remove empty authors
 | 
				
			||||||
@ -259,14 +268,14 @@ class LibraryItemController {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    res.json({
 | 
					    res.json({
 | 
				
			||||||
      updated: hasUpdates,
 | 
					      updated: hasUpdates,
 | 
				
			||||||
      libraryItem
 | 
					      libraryItem: req.oldLibraryItem
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * POST: /api/items/:id/cover
 | 
					   * POST: /api/items/:id/cover
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   * @param {boolean} [updateAndReturnJson=true]
 | 
					   * @param {boolean} [updateAndReturnJson=true]
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
@ -276,15 +285,13 @@ class LibraryItemController {
 | 
				
			|||||||
      return res.sendStatus(403)
 | 
					      return res.sendStatus(403)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let libraryItem = req.libraryItem
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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(libraryItem, req.body.url)
 | 
					      result = await CoverManager.downloadCoverFromUrl(req.oldLibraryItem, req.body.url)
 | 
				
			||||||
    } 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(libraryItem, req.files.cover)
 | 
					      result = await CoverManager.uploadCover(req.oldLibraryItem, 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')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -296,8 +303,8 @@ class LibraryItemController {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (updateAndReturnJson) {
 | 
					    if (updateAndReturnJson) {
 | 
				
			||||||
      await Database.updateLibraryItem(libraryItem)
 | 
					      await Database.updateLibraryItem(req.oldLibraryItem)
 | 
				
			||||||
      SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
 | 
					      SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
 | 
				
			||||||
      res.json({
 | 
					      res.json({
 | 
				
			||||||
        success: true,
 | 
					        success: true,
 | 
				
			||||||
        cover: result.cover
 | 
					        cover: result.cover
 | 
				
			||||||
@ -308,22 +315,21 @@ class LibraryItemController {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * PATCH: /api/items/:id/cover
 | 
					   * PATCH: /api/items/:id/cover
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async updateCover(req, res) {
 | 
					  async updateCover(req, res) {
 | 
				
			||||||
    const libraryItem = req.libraryItem
 | 
					 | 
				
			||||||
    if (!req.body.cover) {
 | 
					    if (!req.body.cover) {
 | 
				
			||||||
      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, libraryItem)
 | 
					    const validationResult = await CoverManager.validateCoverPath(req.body.cover, req.oldLibraryItem)
 | 
				
			||||||
    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(libraryItem)
 | 
					      await Database.updateLibraryItem(req.oldLibraryItem)
 | 
				
			||||||
      SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
 | 
					      SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    res.json({
 | 
					    res.json({
 | 
				
			||||||
      success: true,
 | 
					      success: true,
 | 
				
			||||||
@ -334,17 +340,15 @@ class LibraryItemController {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * DELETE: /api/items/:id/cover
 | 
					   * DELETE: /api/items/:id/cover
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async removeCover(req, res) {
 | 
					  async removeCover(req, res) {
 | 
				
			||||||
    var libraryItem = req.libraryItem
 | 
					    if (req.libraryItem.media.coverPath) {
 | 
				
			||||||
 | 
					      req.oldLibraryItem.updateMediaCover('')
 | 
				
			||||||
    if (libraryItem.media.coverPath) {
 | 
					      await CacheManager.purgeCoverCache(req.libraryItem.id)
 | 
				
			||||||
      libraryItem.updateMediaCover('')
 | 
					      await Database.updateLibraryItem(req.oldLibraryItem)
 | 
				
			||||||
      await CacheManager.purgeCoverCache(libraryItem.id)
 | 
					      SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
 | 
				
			||||||
      await Database.updateLibraryItem(libraryItem)
 | 
					 | 
				
			||||||
      SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.sendStatus(200)
 | 
					    res.sendStatus(200)
 | 
				
			||||||
@ -353,7 +357,7 @@ class LibraryItemController {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * GET: /api/items/:id/cover
 | 
					   * GET: /api/items/:id/cover
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async getCover(req, res) {
 | 
					  async getCover(req, res) {
 | 
				
			||||||
@ -395,11 +399,11 @@ class LibraryItemController {
 | 
				
			|||||||
   *
 | 
					   *
 | 
				
			||||||
   * @this {import('../routers/ApiRouter')}
 | 
					   * @this {import('../routers/ApiRouter')}
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  startPlaybackSession(req, res) {
 | 
					  startPlaybackSession(req, res) {
 | 
				
			||||||
    if (!req.libraryItem.media.numTracks) {
 | 
					    if (!req.libraryItem.hasAudioTracks) {
 | 
				
			||||||
      Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`)
 | 
					      Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`)
 | 
				
			||||||
      return res.sendStatus(404)
 | 
					      return res.sendStatus(404)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -412,18 +416,18 @@ class LibraryItemController {
 | 
				
			|||||||
   *
 | 
					   *
 | 
				
			||||||
   * @this {import('../routers/ApiRouter')}
 | 
					   * @this {import('../routers/ApiRouter')}
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  startEpisodePlaybackSession(req, res) {
 | 
					  startEpisodePlaybackSession(req, res) {
 | 
				
			||||||
    var libraryItem = req.libraryItem
 | 
					    if (!req.libraryItem.isPodcast) {
 | 
				
			||||||
    if (!libraryItem.media.numTracks) {
 | 
					      Logger.error(`[LibraryItemController] startEpisodePlaybackSession invalid media type ${req.libraryItem.id}`)
 | 
				
			||||||
      Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${libraryItem.id}`)
 | 
					      return res.sendStatus(400)
 | 
				
			||||||
      return res.sendStatus(404)
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    var episodeId = req.params.episodeId
 | 
					
 | 
				
			||||||
    if (!libraryItem.media.episodes.find((ep) => ep.id === episodeId)) {
 | 
					    const episodeId = req.params.episodeId
 | 
				
			||||||
      Logger.error(`[LibraryItemController] startPlaybackSession episode ${episodeId} not found for item ${libraryItem.id}`)
 | 
					    if (!req.libraryItem.media.podcastEpisodes.some((ep) => ep.id === episodeId)) {
 | 
				
			||||||
 | 
					      Logger.error(`[LibraryItemController] startPlaybackSession episode ${episodeId} not found for item ${req.libraryItem.id}`)
 | 
				
			||||||
      return res.sendStatus(404)
 | 
					      return res.sendStatus(404)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -433,30 +437,34 @@ class LibraryItemController {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * PATCH: /api/items/:id/tracks
 | 
					   * PATCH: /api/items/:id/tracks
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async updateTracks(req, res) {
 | 
					  async updateTracks(req, res) {
 | 
				
			||||||
    var libraryItem = req.libraryItem
 | 
					    const orderedFileData = req.body?.orderedFileData
 | 
				
			||||||
    var orderedFileData = req.body.orderedFileData
 | 
					
 | 
				
			||||||
    if (!libraryItem.media.updateAudioTracks) {
 | 
					    if (!req.libraryItem.isBook) {
 | 
				
			||||||
      Logger.error(`[LibraryItemController] updateTracks invalid media type ${libraryItem.id}`)
 | 
					      Logger.error(`[LibraryItemController] updateTracks invalid media type ${req.libraryItem.id}`)
 | 
				
			||||||
      return res.sendStatus(500)
 | 
					      return res.sendStatus(400)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    libraryItem.media.updateAudioTracks(orderedFileData)
 | 
					    if (!Array.isArray(orderedFileData) || !orderedFileData.length) {
 | 
				
			||||||
    await Database.updateLibraryItem(libraryItem)
 | 
					      Logger.error(`[LibraryItemController] updateTracks invalid orderedFileData ${req.libraryItem.id}`)
 | 
				
			||||||
    SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
 | 
					      return res.sendStatus(400)
 | 
				
			||||||
    res.json(libraryItem.toJSON())
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    req.oldLibraryItem.media.updateAudioTracks(orderedFileData)
 | 
				
			||||||
 | 
					    await Database.updateLibraryItem(req.oldLibraryItem)
 | 
				
			||||||
 | 
					    SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
 | 
				
			||||||
 | 
					    res.json(req.oldLibraryItem.toJSON())
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * POST /api/items/:id/match
 | 
					   * POST /api/items/:id/match
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async match(req, res) {
 | 
					  async match(req, res) {
 | 
				
			||||||
    const libraryItem = req.libraryItem
 | 
					 | 
				
			||||||
    const reqBody = req.body || {}
 | 
					    const reqBody = req.body || {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const options = {}
 | 
					    const options = {}
 | 
				
			||||||
@ -473,7 +481,7 @@ class LibraryItemController {
 | 
				
			|||||||
      options.overrideDetails = !!reqBody.overrideDetails
 | 
					      options.overrideDetails = !!reqBody.overrideDetails
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var matchResult = await Scanner.quickMatchLibraryItem(this, libraryItem, options)
 | 
					    var matchResult = await Scanner.quickMatchLibraryItem(this, req.oldLibraryItem, options)
 | 
				
			||||||
    res.json(matchResult)
 | 
					    res.json(matchResult)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -741,7 +749,7 @@ class LibraryItemController {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * POST: /api/items/:id/scan
 | 
					   * POST: /api/items/:id/scan
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async scan(req, res) {
 | 
					  async scan(req, res) {
 | 
				
			||||||
@ -765,7 +773,7 @@ class LibraryItemController {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * GET: /api/items/:id/metadata-object
 | 
					   * GET: /api/items/:id/metadata-object
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  getMetadataObject(req, res) {
 | 
					  getMetadataObject(req, res) {
 | 
				
			||||||
@ -774,18 +782,18 @@ class LibraryItemController {
 | 
				
			|||||||
      return res.sendStatus(403)
 | 
					      return res.sendStatus(403)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) {
 | 
					    if (req.libraryItem.isMissing || !req.libraryItem.isBook || !req.libraryItem.media.includedAudioFiles.length) {
 | 
				
			||||||
      Logger.error(`[LibraryItemController] Invalid library item`)
 | 
					      Logger.error(`[LibraryItemController] Invalid library item`)
 | 
				
			||||||
      return res.sendStatus(500)
 | 
					      return res.sendStatus(500)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.json(this.audioMetadataManager.getMetadataObjectForApi(req.libraryItem))
 | 
					    res.json(this.audioMetadataManager.getMetadataObjectForApi(req.oldLibraryItem))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * POST: /api/items/:id/chapters
 | 
					   * POST: /api/items/:id/chapters
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async updateMediaChapters(req, res) {
 | 
					  async updateMediaChapters(req, res) {
 | 
				
			||||||
@ -794,7 +802,7 @@ class LibraryItemController {
 | 
				
			|||||||
      return res.sendStatus(403)
 | 
					      return res.sendStatus(403)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) {
 | 
					    if (req.libraryItem.isMissing || !req.libraryItem.isBook || !req.libraryItem.media.includedAudioFiles.length) {
 | 
				
			||||||
      Logger.error(`[LibraryItemController] Invalid library item`)
 | 
					      Logger.error(`[LibraryItemController] Invalid library item`)
 | 
				
			||||||
      return res.sendStatus(500)
 | 
					      return res.sendStatus(500)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -805,10 +813,10 @@ class LibraryItemController {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const chapters = req.body.chapters || []
 | 
					    const chapters = req.body.chapters || []
 | 
				
			||||||
    const wasUpdated = req.libraryItem.media.updateChapters(chapters)
 | 
					    const wasUpdated = req.oldLibraryItem.media.updateChapters(chapters)
 | 
				
			||||||
    if (wasUpdated) {
 | 
					    if (wasUpdated) {
 | 
				
			||||||
      await Database.updateLibraryItem(req.libraryItem)
 | 
					      await Database.updateLibraryItem(req.oldLibraryItem)
 | 
				
			||||||
      SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
 | 
					      SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.json({
 | 
					    res.json({
 | 
				
			||||||
@ -821,7 +829,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 {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequestWithFile} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async getFFprobeData(req, res) {
 | 
					  async getFFprobeData(req, res) {
 | 
				
			||||||
@ -834,7 +842,7 @@ class LibraryItemController {
 | 
				
			|||||||
      return res.sendStatus(400)
 | 
					      return res.sendStatus(400)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const audioFile = req.libraryItem.media.findFileWithInode(req.params.fileid)
 | 
					    const audioFile = req.oldLibraryItem.media.findFileWithInode(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)
 | 
				
			||||||
@ -847,7 +855,7 @@ class LibraryItemController {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * GET api/items/:id/file/:fileid
 | 
					   * GET api/items/:id/file/:fileid
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequestWithFile} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async getLibraryFile(req, res) {
 | 
					  async getLibraryFile(req, res) {
 | 
				
			||||||
@ -870,7 +878,7 @@ class LibraryItemController {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * DELETE api/items/:id/file/:fileid
 | 
					   * DELETE api/items/:id/file/:fileid
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequestWithFile} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async deleteLibraryFile(req, res) {
 | 
					  async deleteLibraryFile(req, res) {
 | 
				
			||||||
@ -881,17 +889,17 @@ 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.libraryItem.removeLibraryFile(req.params.fileid)
 | 
					    req.oldLibraryItem.removeLibraryFile(req.params.fileid)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (req.libraryItem.media.removeFileWithInode(req.params.fileid)) {
 | 
					    if (req.oldLibraryItem.media.removeFileWithInode(req.params.fileid)) {
 | 
				
			||||||
      // If book has no more media files then mark it as missing
 | 
					      // If book has no more media files then mark it as missing
 | 
				
			||||||
      if (req.libraryItem.mediaType === 'book' && !req.libraryItem.media.hasMediaEntities) {
 | 
					      if (req.libraryItem.mediaType === 'book' && !req.libraryItem.media.hasMediaFiles) {
 | 
				
			||||||
        req.libraryItem.setMissing()
 | 
					        req.oldLibraryItem.setMissing()
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    req.libraryItem.updatedAt = Date.now()
 | 
					    req.oldLibraryItem.updatedAt = Date.now()
 | 
				
			||||||
    await Database.updateLibraryItem(req.libraryItem)
 | 
					    await Database.updateLibraryItem(req.oldLibraryItem)
 | 
				
			||||||
    SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
 | 
					    SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
 | 
				
			||||||
    res.sendStatus(200)
 | 
					    res.sendStatus(200)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -899,7 +907,7 @@ class LibraryItemController {
 | 
				
			|||||||
   * GET api/items/:id/file/:fileid/download
 | 
					   * GET api/items/:id/file/:fileid/download
 | 
				
			||||||
   * Same as GET api/items/:id/file/:fileid but allows logging and restricting downloads
 | 
					   * Same as GET api/items/:id/file/:fileid but allows logging and restricting downloads
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequestWithFile} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async downloadLibraryFile(req, res) {
 | 
					  async downloadLibraryFile(req, res) {
 | 
				
			||||||
@ -911,7 +919,7 @@ class LibraryItemController {
 | 
				
			|||||||
      return res.sendStatus(403)
 | 
					      return res.sendStatus(403)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.metadata.title}" file at "${libraryFile.metadata.path}"`)
 | 
					    Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.title}" file at "${libraryFile.metadata.path}"`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (global.XAccel) {
 | 
					    if (global.XAccel) {
 | 
				
			||||||
      const encodedURI = encodeUriPath(global.XAccel + libraryFile.metadata.path)
 | 
					      const encodedURI = encodeUriPath(global.XAccel + libraryFile.metadata.path)
 | 
				
			||||||
@ -947,28 +955,28 @@ class LibraryItemController {
 | 
				
			|||||||
   * fileid is only required when reading a supplementary ebook
 | 
					   * fileid is only required when reading a supplementary ebook
 | 
				
			||||||
   * when no fileid is passed in the primary ebook will be returned
 | 
					   * when no fileid is passed in the primary ebook will be returned
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async getEBookFile(req, res) {
 | 
					  async getEBookFile(req, res) {
 | 
				
			||||||
    let ebookFile = null
 | 
					    let ebookFile = null
 | 
				
			||||||
    if (req.params.fileid) {
 | 
					    if (req.params.fileid) {
 | 
				
			||||||
      ebookFile = req.libraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid)
 | 
					      ebookFile = req.oldLibraryItem.libraryFiles.find((lf) => lf.ino === 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.libraryItem.media.ebookFile
 | 
					      ebookFile = req.oldLibraryItem.media.ebookFile
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!ebookFile) {
 | 
					    if (!ebookFile) {
 | 
				
			||||||
      Logger.error(`[LibraryItemController] No ebookFile for library item "${req.libraryItem.media.metadata.title}"`)
 | 
					      Logger.error(`[LibraryItemController] No ebookFile for library item "${req.libraryItem.media.title}"`)
 | 
				
			||||||
      return res.sendStatus(404)
 | 
					      return res.sendStatus(404)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const ebookFilePath = ebookFile.metadata.path
 | 
					    const ebookFilePath = ebookFile.metadata.path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.metadata.title}" ebook at "${ebookFilePath}"`)
 | 
					    Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.title}" ebook at "${ebookFilePath}"`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (global.XAccel) {
 | 
					    if (global.XAccel) {
 | 
				
			||||||
      const encodedURI = encodeUriPath(global.XAccel + ebookFilePath)
 | 
					      const encodedURI = encodeUriPath(global.XAccel + ebookFilePath)
 | 
				
			||||||
@ -991,11 +999,11 @@ 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 {RequestWithUser} req
 | 
					   * @param {LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async updateEbookFileStatus(req, res) {
 | 
					  async updateEbookFileStatus(req, res) {
 | 
				
			||||||
    const ebookLibraryFile = req.libraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid)
 | 
					    const ebookLibraryFile = req.oldLibraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid)
 | 
				
			||||||
    if (!ebookLibraryFile?.isEBookFile) {
 | 
					    if (!ebookLibraryFile?.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')
 | 
				
			||||||
@ -1003,16 +1011,16 @@ class LibraryItemController {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    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.libraryItem.setPrimaryEbook(ebookLibraryFile)
 | 
					      req.oldLibraryItem.setPrimaryEbook(ebookLibraryFile)
 | 
				
			||||||
    } 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
 | 
					      ebookLibraryFile.isSupplementary = true
 | 
				
			||||||
      req.libraryItem.setPrimaryEbook(null)
 | 
					      req.oldLibraryItem.setPrimaryEbook(null)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    req.libraryItem.updatedAt = Date.now()
 | 
					    req.oldLibraryItem.updatedAt = Date.now()
 | 
				
			||||||
    await Database.updateLibraryItem(req.libraryItem)
 | 
					    await Database.updateLibraryItem(req.oldLibraryItem)
 | 
				
			||||||
    SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
 | 
					    SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded())
 | 
				
			||||||
    res.sendStatus(200)
 | 
					    res.sendStatus(200)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1023,7 +1031,8 @@ class LibraryItemController {
 | 
				
			|||||||
   * @param {NextFunction} next
 | 
					   * @param {NextFunction} next
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async middleware(req, res, next) {
 | 
					  async middleware(req, res, next) {
 | 
				
			||||||
    req.libraryItem = await Database.libraryItemModel.getOldById(req.params.id)
 | 
					    req.libraryItem = await Database.libraryItemModel.getExpandedById(req.params.id)
 | 
				
			||||||
 | 
					    req.oldLibraryItem = await Database.libraryItemModel.getOldLibraryItem(req.libraryItem)
 | 
				
			||||||
    if (!req.libraryItem?.media) return res.sendStatus(404)
 | 
					    if (!req.libraryItem?.media) return res.sendStatus(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Check user can access this library item
 | 
					    // Check user can access this library item
 | 
				
			||||||
@ -1033,7 +1042,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.libraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid)
 | 
					      req.libraryFile = req.oldLibraryItem.libraryFiles.find((lf) => lf.ino === 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)
 | 
				
			||||||
 | 
				
			|||||||
@ -39,7 +39,7 @@ class PlaybackSessionManager {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {import('../controllers/SessionController').RequestWithUser} req
 | 
					   * @param {import('../controllers/LibraryItemController').LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {Object} [clientDeviceInfo]
 | 
					   * @param {Object} [clientDeviceInfo]
 | 
				
			||||||
   * @returns {Promise<DeviceInfo>}
 | 
					   * @returns {Promise<DeviceInfo>}
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
@ -67,14 +67,14 @@ class PlaybackSessionManager {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {import('../controllers/SessionController').RequestWithUser} req
 | 
					   * @param {import('../controllers/LibraryItemController').LibraryItemControllerRequest} req
 | 
				
			||||||
   * @param {import('express').Response} res
 | 
					   * @param {import('express').Response} res
 | 
				
			||||||
   * @param {string} [episodeId]
 | 
					   * @param {string} [episodeId]
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async startSessionRequest(req, res, episodeId) {
 | 
					  async startSessionRequest(req, res, episodeId) {
 | 
				
			||||||
    const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo)
 | 
					    const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo)
 | 
				
			||||||
    Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`)
 | 
					    Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`)
 | 
				
			||||||
    const { libraryItem, body: options } = req
 | 
					    const { oldLibraryItem: libraryItem, body: options } = req
 | 
				
			||||||
    const session = await this.startSession(req.user, deviceInfo, libraryItem, episodeId, options)
 | 
					    const session = await this.startSession(req.user, deviceInfo, libraryItem, episodeId, options)
 | 
				
			||||||
    res.json(session.toJSONForClient(libraryItem))
 | 
					    res.json(session.toJSONForClient(libraryItem))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,7 @@
 | 
				
			|||||||
const { DataTypes, Model } = require('sequelize')
 | 
					const { DataTypes, Model } = require('sequelize')
 | 
				
			||||||
const Logger = require('../Logger')
 | 
					const Logger = require('../Logger')
 | 
				
			||||||
 | 
					const { getTitlePrefixAtEnd } = require('../utils')
 | 
				
			||||||
 | 
					const parseNameString = require('../utils/parsers/parseNameString')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @typedef EBookFileObject
 | 
					 * @typedef EBookFileObject
 | 
				
			||||||
@ -113,8 +115,12 @@ class Book extends Model {
 | 
				
			|||||||
    /** @type {Date} */
 | 
					    /** @type {Date} */
 | 
				
			||||||
    this.createdAt
 | 
					    this.createdAt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Expanded properties
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** @type {import('./Author')[]} - optional if expanded */
 | 
					    /** @type {import('./Author')[]} - optional if expanded */
 | 
				
			||||||
    this.authors
 | 
					    this.authors
 | 
				
			||||||
 | 
					    /** @type {import('./Series')[]} - optional if expanded */
 | 
				
			||||||
 | 
					    this.series
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static getOldBook(libraryItemExpanded) {
 | 
					  static getOldBook(libraryItemExpanded) {
 | 
				
			||||||
@ -241,32 +247,6 @@ class Book extends Model {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getAbsMetadataJson() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      tags: this.tags || [],
 | 
					 | 
				
			||||||
      chapters: this.chapters?.map((c) => ({ ...c })) || [],
 | 
					 | 
				
			||||||
      title: this.title,
 | 
					 | 
				
			||||||
      subtitle: this.subtitle,
 | 
					 | 
				
			||||||
      authors: this.authors.map((a) => a.name),
 | 
					 | 
				
			||||||
      narrators: this.narrators,
 | 
					 | 
				
			||||||
      series: this.series.map((se) => {
 | 
					 | 
				
			||||||
        const sequence = se.bookSeries?.sequence || ''
 | 
					 | 
				
			||||||
        if (!sequence) return se.name
 | 
					 | 
				
			||||||
        return `${se.name} #${sequence}`
 | 
					 | 
				
			||||||
      }),
 | 
					 | 
				
			||||||
      genres: this.genres || [],
 | 
					 | 
				
			||||||
      publishedYear: this.publishedYear,
 | 
					 | 
				
			||||||
      publishedDate: this.publishedDate,
 | 
					 | 
				
			||||||
      publisher: this.publisher,
 | 
					 | 
				
			||||||
      description: this.description,
 | 
					 | 
				
			||||||
      isbn: this.isbn,
 | 
					 | 
				
			||||||
      asin: this.asin,
 | 
					 | 
				
			||||||
      language: this.language,
 | 
					 | 
				
			||||||
      explicit: !!this.explicit,
 | 
					 | 
				
			||||||
      abridged: !!this.abridged
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Initialize model
 | 
					   * Initialize model
 | 
				
			||||||
   * @param {import('../Database').sequelize} sequelize
 | 
					   * @param {import('../Database').sequelize} sequelize
 | 
				
			||||||
@ -343,9 +323,50 @@ class Book extends Model {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    return this.authors.map((au) => au.name).join(', ')
 | 
					    return this.authors.map((au) => au.name).join(', ')
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Comma separated array of author names in Last, First format
 | 
				
			||||||
 | 
					   * Requires authors to be loaded
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @returns {string}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  get authorNameLF() {
 | 
				
			||||||
 | 
					    if (this.authors === undefined) {
 | 
				
			||||||
 | 
					      Logger.error(`[Book] authorNameLF: Cannot get authorNameLF because authors are not loaded`)
 | 
				
			||||||
 | 
					      return ''
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Last, First
 | 
				
			||||||
 | 
					    if (!this.authors.length) return ''
 | 
				
			||||||
 | 
					    return this.authors.map((au) => parseNameString.nameToLastFirst(au.name)).join(', ')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Comma separated array of series with sequence
 | 
				
			||||||
 | 
					   * Requires series to be loaded
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @returns {string}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  get seriesName() {
 | 
				
			||||||
 | 
					    if (this.series === undefined) {
 | 
				
			||||||
 | 
					      Logger.error(`[Book] seriesName: Cannot get seriesName because series are not loaded`)
 | 
				
			||||||
 | 
					      return ''
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!this.series.length) return ''
 | 
				
			||||||
 | 
					    return this.series
 | 
				
			||||||
 | 
					      .map((se) => {
 | 
				
			||||||
 | 
					        const sequence = se.bookSeries?.sequence || ''
 | 
				
			||||||
 | 
					        if (!sequence) return se.name
 | 
				
			||||||
 | 
					        return `${se.name} #${sequence}`
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .join(', ')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get includedAudioFiles() {
 | 
					  get includedAudioFiles() {
 | 
				
			||||||
    return this.audioFiles.filter((af) => !af.exclude)
 | 
					    return this.audioFiles.filter((af) => !af.exclude)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get trackList() {
 | 
					  get trackList() {
 | 
				
			||||||
    let startOffset = 0
 | 
					    let startOffset = 0
 | 
				
			||||||
    return this.includedAudioFiles.map((af) => {
 | 
					    return this.includedAudioFiles.map((af) => {
 | 
				
			||||||
@ -355,6 +376,189 @@ class Book extends Model {
 | 
				
			|||||||
      return track
 | 
					      return track
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get hasMediaFiles() {
 | 
				
			||||||
 | 
					    return !!this.hasAudioTracks || !!this.ebookFile
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get hasAudioTracks() {
 | 
				
			||||||
 | 
					    return !!this.includedAudioFiles.length
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Total file size of all audio files and ebook file
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @returns {number}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  get size() {
 | 
				
			||||||
 | 
					    let total = 0
 | 
				
			||||||
 | 
					    this.audioFiles.forEach((af) => (total += af.metadata.size))
 | 
				
			||||||
 | 
					    if (this.ebookFile) {
 | 
				
			||||||
 | 
					      total += this.ebookFile.metadata.size
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return total
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getAbsMetadataJson() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      tags: this.tags || [],
 | 
				
			||||||
 | 
					      chapters: this.chapters?.map((c) => ({ ...c })) || [],
 | 
				
			||||||
 | 
					      title: this.title,
 | 
				
			||||||
 | 
					      subtitle: this.subtitle,
 | 
				
			||||||
 | 
					      authors: this.authors.map((a) => a.name),
 | 
				
			||||||
 | 
					      narrators: this.narrators,
 | 
				
			||||||
 | 
					      series: this.series.map((se) => {
 | 
				
			||||||
 | 
					        const sequence = se.bookSeries?.sequence || ''
 | 
				
			||||||
 | 
					        if (!sequence) return se.name
 | 
				
			||||||
 | 
					        return `${se.name} #${sequence}`
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      genres: this.genres || [],
 | 
				
			||||||
 | 
					      publishedYear: this.publishedYear,
 | 
				
			||||||
 | 
					      publishedDate: this.publishedDate,
 | 
				
			||||||
 | 
					      publisher: this.publisher,
 | 
				
			||||||
 | 
					      description: this.description,
 | 
				
			||||||
 | 
					      isbn: this.isbn,
 | 
				
			||||||
 | 
					      asin: this.asin,
 | 
				
			||||||
 | 
					      language: this.language,
 | 
				
			||||||
 | 
					      explicit: !!this.explicit,
 | 
				
			||||||
 | 
					      abridged: !!this.abridged
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Old model kept metadata in a separate object
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  oldMetadataToJSON() {
 | 
				
			||||||
 | 
					    const authors = this.authors.map((au) => ({ id: au.id, name: au.name }))
 | 
				
			||||||
 | 
					    const series = this.series.map((se) => ({ id: se.id, name: se.name, sequence: se.bookSeries.sequence }))
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      title: this.title,
 | 
				
			||||||
 | 
					      subtitle: this.subtitle,
 | 
				
			||||||
 | 
					      authors,
 | 
				
			||||||
 | 
					      narrators: [...(this.narrators || [])],
 | 
				
			||||||
 | 
					      series,
 | 
				
			||||||
 | 
					      genres: [...(this.genres || [])],
 | 
				
			||||||
 | 
					      publishedYear: this.publishedYear,
 | 
				
			||||||
 | 
					      publishedDate: this.publishedDate,
 | 
				
			||||||
 | 
					      publisher: this.publisher,
 | 
				
			||||||
 | 
					      description: this.description,
 | 
				
			||||||
 | 
					      isbn: this.isbn,
 | 
				
			||||||
 | 
					      asin: this.asin,
 | 
				
			||||||
 | 
					      language: this.language,
 | 
				
			||||||
 | 
					      explicit: this.explicit,
 | 
				
			||||||
 | 
					      abridged: this.abridged
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  oldMetadataToJSONMinified() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      title: this.title,
 | 
				
			||||||
 | 
					      titleIgnorePrefix: getTitlePrefixAtEnd(this.title),
 | 
				
			||||||
 | 
					      subtitle: this.subtitle,
 | 
				
			||||||
 | 
					      authorName: this.authorName,
 | 
				
			||||||
 | 
					      authorNameLF: this.authorNameLF,
 | 
				
			||||||
 | 
					      narratorName: (this.narrators || []).join(', '),
 | 
				
			||||||
 | 
					      seriesName: this.seriesName,
 | 
				
			||||||
 | 
					      genres: [...(this.genres || [])],
 | 
				
			||||||
 | 
					      publishedYear: this.publishedYear,
 | 
				
			||||||
 | 
					      publishedDate: this.publishedDate,
 | 
				
			||||||
 | 
					      publisher: this.publisher,
 | 
				
			||||||
 | 
					      description: this.description,
 | 
				
			||||||
 | 
					      isbn: this.isbn,
 | 
				
			||||||
 | 
					      asin: this.asin,
 | 
				
			||||||
 | 
					      language: this.language,
 | 
				
			||||||
 | 
					      explicit: this.explicit,
 | 
				
			||||||
 | 
					      abridged: this.abridged
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  oldMetadataToJSONExpanded() {
 | 
				
			||||||
 | 
					    const oldMetadataJSON = this.oldMetadataToJSON()
 | 
				
			||||||
 | 
					    oldMetadataJSON.titleIgnorePrefix = getTitlePrefixAtEnd(this.title)
 | 
				
			||||||
 | 
					    oldMetadataJSON.authorName = this.authorName
 | 
				
			||||||
 | 
					    oldMetadataJSON.authorNameLF = this.authorNameLF
 | 
				
			||||||
 | 
					    oldMetadataJSON.narratorName = (this.narrators || []).join(', ')
 | 
				
			||||||
 | 
					    oldMetadataJSON.seriesName = this.seriesName
 | 
				
			||||||
 | 
					    return oldMetadataJSON
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * The old model stored a minified series and authors array with the book object.
 | 
				
			||||||
 | 
					   * Minified series is { id, name, sequence }
 | 
				
			||||||
 | 
					   * Minified author is { id, name }
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {string} libraryItemId
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  toOldJSON(libraryItemId) {
 | 
				
			||||||
 | 
					    if (!libraryItemId) {
 | 
				
			||||||
 | 
					      throw new Error(`[Book] Cannot convert to old JSON because libraryItemId is not provided`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.authors) {
 | 
				
			||||||
 | 
					      throw new Error(`[Book] Cannot convert to old JSON because authors are not loaded`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.series) {
 | 
				
			||||||
 | 
					      throw new Error(`[Book] Cannot convert to old JSON because series are not loaded`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: this.id,
 | 
				
			||||||
 | 
					      libraryItemId: libraryItemId,
 | 
				
			||||||
 | 
					      metadata: this.oldMetadataToJSON(),
 | 
				
			||||||
 | 
					      coverPath: this.coverPath,
 | 
				
			||||||
 | 
					      tags: [...(this.tags || [])],
 | 
				
			||||||
 | 
					      audioFiles: structuredClone(this.audioFiles),
 | 
				
			||||||
 | 
					      chapters: structuredClone(this.chapters),
 | 
				
			||||||
 | 
					      ebookFile: structuredClone(this.ebookFile)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toOldJSONMinified() {
 | 
				
			||||||
 | 
					    if (!this.authors) {
 | 
				
			||||||
 | 
					      throw new Error(`[Book] Cannot convert to old JSON because authors are not loaded`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.series) {
 | 
				
			||||||
 | 
					      throw new Error(`[Book] Cannot convert to old JSON because series are not loaded`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: this.id,
 | 
				
			||||||
 | 
					      metadata: this.oldMetadataToJSONMinified(),
 | 
				
			||||||
 | 
					      coverPath: this.coverPath,
 | 
				
			||||||
 | 
					      tags: [...(this.tags || [])],
 | 
				
			||||||
 | 
					      numTracks: this.trackList.length,
 | 
				
			||||||
 | 
					      numAudioFiles: this.audioFiles?.length || 0,
 | 
				
			||||||
 | 
					      numChapters: this.chapters?.length || 0,
 | 
				
			||||||
 | 
					      duration: this.duration,
 | 
				
			||||||
 | 
					      size: this.size,
 | 
				
			||||||
 | 
					      ebookFormat: this.ebookFile?.ebookFormat
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toOldJSONExpanded(libraryItemId) {
 | 
				
			||||||
 | 
					    if (!libraryItemId) {
 | 
				
			||||||
 | 
					      throw new Error(`[Book] Cannot convert to old JSON because libraryItemId is not provided`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.authors) {
 | 
				
			||||||
 | 
					      throw new Error(`[Book] Cannot convert to old JSON because authors are not loaded`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.series) {
 | 
				
			||||||
 | 
					      throw new Error(`[Book] Cannot convert to old JSON because series are not loaded`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: this.id,
 | 
				
			||||||
 | 
					      libraryItemId: libraryItemId,
 | 
				
			||||||
 | 
					      metadata: this.oldMetadataToJSONExpanded(),
 | 
				
			||||||
 | 
					      coverPath: this.coverPath,
 | 
				
			||||||
 | 
					      tags: [...(this.tags || [])],
 | 
				
			||||||
 | 
					      audioFiles: structuredClone(this.audioFiles),
 | 
				
			||||||
 | 
					      chapters: structuredClone(this.chapters),
 | 
				
			||||||
 | 
					      ebookFile: structuredClone(this.ebookFile),
 | 
				
			||||||
 | 
					      duration: this.duration,
 | 
				
			||||||
 | 
					      size: this.size,
 | 
				
			||||||
 | 
					      tracks: structuredClone(this.trackList)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = Book
 | 
					module.exports = Book
 | 
				
			||||||
 | 
				
			|||||||
@ -865,54 +865,6 @@ class LibraryItem extends Model {
 | 
				
			|||||||
    return libraryItem.media.coverPath
 | 
					    return libraryItem.media.coverPath
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   *
 | 
					 | 
				
			||||||
   * @param {import('sequelize').FindOptions} options
 | 
					 | 
				
			||||||
   * @returns {Promise<Book|Podcast>}
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  getMedia(options) {
 | 
					 | 
				
			||||||
    if (!this.mediaType) return Promise.resolve(null)
 | 
					 | 
				
			||||||
    const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaType)}`
 | 
					 | 
				
			||||||
    return this[mixinMethodName](options)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   *
 | 
					 | 
				
			||||||
   * @returns {Promise<Book|Podcast>}
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  getMediaExpanded() {
 | 
					 | 
				
			||||||
    if (this.mediaType === 'podcast') {
 | 
					 | 
				
			||||||
      return this.getMedia({
 | 
					 | 
				
			||||||
        include: [
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            model: this.sequelize.models.podcastEpisode
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      return this.getMedia({
 | 
					 | 
				
			||||||
        include: [
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            model: this.sequelize.models.author,
 | 
					 | 
				
			||||||
            through: {
 | 
					 | 
				
			||||||
              attributes: []
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            model: this.sequelize.models.series,
 | 
					 | 
				
			||||||
            through: {
 | 
					 | 
				
			||||||
              attributes: ['sequence']
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        order: [
 | 
					 | 
				
			||||||
          [this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'],
 | 
					 | 
				
			||||||
          [this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC']
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @returns {Promise}
 | 
					   * @returns {Promise}
 | 
				
			||||||
@ -1131,6 +1083,64 @@ class LibraryItem extends Model {
 | 
				
			|||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get isBook() {
 | 
				
			||||||
 | 
					    return this.mediaType === 'book'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  get isPodcast() {
 | 
				
			||||||
 | 
					    return this.mediaType === 'podcast'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  get hasAudioTracks() {
 | 
				
			||||||
 | 
					    return this.media.hasAudioTracks()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {import('sequelize').FindOptions} options
 | 
				
			||||||
 | 
					   * @returns {Promise<Book|Podcast>}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  getMedia(options) {
 | 
				
			||||||
 | 
					    if (!this.mediaType) return Promise.resolve(null)
 | 
				
			||||||
 | 
					    const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaType)}`
 | 
				
			||||||
 | 
					    return this[mixinMethodName](options)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @returns {Promise<Book|Podcast>}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  getMediaExpanded() {
 | 
				
			||||||
 | 
					    if (this.mediaType === 'podcast') {
 | 
				
			||||||
 | 
					      return this.getMedia({
 | 
				
			||||||
 | 
					        include: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            model: this.sequelize.models.podcastEpisode
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return this.getMedia({
 | 
				
			||||||
 | 
					        include: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            model: this.sequelize.models.author,
 | 
				
			||||||
 | 
					            through: {
 | 
				
			||||||
 | 
					              attributes: []
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            model: this.sequelize.models.series,
 | 
				
			||||||
 | 
					            through: {
 | 
				
			||||||
 | 
					              attributes: ['sequence']
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        order: [
 | 
				
			||||||
 | 
					          [this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'],
 | 
				
			||||||
 | 
					          [this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC']
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Check if book or podcast library item has audio tracks
 | 
					   * Check if book or podcast library item has audio tracks
 | 
				
			||||||
   * Requires expanded library item
 | 
					   * Requires expanded library item
 | 
				
			||||||
@ -1148,6 +1158,89 @@ class LibraryItem extends Model {
 | 
				
			|||||||
      return this.media.podcastEpisodes?.length > 0
 | 
					      return this.media.podcastEpisodes?.length > 0
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toOldJSON() {
 | 
				
			||||||
 | 
					    if (!this.media) {
 | 
				
			||||||
 | 
					      throw new Error(`[LibraryItem] Cannot convert to old JSON without media for library item "${this.id}"`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: this.id,
 | 
				
			||||||
 | 
					      ino: this.ino,
 | 
				
			||||||
 | 
					      oldLibraryItemId: this.extraData?.oldLibraryItemId || null,
 | 
				
			||||||
 | 
					      libraryId: this.libraryId,
 | 
				
			||||||
 | 
					      folderId: this.libraryFolderId,
 | 
				
			||||||
 | 
					      path: this.path,
 | 
				
			||||||
 | 
					      relPath: this.relPath,
 | 
				
			||||||
 | 
					      isFile: this.isFile,
 | 
				
			||||||
 | 
					      mtimeMs: this.mtime?.valueOf(),
 | 
				
			||||||
 | 
					      ctimeMs: this.ctime?.valueOf(),
 | 
				
			||||||
 | 
					      birthtimeMs: this.birthtime?.valueOf(),
 | 
				
			||||||
 | 
					      addedAt: this.createdAt.valueOf(),
 | 
				
			||||||
 | 
					      updatedAt: this.updatedAt.valueOf(),
 | 
				
			||||||
 | 
					      lastScan: this.lastScan?.valueOf(),
 | 
				
			||||||
 | 
					      scanVersion: this.lastScanVersion,
 | 
				
			||||||
 | 
					      isMissing: !!this.isMissing,
 | 
				
			||||||
 | 
					      isInvalid: !!this.isInvalid,
 | 
				
			||||||
 | 
					      mediaType: this.mediaType,
 | 
				
			||||||
 | 
					      media: this.media.toOldJSON(this.id),
 | 
				
			||||||
 | 
					      libraryFiles: structuredClone(this.libraryFiles)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toOldJSONMinified() {
 | 
				
			||||||
 | 
					    if (!this.media) {
 | 
				
			||||||
 | 
					      throw new Error(`[LibraryItem] Cannot convert to old JSON without media for library item "${this.id}"`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: this.id,
 | 
				
			||||||
 | 
					      ino: this.ino,
 | 
				
			||||||
 | 
					      oldLibraryItemId: this.extraData?.oldLibraryItemId || null,
 | 
				
			||||||
 | 
					      libraryId: this.libraryId,
 | 
				
			||||||
 | 
					      folderId: this.libraryFolderId,
 | 
				
			||||||
 | 
					      path: this.path,
 | 
				
			||||||
 | 
					      relPath: this.relPath,
 | 
				
			||||||
 | 
					      isFile: this.isFile,
 | 
				
			||||||
 | 
					      mtimeMs: this.mtime?.valueOf(),
 | 
				
			||||||
 | 
					      ctimeMs: this.ctime?.valueOf(),
 | 
				
			||||||
 | 
					      birthtimeMs: this.birthtime?.valueOf(),
 | 
				
			||||||
 | 
					      addedAt: this.createdAt.valueOf(),
 | 
				
			||||||
 | 
					      updatedAt: this.updatedAt.valueOf(),
 | 
				
			||||||
 | 
					      isMissing: !!this.isMissing,
 | 
				
			||||||
 | 
					      isInvalid: !!this.isInvalid,
 | 
				
			||||||
 | 
					      mediaType: this.mediaType,
 | 
				
			||||||
 | 
					      media: this.media.toOldJSONMinified(),
 | 
				
			||||||
 | 
					      numFiles: this.libraryFiles.length,
 | 
				
			||||||
 | 
					      size: this.size
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toOldJSONExpanded() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: this.id,
 | 
				
			||||||
 | 
					      ino: this.ino,
 | 
				
			||||||
 | 
					      oldLibraryItemId: this.extraData?.oldLibraryItemId || null,
 | 
				
			||||||
 | 
					      libraryId: this.libraryId,
 | 
				
			||||||
 | 
					      folderId: this.libraryFolderId,
 | 
				
			||||||
 | 
					      path: this.path,
 | 
				
			||||||
 | 
					      relPath: this.relPath,
 | 
				
			||||||
 | 
					      isFile: this.isFile,
 | 
				
			||||||
 | 
					      mtimeMs: this.mtime?.valueOf(),
 | 
				
			||||||
 | 
					      ctimeMs: this.ctime?.valueOf(),
 | 
				
			||||||
 | 
					      birthtimeMs: this.birthtime?.valueOf(),
 | 
				
			||||||
 | 
					      addedAt: this.createdAt.valueOf(),
 | 
				
			||||||
 | 
					      updatedAt: this.updatedAt.valueOf(),
 | 
				
			||||||
 | 
					      lastScan: this.lastScan?.valueOf(),
 | 
				
			||||||
 | 
					      scanVersion: this.lastScanVersion,
 | 
				
			||||||
 | 
					      isMissing: !!this.isMissing,
 | 
				
			||||||
 | 
					      isInvalid: !!this.isInvalid,
 | 
				
			||||||
 | 
					      mediaType: this.mediaType,
 | 
				
			||||||
 | 
					      media: this.media.toOldJSONExpanded(this.id),
 | 
				
			||||||
 | 
					      libraryFiles: structuredClone(this.libraryFiles),
 | 
				
			||||||
 | 
					      size: this.size
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = LibraryItem
 | 
					module.exports = LibraryItem
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
const { DataTypes, Model } = require('sequelize')
 | 
					const { DataTypes, Model } = require('sequelize')
 | 
				
			||||||
 | 
					const { getTitlePrefixAtEnd } = require('../utils')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @typedef PodcastExpandedProperties
 | 
					 * @typedef PodcastExpandedProperties
 | 
				
			||||||
@ -47,6 +48,8 @@ class Podcast extends Model {
 | 
				
			|||||||
    this.lastEpisodeCheck
 | 
					    this.lastEpisodeCheck
 | 
				
			||||||
    /** @type {number} */
 | 
					    /** @type {number} */
 | 
				
			||||||
    this.maxEpisodesToKeep
 | 
					    this.maxEpisodesToKeep
 | 
				
			||||||
 | 
					    /** @type {number} */
 | 
				
			||||||
 | 
					    this.maxNewEpisodesToDownload
 | 
				
			||||||
    /** @type {string} */
 | 
					    /** @type {string} */
 | 
				
			||||||
    this.coverPath
 | 
					    this.coverPath
 | 
				
			||||||
    /** @type {string[]} */
 | 
					    /** @type {string[]} */
 | 
				
			||||||
@ -57,6 +60,9 @@ class Podcast extends Model {
 | 
				
			|||||||
    this.createdAt
 | 
					    this.createdAt
 | 
				
			||||||
    /** @type {Date} */
 | 
					    /** @type {Date} */
 | 
				
			||||||
    this.updatedAt
 | 
					    this.updatedAt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @type {import('./PodcastEpisode')[]} */
 | 
				
			||||||
 | 
					    this.podcastEpisodes
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static getOldPodcast(libraryItemExpanded) {
 | 
					  static getOldPodcast(libraryItemExpanded) {
 | 
				
			||||||
@ -119,25 +125,6 @@ class Podcast extends Model {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getAbsMetadataJson() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      tags: this.tags || [],
 | 
					 | 
				
			||||||
      title: this.title,
 | 
					 | 
				
			||||||
      author: this.author,
 | 
					 | 
				
			||||||
      description: this.description,
 | 
					 | 
				
			||||||
      releaseDate: this.releaseDate,
 | 
					 | 
				
			||||||
      genres: this.genres || [],
 | 
					 | 
				
			||||||
      feedURL: this.feedURL,
 | 
					 | 
				
			||||||
      imageURL: this.imageURL,
 | 
					 | 
				
			||||||
      itunesPageURL: this.itunesPageURL,
 | 
					 | 
				
			||||||
      itunesId: this.itunesId,
 | 
					 | 
				
			||||||
      itunesArtistId: this.itunesArtistId,
 | 
					 | 
				
			||||||
      language: this.language,
 | 
					 | 
				
			||||||
      explicit: !!this.explicit,
 | 
					 | 
				
			||||||
      podcastType: this.podcastType
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Initialize model
 | 
					   * Initialize model
 | 
				
			||||||
   * @param {import('../Database').sequelize} sequelize
 | 
					   * @param {import('../Database').sequelize} sequelize
 | 
				
			||||||
@ -179,6 +166,134 @@ class Podcast extends Model {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get hasMediaFiles() {
 | 
				
			||||||
 | 
					    return !!this.podcastEpisodes?.length
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get hasAudioTracks() {
 | 
				
			||||||
 | 
					    return this.hasMediaFiles
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get size() {
 | 
				
			||||||
 | 
					    if (!this.podcastEpisodes?.length) return 0
 | 
				
			||||||
 | 
					    return this.podcastEpisodes.reduce((total, episode) => total + episode.size, 0)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getAbsMetadataJson() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      tags: this.tags || [],
 | 
				
			||||||
 | 
					      title: this.title,
 | 
				
			||||||
 | 
					      author: this.author,
 | 
				
			||||||
 | 
					      description: this.description,
 | 
				
			||||||
 | 
					      releaseDate: this.releaseDate,
 | 
				
			||||||
 | 
					      genres: this.genres || [],
 | 
				
			||||||
 | 
					      feedURL: this.feedURL,
 | 
				
			||||||
 | 
					      imageURL: this.imageURL,
 | 
				
			||||||
 | 
					      itunesPageURL: this.itunesPageURL,
 | 
				
			||||||
 | 
					      itunesId: this.itunesId,
 | 
				
			||||||
 | 
					      itunesArtistId: this.itunesArtistId,
 | 
				
			||||||
 | 
					      language: this.language,
 | 
				
			||||||
 | 
					      explicit: !!this.explicit,
 | 
				
			||||||
 | 
					      podcastType: this.podcastType
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Old model kept metadata in a separate object
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  oldMetadataToJSON() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      title: this.title,
 | 
				
			||||||
 | 
					      author: this.author,
 | 
				
			||||||
 | 
					      description: this.description,
 | 
				
			||||||
 | 
					      releaseDate: this.releaseDate,
 | 
				
			||||||
 | 
					      genres: [...(this.genres || [])],
 | 
				
			||||||
 | 
					      feedUrl: this.feedURL,
 | 
				
			||||||
 | 
					      imageUrl: this.imageURL,
 | 
				
			||||||
 | 
					      itunesPageUrl: this.itunesPageURL,
 | 
				
			||||||
 | 
					      itunesId: this.itunesId,
 | 
				
			||||||
 | 
					      itunesArtistId: this.itunesArtistId,
 | 
				
			||||||
 | 
					      explicit: this.explicit,
 | 
				
			||||||
 | 
					      language: this.language,
 | 
				
			||||||
 | 
					      type: this.podcastType
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  oldMetadataToJSONExpanded() {
 | 
				
			||||||
 | 
					    const oldMetadataJSON = this.oldMetadataToJSON()
 | 
				
			||||||
 | 
					    oldMetadataJSON.titleIgnorePrefix = getTitlePrefixAtEnd(this.title)
 | 
				
			||||||
 | 
					    return oldMetadataJSON
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * The old model stored episodes with the podcast object
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {string} libraryItemId
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  toOldJSON(libraryItemId) {
 | 
				
			||||||
 | 
					    if (!libraryItemId) {
 | 
				
			||||||
 | 
					      throw new Error(`[Podcast] Cannot convert to old JSON because libraryItemId is not provided`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.podcastEpisodes) {
 | 
				
			||||||
 | 
					      throw new Error(`[Podcast] Cannot convert to old JSON because episodes are not provided`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: this.id,
 | 
				
			||||||
 | 
					      libraryItemId: libraryItemId,
 | 
				
			||||||
 | 
					      metadata: this.oldMetadataToJSON(),
 | 
				
			||||||
 | 
					      coverPath: this.coverPath,
 | 
				
			||||||
 | 
					      tags: [...(this.tags || [])],
 | 
				
			||||||
 | 
					      episodes: this.podcastEpisodes.map((episode) => episode.toOldJSON(libraryItemId)),
 | 
				
			||||||
 | 
					      autoDownloadEpisodes: this.autoDownloadEpisodes,
 | 
				
			||||||
 | 
					      autoDownloadSchedule: this.autoDownloadSchedule,
 | 
				
			||||||
 | 
					      lastEpisodeCheck: this.lastEpisodeCheck?.valueOf() || null,
 | 
				
			||||||
 | 
					      maxEpisodesToKeep: this.maxEpisodesToKeep,
 | 
				
			||||||
 | 
					      maxNewEpisodesToDownload: this.maxNewEpisodesToDownload
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toOldJSONMinified() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: this.id,
 | 
				
			||||||
 | 
					      // Minified metadata and expanded metadata are the same
 | 
				
			||||||
 | 
					      metadata: this.oldMetadataToJSONExpanded(),
 | 
				
			||||||
 | 
					      coverPath: this.coverPath,
 | 
				
			||||||
 | 
					      tags: [...(this.tags || [])],
 | 
				
			||||||
 | 
					      numEpisodes: this.podcastEpisodes?.length || 0,
 | 
				
			||||||
 | 
					      autoDownloadEpisodes: this.autoDownloadEpisodes,
 | 
				
			||||||
 | 
					      autoDownloadSchedule: this.autoDownloadSchedule,
 | 
				
			||||||
 | 
					      lastEpisodeCheck: this.lastEpisodeCheck?.valueOf() || null,
 | 
				
			||||||
 | 
					      maxEpisodesToKeep: this.maxEpisodesToKeep,
 | 
				
			||||||
 | 
					      maxNewEpisodesToDownload: this.maxNewEpisodesToDownload,
 | 
				
			||||||
 | 
					      size: this.size
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toOldJSONExpanded(libraryItemId) {
 | 
				
			||||||
 | 
					    if (!libraryItemId) {
 | 
				
			||||||
 | 
					      throw new Error(`[Podcast] Cannot convert to old JSON because libraryItemId is not provided`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.podcastEpisodes) {
 | 
				
			||||||
 | 
					      throw new Error(`[Podcast] Cannot convert to old JSON because episodes are not provided`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: this.id,
 | 
				
			||||||
 | 
					      libraryItemId: libraryItemId,
 | 
				
			||||||
 | 
					      metadata: this.oldMetadataToJSONExpanded(),
 | 
				
			||||||
 | 
					      coverPath: this.coverPath,
 | 
				
			||||||
 | 
					      tags: [...(this.tags || [])],
 | 
				
			||||||
 | 
					      episodes: this.podcastEpisodes.map((e) => e.toOldJSONExpanded(libraryItemId)),
 | 
				
			||||||
 | 
					      autoDownloadEpisodes: this.autoDownloadEpisodes,
 | 
				
			||||||
 | 
					      autoDownloadSchedule: this.autoDownloadSchedule,
 | 
				
			||||||
 | 
					      lastEpisodeCheck: this.lastEpisodeCheck?.valueOf() || null,
 | 
				
			||||||
 | 
					      maxEpisodesToKeep: this.maxEpisodesToKeep,
 | 
				
			||||||
 | 
					      maxNewEpisodesToDownload: this.maxNewEpisodesToDownload,
 | 
				
			||||||
 | 
					      size: this.size
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = Podcast
 | 
					module.exports = Podcast
 | 
				
			||||||
 | 
				
			|||||||
@ -53,42 +53,6 @@ class PodcastEpisode extends Model {
 | 
				
			|||||||
    this.updatedAt
 | 
					    this.updatedAt
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * @param {string} libraryItemId
 | 
					 | 
				
			||||||
   * @returns {oldPodcastEpisode}
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  getOldPodcastEpisode(libraryItemId = null) {
 | 
					 | 
				
			||||||
    let enclosure = null
 | 
					 | 
				
			||||||
    if (this.enclosureURL) {
 | 
					 | 
				
			||||||
      enclosure = {
 | 
					 | 
				
			||||||
        url: this.enclosureURL,
 | 
					 | 
				
			||||||
        type: this.enclosureType,
 | 
					 | 
				
			||||||
        length: this.enclosureSize !== null ? String(this.enclosureSize) : null
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return new oldPodcastEpisode({
 | 
					 | 
				
			||||||
      libraryItemId: libraryItemId || null,
 | 
					 | 
				
			||||||
      podcastId: this.podcastId,
 | 
					 | 
				
			||||||
      id: this.id,
 | 
					 | 
				
			||||||
      oldEpisodeId: this.extraData?.oldEpisodeId || null,
 | 
					 | 
				
			||||||
      index: this.index,
 | 
					 | 
				
			||||||
      season: this.season,
 | 
					 | 
				
			||||||
      episode: this.episode,
 | 
					 | 
				
			||||||
      episodeType: this.episodeType,
 | 
					 | 
				
			||||||
      title: this.title,
 | 
					 | 
				
			||||||
      subtitle: this.subtitle,
 | 
					 | 
				
			||||||
      description: this.description,
 | 
					 | 
				
			||||||
      enclosure,
 | 
					 | 
				
			||||||
      guid: this.extraData?.guid || null,
 | 
					 | 
				
			||||||
      pubDate: this.pubDate,
 | 
					 | 
				
			||||||
      chapters: this.chapters,
 | 
					 | 
				
			||||||
      audioFile: this.audioFile,
 | 
					 | 
				
			||||||
      publishedAt: this.publishedAt?.valueOf() || null,
 | 
					 | 
				
			||||||
      addedAt: this.createdAt.valueOf(),
 | 
					 | 
				
			||||||
      updatedAt: this.updatedAt.valueOf()
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static createFromOld(oldEpisode) {
 | 
					  static createFromOld(oldEpisode) {
 | 
				
			||||||
    const podcastEpisode = this.getFromOld(oldEpisode)
 | 
					    const podcastEpisode = this.getFromOld(oldEpisode)
 | 
				
			||||||
    return this.create(podcastEpisode)
 | 
					    return this.create(podcastEpisode)
 | 
				
			||||||
@ -184,7 +148,51 @@ class PodcastEpisode extends Model {
 | 
				
			|||||||
    return track
 | 
					    return track
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get size() {
 | 
				
			||||||
 | 
					    return this.audioFile?.metadata.size || 0
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * @param {string} libraryItemId
 | 
				
			||||||
 | 
					   * @returns {oldPodcastEpisode}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  getOldPodcastEpisode(libraryItemId = null) {
 | 
				
			||||||
 | 
					    let enclosure = null
 | 
				
			||||||
 | 
					    if (this.enclosureURL) {
 | 
				
			||||||
 | 
					      enclosure = {
 | 
				
			||||||
 | 
					        url: this.enclosureURL,
 | 
				
			||||||
 | 
					        type: this.enclosureType,
 | 
				
			||||||
 | 
					        length: this.enclosureSize !== null ? String(this.enclosureSize) : null
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return new oldPodcastEpisode({
 | 
				
			||||||
 | 
					      libraryItemId: libraryItemId || null,
 | 
				
			||||||
 | 
					      podcastId: this.podcastId,
 | 
				
			||||||
 | 
					      id: this.id,
 | 
				
			||||||
 | 
					      oldEpisodeId: this.extraData?.oldEpisodeId || null,
 | 
				
			||||||
 | 
					      index: this.index,
 | 
				
			||||||
 | 
					      season: this.season,
 | 
				
			||||||
 | 
					      episode: this.episode,
 | 
				
			||||||
 | 
					      episodeType: this.episodeType,
 | 
				
			||||||
 | 
					      title: this.title,
 | 
				
			||||||
 | 
					      subtitle: this.subtitle,
 | 
				
			||||||
 | 
					      description: this.description,
 | 
				
			||||||
 | 
					      enclosure,
 | 
				
			||||||
 | 
					      guid: this.extraData?.guid || null,
 | 
				
			||||||
 | 
					      pubDate: this.pubDate,
 | 
				
			||||||
 | 
					      chapters: this.chapters,
 | 
				
			||||||
 | 
					      audioFile: this.audioFile,
 | 
				
			||||||
 | 
					      publishedAt: this.publishedAt?.valueOf() || null,
 | 
				
			||||||
 | 
					      addedAt: this.createdAt.valueOf(),
 | 
				
			||||||
 | 
					      updatedAt: this.updatedAt.valueOf()
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  toOldJSON(libraryItemId) {
 | 
					  toOldJSON(libraryItemId) {
 | 
				
			||||||
 | 
					    if (!libraryItemId) {
 | 
				
			||||||
 | 
					      throw new Error(`[PodcastEpisode] Cannot convert to old JSON because libraryItemId is not provided`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let enclosure = null
 | 
					    let enclosure = null
 | 
				
			||||||
    if (this.enclosureURL) {
 | 
					    if (this.enclosureURL) {
 | 
				
			||||||
      enclosure = {
 | 
					      enclosure = {
 | 
				
			||||||
@ -209,8 +217,8 @@ class PodcastEpisode extends Model {
 | 
				
			|||||||
      enclosure,
 | 
					      enclosure,
 | 
				
			||||||
      guid: this.extraData?.guid || null,
 | 
					      guid: this.extraData?.guid || null,
 | 
				
			||||||
      pubDate: this.pubDate,
 | 
					      pubDate: this.pubDate,
 | 
				
			||||||
      chapters: this.chapters?.map((ch) => ({ ...ch })) || [],
 | 
					      chapters: structuredClone(this.chapters),
 | 
				
			||||||
      audioFile: this.audioFile || null,
 | 
					      audioFile: structuredClone(this.audioFile),
 | 
				
			||||||
      publishedAt: this.publishedAt?.valueOf() || null,
 | 
					      publishedAt: this.publishedAt?.valueOf() || null,
 | 
				
			||||||
      addedAt: this.createdAt.valueOf(),
 | 
					      addedAt: this.createdAt.valueOf(),
 | 
				
			||||||
      updatedAt: this.updatedAt.valueOf()
 | 
					      updatedAt: this.updatedAt.valueOf()
 | 
				
			||||||
@ -221,7 +229,7 @@ class PodcastEpisode extends Model {
 | 
				
			|||||||
    const json = this.toOldJSON(libraryItemId)
 | 
					    const json = this.toOldJSON(libraryItemId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    json.audioTrack = this.track
 | 
					    json.audioTrack = this.track
 | 
				
			||||||
    json.size = this.audioFile?.metadata.size || 0
 | 
					    json.size = this.size
 | 
				
			||||||
    json.duration = this.audioFile?.duration || 0
 | 
					    json.duration = this.audioFile?.duration || 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return json
 | 
					    return json
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user