audiobookshelf/server/managers/CronManager.js

232 lines
8.4 KiB
JavaScript

const Sequelize = require('sequelize')
const cron = require('../libs/nodeCron')
const Logger = require('../Logger')
const Database = require('../Database')
const LibraryScanner = require('../scanner/LibraryScanner')
const ShareManager = require('./ShareManager')
class CronManager {
constructor(podcastManager, playbackSessionManager) {
/** @type {import('./PodcastManager')} */
this.podcastManager = podcastManager
/** @type {import('./PlaybackSessionManager')} */
this.playbackSessionManager = playbackSessionManager
this.libraryScanCrons = []
this.podcastCrons = []
this.podcastCronExpressionsExecuting = []
}
/**
* Initialize library scan crons & podcast download crons
* @param {import('../objects/Library')[]} libraries
*/
async init(libraries) {
this.initOpenSessionCleanupCron()
this.initLibraryScanCrons(libraries)
await this.initPodcastCrons()
}
/**
* Initialize open session cleanup cron
* Runs every day at 00:30
* Closes open share sessions that have not been updated in 24 hours
* Closes open playback sessions that have not been updated in 36 hours
* TODO: Clients should re-open the session if it is closed so that stale sessions can be closed sooner
*/
initOpenSessionCleanupCron() {
cron.schedule('30 0 * * *', async () => {
Logger.debug('[CronManager] Open session cleanup cron executing')
ShareManager.closeStaleOpenShareSessions()
await this.playbackSessionManager.closeStaleOpenSessions()
})
}
/**
* Initialize library scan crons
* @param {import('../objects/Library')[]} libraries
*/
initLibraryScanCrons(libraries) {
for (const library of libraries) {
if (library.settings.autoScanCronExpression) {
this.startCronForLibrary(library)
}
}
}
/**
* Start cron schedule for library
*
* @param {import('../objects/Library')} _library
*/
startCronForLibrary(_library) {
Logger.debug(`[CronManager] Init library scan cron for ${_library.name} on schedule ${_library.settings.autoScanCronExpression}`)
const libScanCron = cron.schedule(_library.settings.autoScanCronExpression, async () => {
const library = await Database.libraryModel.getOldById(_library.id)
if (!library) {
Logger.error(`[CronManager] Library not found for scan cron ${_library.id}`)
} else {
Logger.debug(`[CronManager] Library scan cron executing for ${library.name}`)
LibraryScanner.scan(library)
}
})
this.libraryScanCrons.push({
libraryId: _library.id,
expression: _library.settings.autoScanCronExpression,
task: libScanCron
})
}
removeCronForLibrary(library) {
Logger.debug(`[CronManager] Removing library scan cron for ${library.name}`)
this.libraryScanCrons = this.libraryScanCrons.filter((lsc) => lsc.libraryId !== library.id)
}
updateLibraryScanCron(library) {
const expression = library.settings.autoScanCronExpression
const existingCron = this.libraryScanCrons.find((lsc) => lsc.libraryId === library.id)
if (!expression && existingCron) {
if (existingCron.task.stop) existingCron.task.stop()
this.removeCronForLibrary(library)
} else if (!existingCron && expression) {
this.startCronForLibrary(library)
} else if (existingCron && existingCron.expression !== expression) {
if (existingCron.task.stop) existingCron.task.stop()
this.removeCronForLibrary(library)
this.startCronForLibrary(library)
}
}
/**
* Init cron jobs for auto-download podcasts
*/
async initPodcastCrons() {
const cronExpressionMap = {}
const podcastsWithAutoDownload = await Database.podcastModel.findAll({
where: {
autoDownloadEpisodes: true,
autoDownloadSchedule: {
[Sequelize.Op.not]: null
}
},
include: {
model: Database.libraryItemModel
}
})
for (const podcast of podcastsWithAutoDownload) {
if (!cronExpressionMap[podcast.autoDownloadSchedule]) {
cronExpressionMap[podcast.autoDownloadSchedule] = {
expression: podcast.autoDownloadSchedule,
libraryItemIds: []
}
}
cronExpressionMap[podcast.autoDownloadSchedule].libraryItemIds.push(podcast.libraryItem.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) {
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)
const libraryItemIds = podcastCron.libraryItemIds
Logger.debug(`[CronManager] Start executing podcast cron ${expression} for ${libraryItemIds.length} item(s)`)
// Get podcast library items to check
const libraryItems = []
for (const libraryItemId of libraryItemIds) {
const libraryItem = await Database.libraryItemModel.getOldById(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 !== libraryItem.id) // 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