mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-26 00:14:49 +01:00
Update podcasts to new library item model
This commit is contained in:
parent
0357dc90d4
commit
69d1744496
@ -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)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -162,14 +162,11 @@ 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