mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add: Backup notification (#3225)
* Formatting updates * Add: backup completion notification * Fix: comment for backup * Add: backup size units to notification * Add: failed backup notification * Add: calls to failed backup notification * Update: notification OpenAPI spec * Update notifications to first check if any are active for an event, update JS docs --------- Co-authored-by: advplyr <advplyr@protonmail.com>
This commit is contained in:
		
							parent
							
								
									5308fd8b46
								
							
						
					
					
						commit
						27b3a44147
					
				@ -22,7 +22,7 @@ components:
 | 
			
		||||
    notificationEventName:
 | 
			
		||||
      type: string
 | 
			
		||||
      description: The name of the event the notification will fire on.
 | 
			
		||||
      enum: ['onPodcastEpisodeDownloaded', 'onTest']
 | 
			
		||||
      enum: ['onPodcastEpisodeDownloaded', 'onBackupCompleted', 'onBackupFailed', 'onTest']
 | 
			
		||||
    urls:
 | 
			
		||||
      type: array
 | 
			
		||||
      items:
 | 
			
		||||
 | 
			
		||||
@ -3226,6 +3226,8 @@
 | 
			
		||||
        "description": "The name of the event the notification will fire on.",
 | 
			
		||||
        "enum": [
 | 
			
		||||
          "onPodcastEpisodeDownloaded",
 | 
			
		||||
          "onBackupCompleted",
 | 
			
		||||
          "onBackupFailed",
 | 
			
		||||
          "onTest"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
@ -68,7 +68,7 @@ class Server {
 | 
			
		||||
    // Managers
 | 
			
		||||
    this.notificationManager = new NotificationManager()
 | 
			
		||||
    this.emailManager = new EmailManager()
 | 
			
		||||
    this.backupManager = new BackupManager()
 | 
			
		||||
    this.backupManager = new BackupManager(this.notificationManager)
 | 
			
		||||
    this.abMergeManager = new AbMergeManager()
 | 
			
		||||
    this.playbackSessionManager = new PlaybackSessionManager()
 | 
			
		||||
    this.podcastManager = new PodcastManager(this.watcher, this.notificationManager)
 | 
			
		||||
 | 
			
		||||
@ -16,9 +16,11 @@ const { getFileSize } = require('../utils/fileUtils')
 | 
			
		||||
const Backup = require('../objects/Backup')
 | 
			
		||||
 | 
			
		||||
class BackupManager {
 | 
			
		||||
  constructor() {
 | 
			
		||||
  constructor(notificationManager) {
 | 
			
		||||
    this.ItemsMetadataPath = Path.join(global.MetadataPath, 'items')
 | 
			
		||||
    this.AuthorsMetadataPath = Path.join(global.MetadataPath, 'authors')
 | 
			
		||||
    /** @type {import('./NotificationManager')} */
 | 
			
		||||
    this.notificationManager = notificationManager
 | 
			
		||||
 | 
			
		||||
    this.scheduleTask = null
 | 
			
		||||
 | 
			
		||||
@ -294,6 +296,8 @@ class BackupManager {
 | 
			
		||||
    // Create backup sqlite file
 | 
			
		||||
    const sqliteBackupPath = await this.backupSqliteDb(newBackup).catch((error) => {
 | 
			
		||||
      Logger.error(`[BackupManager] Failed to backup sqlite db`, error)
 | 
			
		||||
      const errorMsg = error?.message || error || 'Unknown Error'
 | 
			
		||||
      this.notificationManager.onBackupFailed(errorMsg)
 | 
			
		||||
      return false
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@ -304,6 +308,8 @@ class BackupManager {
 | 
			
		||||
    // Zip sqlite file, /metadata/items, and /metadata/authors folders
 | 
			
		||||
    const zipResult = await this.zipBackup(sqliteBackupPath, newBackup).catch((error) => {
 | 
			
		||||
      Logger.error(`[BackupManager] Backup Failed ${error}`)
 | 
			
		||||
      const errorMsg = error?.message || error || 'Unknown Error'
 | 
			
		||||
      this.notificationManager.onBackupFailed(errorMsg)
 | 
			
		||||
      return false
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@ -324,13 +330,18 @@ class BackupManager {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check remove oldest backup
 | 
			
		||||
    if (this.backups.length > this.backupsToKeep) {
 | 
			
		||||
    const removeOldest = this.backups.length > this.backupsToKeep
 | 
			
		||||
    if (removeOldest) {
 | 
			
		||||
      this.backups.sort((a, b) => a.createdAt - b.createdAt)
 | 
			
		||||
 | 
			
		||||
      const oldBackup = this.backups.shift()
 | 
			
		||||
      Logger.debug(`[BackupManager] Removing old backup ${oldBackup.id}`)
 | 
			
		||||
      this.removeBackup(oldBackup)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Notification for backup successfully completed
 | 
			
		||||
    this.notificationManager.onBackupCompleted(newBackup, this.backups.length, removeOldest)
 | 
			
		||||
 | 
			
		||||
    return true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -348,7 +359,6 @@ class BackupManager {
 | 
			
		||||
  /**
 | 
			
		||||
   * @see https://github.com/TryGhost/node-sqlite3/pull/1116
 | 
			
		||||
   * @param {Backup} backup
 | 
			
		||||
   * @promise
 | 
			
		||||
   */
 | 
			
		||||
  backupSqliteDb(backup) {
 | 
			
		||||
    const db = new sqlite3.Database(Database.dbPath)
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
const axios = require('axios')
 | 
			
		||||
const Logger = require("../Logger")
 | 
			
		||||
const Logger = require('../Logger')
 | 
			
		||||
const SocketAuthority = require('../SocketAuthority')
 | 
			
		||||
const Database = require('../Database')
 | 
			
		||||
const { notificationData } = require('../utils/notifications')
 | 
			
		||||
@ -17,6 +17,11 @@ class NotificationManager {
 | 
			
		||||
  async onPodcastEpisodeDownloaded(libraryItem, episode) {
 | 
			
		||||
    if (!Database.notificationSettings.isUseable) return
 | 
			
		||||
 | 
			
		||||
    if (!Database.notificationSettings.getHasActiveNotificationsForEvent('onPodcastEpisodeDownloaded')) {
 | 
			
		||||
      Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: No active notifications`)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`)
 | 
			
		||||
    const library = await Database.libraryModel.getOldById(libraryItem.libraryId)
 | 
			
		||||
    const eventData = {
 | 
			
		||||
@ -36,10 +41,60 @@ class NotificationManager {
 | 
			
		||||
    this.triggerNotification('onPodcastEpisodeDownloaded', eventData)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   *
 | 
			
		||||
   * @param {import('../objects/Backup')} backup
 | 
			
		||||
   * @param {number} totalBackupCount
 | 
			
		||||
   * @param {boolean} removedOldest - If oldest backup was removed
 | 
			
		||||
   */
 | 
			
		||||
  async onBackupCompleted(backup, totalBackupCount, removedOldest) {
 | 
			
		||||
    if (!Database.notificationSettings.isUseable) return
 | 
			
		||||
 | 
			
		||||
    if (!Database.notificationSettings.getHasActiveNotificationsForEvent('onBackupCompleted')) {
 | 
			
		||||
      Logger.debug(`[NotificationManager] onBackupCompleted: No active notifications`)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Logger.debug(`[NotificationManager] onBackupCompleted: Backup completed`)
 | 
			
		||||
    const eventData = {
 | 
			
		||||
      completionTime: backup.createdAt,
 | 
			
		||||
      backupPath: backup.fullPath,
 | 
			
		||||
      backupSize: backup.fileSize,
 | 
			
		||||
      backupCount: totalBackupCount || 'Invalid',
 | 
			
		||||
      removedOldest: removedOldest || 'false'
 | 
			
		||||
    }
 | 
			
		||||
    this.triggerNotification('onBackupCompleted', eventData)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   *
 | 
			
		||||
   * @param {string} errorMsg
 | 
			
		||||
   */
 | 
			
		||||
  async onBackupFailed(errorMsg) {
 | 
			
		||||
    if (!Database.notificationSettings.isUseable) return
 | 
			
		||||
 | 
			
		||||
    if (!Database.notificationSettings.getHasActiveNotificationsForEvent('onBackupFailed')) {
 | 
			
		||||
      Logger.debug(`[NotificationManager] onBackupFailed: No active notifications`)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Logger.debug(`[NotificationManager] onBackupFailed: Backup failed (${errorMsg})`)
 | 
			
		||||
    const eventData = {
 | 
			
		||||
      errorMsg: errorMsg || 'Backup failed'
 | 
			
		||||
    }
 | 
			
		||||
    this.triggerNotification('onBackupFailed', eventData)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onTest() {
 | 
			
		||||
    this.triggerNotification('onTest')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   *
 | 
			
		||||
   * @param {string} eventName
 | 
			
		||||
   * @param {any} eventData
 | 
			
		||||
   * @param {boolean} [intentionallyFail=false] - If true, will intentionally fail the notification
 | 
			
		||||
   */
 | 
			
		||||
  async triggerNotification(eventName, eventData, intentionallyFail = false) {
 | 
			
		||||
    if (!Database.notificationSettings.isUseable) return
 | 
			
		||||
 | 
			
		||||
@ -52,7 +107,8 @@ class NotificationManager {
 | 
			
		||||
      const success = intentionallyFail ? false : await this.sendNotification(notification, eventData)
 | 
			
		||||
 | 
			
		||||
      notification.updateNotificationFired(success)
 | 
			
		||||
      if (!success) { // Failed notification
 | 
			
		||||
      if (!success) {
 | 
			
		||||
        // Failed notification
 | 
			
		||||
        if (notification.numConsecutiveFailedAttempts >= Database.notificationSettings.maxFailedAttempts) {
 | 
			
		||||
          Logger.error(`[NotificationManager] triggerNotification: ${notification.eventName}/${notification.id} reached max failed attempts`)
 | 
			
		||||
          notification.enabled = false
 | 
			
		||||
@ -68,7 +124,12 @@ class NotificationManager {
 | 
			
		||||
    this.notificationFinished()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Return TRUE if notification should be triggered now
 | 
			
		||||
  /**
 | 
			
		||||
   *
 | 
			
		||||
   * @param {string} eventName
 | 
			
		||||
   * @param {any} eventData
 | 
			
		||||
   * @returns {boolean} - TRUE if notification should be triggered now
 | 
			
		||||
   */
 | 
			
		||||
  checkTriggerNotification(eventName, eventData) {
 | 
			
		||||
    if (this.sendingNotification) {
 | 
			
		||||
      if (this.notificationQueue.length >= Database.notificationSettings.maxNotificationQueue) {
 | 
			
		||||
@ -87,7 +148,8 @@ class NotificationManager {
 | 
			
		||||
    // Delay between events then run next notification in queue
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      this.sendingNotification = false
 | 
			
		||||
      if (this.notificationQueue.length) { // Send next notification in queue
 | 
			
		||||
      if (this.notificationQueue.length) {
 | 
			
		||||
        // Send next notification in queue
 | 
			
		||||
        const nextNotificationEvent = this.notificationQueue.shift()
 | 
			
		||||
        this.triggerNotification(nextNotificationEvent.eventName, nextNotificationEvent.eventData)
 | 
			
		||||
      }
 | 
			
		||||
@ -95,7 +157,7 @@ class NotificationManager {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  sendTestNotification(notification) {
 | 
			
		||||
    const eventData = notificationData.events.find(e => e.name === notification.eventName)
 | 
			
		||||
    const eventData = notificationData.events.find((e) => e.name === notification.eventName)
 | 
			
		||||
    if (!eventData) {
 | 
			
		||||
      Logger.error(`[NotificationManager] sendTestNotification: Event not found ${notification.eventName}`)
 | 
			
		||||
      return false
 | 
			
		||||
@ -106,13 +168,16 @@ class NotificationManager {
 | 
			
		||||
 | 
			
		||||
  sendNotification(notification, eventData) {
 | 
			
		||||
    const payload = notification.getApprisePayload(eventData)
 | 
			
		||||
    return axios.post(Database.notificationSettings.appriseApiUrl, payload, { timeout: 6000 }).then((response) => {
 | 
			
		||||
      Logger.debug(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} response=`, response.data)
 | 
			
		||||
      return true
 | 
			
		||||
    }).catch((error) => {
 | 
			
		||||
      Logger.error(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} error=`, error)
 | 
			
		||||
      return false
 | 
			
		||||
    })
 | 
			
		||||
    return axios
 | 
			
		||||
      .post(Database.notificationSettings.appriseApiUrl, payload, { timeout: 6000 })
 | 
			
		||||
      .then((response) => {
 | 
			
		||||
        Logger.debug(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} response=`, response.data)
 | 
			
		||||
        return true
 | 
			
		||||
      })
 | 
			
		||||
      .catch((error) => {
 | 
			
		||||
        Logger.error(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} error=`, error)
 | 
			
		||||
        return false
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
module.exports = NotificationManager
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ class NotificationSettings {
 | 
			
		||||
  construct(settings) {
 | 
			
		||||
    this.appriseType = settings.appriseType
 | 
			
		||||
    this.appriseApiUrl = settings.appriseApiUrl || null
 | 
			
		||||
    this.notifications = (settings.notifications || []).map(n => new Notification(n))
 | 
			
		||||
    this.notifications = (settings.notifications || []).map((n) => new Notification(n))
 | 
			
		||||
    this.maxFailedAttempts = settings.maxFailedAttempts || 5
 | 
			
		||||
    this.maxNotificationQueue = settings.maxNotificationQueue || 20
 | 
			
		||||
    this.notificationDelay = settings.notificationDelay || 1000
 | 
			
		||||
@ -31,7 +31,7 @@ class NotificationSettings {
 | 
			
		||||
      id: this.id,
 | 
			
		||||
      appriseType: this.appriseType,
 | 
			
		||||
      appriseApiUrl: this.appriseApiUrl,
 | 
			
		||||
      notifications: this.notifications.map(n => n.toJSON()),
 | 
			
		||||
      notifications: this.notifications.map((n) => n.toJSON()),
 | 
			
		||||
      maxFailedAttempts: this.maxFailedAttempts,
 | 
			
		||||
      maxNotificationQueue: this.maxNotificationQueue,
 | 
			
		||||
      notificationDelay: this.notificationDelay
 | 
			
		||||
@ -42,17 +42,29 @@ class NotificationSettings {
 | 
			
		||||
    return !!this.appriseApiUrl
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {string} eventName
 | 
			
		||||
   * @returns {boolean} - TRUE if there are active notifications for the event
 | 
			
		||||
   */
 | 
			
		||||
  getHasActiveNotificationsForEvent(eventName) {
 | 
			
		||||
    return this.notifications.some((n) => n.eventName === eventName && n.enabled)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {string} eventName
 | 
			
		||||
   * @returns {Notification[]}
 | 
			
		||||
   */
 | 
			
		||||
  getActiveNotificationsForEvent(eventName) {
 | 
			
		||||
    return this.notifications.filter(n => n.eventName === eventName && n.enabled)
 | 
			
		||||
    return this.notifications.filter((n) => n.eventName === eventName && n.enabled)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getNotification(id) {
 | 
			
		||||
    return this.notifications.find(n => n.id === id)
 | 
			
		||||
    return this.notifications.find((n) => n.id === id)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeNotification(id) {
 | 
			
		||||
    if (this.notifications.some(n => n.id === id)) {
 | 
			
		||||
      this.notifications = this.notifications.filter(n => n.id !== id)
 | 
			
		||||
    if (this.notifications.some((n) => n.id === id)) {
 | 
			
		||||
      this.notifications = this.notifications.filter((n) => n.id !== id)
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
    return false
 | 
			
		||||
@ -94,7 +106,7 @@ class NotificationSettings {
 | 
			
		||||
 | 
			
		||||
  updateNotification(payload) {
 | 
			
		||||
    if (!payload) return false
 | 
			
		||||
    const notification = this.notifications.find(n => n.id === payload.id)
 | 
			
		||||
    const notification = this.notifications.find((n) => n.id === payload.id)
 | 
			
		||||
    if (!notification) {
 | 
			
		||||
      Logger.error(`[NotificationSettings] updateNotification: Notification not found ${payload.id}`)
 | 
			
		||||
      return false
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,36 @@ module.exports.notificationData = {
 | 
			
		||||
        episodeDescription: 'Some description of the podcast episode.'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'onBackupCompleted',
 | 
			
		||||
      requiresLibrary: false,
 | 
			
		||||
      description: 'Triggered when a backup is completed',
 | 
			
		||||
      variables: ['completionTime', 'backupPath', 'backupSize', 'backupCount', 'removedOldest'],
 | 
			
		||||
      defaults: {
 | 
			
		||||
        title: 'Backup Completed',
 | 
			
		||||
        body: 'Backup has been completed successfully.\n\nPath: {{backupPath}}\nSize: {{backupSize}} bytes\nCount: {{backupCount}}\nRemoved Oldest: {{removedOldest}}'
 | 
			
		||||
      },
 | 
			
		||||
      testData: {
 | 
			
		||||
        completionTime: '12:00 AM',
 | 
			
		||||
        backupPath: 'path/to/backup',
 | 
			
		||||
        backupSize: '1.23 MB',
 | 
			
		||||
        backupCount: '1',
 | 
			
		||||
        removedOldest: 'false'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'onBackupFailed',
 | 
			
		||||
      requiresLibrary: false,
 | 
			
		||||
      description: 'Triggered when a backup fails',
 | 
			
		||||
      variables: ['errorMsg'],
 | 
			
		||||
      defaults: {
 | 
			
		||||
        title: 'Backup Failed',
 | 
			
		||||
        body: 'Backup failed, check ABS logs for more information.\nError message: {{errorMsg}}'
 | 
			
		||||
      },
 | 
			
		||||
      testData: {
 | 
			
		||||
        errorMsg: 'Example error message'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'onTest',
 | 
			
		||||
      requiresLibrary: false,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user