Add podcast episode auto download new episodes cron

This commit is contained in:
advplyr 2022-03-26 19:58:59 -05:00
parent d5e96a3422
commit 0dd219f303
10 changed files with 155 additions and 15 deletions

View File

@ -1,6 +1,12 @@
<template> <template>
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6"> <div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
<div class="w-full mb-4"> <div class="w-full mb-4">
<div class="flex items-center mb-4">
<p v-if="autoDownloadEpisodes">Last new episode check {{ $formatDate(lastEpisodeCheck) }}</p>
<div class="flex-grow" />
<ui-btn :loading="checkingNewEpisodes" @click="checkForNewEpisodes">Check for new episodes</ui-btn>
</div>
<div class="w-full p-4 bg-primary"> <div class="w-full p-4 bg-primary">
<p>Podcast Episodes</p> <p>Podcast Episodes</p>
</div> </div>
@ -40,16 +46,48 @@ export default {
} }
}, },
data() { data() {
return {} return {
checkingNewEpisodes: false
}
}, },
computed: { computed: {
autoDownloadEpisodes() {
return !!this.media.autoDownloadEpisodes
},
lastEpisodeCheck() {
return this.media.lastEpisodeCheck
},
media() { media() {
return this.libraryItem ? this.libraryItem.media || {} : {} return this.libraryItem ? this.libraryItem.media || {} : {}
}, },
libraryItemId() {
return this.libraryItem ? this.libraryItem.id : null
},
episodes() { episodes() {
return this.media.episodes || [] return this.media.episodes || []
} }
}, },
methods: {} methods: {
checkForNewEpisodes() {
this.checkingNewEpisodes = true
this.$axios
.$get(`/api/podcasts/${this.libraryItemId}/checknew`)
.then((response) => {
if (response.episodes && response.episodes.length) {
console.log('New episodes', response.episodes.length)
this.$toast.success(`${response.episodes.length} new episodes found!`)
} else {
this.$toast.info('No new episodes found')
}
this.checkingNewEpisodes = false
})
.catch((error) => {
console.error('Failed', error)
var errorMsg = error.response && error.response.data ? error.response.data : 'Unknown Error'
this.$toast.error(errorMsg)
this.checkingNewEpisodes = false
})
}
}
} }
</script> </script>

View File

@ -250,6 +250,9 @@ export default {
this.folderUpdated() this.folderUpdated()
} }
} }
},
mounted() {
console.log('Podcast feed data', this.podcastFeedData)
} }
} }
</script> </script>

View File

@ -183,6 +183,10 @@ class Db {
} }
} }
getLibraryItem(id) {
return this.libraryItems.find(li => li.id === id)
}
async updateLibraryItem(libraryItem) { async updateLibraryItem(libraryItem) {
return this.updateLibraryItems([libraryItem]) return this.updateLibraryItems([libraryItem])
} }

View File

@ -130,6 +130,7 @@ class Server {
await this.backupManager.init() await this.backupManager.init()
await this.logManager.init() await this.logManager.init()
this.podcastManager.init()
if (this.db.serverSettings.scannerDisableWatcher) { if (this.db.serverSettings.scannerDisableWatcher) {
Logger.info(`[Server] Watcher is disabled`) Logger.info(`[Server] Watcher is disabled`)

View File

@ -37,6 +37,12 @@ class LibraryItemController {
var hasUpdates = libraryItem.update(req.body) var hasUpdates = libraryItem.update(req.body)
if (hasUpdates) { if (hasUpdates) {
// Turn on podcast auto download cron if not already on
if (libraryItem.mediaType == 'podcast' && req.body.media.autoDownloadEpisodes && !this.podcastManager.episodeScheduleTask) {
this.podcastManager.schedulePodcastEpisodeCron()
}
Logger.debug(`[LibraryItemController] Updated now saving`) Logger.debug(`[LibraryItemController] Updated now saving`)
await this.db.updateLibraryItem(libraryItem) await this.db.updateLibraryItem(libraryItem)
this.emitter('item_updated', libraryItem.toJSONExpanded()) this.emitter('item_updated', libraryItem.toJSONExpanded())

View File

@ -82,6 +82,11 @@ class PodcastController {
Logger.info(`[PodcastController] Podcast created now starting ${payload.episodesToDownload.length} episode downloads`) Logger.info(`[PodcastController] Podcast created now starting ${payload.episodesToDownload.length} episode downloads`)
this.podcastManager.downloadPodcastEpisodes(libraryItem, payload.episodesToDownload) this.podcastManager.downloadPodcastEpisodes(libraryItem, payload.episodesToDownload)
} }
// Turn on podcast auto download cron if not already on
if (libraryItem.media.autoDownloadEpisodes && !this.podcastManager.episodeScheduleTask) {
this.podcastManager.schedulePodcastEpisodeCron()
}
} }
getPodcastFeed(req, res) { getPodcastFeed(req, res) {
@ -105,5 +110,21 @@ class PodcastController {
res.status(500).send(error) res.status(500).send(error)
}) })
} }
async checkNewEpisodes(req, res) {
var libraryItem = this.db.getLibraryItem(req.params.id)
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
return res.sendStatus(404)
}
if (!libraryItem.media.metadata.feedUrl) {
Logger.error(`[PodcastController] checkNewEpisodes no feed url for item ${libraryItem.id}`)
return res.status(500).send('Podcast has no rss feed url')
}
var newEpisodes = await this.podcastManager.checkPodcastForNewEpisodes(libraryItem)
res.json({
episodes: newEpisodes || []
})
}
} }
module.exports = new PodcastController() module.exports = new PodcastController()

View File

@ -24,15 +24,19 @@ class PodcastManager {
this.episodeScheduleTask = null this.episodeScheduleTask = null
} }
get serverSettings() {
return this.db.serverSettings || {}
}
init() { init() {
var podcastsWithAutoDownload = this.db.libraryItems.find(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes) var podcastsWithAutoDownload = this.db.libraryItems.some(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes)
if (podcastsWithAutoDownload.length) { if (podcastsWithAutoDownload) {
this.schedulePodcastEpisodeCron() this.schedulePodcastEpisodeCron()
} }
} }
async downloadPodcastEpisodes(libraryItem, episodesToDownload) { async downloadPodcastEpisodes(libraryItem, episodesToDownload) {
var index = 1 var index = libraryItem.media.episodes.length + 1
episodesToDownload.forEach((ep) => { episodesToDownload.forEach((ep) => {
var newPe = new PodcastEpisode() var newPe = new PodcastEpisode()
newPe.setData(ep, index++) newPe.setData(ep, index++)
@ -115,33 +119,81 @@ class PodcastManager {
schedulePodcastEpisodeCron() { schedulePodcastEpisodeCron() {
try { try {
Logger.debug(`[PodcastManager] Scheduled podcast episode check cron "${this.serverSettings.podcastEpisodeSchedule}"`)
this.episodeScheduleTask = cron.schedule(this.serverSettings.podcastEpisodeSchedule, this.checkForNewEpisodes.bind(this)) this.episodeScheduleTask = cron.schedule(this.serverSettings.podcastEpisodeSchedule, this.checkForNewEpisodes.bind(this))
} catch (error) { } catch (error) {
Logger.error(`[PodcastManager] Failed to schedule podcast cron ${this.serverSettings.backupSchedule}`, error) Logger.error(`[PodcastManager] Failed to schedule podcast cron ${this.serverSettings.podcastEpisodeSchedule}`, error)
} }
} }
checkForNewEpisodes() { cancelCron() {
Logger.debug(`[PodcastManager] Canceled new podcast episode check cron`)
if (this.episodeScheduleTask) {
this.episodeScheduleTask.destroy()
this.episodeScheduleTask = null
}
}
async checkForNewEpisodes() {
var podcastsWithAutoDownload = this.db.libraryItems.find(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes) var podcastsWithAutoDownload = this.db.libraryItems.find(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes)
for (const libraryItem of podcastsWithAutoDownload) { if (!podcastsWithAutoDownload.length) {
this.cancelCron()
return
}
for (const libraryItem of podcastsWithAutoDownload) {
Logger.info(`[PodcastManager] checkForNewEpisodes Cron for "${libraryItem.media.metadata.title}"`)
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem)
var hasUpdates = false
if (!newEpisodes) { // Failed
libraryItem.media.autoDownloadEpisodes = false
hasUpdates = true
} else if (newEpisodes.length) {
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
this.downloadPodcastEpisodes(libraryItem, newEpisodes)
hasUpdates = true
}
if (hasUpdates) {
libraryItem.media.lastEpisodeCheck = Date.now()
libraryItem.updatedAt = Date.now()
await this.db.updateLibraryItem(libraryItem)
this.emitter('item_updated', libraryItem.toJSONExpanded())
}
} }
} }
getPodcastFeed(podcastMedia) { async checkPodcastForNewEpisodes(podcastLibraryItem) {
axios.get(podcastMedia.feedUrl).then(async (data) => { if (!podcastLibraryItem.media.metadata.feedUrl) {
Logger.error(`[PodcastManager] checkPodcastForNewEpisodes no feed url for item ${podcastLibraryItem.id} - disabling auto download`)
return false
}
var feed = await this.getPodcastFeed(podcastLibraryItem.media.metadata.feedUrl)
if (!feed || !feed.episodes) {
Logger.error(`[PodcastManager] checkPodcastForNewEpisodes invalid feed payload ${podcastLibraryItem.id} - disabling auto download`)
return false
}
// Filter new and not already has
var newEpisodes = feed.episodes.filter(ep => ep.publishedAt > podcastLibraryItem.media.lastEpisodeCheck && !podcastLibraryItem.media.checkHasEpisodeByFeedUrl(ep.enclosure.url))
// Max new episodes for safety = 2
newEpisodes = newEpisodes.slice(0, 2)
return newEpisodes
}
getPodcastFeed(feedUrl) {
return axios.get(feedUrl).then(async (data) => {
if (!data || !data.data) { if (!data || !data.data) {
Logger.error('Invalid podcast feed request response') Logger.error('Invalid podcast feed request response')
return res.status(500).send('Bad response from feed request') return false
} }
var podcast = await parsePodcastRssFeedXml(data.data) var podcast = await parsePodcastRssFeedXml(data.data)
if (!podcast) { if (!podcast) {
return res.status(500).send('Invalid podcast RSS feed') return false
} }
res.json(podcast) return podcast
}).catch((error) => { }).catch((error) => {
console.error('Failed', error) console.error('Failed', error)
res.status(500).send(error) return false
}) })
} }
} }

View File

@ -126,5 +126,10 @@ class PodcastEpisode {
audioTrack.setData(libraryItemId, this.audioFile, 0) audioTrack.setData(libraryItemId, this.audioFile, 0)
return [audioTrack] return [audioTrack]
} }
checkEqualsEnclosureUrl(url) {
if (!this.enclosure || !this.enclosure.url) return false
return this.enclosure.url == url
}
} }
module.exports = PodcastEpisode module.exports = PodcastEpisode

View File

@ -15,6 +15,7 @@ class Podcast {
this.episodes = [] this.episodes = []
this.autoDownloadEpisodes = false this.autoDownloadEpisodes = false
this.lastEpisodeCheck = 0
this.lastCoverSearch = null this.lastCoverSearch = null
this.lastCoverSearchQuery = null this.lastCoverSearchQuery = null
@ -30,6 +31,7 @@ class Podcast {
this.tags = [...podcast.tags] this.tags = [...podcast.tags]
this.episodes = podcast.episodes.map((e) => new PodcastEpisode(e)) this.episodes = podcast.episodes.map((e) => new PodcastEpisode(e))
this.autoDownloadEpisodes = !!podcast.autoDownloadEpisodes this.autoDownloadEpisodes = !!podcast.autoDownloadEpisodes
this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0
} }
toJSON() { toJSON() {
@ -38,7 +40,8 @@ class Podcast {
coverPath: this.coverPath, coverPath: this.coverPath,
tags: [...this.tags], tags: [...this.tags],
episodes: this.episodes.map(e => e.toJSON()), episodes: this.episodes.map(e => e.toJSON()),
autoDownloadEpisodes: this.autoDownloadEpisodes autoDownloadEpisodes: this.autoDownloadEpisodes,
lastEpisodeCheck: this.lastEpisodeCheck
} }
} }
@ -49,6 +52,7 @@ class Podcast {
tags: [...this.tags], tags: [...this.tags],
episodes: this.episodes.map(e => e.toJSON()), episodes: this.episodes.map(e => e.toJSON()),
autoDownloadEpisodes: this.autoDownloadEpisodes, autoDownloadEpisodes: this.autoDownloadEpisodes,
lastEpisodeCheck: this.lastEpisodeCheck,
size: this.size size: this.size
} }
} }
@ -60,6 +64,7 @@ class Podcast {
tags: [...this.tags], tags: [...this.tags],
episodes: this.episodes.map(e => e.toJSONExpanded()), episodes: this.episodes.map(e => e.toJSONExpanded()),
autoDownloadEpisodes: this.autoDownloadEpisodes, autoDownloadEpisodes: this.autoDownloadEpisodes,
lastEpisodeCheck: this.lastEpisodeCheck,
size: this.size size: this.size
} }
} }
@ -135,6 +140,7 @@ class Podcast {
this.coverPath = mediaMetadata.coverPath || null this.coverPath = mediaMetadata.coverPath || null
this.autoDownloadEpisodes = !!mediaMetadata.autoDownloadEpisodes this.autoDownloadEpisodes = !!mediaMetadata.autoDownloadEpisodes
this.lastEpisodeCheck = Date.now() // Makes sure new episodes are after this
} }
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) { async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
@ -149,6 +155,9 @@ class Podcast {
checkHasEpisode(episodeId) { checkHasEpisode(episodeId) {
return this.episodes.some(ep => ep.id === episodeId) return this.episodes.some(ep => ep.id === episodeId)
} }
checkHasEpisodeByFeedUrl(url) {
return this.episodes.some(ep => ep.checkEqualsEnclosureUrl(url))
}
// Only checks container format // Only checks container format
checkCanDirectPlay(payload, episodeId) { checkCanDirectPlay(payload, episodeId) {

View File

@ -176,6 +176,7 @@ class ApiRouter {
// //
this.router.post('/podcasts', PodcastController.create.bind(this)) this.router.post('/podcasts', PodcastController.create.bind(this))
this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this)) this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this))
this.router.get('/podcasts/:id/checknew', PodcastController.checkNewEpisodes.bind(this))
// //
// Misc Routes // Misc Routes