2023-08-12 22:52:09 +02:00
|
|
|
const Sequelize = require('sequelize')
|
2022-08-18 01:44:21 +02:00
|
|
|
const cron = require('../libs/nodeCron')
|
|
|
|
const Logger = require('../Logger')
|
2023-07-05 01:14:44 +02:00
|
|
|
const Database = require('../Database')
|
2023-09-04 18:50:55 +02:00
|
|
|
const LibraryScanner = require('../scanner/LibraryScanner')
|
2022-08-18 01:44:21 +02:00
|
|
|
|
2024-07-04 19:00:54 +02:00
|
|
|
const ShareManager = require('./ShareManager')
|
|
|
|
|
2022-08-18 01:44:21 +02:00
|
|
|
class CronManager {
|
2024-07-04 19:00:54 +02:00
|
|
|
constructor(podcastManager, playbackSessionManager) {
|
|
|
|
/** @type {import('./PodcastManager')} */
|
2022-08-20 01:41:58 +02:00
|
|
|
this.podcastManager = podcastManager
|
2024-07-04 19:00:54 +02:00
|
|
|
/** @type {import('./PlaybackSessionManager')} */
|
|
|
|
this.playbackSessionManager = playbackSessionManager
|
2022-08-18 01:44:21 +02:00
|
|
|
|
|
|
|
this.libraryScanCrons = []
|
2022-08-20 01:41:58 +02:00
|
|
|
this.podcastCrons = []
|
|
|
|
|
|
|
|
this.podcastCronExpressionsExecuting = []
|
2022-08-18 01:44:21 +02:00
|
|
|
}
|
|
|
|
|
2023-07-22 21:25:20 +02:00
|
|
|
/**
|
|
|
|
* Initialize library scan crons & podcast download crons
|
2024-06-21 00:08:18 +02:00
|
|
|
* @param {import('../objects/Library')[]} libraries
|
2023-07-22 21:25:20 +02:00
|
|
|
*/
|
2023-08-12 22:52:09 +02:00
|
|
|
async init(libraries) {
|
2024-07-04 19:00:54 +02:00
|
|
|
this.initOpenSessionCleanupCron()
|
2023-07-22 21:25:20 +02:00
|
|
|
this.initLibraryScanCrons(libraries)
|
2023-08-12 22:52:09 +02:00
|
|
|
await this.initPodcastCrons()
|
2022-08-18 01:44:21 +02:00
|
|
|
}
|
|
|
|
|
2024-07-04 19:00:54 +02:00
|
|
|
/**
|
|
|
|
* 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()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-07-22 21:25:20 +02:00
|
|
|
/**
|
|
|
|
* Initialize library scan crons
|
2024-06-21 00:08:18 +02:00
|
|
|
* @param {import('../objects/Library')[]} libraries
|
2023-07-22 21:25:20 +02:00
|
|
|
*/
|
|
|
|
initLibraryScanCrons(libraries) {
|
|
|
|
for (const library of libraries) {
|
2022-08-18 01:44:21 +02:00
|
|
|
if (library.settings.autoScanCronExpression) {
|
|
|
|
this.startCronForLibrary(library)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-21 00:08:18 +02:00
|
|
|
/**
|
|
|
|
* 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)
|
|
|
|
}
|
2022-08-18 01:44:21 +02:00
|
|
|
})
|
|
|
|
this.libraryScanCrons.push({
|
2024-06-21 00:08:18 +02:00
|
|
|
libraryId: _library.id,
|
|
|
|
expression: _library.settings.autoScanCronExpression,
|
2022-08-18 01:44:21 +02:00
|
|
|
task: libScanCron
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
removeCronForLibrary(library) {
|
|
|
|
Logger.debug(`[CronManager] Removing library scan cron for ${library.name}`)
|
2024-06-21 00:08:18 +02:00
|
|
|
this.libraryScanCrons = this.libraryScanCrons.filter((lsc) => lsc.libraryId !== library.id)
|
2022-08-18 01:44:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
updateLibraryScanCron(library) {
|
|
|
|
const expression = library.settings.autoScanCronExpression
|
2024-06-21 00:08:18 +02:00
|
|
|
const existingCron = this.libraryScanCrons.find((lsc) => lsc.libraryId === library.id)
|
2022-08-18 01:44:21 +02:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-12 22:52:09 +02:00
|
|
|
/**
|
|
|
|
* Init cron jobs for auto-download podcasts
|
|
|
|
*/
|
|
|
|
async initPodcastCrons() {
|
2022-08-20 01:41:58 +02:00
|
|
|
const cronExpressionMap = {}
|
2023-08-12 22:52:09 +02:00
|
|
|
|
2023-08-20 20:34:03 +02:00
|
|
|
const podcastsWithAutoDownload = await Database.podcastModel.findAll({
|
2023-08-12 22:52:09 +02:00
|
|
|
where: {
|
|
|
|
autoDownloadEpisodes: true,
|
|
|
|
autoDownloadSchedule: {
|
|
|
|
[Sequelize.Op.not]: null
|
2022-08-20 01:41:58 +02:00
|
|
|
}
|
2023-08-12 22:52:09 +02:00
|
|
|
},
|
|
|
|
include: {
|
2023-08-20 20:34:03 +02:00
|
|
|
model: Database.libraryItemModel
|
2022-08-20 01:41:58 +02:00
|
|
|
}
|
|
|
|
})
|
2023-08-12 22:52:09 +02:00
|
|
|
|
|
|
|
for (const podcast of podcastsWithAutoDownload) {
|
|
|
|
if (!cronExpressionMap[podcast.autoDownloadSchedule]) {
|
|
|
|
cronExpressionMap[podcast.autoDownloadSchedule] = {
|
|
|
|
expression: podcast.autoDownloadSchedule,
|
|
|
|
libraryItemIds: []
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cronExpressionMap[podcast.autoDownloadSchedule].libraryItemIds.push(podcast.libraryItem.id)
|
|
|
|
}
|
|
|
|
|
2022-08-20 01:41:58 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-03 13:07:58 +01:00
|
|
|
async executePodcastCron(expression) {
|
2024-06-21 00:08:18 +02:00
|
|
|
const podcastCron = this.podcastCrons.find((cron) => cron.expression === expression)
|
2022-08-20 01:41:58 +02:00
|
|
|
if (!podcastCron) {
|
|
|
|
Logger.error(`[CronManager] Podcast cron not found for expression ${expression}`)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
this.podcastCronExpressionsExecuting.push(expression)
|
|
|
|
|
2023-11-03 13:07:58 +01:00
|
|
|
const libraryItemIds = podcastCron.libraryItemIds
|
|
|
|
Logger.debug(`[CronManager] Start executing podcast cron ${expression} for ${libraryItemIds.length} item(s)`)
|
|
|
|
|
2022-08-20 01:41:58 +02:00
|
|
|
// Get podcast library items to check
|
|
|
|
const libraryItems = []
|
|
|
|
for (const libraryItemId of libraryItemIds) {
|
2023-08-20 20:34:03 +02:00
|
|
|
const libraryItem = await Database.libraryItemModel.getOldById(libraryItemId)
|
2022-08-20 01:41:58 +02:00
|
|
|
if (!libraryItem) {
|
|
|
|
Logger.error(`[CronManager] Library item ${libraryItemId} not found for episode check cron ${expression}`)
|
2024-06-21 00:08:18 +02:00
|
|
|
podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter((lid) => lid !== libraryItemId) // Filter it out
|
2022-08-20 01:41:58 +02:00
|
|
|
} else {
|
|
|
|
libraryItems.push(libraryItem)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run episode checks
|
|
|
|
for (const libraryItem of libraryItems) {
|
|
|
|
const keepAutoDownloading = await this.podcastManager.runEpisodeCheck(libraryItem)
|
2024-06-21 00:08:18 +02:00
|
|
|
if (!keepAutoDownloading) {
|
|
|
|
// auto download was disabled
|
|
|
|
podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter((lid) => lid !== libraryItem.id) // Filter it out
|
2022-08-20 01:41:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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)`)
|
2024-06-21 00:08:18 +02:00
|
|
|
this.podcastCronExpressionsExecuting = this.podcastCronExpressionsExecuting.filter((exp) => exp !== expression)
|
2022-08-20 01:41:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
removePodcastEpisodeCron(podcastCron) {
|
|
|
|
Logger.info(`[CronManager] Stopping & removing podcast episode cron for ${podcastCron.expression}`)
|
|
|
|
if (podcastCron.task) podcastCron.task.stop()
|
2024-06-21 00:08:18 +02:00
|
|
|
this.podcastCrons = this.podcastCrons.filter((pc) => pc.expression !== podcastCron.expression)
|
2022-08-20 01:41:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
checkUpdatePodcastCron(libraryItem) {
|
|
|
|
// Remove from old cron by library item id
|
2024-06-21 00:08:18 +02:00
|
|
|
const existingCron = this.podcastCrons.find((pc) => pc.libraryItemIds.includes(libraryItem.id))
|
2022-08-20 01:41:58 +02:00
|
|
|
if (existingCron) {
|
2024-06-21 00:08:18 +02:00
|
|
|
existingCron.libraryItemIds = existingCron.libraryItemIds.filter((lid) => lid !== libraryItem.id)
|
2022-08-20 01:41:58 +02:00
|
|
|
if (!existingCron.libraryItemIds.length) {
|
|
|
|
this.removePodcastEpisodeCron(existingCron)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add to cron or start new cron
|
|
|
|
if (libraryItem.media.autoDownloadEpisodes && libraryItem.media.autoDownloadSchedule) {
|
2024-06-21 00:08:18 +02:00
|
|
|
const cronMatchingExpression = this.podcastCrons.find((pc) => pc.expression === libraryItem.media.autoDownloadSchedule)
|
2022-08-20 01:41:58 +02:00
|
|
|
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])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-08-18 01:44:21 +02:00
|
|
|
}
|
2024-06-21 00:08:18 +02:00
|
|
|
module.exports = CronManager
|