mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-08-05 02:34:56 +02:00
Add podcast episode auto download new episodes cron
This commit is contained in:
parent
d5e96a3422
commit
0dd219f303
10 changed files with 155 additions and 15 deletions
|
@ -183,6 +183,10 @@ class Db {
|
|||
}
|
||||
}
|
||||
|
||||
getLibraryItem(id) {
|
||||
return this.libraryItems.find(li => li.id === id)
|
||||
}
|
||||
|
||||
async updateLibraryItem(libraryItem) {
|
||||
return this.updateLibraryItems([libraryItem])
|
||||
}
|
||||
|
|
|
@ -130,6 +130,7 @@ class Server {
|
|||
|
||||
await this.backupManager.init()
|
||||
await this.logManager.init()
|
||||
this.podcastManager.init()
|
||||
|
||||
if (this.db.serverSettings.scannerDisableWatcher) {
|
||||
Logger.info(`[Server] Watcher is disabled`)
|
||||
|
|
|
@ -37,6 +37,12 @@ class LibraryItemController {
|
|||
|
||||
var hasUpdates = libraryItem.update(req.body)
|
||||
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`)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
|
|
|
@ -82,6 +82,11 @@ class PodcastController {
|
|||
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
|
||||
if (libraryItem.media.autoDownloadEpisodes && !this.podcastManager.episodeScheduleTask) {
|
||||
this.podcastManager.schedulePodcastEpisodeCron()
|
||||
}
|
||||
}
|
||||
|
||||
getPodcastFeed(req, res) {
|
||||
|
@ -105,5 +110,21 @@ class PodcastController {
|
|||
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()
|
|
@ -24,15 +24,19 @@ class PodcastManager {
|
|||
this.episodeScheduleTask = null
|
||||
}
|
||||
|
||||
get serverSettings() {
|
||||
return this.db.serverSettings || {}
|
||||
}
|
||||
|
||||
init() {
|
||||
var podcastsWithAutoDownload = this.db.libraryItems.find(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes)
|
||||
if (podcastsWithAutoDownload.length) {
|
||||
var podcastsWithAutoDownload = this.db.libraryItems.some(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes)
|
||||
if (podcastsWithAutoDownload) {
|
||||
this.schedulePodcastEpisodeCron()
|
||||
}
|
||||
}
|
||||
|
||||
async downloadPodcastEpisodes(libraryItem, episodesToDownload) {
|
||||
var index = 1
|
||||
var index = libraryItem.media.episodes.length + 1
|
||||
episodesToDownload.forEach((ep) => {
|
||||
var newPe = new PodcastEpisode()
|
||||
newPe.setData(ep, index++)
|
||||
|
@ -115,33 +119,81 @@ class PodcastManager {
|
|||
|
||||
schedulePodcastEpisodeCron() {
|
||||
try {
|
||||
Logger.debug(`[PodcastManager] Scheduled podcast episode check cron "${this.serverSettings.podcastEpisodeSchedule}"`)
|
||||
this.episodeScheduleTask = cron.schedule(this.serverSettings.podcastEpisodeSchedule, this.checkForNewEpisodes.bind(this))
|
||||
} 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)
|
||||
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) {
|
||||
axios.get(podcastMedia.feedUrl).then(async (data) => {
|
||||
async checkPodcastForNewEpisodes(podcastLibraryItem) {
|
||||
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) {
|
||||
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)
|
||||
if (!podcast) {
|
||||
return res.status(500).send('Invalid podcast RSS feed')
|
||||
return false
|
||||
}
|
||||
res.json(podcast)
|
||||
return podcast
|
||||
}).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
res.status(500).send(error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,5 +126,10 @@ class PodcastEpisode {
|
|||
audioTrack.setData(libraryItemId, this.audioFile, 0)
|
||||
return [audioTrack]
|
||||
}
|
||||
|
||||
checkEqualsEnclosureUrl(url) {
|
||||
if (!this.enclosure || !this.enclosure.url) return false
|
||||
return this.enclosure.url == url
|
||||
}
|
||||
}
|
||||
module.exports = PodcastEpisode
|
|
@ -15,6 +15,7 @@ class Podcast {
|
|||
this.episodes = []
|
||||
|
||||
this.autoDownloadEpisodes = false
|
||||
this.lastEpisodeCheck = 0
|
||||
|
||||
this.lastCoverSearch = null
|
||||
this.lastCoverSearchQuery = null
|
||||
|
@ -30,6 +31,7 @@ class Podcast {
|
|||
this.tags = [...podcast.tags]
|
||||
this.episodes = podcast.episodes.map((e) => new PodcastEpisode(e))
|
||||
this.autoDownloadEpisodes = !!podcast.autoDownloadEpisodes
|
||||
this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
|
@ -38,7 +40,8 @@ class Podcast {
|
|||
coverPath: this.coverPath,
|
||||
tags: [...this.tags],
|
||||
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],
|
||||
episodes: this.episodes.map(e => e.toJSON()),
|
||||
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||
lastEpisodeCheck: this.lastEpisodeCheck,
|
||||
size: this.size
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +64,7 @@ class Podcast {
|
|||
tags: [...this.tags],
|
||||
episodes: this.episodes.map(e => e.toJSONExpanded()),
|
||||
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||
lastEpisodeCheck: this.lastEpisodeCheck,
|
||||
size: this.size
|
||||
}
|
||||
}
|
||||
|
@ -135,6 +140,7 @@ class Podcast {
|
|||
|
||||
this.coverPath = mediaMetadata.coverPath || null
|
||||
this.autoDownloadEpisodes = !!mediaMetadata.autoDownloadEpisodes
|
||||
this.lastEpisodeCheck = Date.now() // Makes sure new episodes are after this
|
||||
}
|
||||
|
||||
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
|
||||
|
@ -149,6 +155,9 @@ class Podcast {
|
|||
checkHasEpisode(episodeId) {
|
||||
return this.episodes.some(ep => ep.id === episodeId)
|
||||
}
|
||||
checkHasEpisodeByFeedUrl(url) {
|
||||
return this.episodes.some(ep => ep.checkEqualsEnclosureUrl(url))
|
||||
}
|
||||
|
||||
// Only checks container format
|
||||
checkCanDirectPlay(payload, episodeId) {
|
||||
|
|
|
@ -176,6 +176,7 @@ class ApiRouter {
|
|||
//
|
||||
this.router.post('/podcasts', PodcastController.create.bind(this))
|
||||
this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this))
|
||||
this.router.get('/podcasts/:id/checknew', PodcastController.checkNewEpisodes.bind(this))
|
||||
|
||||
//
|
||||
// Misc Routes
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue