mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge pull request #3789 from advplyr/migrate-podcasts-new-library-item
Update podcasts to new library item model
This commit is contained in:
		
						commit
						4a398f6113
					
				@ -170,6 +170,12 @@ export default {
 | 
				
			|||||||
        this.show = false
 | 
					        this.show = false
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    libraryItemUpdated(libraryItem) {
 | 
				
			||||||
 | 
					      const episode = libraryItem.media.episodes.find((e) => e.id === this.selectedEpisodeId)
 | 
				
			||||||
 | 
					      if (episode) {
 | 
				
			||||||
 | 
					        this.episodeItem = episode
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    hotkey(action) {
 | 
					    hotkey(action) {
 | 
				
			||||||
      if (action === this.$hotkeys.Modal.NEXT_PAGE) {
 | 
					      if (action === this.$hotkeys.Modal.NEXT_PAGE) {
 | 
				
			||||||
        this.goNextEpisode()
 | 
					        this.goNextEpisode()
 | 
				
			||||||
@ -178,9 +184,15 @@ export default {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    registerListeners() {
 | 
					    registerListeners() {
 | 
				
			||||||
 | 
					      if (this.libraryItem) {
 | 
				
			||||||
 | 
					        this.$eventBus.$on(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      this.$eventBus.$on('modal-hotkey', this.hotkey)
 | 
					      this.$eventBus.$on('modal-hotkey', this.hotkey)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    unregisterListeners() {
 | 
					    unregisterListeners() {
 | 
				
			||||||
 | 
					      if (this.libraryItem) {
 | 
				
			||||||
 | 
					        this.$eventBus.$on(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      this.$eventBus.$off('modal-hotkey', this.hotkey)
 | 
					      this.$eventBus.$off('modal-hotkey', this.hotkey)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
				
			|||||||
@ -163,13 +163,10 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      this.isProcessing = false
 | 
					      this.isProcessing = false
 | 
				
			||||||
      if (updateResult) {
 | 
					      if (updateResult) {
 | 
				
			||||||
        if (updateResult) {
 | 
					        this.$toast.success(this.$strings.ToastItemUpdateSuccess)
 | 
				
			||||||
          this.$toast.success(this.$strings.ToastItemUpdateSuccess)
 | 
					        return true
 | 
				
			||||||
          return true
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return false
 | 
					      return false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
				
			|||||||
@ -19,6 +19,11 @@ const LibraryItem = require('../objects/LibraryItem')
 | 
				
			|||||||
 * @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
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @typedef {RequestWithUser & RequestEntityObject} RequestWithLibraryItem
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PodcastController {
 | 
					class PodcastController {
 | 
				
			||||||
@ -112,11 +117,6 @@ class PodcastController {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    res.json(libraryItem.toJSONExpanded())
 | 
					    res.json(libraryItem.toJSONExpanded())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (payload.episodesToDownload?.length) {
 | 
					 | 
				
			||||||
      Logger.info(`[PodcastController] Podcast created now starting ${payload.episodesToDownload.length} episode downloads`)
 | 
					 | 
				
			||||||
      this.podcastManager.downloadPodcastEpisodes(libraryItem, payload.episodesToDownload)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Turn on podcast auto download cron if not already on
 | 
					    // Turn on podcast auto download cron if not already on
 | 
				
			||||||
    if (libraryItem.media.autoDownloadEpisodes) {
 | 
					    if (libraryItem.media.autoDownloadEpisodes) {
 | 
				
			||||||
      this.cronManager.checkUpdatePodcastCron(libraryItem)
 | 
					      this.cronManager.checkUpdatePodcastCron(libraryItem)
 | 
				
			||||||
@ -213,7 +213,7 @@ class PodcastController {
 | 
				
			|||||||
   *
 | 
					   *
 | 
				
			||||||
   * @this import('../routers/ApiRouter')
 | 
					   * @this import('../routers/ApiRouter')
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {RequestWithLibraryItem} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async checkNewEpisodes(req, res) {
 | 
					  async checkNewEpisodes(req, res) {
 | 
				
			||||||
@ -222,15 +222,14 @@ class PodcastController {
 | 
				
			|||||||
      return res.sendStatus(403)
 | 
					      return res.sendStatus(403)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var libraryItem = req.libraryItem
 | 
					    if (!req.libraryItem.media.feedURL) {
 | 
				
			||||||
    if (!libraryItem.media.metadata.feedUrl) {
 | 
					      Logger.error(`[PodcastController] checkNewEpisodes no feed url for item ${req.libraryItem.id}`)
 | 
				
			||||||
      Logger.error(`[PodcastController] checkNewEpisodes no feed url for item ${libraryItem.id}`)
 | 
					      return res.status(400).send('Podcast has no rss feed url')
 | 
				
			||||||
      return res.status(500).send('Podcast has no rss feed url')
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const maxEpisodesToDownload = !isNaN(req.query.limit) ? Number(req.query.limit) : 3
 | 
					    const maxEpisodesToDownload = !isNaN(req.query.limit) ? Number(req.query.limit) : 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var newEpisodes = await this.podcastManager.checkAndDownloadNewEpisodes(libraryItem, maxEpisodesToDownload)
 | 
					    const newEpisodes = await this.podcastManager.checkAndDownloadNewEpisodes(req.libraryItem, maxEpisodesToDownload)
 | 
				
			||||||
    res.json({
 | 
					    res.json({
 | 
				
			||||||
      episodes: newEpisodes || []
 | 
					      episodes: newEpisodes || []
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
@ -258,23 +257,28 @@ class PodcastController {
 | 
				
			|||||||
   *
 | 
					   *
 | 
				
			||||||
   * @this {import('../routers/ApiRouter')}
 | 
					   * @this {import('../routers/ApiRouter')}
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {RequestWithLibraryItem} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  getEpisodeDownloads(req, res) {
 | 
					  getEpisodeDownloads(req, res) {
 | 
				
			||||||
    var libraryItem = req.libraryItem
 | 
					    const downloadsInQueue = this.podcastManager.getEpisodeDownloadsInQueue(req.libraryItem.id)
 | 
				
			||||||
 | 
					 | 
				
			||||||
    var downloadsInQueue = this.podcastManager.getEpisodeDownloadsInQueue(libraryItem.id)
 | 
					 | 
				
			||||||
    res.json({
 | 
					    res.json({
 | 
				
			||||||
      downloads: downloadsInQueue.map((d) => d.toJSONForClient())
 | 
					      downloads: downloadsInQueue.map((d) => d.toJSONForClient())
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * GET: /api/podcasts/:id/search-episode
 | 
				
			||||||
 | 
					   * Search for an episode in a podcast
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {RequestWithLibraryItem} req
 | 
				
			||||||
 | 
					   * @param {Response} res
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
  async findEpisode(req, res) {
 | 
					  async findEpisode(req, res) {
 | 
				
			||||||
    const rssFeedUrl = req.libraryItem.media.metadata.feedUrl
 | 
					    const rssFeedUrl = req.libraryItem.media.feedURL
 | 
				
			||||||
    if (!rssFeedUrl) {
 | 
					    if (!rssFeedUrl) {
 | 
				
			||||||
      Logger.error(`[PodcastController] findEpisode: Podcast has no feed url`)
 | 
					      Logger.error(`[PodcastController] findEpisode: Podcast has no feed url`)
 | 
				
			||||||
      return res.status(500).send('Podcast does not have an RSS feed URL')
 | 
					      return res.status(400).send('Podcast does not have an RSS feed URL')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const searchTitle = req.query.title
 | 
					    const searchTitle = req.query.title
 | 
				
			||||||
@ -292,7 +296,7 @@ class PodcastController {
 | 
				
			|||||||
   *
 | 
					   *
 | 
				
			||||||
   * @this {import('../routers/ApiRouter')}
 | 
					   * @this {import('../routers/ApiRouter')}
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {RequestWithLibraryItem} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async downloadEpisodes(req, res) {
 | 
					  async downloadEpisodes(req, res) {
 | 
				
			||||||
@ -300,13 +304,13 @@ class PodcastController {
 | 
				
			|||||||
      Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to download episodes`)
 | 
					      Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to download episodes`)
 | 
				
			||||||
      return res.sendStatus(403)
 | 
					      return res.sendStatus(403)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const libraryItem = req.libraryItem
 | 
					
 | 
				
			||||||
    const episodes = req.body
 | 
					    const episodes = req.body
 | 
				
			||||||
    if (!episodes?.length) {
 | 
					    if (!Array.isArray(episodes) || !episodes.length) {
 | 
				
			||||||
      return res.sendStatus(400)
 | 
					      return res.sendStatus(400)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.podcastManager.downloadPodcastEpisodes(libraryItem, episodes)
 | 
					    this.podcastManager.downloadPodcastEpisodes(req.libraryItem, episodes)
 | 
				
			||||||
    res.sendStatus(200)
 | 
					    res.sendStatus(200)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -315,7 +319,7 @@ class PodcastController {
 | 
				
			|||||||
   *
 | 
					   *
 | 
				
			||||||
   * @this {import('../routers/ApiRouter')}
 | 
					   * @this {import('../routers/ApiRouter')}
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {RequestWithLibraryItem} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async quickMatchEpisodes(req, res) {
 | 
					  async quickMatchEpisodes(req, res) {
 | 
				
			||||||
@ -325,10 +329,11 @@ class PodcastController {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const overrideDetails = req.query.override === '1'
 | 
					    const overrideDetails = req.query.override === '1'
 | 
				
			||||||
    const episodesUpdated = await Scanner.quickMatchPodcastEpisodes(req.libraryItem, { overrideDetails })
 | 
					    const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(req.libraryItem)
 | 
				
			||||||
 | 
					    const episodesUpdated = await Scanner.quickMatchPodcastEpisodes(oldLibraryItem, { overrideDetails })
 | 
				
			||||||
    if (episodesUpdated) {
 | 
					    if (episodesUpdated) {
 | 
				
			||||||
      await Database.updateLibraryItem(req.libraryItem)
 | 
					      await Database.updateLibraryItem(oldLibraryItem)
 | 
				
			||||||
      SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
 | 
					      SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.json({
 | 
					    res.json({
 | 
				
			||||||
@ -339,58 +344,76 @@ class PodcastController {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * PATCH: /api/podcasts/:id/episode/:episodeId
 | 
					   * PATCH: /api/podcasts/:id/episode/:episodeId
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {RequestWithLibraryItem} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async updateEpisode(req, res) {
 | 
					  async updateEpisode(req, res) {
 | 
				
			||||||
    const libraryItem = req.libraryItem
 | 
					    /** @type {import('../models/PodcastEpisode')} */
 | 
				
			||||||
 | 
					    const episode = req.libraryItem.media.podcastEpisodes.find((ep) => ep.id === req.params.episodeId)
 | 
				
			||||||
    var episodeId = req.params.episodeId
 | 
					    if (!episode) {
 | 
				
			||||||
    if (!libraryItem.media.checkHasEpisode(episodeId)) {
 | 
					 | 
				
			||||||
      return res.status(404).send('Episode not found')
 | 
					      return res.status(404).send('Episode not found')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (libraryItem.media.updateEpisode(episodeId, req.body)) {
 | 
					    const updatePayload = {}
 | 
				
			||||||
      await Database.updateLibraryItem(libraryItem)
 | 
					    const supportedStringKeys = ['title', 'subtitle', 'description', 'pubDate', 'episode', 'season', 'episodeType']
 | 
				
			||||||
      SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
 | 
					    for (const key in req.body) {
 | 
				
			||||||
 | 
					      if (supportedStringKeys.includes(key) && typeof req.body[key] === 'string') {
 | 
				
			||||||
 | 
					        updatePayload[key] = req.body[key]
 | 
				
			||||||
 | 
					      } else if (key === 'chapters' && Array.isArray(req.body[key]) && req.body[key].every((ch) => typeof ch === 'object' && ch.title && ch.start)) {
 | 
				
			||||||
 | 
					        updatePayload[key] = req.body[key]
 | 
				
			||||||
 | 
					      } else if (key === 'publishedAt' && typeof req.body[key] === 'number') {
 | 
				
			||||||
 | 
					        updatePayload[key] = req.body[key]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.json(libraryItem.toJSONExpanded())
 | 
					    if (Object.keys(updatePayload).length) {
 | 
				
			||||||
 | 
					      episode.set(updatePayload)
 | 
				
			||||||
 | 
					      if (episode.changed()) {
 | 
				
			||||||
 | 
					        Logger.info(`[PodcastController] Updated episode "${episode.title}" keys`, episode.changed())
 | 
				
			||||||
 | 
					        await episode.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        Logger.info(`[PodcastController] No changes to episode "${episode.title}"`)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json(req.libraryItem.toOldJSONExpanded())
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * GET: /api/podcasts/:id/episode/:episodeId
 | 
					   * GET: /api/podcasts/:id/episode/:episodeId
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {RequestWithLibraryItem} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async getEpisode(req, res) {
 | 
					  async getEpisode(req, res) {
 | 
				
			||||||
    const episodeId = req.params.episodeId
 | 
					    const episodeId = req.params.episodeId
 | 
				
			||||||
    const libraryItem = req.libraryItem
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const episode = libraryItem.media.episodes.find((ep) => ep.id === episodeId)
 | 
					    /** @type {import('../models/PodcastEpisode')} */
 | 
				
			||||||
 | 
					    const episode = req.libraryItem.media.podcastEpisodes.find((ep) => ep.id === episodeId)
 | 
				
			||||||
    if (!episode) {
 | 
					    if (!episode) {
 | 
				
			||||||
      Logger.error(`[PodcastController] getEpisode episode ${episodeId} not found for item ${libraryItem.id}`)
 | 
					      Logger.error(`[PodcastController] getEpisode episode ${episodeId} not found for item ${req.libraryItem.id}`)
 | 
				
			||||||
      return res.sendStatus(404)
 | 
					      return res.sendStatus(404)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.json(episode)
 | 
					    res.json(episode.toOldJSON(req.libraryItem.id))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * DELETE: /api/podcasts/:id/episode/:episodeId
 | 
					   * DELETE: /api/podcasts/:id/episode/:episodeId
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {RequestWithUser} req
 | 
					   * @param {RequestWithLibraryItem} req
 | 
				
			||||||
   * @param {Response} res
 | 
					   * @param {Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async removeEpisode(req, res) {
 | 
					  async removeEpisode(req, res) {
 | 
				
			||||||
    const episodeId = req.params.episodeId
 | 
					    const episodeId = req.params.episodeId
 | 
				
			||||||
    const libraryItem = req.libraryItem
 | 
					 | 
				
			||||||
    const hardDelete = req.query.hard === '1'
 | 
					    const hardDelete = req.query.hard === '1'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const episode = libraryItem.media.episodes.find((ep) => ep.id === episodeId)
 | 
					    /** @type {import('../models/PodcastEpisode')} */
 | 
				
			||||||
 | 
					    const episode = req.libraryItem.media.podcastEpisodes.find((ep) => ep.id === episodeId)
 | 
				
			||||||
    if (!episode) {
 | 
					    if (!episode) {
 | 
				
			||||||
      Logger.error(`[PodcastController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`)
 | 
					      Logger.error(`[PodcastController] removeEpisode episode ${episodeId} not found for item ${req.libraryItem.id}`)
 | 
				
			||||||
      return res.sendStatus(404)
 | 
					      return res.sendStatus(404)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -407,36 +430,8 @@ class PodcastController {
 | 
				
			|||||||
        })
 | 
					        })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Remove episode from Podcast and library file
 | 
					    // Remove episode from playlists
 | 
				
			||||||
    const episodeRemoved = libraryItem.media.removeEpisode(episodeId)
 | 
					    await Database.playlistModel.removeMediaItemsFromPlaylists([episodeId])
 | 
				
			||||||
    if (episodeRemoved?.audioFile) {
 | 
					 | 
				
			||||||
      libraryItem.removeLibraryFile(episodeRemoved.audioFile.ino)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Update/remove playlists that had this podcast episode
 | 
					 | 
				
			||||||
    const playlistMediaItems = await Database.playlistMediaItemModel.findAll({
 | 
					 | 
				
			||||||
      where: {
 | 
					 | 
				
			||||||
        mediaItemId: episodeId
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      include: {
 | 
					 | 
				
			||||||
        model: Database.playlistModel,
 | 
					 | 
				
			||||||
        include: Database.playlistMediaItemModel
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    for (const pmi of playlistMediaItems) {
 | 
					 | 
				
			||||||
      const numItems = pmi.playlist.playlistMediaItems.length - 1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (!numItems) {
 | 
					 | 
				
			||||||
        Logger.info(`[PodcastController] Playlist "${pmi.playlist.name}" has no more items - removing it`)
 | 
					 | 
				
			||||||
        const jsonExpanded = await pmi.playlist.getOldJsonExpanded()
 | 
					 | 
				
			||||||
        SocketAuthority.clientEmitter(pmi.playlist.userId, 'playlist_removed', jsonExpanded)
 | 
					 | 
				
			||||||
        await pmi.playlist.destroy()
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        await pmi.destroy()
 | 
					 | 
				
			||||||
        const jsonExpanded = await pmi.playlist.getOldJsonExpanded()
 | 
					 | 
				
			||||||
        SocketAuthority.clientEmitter(pmi.playlist.userId, 'playlist_updated', jsonExpanded)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Remove media progress for this episode
 | 
					    // Remove media progress for this episode
 | 
				
			||||||
    const mediaProgressRemoved = await Database.mediaProgressModel.destroy({
 | 
					    const mediaProgressRemoved = await Database.mediaProgressModel.destroy({
 | 
				
			||||||
@ -448,9 +443,16 @@ class PodcastController {
 | 
				
			|||||||
      Logger.info(`[PodcastController] Removed ${mediaProgressRemoved} media progress for episode ${episode.id}`)
 | 
					      Logger.info(`[PodcastController] Removed ${mediaProgressRemoved} media progress for episode ${episode.id}`)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await Database.updateLibraryItem(libraryItem)
 | 
					    // Remove episode
 | 
				
			||||||
    SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
 | 
					    await episode.destroy()
 | 
				
			||||||
    res.json(libraryItem.toJSON())
 | 
					
 | 
				
			||||||
 | 
					    // Remove library file
 | 
				
			||||||
 | 
					    req.libraryItem.libraryFiles = req.libraryItem.libraryFiles.filter((file) => file.ino !== episode.audioFile.ino)
 | 
				
			||||||
 | 
					    req.libraryItem.changed('libraryFiles', true)
 | 
				
			||||||
 | 
					    await req.libraryItem.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
 | 
				
			||||||
 | 
					    res.json(req.libraryItem.toOldJSON())
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@ -460,15 +462,15 @@ class PodcastController {
 | 
				
			|||||||
   * @param {NextFunction} next
 | 
					   * @param {NextFunction} next
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async middleware(req, res, next) {
 | 
					  async middleware(req, res, next) {
 | 
				
			||||||
    const item = await Database.libraryItemModel.getOldById(req.params.id)
 | 
					    const libraryItem = await Database.libraryItemModel.getExpandedById(req.params.id)
 | 
				
			||||||
    if (!item?.media) return res.sendStatus(404)
 | 
					    if (!libraryItem?.media) return res.sendStatus(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!item.isPodcast) {
 | 
					    if (!libraryItem.isPodcast) {
 | 
				
			||||||
      return res.sendStatus(500)
 | 
					      return res.sendStatus(500)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Check user can access this library item
 | 
					    // Check user can access this library item
 | 
				
			||||||
    if (!req.user.checkCanAccessLibraryItem(item)) {
 | 
					    if (!req.user.checkCanAccessLibraryItem(libraryItem)) {
 | 
				
			||||||
      return res.sendStatus(403)
 | 
					      return res.sendStatus(403)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -480,7 +482,7 @@ class PodcastController {
 | 
				
			|||||||
      return res.sendStatus(403)
 | 
					      return res.sendStatus(403)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    req.libraryItem = item
 | 
					    req.libraryItem = libraryItem
 | 
				
			||||||
    next()
 | 
					    next()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -181,7 +181,7 @@ class CronManager {
 | 
				
			|||||||
    // Get podcast library items to check
 | 
					    // Get podcast library items to check
 | 
				
			||||||
    const libraryItems = []
 | 
					    const libraryItems = []
 | 
				
			||||||
    for (const libraryItemId of libraryItemIds) {
 | 
					    for (const libraryItemId of libraryItemIds) {
 | 
				
			||||||
      const libraryItem = await Database.libraryItemModel.getOldById(libraryItemId)
 | 
					      const libraryItem = await Database.libraryItemModel.getExpandedById(libraryItemId)
 | 
				
			||||||
      if (!libraryItem) {
 | 
					      if (!libraryItem) {
 | 
				
			||||||
        Logger.error(`[CronManager] Library item ${libraryItemId} not found for episode check cron ${expression}`)
 | 
					        Logger.error(`[CronManager] Library item ${libraryItemId} not found for episode check cron ${expression}`)
 | 
				
			||||||
        podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter((lid) => lid !== libraryItemId) // Filter it out
 | 
					        podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter((lid) => lid !== libraryItemId) // Filter it out
 | 
				
			||||||
 | 
				
			|||||||
@ -52,11 +52,16 @@ class PodcastManager {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {import('../models/LibraryItem')} libraryItem
 | 
				
			||||||
 | 
					   * @param {*} episodesToDownload
 | 
				
			||||||
 | 
					   * @param {*} isAutoDownload
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
  async downloadPodcastEpisodes(libraryItem, episodesToDownload, isAutoDownload) {
 | 
					  async downloadPodcastEpisodes(libraryItem, episodesToDownload, isAutoDownload) {
 | 
				
			||||||
    let index = Math.max(...libraryItem.media.episodes.filter((ep) => ep.index == null || isNaN(ep.index)).map((ep) => Number(ep.index))) + 1
 | 
					 | 
				
			||||||
    for (const ep of episodesToDownload) {
 | 
					    for (const ep of episodesToDownload) {
 | 
				
			||||||
      const newPe = new PodcastEpisode()
 | 
					      const newPe = new PodcastEpisode()
 | 
				
			||||||
      newPe.setData(ep, index++)
 | 
					      newPe.setData(ep, null)
 | 
				
			||||||
      newPe.libraryItemId = libraryItem.id
 | 
					      newPe.libraryItemId = libraryItem.id
 | 
				
			||||||
      newPe.podcastId = libraryItem.media.id
 | 
					      newPe.podcastId = libraryItem.media.id
 | 
				
			||||||
      const newPeDl = new PodcastEpisodeDownload()
 | 
					      const newPeDl = new PodcastEpisodeDownload()
 | 
				
			||||||
@ -263,16 +268,21 @@ class PodcastManager {
 | 
				
			|||||||
    return newAudioFile
 | 
					    return newAudioFile
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Returns false if auto download episodes was disabled (disabled if reaches max failed checks)
 | 
					  /**
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {import('../models/LibraryItem')} libraryItem
 | 
				
			||||||
 | 
					   * @returns {Promise<boolean>} - Returns false if auto download episodes was disabled (disabled if reaches max failed checks)
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
  async runEpisodeCheck(libraryItem) {
 | 
					  async runEpisodeCheck(libraryItem) {
 | 
				
			||||||
    const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0)
 | 
					    const lastEpisodeCheck = libraryItem.media.lastEpisodeCheck?.valueOf() || 0
 | 
				
			||||||
    const latestEpisodePublishedAt = libraryItem.media.latestEpisodePublished
 | 
					    const latestEpisodePublishedAt = libraryItem.media.getLatestEpisodePublishedAt()
 | 
				
			||||||
    Logger.info(`[PodcastManager] runEpisodeCheck: "${libraryItem.media.metadata.title}" | Last check: ${lastEpisodeCheckDate} | ${latestEpisodePublishedAt ? `Latest episode pubDate: ${new Date(latestEpisodePublishedAt)}` : 'No latest episode'}`)
 | 
					
 | 
				
			||||||
 | 
					    Logger.info(`[PodcastManager] runEpisodeCheck: "${libraryItem.media.title}" | Last check: ${new Date(lastEpisodeCheck)} | ${latestEpisodePublishedAt ? `Latest episode pubDate: ${new Date(latestEpisodePublishedAt)}` : 'No latest episode'}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Use latest episode pubDate if exists OR fallback to using lastEpisodeCheckDate
 | 
					    // Use latest episode pubDate if exists OR fallback to using lastEpisodeCheckDate
 | 
				
			||||||
    //    lastEpisodeCheckDate will be the current time when adding a new podcast
 | 
					    //    lastEpisodeCheckDate will be the current time when adding a new podcast
 | 
				
			||||||
    const dateToCheckForEpisodesAfter = latestEpisodePublishedAt || lastEpisodeCheckDate
 | 
					    const dateToCheckForEpisodesAfter = latestEpisodePublishedAt || lastEpisodeCheckDate
 | 
				
			||||||
    Logger.debug(`[PodcastManager] runEpisodeCheck: "${libraryItem.media.metadata.title}" checking for episodes after ${new Date(dateToCheckForEpisodesAfter)}`)
 | 
					    Logger.debug(`[PodcastManager] runEpisodeCheck: "${libraryItem.media.title}" checking for episodes after ${new Date(dateToCheckForEpisodesAfter)}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter, libraryItem.media.maxNewEpisodesToDownload)
 | 
					    var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter, libraryItem.media.maxNewEpisodesToDownload)
 | 
				
			||||||
    Logger.debug(`[PodcastManager] runEpisodeCheck: ${newEpisodes?.length || 'N/A'} episodes found`)
 | 
					    Logger.debug(`[PodcastManager] runEpisodeCheck: ${newEpisodes?.length || 'N/A'} episodes found`)
 | 
				
			||||||
@ -283,36 +293,47 @@ class PodcastManager {
 | 
				
			|||||||
      if (!this.failedCheckMap[libraryItem.id]) this.failedCheckMap[libraryItem.id] = 0
 | 
					      if (!this.failedCheckMap[libraryItem.id]) this.failedCheckMap[libraryItem.id] = 0
 | 
				
			||||||
      this.failedCheckMap[libraryItem.id]++
 | 
					      this.failedCheckMap[libraryItem.id]++
 | 
				
			||||||
      if (this.failedCheckMap[libraryItem.id] >= this.MaxFailedEpisodeChecks) {
 | 
					      if (this.failedCheckMap[libraryItem.id] >= this.MaxFailedEpisodeChecks) {
 | 
				
			||||||
        Logger.error(`[PodcastManager] runEpisodeCheck ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.metadata.title}" - disabling auto download`)
 | 
					        Logger.error(`[PodcastManager] runEpisodeCheck ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.title}" - disabling auto download`)
 | 
				
			||||||
        libraryItem.media.autoDownloadEpisodes = false
 | 
					        libraryItem.media.autoDownloadEpisodes = false
 | 
				
			||||||
        delete this.failedCheckMap[libraryItem.id]
 | 
					        delete this.failedCheckMap[libraryItem.id]
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        Logger.warn(`[PodcastManager] runEpisodeCheck ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.metadata.title}"`)
 | 
					        Logger.warn(`[PodcastManager] runEpisodeCheck ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.title}"`)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else if (newEpisodes.length) {
 | 
					    } else if (newEpisodes.length) {
 | 
				
			||||||
      delete this.failedCheckMap[libraryItem.id]
 | 
					      delete this.failedCheckMap[libraryItem.id]
 | 
				
			||||||
      Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
 | 
					      Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.title}" - starting download`)
 | 
				
			||||||
      this.downloadPodcastEpisodes(libraryItem, newEpisodes, true)
 | 
					      this.downloadPodcastEpisodes(libraryItem, newEpisodes, true)
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      delete this.failedCheckMap[libraryItem.id]
 | 
					      delete this.failedCheckMap[libraryItem.id]
 | 
				
			||||||
      Logger.debug(`[PodcastManager] No new episodes for "${libraryItem.media.metadata.title}"`)
 | 
					      Logger.debug(`[PodcastManager] No new episodes for "${libraryItem.media.title}"`)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    libraryItem.media.lastEpisodeCheck = Date.now()
 | 
					    libraryItem.media.lastEpisodeCheck = new Date()
 | 
				
			||||||
    libraryItem.updatedAt = Date.now()
 | 
					    await libraryItem.media.save()
 | 
				
			||||||
    await Database.updateLibraryItem(libraryItem)
 | 
					
 | 
				
			||||||
    SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
 | 
					    libraryItem.changed('updatedAt', true)
 | 
				
			||||||
 | 
					    await libraryItem.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SocketAuthority.emitter('item_updated', libraryItem.toOldJSONExpanded())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return libraryItem.media.autoDownloadEpisodes
 | 
					    return libraryItem.media.autoDownloadEpisodes
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {import('../models/LibraryItem')} podcastLibraryItem
 | 
				
			||||||
 | 
					   * @param {number} dateToCheckForEpisodesAfter - Unix timestamp
 | 
				
			||||||
 | 
					   * @param {number} maxNewEpisodes
 | 
				
			||||||
 | 
					   * @returns
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
  async checkPodcastForNewEpisodes(podcastLibraryItem, dateToCheckForEpisodesAfter, maxNewEpisodes = 3) {
 | 
					  async checkPodcastForNewEpisodes(podcastLibraryItem, dateToCheckForEpisodesAfter, maxNewEpisodes = 3) {
 | 
				
			||||||
    if (!podcastLibraryItem.media.metadata.feedUrl) {
 | 
					    if (!podcastLibraryItem.media.feedURL) {
 | 
				
			||||||
      Logger.error(`[PodcastManager] checkPodcastForNewEpisodes no feed url for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id})`)
 | 
					      Logger.error(`[PodcastManager] checkPodcastForNewEpisodes no feed url for ${podcastLibraryItem.media.title} (ID: ${podcastLibraryItem.id})`)
 | 
				
			||||||
      return false
 | 
					      return false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const feed = await getPodcastFeed(podcastLibraryItem.media.metadata.feedUrl)
 | 
					    const feed = await getPodcastFeed(podcastLibraryItem.media.feedURL)
 | 
				
			||||||
    if (!feed?.episodes) {
 | 
					    if (!feed?.episodes) {
 | 
				
			||||||
      Logger.error(`[PodcastManager] checkPodcastForNewEpisodes invalid feed payload for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id})`, feed)
 | 
					      Logger.error(`[PodcastManager] checkPodcastForNewEpisodes invalid feed payload for ${podcastLibraryItem.media.title} (ID: ${podcastLibraryItem.id})`, feed)
 | 
				
			||||||
      return false
 | 
					      return false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -326,21 +347,32 @@ class PodcastManager {
 | 
				
			|||||||
    return newEpisodes
 | 
					    return newEpisodes
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {import('../models/LibraryItem')} libraryItem
 | 
				
			||||||
 | 
					   * @param {*} maxEpisodesToDownload
 | 
				
			||||||
 | 
					   * @returns
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
  async checkAndDownloadNewEpisodes(libraryItem, maxEpisodesToDownload) {
 | 
					  async checkAndDownloadNewEpisodes(libraryItem, maxEpisodesToDownload) {
 | 
				
			||||||
    const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0)
 | 
					    const lastEpisodeCheck = libraryItem.media.lastEpisodeCheck?.valueOf() || 0
 | 
				
			||||||
    Logger.info(`[PodcastManager] checkAndDownloadNewEpisodes for "${libraryItem.media.metadata.title}" - Last episode check: ${lastEpisodeCheckDate}`)
 | 
					    const lastEpisodeCheckDate = lastEpisodeCheck > 0 ? libraryItem.media.lastEpisodeCheck : 'Never'
 | 
				
			||||||
    var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, libraryItem.media.lastEpisodeCheck, maxEpisodesToDownload)
 | 
					    Logger.info(`[PodcastManager] checkAndDownloadNewEpisodes for "${libraryItem.media.title}" - Last episode check: ${lastEpisodeCheckDate}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, lastEpisodeCheck, maxEpisodesToDownload)
 | 
				
			||||||
    if (newEpisodes.length) {
 | 
					    if (newEpisodes.length) {
 | 
				
			||||||
      Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
 | 
					      Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.title}" - starting download`)
 | 
				
			||||||
      this.downloadPodcastEpisodes(libraryItem, newEpisodes, false)
 | 
					      this.downloadPodcastEpisodes(libraryItem, newEpisodes, false)
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      Logger.info(`[PodcastManager] No new episodes found for podcast "${libraryItem.media.metadata.title}"`)
 | 
					      Logger.info(`[PodcastManager] No new episodes found for podcast "${libraryItem.media.title}"`)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    libraryItem.media.lastEpisodeCheck = Date.now()
 | 
					    libraryItem.media.lastEpisodeCheck = new Date()
 | 
				
			||||||
    libraryItem.updatedAt = Date.now()
 | 
					    await libraryItem.media.save()
 | 
				
			||||||
    await Database.updateLibraryItem(libraryItem)
 | 
					
 | 
				
			||||||
    SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
 | 
					    libraryItem.changed('updatedAt', true)
 | 
				
			||||||
 | 
					    await libraryItem.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SocketAuthority.emitter('item_updated', libraryItem.toOldJSONExpanded())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return newEpisodes
 | 
					    return newEpisodes
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -259,6 +259,10 @@ class Podcast extends Model {
 | 
				
			|||||||
      this.autoDownloadSchedule = payload.autoDownloadSchedule
 | 
					      this.autoDownloadSchedule = payload.autoDownloadSchedule
 | 
				
			||||||
      hasUpdates = true
 | 
					      hasUpdates = true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (typeof payload.lastEpisodeCheck === 'number' && payload.lastEpisodeCheck !== this.lastEpisodeCheck?.valueOf()) {
 | 
				
			||||||
 | 
					      this.lastEpisodeCheck = payload.lastEpisodeCheck
 | 
				
			||||||
 | 
					      hasUpdates = true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const numberKeys = ['maxEpisodesToKeep', 'maxNewEpisodesToDownload']
 | 
					    const numberKeys = ['maxEpisodesToKeep', 'maxNewEpisodesToDownload']
 | 
				
			||||||
    numberKeys.forEach((key) => {
 | 
					    numberKeys.forEach((key) => {
 | 
				
			||||||
@ -348,6 +352,31 @@ class Podcast extends Model {
 | 
				
			|||||||
    return episode.duration
 | 
					    return episode.duration
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @returns {number} - Unix timestamp
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  getLatestEpisodePublishedAt() {
 | 
				
			||||||
 | 
					    return this.podcastEpisodes.reduce((latest, episode) => {
 | 
				
			||||||
 | 
					      if (episode.publishedAt?.valueOf() > latest) {
 | 
				
			||||||
 | 
					        return episode.publishedAt.valueOf()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return latest
 | 
				
			||||||
 | 
					    }, 0)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Used for checking if an rss feed episode is already in the podcast
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {Object} feedEpisode - object from rss feed
 | 
				
			||||||
 | 
					   * @returns {boolean}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  checkHasEpisodeByFeedEpisode(feedEpisode) {
 | 
				
			||||||
 | 
					    const guid = feedEpisode.guid
 | 
				
			||||||
 | 
					    const url = feedEpisode.enclosure.url
 | 
				
			||||||
 | 
					    return this.podcastEpisodes.some((ep) => ep.checkMatchesGuidOrEnclosureUrl(guid, url))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Old model kept metadata in a separate object
 | 
					   * Old model kept metadata in a separate object
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
 | 
				
			|||||||
@ -143,6 +143,23 @@ class PodcastEpisode extends Model {
 | 
				
			|||||||
    return this.audioFile?.duration || 0
 | 
					    return this.audioFile?.duration || 0
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Used for matching the episode with an episode in the RSS feed
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {string} guid
 | 
				
			||||||
 | 
					   * @param {string} enclosureURL
 | 
				
			||||||
 | 
					   * @returns {boolean}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  checkMatchesGuidOrEnclosureUrl(guid, enclosureURL) {
 | 
				
			||||||
 | 
					    if (this.extraData?.guid && this.extraData.guid === guid) {
 | 
				
			||||||
 | 
					      return true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (this.enclosureURL && this.enclosureURL === enclosureURL) {
 | 
				
			||||||
 | 
					      return true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Used in client players
 | 
					   * Used in client players
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
 | 
				
			|||||||
@ -6,8 +6,10 @@ const globals = require('../utils/globals')
 | 
				
			|||||||
class PodcastEpisodeDownload {
 | 
					class PodcastEpisodeDownload {
 | 
				
			||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
    this.id = null
 | 
					    this.id = null
 | 
				
			||||||
 | 
					    /** @type {import('../objects/entities/PodcastEpisode')} */
 | 
				
			||||||
    this.podcastEpisode = null
 | 
					    this.podcastEpisode = null
 | 
				
			||||||
    this.url = null
 | 
					    this.url = null
 | 
				
			||||||
 | 
					    /** @type {import('../models/LibraryItem')} */
 | 
				
			||||||
    this.libraryItem = null
 | 
					    this.libraryItem = null
 | 
				
			||||||
    this.libraryId = null
 | 
					    this.libraryId = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -27,7 +29,7 @@ class PodcastEpisodeDownload {
 | 
				
			|||||||
      id: this.id,
 | 
					      id: this.id,
 | 
				
			||||||
      episodeDisplayTitle: this.podcastEpisode?.title ?? null,
 | 
					      episodeDisplayTitle: this.podcastEpisode?.title ?? null,
 | 
				
			||||||
      url: this.url,
 | 
					      url: this.url,
 | 
				
			||||||
      libraryItemId: this.libraryItem?.id || null,
 | 
					      libraryItemId: this.libraryItemId,
 | 
				
			||||||
      libraryId: this.libraryId || null,
 | 
					      libraryId: this.libraryId || null,
 | 
				
			||||||
      isFinished: this.isFinished,
 | 
					      isFinished: this.isFinished,
 | 
				
			||||||
      failed: this.failed,
 | 
					      failed: this.failed,
 | 
				
			||||||
@ -35,8 +37,8 @@ class PodcastEpisodeDownload {
 | 
				
			|||||||
      startedAt: this.startedAt,
 | 
					      startedAt: this.startedAt,
 | 
				
			||||||
      createdAt: this.createdAt,
 | 
					      createdAt: this.createdAt,
 | 
				
			||||||
      finishedAt: this.finishedAt,
 | 
					      finishedAt: this.finishedAt,
 | 
				
			||||||
      podcastTitle: this.libraryItem?.media.metadata.title ?? null,
 | 
					      podcastTitle: this.libraryItem?.media.title ?? null,
 | 
				
			||||||
      podcastExplicit: !!this.libraryItem?.media.metadata.explicit,
 | 
					      podcastExplicit: !!this.libraryItem?.media.explicit,
 | 
				
			||||||
      season: this.podcastEpisode?.season ?? null,
 | 
					      season: this.podcastEpisode?.season ?? null,
 | 
				
			||||||
      episode: this.podcastEpisode?.episode ?? null,
 | 
					      episode: this.podcastEpisode?.episode ?? null,
 | 
				
			||||||
      episodeType: this.podcastEpisode?.episodeType ?? 'full',
 | 
					      episodeType: this.podcastEpisode?.episodeType ?? 'full',
 | 
				
			||||||
@ -80,9 +82,16 @@ class PodcastEpisodeDownload {
 | 
				
			|||||||
    return this.targetFilename
 | 
					    return this.targetFilename
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  get libraryItemId() {
 | 
					  get libraryItemId() {
 | 
				
			||||||
    return this.libraryItem ? this.libraryItem.id : null
 | 
					    return this.libraryItem?.id || null
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {import('../objects/entities/PodcastEpisode')} podcastEpisode - old model
 | 
				
			||||||
 | 
					   * @param {import('../models/LibraryItem')} libraryItem
 | 
				
			||||||
 | 
					   * @param {*} isAutoDownload
 | 
				
			||||||
 | 
					   * @param {*} libraryId
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
  setData(podcastEpisode, libraryItem, isAutoDownload, libraryId) {
 | 
					  setData(podcastEpisode, libraryItem, isAutoDownload, libraryId) {
 | 
				
			||||||
    this.id = uuidv4()
 | 
					    this.id = uuidv4()
 | 
				
			||||||
    this.podcastEpisode = podcastEpisode
 | 
					    this.podcastEpisode = podcastEpisode
 | 
				
			||||||
 | 
				
			|||||||
@ -167,10 +167,5 @@ class PodcastEpisode {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    return hasUpdates
 | 
					    return hasUpdates
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  checkEqualsEnclosureUrl(url) {
 | 
					 | 
				
			||||||
    if (!this.enclosure?.url) return false
 | 
					 | 
				
			||||||
    return this.enclosure.url == url
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
module.exports = PodcastEpisode
 | 
					module.exports = PodcastEpisode
 | 
				
			||||||
 | 
				
			|||||||
@ -193,11 +193,6 @@ class Podcast {
 | 
				
			|||||||
  checkHasEpisode(episodeId) {
 | 
					  checkHasEpisode(episodeId) {
 | 
				
			||||||
    return this.episodes.some((ep) => ep.id === episodeId)
 | 
					    return this.episodes.some((ep) => ep.id === episodeId)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  checkHasEpisodeByFeedEpisode(feedEpisode) {
 | 
					 | 
				
			||||||
    const guid = feedEpisode.guid
 | 
					 | 
				
			||||||
    const url = feedEpisode.enclosure.url
 | 
					 | 
				
			||||||
    return this.episodes.some((ep) => (ep.guid && ep.guid === guid) || ep.checkEqualsEnclosureUrl(url))
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  addPodcastEpisode(podcastEpisode) {
 | 
					  addPodcastEpisode(podcastEpisode) {
 | 
				
			||||||
    this.episodes.push(podcastEpisode)
 | 
					    this.episodes.push(podcastEpisode)
 | 
				
			||||||
 | 
				
			|||||||
@ -97,6 +97,11 @@ async function resizeImage(filePath, outputPath, width, height) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
module.exports.resizeImage = resizeImage
 | 
					module.exports.resizeImage = resizeImage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {import('../objects/PodcastEpisodeDownload')} podcastEpisodeDownload
 | 
				
			||||||
 | 
					 * @returns
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
 | 
					module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
 | 
				
			||||||
  return new Promise(async (resolve) => {
 | 
					  return new Promise(async (resolve) => {
 | 
				
			||||||
    const response = await axios({
 | 
					    const response = await axios({
 | 
				
			||||||
@ -118,21 +123,22 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
 | 
				
			|||||||
    ffmpeg.addOption('-loglevel debug') // Debug logs printed on error
 | 
					    ffmpeg.addOption('-loglevel debug') // Debug logs printed on error
 | 
				
			||||||
    ffmpeg.outputOptions('-c:a', 'copy', '-map', '0:a', '-metadata', 'podcast=1')
 | 
					    ffmpeg.outputOptions('-c:a', 'copy', '-map', '0:a', '-metadata', 'podcast=1')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const podcastMetadata = podcastEpisodeDownload.libraryItem.media.metadata
 | 
					    /** @type {import('../models/Podcast')} */
 | 
				
			||||||
 | 
					    const podcast = podcastEpisodeDownload.libraryItem.media
 | 
				
			||||||
    const podcastEpisode = podcastEpisodeDownload.podcastEpisode
 | 
					    const podcastEpisode = podcastEpisodeDownload.podcastEpisode
 | 
				
			||||||
    const finalSizeInBytes = Number(podcastEpisode.enclosure?.length || 0)
 | 
					    const finalSizeInBytes = Number(podcastEpisode.enclosure?.length || 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const taggings = {
 | 
					    const taggings = {
 | 
				
			||||||
      album: podcastMetadata.title,
 | 
					      album: podcast.title,
 | 
				
			||||||
      'album-sort': podcastMetadata.title,
 | 
					      'album-sort': podcast.title,
 | 
				
			||||||
      artist: podcastMetadata.author,
 | 
					      artist: podcast.author,
 | 
				
			||||||
      'artist-sort': podcastMetadata.author,
 | 
					      'artist-sort': podcast.author,
 | 
				
			||||||
      comment: podcastEpisode.description,
 | 
					      comment: podcastEpisode.description,
 | 
				
			||||||
      subtitle: podcastEpisode.subtitle,
 | 
					      subtitle: podcastEpisode.subtitle,
 | 
				
			||||||
      disc: podcastEpisode.season,
 | 
					      disc: podcastEpisode.season,
 | 
				
			||||||
      genre: podcastMetadata.genres.length ? podcastMetadata.genres.join(';') : null,
 | 
					      genre: podcast.genres.length ? podcast.genres.join(';') : null,
 | 
				
			||||||
      language: podcastMetadata.language,
 | 
					      language: podcast.language,
 | 
				
			||||||
      MVNM: podcastMetadata.title,
 | 
					      MVNM: podcast.title,
 | 
				
			||||||
      MVIN: podcastEpisode.episode,
 | 
					      MVIN: podcastEpisode.episode,
 | 
				
			||||||
      track: podcastEpisode.episode,
 | 
					      track: podcastEpisode.episode,
 | 
				
			||||||
      'series-part': podcastEpisode.episode,
 | 
					      'series-part': podcastEpisode.episode,
 | 
				
			||||||
@ -141,9 +147,9 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
 | 
				
			|||||||
      year: podcastEpisode.pubYear,
 | 
					      year: podcastEpisode.pubYear,
 | 
				
			||||||
      date: podcastEpisode.pubDate,
 | 
					      date: podcastEpisode.pubDate,
 | 
				
			||||||
      releasedate: podcastEpisode.pubDate,
 | 
					      releasedate: podcastEpisode.pubDate,
 | 
				
			||||||
      'itunes-id': podcastMetadata.itunesId,
 | 
					      'itunes-id': podcast.itunesId,
 | 
				
			||||||
      'podcast-type': podcastMetadata.type,
 | 
					      'podcast-type': podcast.podcastType,
 | 
				
			||||||
      'episode-type': podcastMetadata.episodeType
 | 
					      'episode-type': podcastEpisode.episodeType
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const tag in taggings) {
 | 
					    for (const tag in taggings) {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user