mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-08-05 18:54:39 +02:00
Add:Schedule podcast new episode checks
This commit is contained in:
parent
a690dfe671
commit
46668854ad
7 changed files with 315 additions and 85 deletions
|
@ -75,7 +75,7 @@ class Server {
|
|||
this.rssFeedManager = new RssFeedManager(this.db, this.emitter.bind(this))
|
||||
|
||||
this.scanner = new Scanner(this.db, this.coverManager, this.emitter.bind(this))
|
||||
this.cronManager = new CronManager(this.db, this.scanner)
|
||||
this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
|
||||
|
||||
// Routers
|
||||
this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.audioMetadataManager, this.rssFeedManager, this.cronManager, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||
|
@ -152,7 +152,6 @@ class Server {
|
|||
await this.backupManager.init()
|
||||
await this.logManager.init()
|
||||
await this.rssFeedManager.init()
|
||||
this.podcastManager.init()
|
||||
this.cronManager.init()
|
||||
|
||||
if (this.db.serverSettings.scannerDisableWatcher) {
|
||||
|
|
|
@ -79,12 +79,27 @@ class LibraryItemController {
|
|||
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||
}
|
||||
|
||||
// Book specific
|
||||
if (libraryItem.isBook) {
|
||||
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload)
|
||||
}
|
||||
|
||||
// Podcast specific
|
||||
var isPodcastAutoDownloadUpdated = false
|
||||
if (libraryItem.isPodcast) {
|
||||
if (mediaPayload.autoDownloadEpisodes !== undefined && libraryItem.media.autoDownloadEpisodes !== mediaPayload.autoDownloadEpisodes) {
|
||||
isPodcastAutoDownloadUpdated = true
|
||||
} else if (mediaPayload.autoDownloadSchedule !== undefined && libraryItem.media.autoDownloadSchedule !== mediaPayload.autoDownloadSchedule) {
|
||||
isPodcastAutoDownloadUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
var hasUpdates = libraryItem.media.update(mediaPayload)
|
||||
if (hasUpdates) {
|
||||
if (isPodcastAutoDownloadUpdated) {
|
||||
this.cronManager.checkUpdatePodcastCron(libraryItem)
|
||||
}
|
||||
|
||||
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
|
|
|
@ -2,15 +2,20 @@ const cron = require('../libs/nodeCron')
|
|||
const Logger = require('../Logger')
|
||||
|
||||
class CronManager {
|
||||
constructor(db, scanner) {
|
||||
constructor(db, scanner, podcastManager) {
|
||||
this.db = db
|
||||
this.scanner = scanner
|
||||
this.podcastManager = podcastManager
|
||||
|
||||
this.libraryScanCrons = []
|
||||
this.podcastCrons = []
|
||||
|
||||
this.podcastCronExpressionsExecuting = []
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initLibraryScanCrons()
|
||||
this.initPodcastCrons()
|
||||
}
|
||||
|
||||
initLibraryScanCrons() {
|
||||
|
@ -57,5 +62,116 @@ class CronManager {
|
|||
}
|
||||
}
|
||||
|
||||
initPodcastCrons() {
|
||||
const cronExpressionMap = {}
|
||||
this.db.libraryItems.forEach((li) => {
|
||||
if (li.mediaType === 'podcast' && li.media.autoDownloadEpisodes) {
|
||||
if (!li.media.autoDownloadSchedule) {
|
||||
Logger.error(`[CronManager] Podcast auto download schedule is not set for ${li.media.metadata.title}`)
|
||||
} else {
|
||||
if (!cronExpressionMap[li.media.autoDownloadSchedule]) {
|
||||
cronExpressionMap[li.media.autoDownloadSchedule] = {
|
||||
expression: li.media.autoDownloadSchedule,
|
||||
libraryItemIds: []
|
||||
}
|
||||
}
|
||||
cronExpressionMap[li.media.autoDownloadSchedule].libraryItemIds.push(li.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (!Object.keys(cronExpressionMap).length) return
|
||||
|
||||
Logger.debug(`[CronManager] Found ${Object.keys(cronExpressionMap).length} podcast episode schedules to start`)
|
||||
for (const expression in cronExpressionMap) {
|
||||
this.startPodcastCron(expression, cronExpressionMap[expression].libraryItemIds)
|
||||
}
|
||||
}
|
||||
|
||||
startPodcastCron(expression, libraryItemIds) {
|
||||
try {
|
||||
Logger.debug(`[CronManager] Scheduling podcast episode check cron "${expression}" for ${libraryItemIds.length} item(s)`)
|
||||
const task = cron.schedule(expression, () => {
|
||||
if (this.podcastCronExpressionsExecuting.includes(expression)) {
|
||||
Logger.warn(`[CronManager] Podcast cron "${expression}" is already executing`)
|
||||
} else {
|
||||
this.executePodcastCron(expression, libraryItemIds)
|
||||
}
|
||||
})
|
||||
this.podcastCrons.push({
|
||||
libraryItemIds,
|
||||
expression,
|
||||
task
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error(`[PodcastManager] Failed to schedule podcast cron ${this.serverSettings.podcastEpisodeSchedule}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
async executePodcastCron(expression, libraryItemIds) {
|
||||
Logger.debug(`[CronManager] Start executing podcast cron ${expression} for ${libraryItemIds.length} item(s)`)
|
||||
const podcastCron = this.podcastCrons.find(cron => cron.expression === expression)
|
||||
if (!podcastCron) {
|
||||
Logger.error(`[CronManager] Podcast cron not found for expression ${expression}`)
|
||||
return
|
||||
}
|
||||
this.podcastCronExpressionsExecuting.push(expression)
|
||||
|
||||
// Get podcast library items to check
|
||||
const libraryItems = []
|
||||
for (const libraryItemId of libraryItemIds) {
|
||||
const libraryItem = this.db.libraryItems.find(li => li.id === libraryItemId)
|
||||
if (!libraryItem) {
|
||||
Logger.error(`[CronManager] Library item ${libraryItemId} not found for episode check cron ${expression}`)
|
||||
podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItemId) // Filter it out
|
||||
} else {
|
||||
libraryItems.push(libraryItem)
|
||||
}
|
||||
}
|
||||
|
||||
// Run episode checks
|
||||
for (const libraryItem of libraryItems) {
|
||||
const keepAutoDownloading = await this.podcastManager.runEpisodeCheck(libraryItem)
|
||||
if (!keepAutoDownloading) { // auto download was disabled
|
||||
podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItemId) // Filter it out
|
||||
}
|
||||
}
|
||||
|
||||
// Stop and remove cron if no more library items
|
||||
if (!podcastCron.libraryItemIds.length) {
|
||||
this.removePodcastEpisodeCron(podcastCron)
|
||||
return
|
||||
}
|
||||
|
||||
Logger.debug(`[CronManager] Finished executing podcast cron ${expression} for ${libraryItems.length} item(s)`)
|
||||
this.podcastCronExpressionsExecuting = this.podcastCronExpressionsExecuting.filter(exp => exp !== expression)
|
||||
}
|
||||
|
||||
removePodcastEpisodeCron(podcastCron) {
|
||||
Logger.info(`[CronManager] Stopping & removing podcast episode cron for ${podcastCron.expression}`)
|
||||
if (podcastCron.task) podcastCron.task.stop()
|
||||
this.podcastCrons = this.podcastCrons.filter(pc => pc.expression !== podcastCron.expression)
|
||||
}
|
||||
|
||||
checkUpdatePodcastCron(libraryItem) {
|
||||
// Remove from old cron by library item id
|
||||
const existingCron = this.podcastCrons.find(pc => pc.libraryItemIds.includes(libraryItem.id))
|
||||
if (existingCron) {
|
||||
existingCron.libraryItemIds = existingCron.libraryItemIds.filter(lid => lid !== libraryItem.id)
|
||||
if (!existingCron.libraryItemIds.length) {
|
||||
this.removePodcastEpisodeCron(existingCron)
|
||||
}
|
||||
}
|
||||
|
||||
// Add to cron or start new cron
|
||||
if (libraryItem.media.autoDownloadEpisodes && libraryItem.media.autoDownloadSchedule) {
|
||||
const cronMatchingExpression = this.podcastCrons.find(pc => pc.expression === libraryItem.media.autoDownloadSchedule)
|
||||
if (cronMatchingExpression) {
|
||||
cronMatchingExpression.libraryItemIds.push(libraryItem.id)
|
||||
Logger.info(`[CronManager] Added podcast "${libraryItem.media.metadata.title}" to auto dl episode cron "${cronMatchingExpression.expression}"`)
|
||||
} else {
|
||||
this.startPodcastCron(libraryItem.media.autoDownloadSchedule, [libraryItem.id])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = CronManager
|
|
@ -1,5 +1,4 @@
|
|||
const fs = require('../libs/fsExtra')
|
||||
const cron = require('../libs/nodeCron')
|
||||
const axios = require('axios')
|
||||
|
||||
const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
|
||||
|
@ -23,22 +22,14 @@ class PodcastManager {
|
|||
this.downloadQueue = []
|
||||
this.currentDownload = null
|
||||
|
||||
this.episodeScheduleTask = null
|
||||
this.failedCheckMap = {},
|
||||
this.MaxFailedEpisodeChecks = 24
|
||||
this.failedCheckMap = {}
|
||||
this.MaxFailedEpisodeChecks = 24
|
||||
}
|
||||
|
||||
get serverSettings() {
|
||||
return this.db.serverSettings || {}
|
||||
}
|
||||
|
||||
init() {
|
||||
var podcastsWithAutoDownload = this.db.libraryItems.some(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes)
|
||||
if (podcastsWithAutoDownload) {
|
||||
this.schedulePodcastEpisodeCron()
|
||||
}
|
||||
}
|
||||
|
||||
getEpisodeDownloadsInQueue(libraryItemId) {
|
||||
return this.downloadQueue.filter(d => d.libraryItemId === libraryItemId)
|
||||
}
|
||||
|
@ -189,73 +180,45 @@ class PodcastManager {
|
|||
return newAudioFile
|
||||
}
|
||||
|
||||
schedulePodcastEpisodeCron() {
|
||||
try {
|
||||
Logger.debug(`[PodcastManager] Scheduled podcast episode check cron "${this.serverSettings.podcastEpisodeSchedule}"`)
|
||||
this.episodeScheduleTask = cron.schedule(this.serverSettings.podcastEpisodeSchedule, () => {
|
||||
Logger.debug(`[PodcastManager] Running cron`)
|
||||
this.checkForNewEpisodes()
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error(`[PodcastManager] Failed to schedule podcast cron ${this.serverSettings.podcastEpisodeSchedule}`, error)
|
||||
}
|
||||
}
|
||||
// Returns false if auto download episodes was disabled (disabled if reaches max failed checks)
|
||||
async runEpisodeCheck(libraryItem) {
|
||||
const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0)
|
||||
const latestEpisodePublishedAt = libraryItem.media.latestEpisodePublished
|
||||
Logger.info(`[PodcastManager] runEpisodeCheck: "${libraryItem.media.metadata.title}" | Last check: ${lastEpisodeCheckDate} | ${latestEpisodePublishedAt ? `Latest episode pubDate: ${new Date(latestEpisodePublishedAt)}` : 'No latest episode'}`)
|
||||
|
||||
cancelCron() {
|
||||
Logger.debug(`[PodcastManager] Canceled new podcast episode check cron`)
|
||||
if (this.episodeScheduleTask) {
|
||||
this.episodeScheduleTask.destroy()
|
||||
this.episodeScheduleTask = null
|
||||
}
|
||||
}
|
||||
// Use latest episode pubDate if exists OR fallback to using lastEpisodeCheckDate
|
||||
// lastEpisodeCheckDate will be the current time when adding a new podcast
|
||||
const dateToCheckForEpisodesAfter = latestEpisodePublishedAt || lastEpisodeCheckDate
|
||||
Logger.debug(`[PodcastManager] runEpisodeCheck: "${libraryItem.media.metadata.title}" checking for episodes after ${new Date(dateToCheckForEpisodesAfter)}`)
|
||||
|
||||
async checkForNewEpisodes() {
|
||||
var podcastsWithAutoDownload = this.db.libraryItems.filter(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes)
|
||||
if (!podcastsWithAutoDownload.length) {
|
||||
Logger.info(`[PodcastManager] checkForNewEpisodes - No podcasts with auto download set`)
|
||||
this.cancelCron()
|
||||
return
|
||||
}
|
||||
Logger.debug(`[PodcastManager] checkForNewEpisodes - Checking ${podcastsWithAutoDownload.length} Podcasts`)
|
||||
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter)
|
||||
Logger.debug(`[PodcastManager] runEpisodeCheck: ${newEpisodes ? newEpisodes.length : 'N/A'} episodes found`)
|
||||
|
||||
for (const libraryItem of podcastsWithAutoDownload) {
|
||||
const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0)
|
||||
const latestEpisodePublishedAt = libraryItem.media.latestEpisodePublished
|
||||
Logger.info(`[PodcastManager] checkForNewEpisodes: "${libraryItem.media.metadata.title}" | Last check: ${lastEpisodeCheckDate} | ${latestEpisodePublishedAt ? `Latest episode pubDate: ${new Date(latestEpisodePublishedAt)}` : 'No latest episode'}`)
|
||||
|
||||
// Use latest episode pubDate if exists OR fallback to using lastEpisodeCheckDate
|
||||
// lastEpisodeCheckDate will be the current time when adding a new podcast
|
||||
const dateToCheckForEpisodesAfter = latestEpisodePublishedAt || lastEpisodeCheckDate
|
||||
Logger.debug(`[PodcastManager] checkForNewEpisodes: "${libraryItem.media.metadata.title}" checking for episodes after ${new Date(dateToCheckForEpisodesAfter)}`)
|
||||
|
||||
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter)
|
||||
Logger.debug(`[PodcastManager] checkForNewEpisodes checked result ${newEpisodes ? newEpisodes.length : 'N/A'}`)
|
||||
|
||||
if (!newEpisodes) { // Failed
|
||||
// Allow up to 3 failed attempts before disabling auto download
|
||||
if (!this.failedCheckMap[libraryItem.id]) this.failedCheckMap[libraryItem.id] = 0
|
||||
this.failedCheckMap[libraryItem.id]++
|
||||
if (this.failedCheckMap[libraryItem.id] >= this.MaxFailedEpisodeChecks) {
|
||||
Logger.error(`[PodcastManager] checkForNewEpisodes ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.metadata.title}" - disabling auto download`)
|
||||
libraryItem.media.autoDownloadEpisodes = false
|
||||
delete this.failedCheckMap[libraryItem.id]
|
||||
} else {
|
||||
Logger.warn(`[PodcastManager] checkForNewEpisodes ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.metadata.title}"`)
|
||||
}
|
||||
} else if (newEpisodes.length) {
|
||||
if (!newEpisodes) { // Failed
|
||||
// Allow up to MaxFailedEpisodeChecks failed attempts before disabling auto download
|
||||
if (!this.failedCheckMap[libraryItem.id]) this.failedCheckMap[libraryItem.id] = 0
|
||||
this.failedCheckMap[libraryItem.id]++
|
||||
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`)
|
||||
libraryItem.media.autoDownloadEpisodes = false
|
||||
delete this.failedCheckMap[libraryItem.id]
|
||||
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
|
||||
this.downloadPodcastEpisodes(libraryItem, newEpisodes, true)
|
||||
} else {
|
||||
delete this.failedCheckMap[libraryItem.id]
|
||||
Logger.debug(`[PodcastManager] No new episodes for "${libraryItem.media.metadata.title}"`)
|
||||
Logger.warn(`[PodcastManager] runEpisodeCheck ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.metadata.title}"`)
|
||||
}
|
||||
|
||||
libraryItem.media.lastEpisodeCheck = Date.now()
|
||||
libraryItem.updatedAt = Date.now()
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
} else if (newEpisodes.length) {
|
||||
delete this.failedCheckMap[libraryItem.id]
|
||||
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
|
||||
this.downloadPodcastEpisodes(libraryItem, newEpisodes, true)
|
||||
} else {
|
||||
delete this.failedCheckMap[libraryItem.id]
|
||||
Logger.debug(`[PodcastManager] No new episodes for "${libraryItem.media.metadata.title}"`)
|
||||
}
|
||||
|
||||
libraryItem.media.lastEpisodeCheck = Date.now()
|
||||
libraryItem.updatedAt = Date.now()
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
return libraryItem.media.autoDownloadEpisodes
|
||||
}
|
||||
|
||||
async checkPodcastForNewEpisodes(podcastLibraryItem, dateToCheckForEpisodesAfter) {
|
||||
|
|
|
@ -18,6 +18,7 @@ class Podcast {
|
|||
this.episodes = []
|
||||
|
||||
this.autoDownloadEpisodes = false
|
||||
this.autoDownloadSchedule = null
|
||||
this.lastEpisodeCheck = 0
|
||||
this.maxEpisodesToKeep = 0
|
||||
|
||||
|
@ -40,6 +41,7 @@ class Podcast {
|
|||
return podcastEpisode
|
||||
})
|
||||
this.autoDownloadEpisodes = !!podcast.autoDownloadEpisodes
|
||||
this.autoDownloadSchedule = podcast.autoDownloadSchedule || '0 * * * *' // Added in 2.1.3 so default to hourly
|
||||
this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0
|
||||
this.maxEpisodesToKeep = podcast.maxEpisodesToKeep || 0
|
||||
}
|
||||
|
@ -52,6 +54,7 @@ class Podcast {
|
|||
tags: [...this.tags],
|
||||
episodes: this.episodes.map(e => e.toJSON()),
|
||||
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||
autoDownloadSchedule: this.autoDownloadSchedule,
|
||||
lastEpisodeCheck: this.lastEpisodeCheck,
|
||||
maxEpisodesToKeep: this.maxEpisodesToKeep
|
||||
}
|
||||
|
@ -64,6 +67,7 @@ class Podcast {
|
|||
tags: [...this.tags],
|
||||
numEpisodes: this.episodes.length,
|
||||
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||
autoDownloadSchedule: this.autoDownloadSchedule,
|
||||
lastEpisodeCheck: this.lastEpisodeCheck,
|
||||
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
||||
size: this.size
|
||||
|
@ -78,6 +82,7 @@ class Podcast {
|
|||
tags: [...this.tags],
|
||||
episodes: this.episodes.map(e => e.toJSONExpanded()),
|
||||
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||
autoDownloadSchedule: this.autoDownloadSchedule,
|
||||
lastEpisodeCheck: this.lastEpisodeCheck,
|
||||
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
||||
size: this.size
|
||||
|
@ -165,14 +170,15 @@ class Podcast {
|
|||
return null
|
||||
}
|
||||
|
||||
setData(mediaMetadata) {
|
||||
setData(mediaData) {
|
||||
this.metadata = new PodcastMetadata()
|
||||
if (mediaMetadata.metadata) {
|
||||
this.metadata.setData(mediaMetadata.metadata)
|
||||
if (mediaData.metadata) {
|
||||
this.metadata.setData(mediaData.metadata)
|
||||
}
|
||||
|
||||
this.coverPath = mediaMetadata.coverPath || null
|
||||
this.autoDownloadEpisodes = !!mediaMetadata.autoDownloadEpisodes
|
||||
this.coverPath = mediaData.coverPath || null
|
||||
this.autoDownloadEpisodes = !!mediaData.autoDownloadEpisodes
|
||||
this.autoDownloadSchedule = mediaData.autoDownloadSchedule || global.ServerSettings.podcastEpisodeSchedule
|
||||
this.lastEpisodeCheck = Date.now() // Makes sure new episodes are after this
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue