mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-03 19:07:00 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			232 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			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
 |