mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +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:
|
notificationEventName:
|
||||||
type: string
|
type: string
|
||||||
description: The name of the event the notification will fire on.
|
description: The name of the event the notification will fire on.
|
||||||
enum: ['onPodcastEpisodeDownloaded', 'onTest']
|
enum: ['onPodcastEpisodeDownloaded', 'onBackupCompleted', 'onBackupFailed', 'onTest']
|
||||||
urls:
|
urls:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
@ -3226,6 +3226,8 @@
|
|||||||
"description": "The name of the event the notification will fire on.",
|
"description": "The name of the event the notification will fire on.",
|
||||||
"enum": [
|
"enum": [
|
||||||
"onPodcastEpisodeDownloaded",
|
"onPodcastEpisodeDownloaded",
|
||||||
|
"onBackupCompleted",
|
||||||
|
"onBackupFailed",
|
||||||
"onTest"
|
"onTest"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -68,7 +68,7 @@ class Server {
|
|||||||
// Managers
|
// Managers
|
||||||
this.notificationManager = new NotificationManager()
|
this.notificationManager = new NotificationManager()
|
||||||
this.emailManager = new EmailManager()
|
this.emailManager = new EmailManager()
|
||||||
this.backupManager = new BackupManager()
|
this.backupManager = new BackupManager(this.notificationManager)
|
||||||
this.abMergeManager = new AbMergeManager()
|
this.abMergeManager = new AbMergeManager()
|
||||||
this.playbackSessionManager = new PlaybackSessionManager()
|
this.playbackSessionManager = new PlaybackSessionManager()
|
||||||
this.podcastManager = new PodcastManager(this.watcher, this.notificationManager)
|
this.podcastManager = new PodcastManager(this.watcher, this.notificationManager)
|
||||||
|
@ -16,9 +16,11 @@ const { getFileSize } = require('../utils/fileUtils')
|
|||||||
const Backup = require('../objects/Backup')
|
const Backup = require('../objects/Backup')
|
||||||
|
|
||||||
class BackupManager {
|
class BackupManager {
|
||||||
constructor() {
|
constructor(notificationManager) {
|
||||||
this.ItemsMetadataPath = Path.join(global.MetadataPath, 'items')
|
this.ItemsMetadataPath = Path.join(global.MetadataPath, 'items')
|
||||||
this.AuthorsMetadataPath = Path.join(global.MetadataPath, 'authors')
|
this.AuthorsMetadataPath = Path.join(global.MetadataPath, 'authors')
|
||||||
|
/** @type {import('./NotificationManager')} */
|
||||||
|
this.notificationManager = notificationManager
|
||||||
|
|
||||||
this.scheduleTask = null
|
this.scheduleTask = null
|
||||||
|
|
||||||
@ -294,6 +296,8 @@ class BackupManager {
|
|||||||
// Create backup sqlite file
|
// Create backup sqlite file
|
||||||
const sqliteBackupPath = await this.backupSqliteDb(newBackup).catch((error) => {
|
const sqliteBackupPath = await this.backupSqliteDb(newBackup).catch((error) => {
|
||||||
Logger.error(`[BackupManager] Failed to backup sqlite db`, error)
|
Logger.error(`[BackupManager] Failed to backup sqlite db`, error)
|
||||||
|
const errorMsg = error?.message || error || 'Unknown Error'
|
||||||
|
this.notificationManager.onBackupFailed(errorMsg)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -304,6 +308,8 @@ class BackupManager {
|
|||||||
// Zip sqlite file, /metadata/items, and /metadata/authors folders
|
// Zip sqlite file, /metadata/items, and /metadata/authors folders
|
||||||
const zipResult = await this.zipBackup(sqliteBackupPath, newBackup).catch((error) => {
|
const zipResult = await this.zipBackup(sqliteBackupPath, newBackup).catch((error) => {
|
||||||
Logger.error(`[BackupManager] Backup Failed ${error}`)
|
Logger.error(`[BackupManager] Backup Failed ${error}`)
|
||||||
|
const errorMsg = error?.message || error || 'Unknown Error'
|
||||||
|
this.notificationManager.onBackupFailed(errorMsg)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -324,13 +330,18 @@ class BackupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check remove oldest backup
|
// 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)
|
this.backups.sort((a, b) => a.createdAt - b.createdAt)
|
||||||
|
|
||||||
const oldBackup = this.backups.shift()
|
const oldBackup = this.backups.shift()
|
||||||
Logger.debug(`[BackupManager] Removing old backup ${oldBackup.id}`)
|
Logger.debug(`[BackupManager] Removing old backup ${oldBackup.id}`)
|
||||||
this.removeBackup(oldBackup)
|
this.removeBackup(oldBackup)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notification for backup successfully completed
|
||||||
|
this.notificationManager.onBackupCompleted(newBackup, this.backups.length, removeOldest)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,7 +359,6 @@ class BackupManager {
|
|||||||
/**
|
/**
|
||||||
* @see https://github.com/TryGhost/node-sqlite3/pull/1116
|
* @see https://github.com/TryGhost/node-sqlite3/pull/1116
|
||||||
* @param {Backup} backup
|
* @param {Backup} backup
|
||||||
* @promise
|
|
||||||
*/
|
*/
|
||||||
backupSqliteDb(backup) {
|
backupSqliteDb(backup) {
|
||||||
const db = new sqlite3.Database(Database.dbPath)
|
const db = new sqlite3.Database(Database.dbPath)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const Logger = require("../Logger")
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
const Database = require('../Database')
|
const Database = require('../Database')
|
||||||
const { notificationData } = require('../utils/notifications')
|
const { notificationData } = require('../utils/notifications')
|
||||||
@ -17,6 +17,11 @@ class NotificationManager {
|
|||||||
async onPodcastEpisodeDownloaded(libraryItem, episode) {
|
async onPodcastEpisodeDownloaded(libraryItem, episode) {
|
||||||
if (!Database.notificationSettings.isUseable) return
|
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}`)
|
Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`)
|
||||||
const library = await Database.libraryModel.getOldById(libraryItem.libraryId)
|
const library = await Database.libraryModel.getOldById(libraryItem.libraryId)
|
||||||
const eventData = {
|
const eventData = {
|
||||||
@ -36,10 +41,60 @@ class NotificationManager {
|
|||||||
this.triggerNotification('onPodcastEpisodeDownloaded', eventData)
|
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() {
|
onTest() {
|
||||||
this.triggerNotification('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) {
|
async triggerNotification(eventName, eventData, intentionallyFail = false) {
|
||||||
if (!Database.notificationSettings.isUseable) return
|
if (!Database.notificationSettings.isUseable) return
|
||||||
|
|
||||||
@ -52,7 +107,8 @@ class NotificationManager {
|
|||||||
const success = intentionallyFail ? false : await this.sendNotification(notification, eventData)
|
const success = intentionallyFail ? false : await this.sendNotification(notification, eventData)
|
||||||
|
|
||||||
notification.updateNotificationFired(success)
|
notification.updateNotificationFired(success)
|
||||||
if (!success) { // Failed notification
|
if (!success) {
|
||||||
|
// Failed notification
|
||||||
if (notification.numConsecutiveFailedAttempts >= Database.notificationSettings.maxFailedAttempts) {
|
if (notification.numConsecutiveFailedAttempts >= Database.notificationSettings.maxFailedAttempts) {
|
||||||
Logger.error(`[NotificationManager] triggerNotification: ${notification.eventName}/${notification.id} reached max failed attempts`)
|
Logger.error(`[NotificationManager] triggerNotification: ${notification.eventName}/${notification.id} reached max failed attempts`)
|
||||||
notification.enabled = false
|
notification.enabled = false
|
||||||
@ -68,7 +124,12 @@ class NotificationManager {
|
|||||||
this.notificationFinished()
|
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) {
|
checkTriggerNotification(eventName, eventData) {
|
||||||
if (this.sendingNotification) {
|
if (this.sendingNotification) {
|
||||||
if (this.notificationQueue.length >= Database.notificationSettings.maxNotificationQueue) {
|
if (this.notificationQueue.length >= Database.notificationSettings.maxNotificationQueue) {
|
||||||
@ -87,7 +148,8 @@ class NotificationManager {
|
|||||||
// Delay between events then run next notification in queue
|
// Delay between events then run next notification in queue
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.sendingNotification = false
|
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()
|
const nextNotificationEvent = this.notificationQueue.shift()
|
||||||
this.triggerNotification(nextNotificationEvent.eventName, nextNotificationEvent.eventData)
|
this.triggerNotification(nextNotificationEvent.eventName, nextNotificationEvent.eventData)
|
||||||
}
|
}
|
||||||
@ -95,7 +157,7 @@ class NotificationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendTestNotification(notification) {
|
sendTestNotification(notification) {
|
||||||
const eventData = notificationData.events.find(e => e.name === notification.eventName)
|
const eventData = notificationData.events.find((e) => e.name === notification.eventName)
|
||||||
if (!eventData) {
|
if (!eventData) {
|
||||||
Logger.error(`[NotificationManager] sendTestNotification: Event not found ${notification.eventName}`)
|
Logger.error(`[NotificationManager] sendTestNotification: Event not found ${notification.eventName}`)
|
||||||
return false
|
return false
|
||||||
@ -106,13 +168,16 @@ class NotificationManager {
|
|||||||
|
|
||||||
sendNotification(notification, eventData) {
|
sendNotification(notification, eventData) {
|
||||||
const payload = notification.getApprisePayload(eventData)
|
const payload = notification.getApprisePayload(eventData)
|
||||||
return axios.post(Database.notificationSettings.appriseApiUrl, payload, { timeout: 6000 }).then((response) => {
|
return axios
|
||||||
Logger.debug(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} response=`, response.data)
|
.post(Database.notificationSettings.appriseApiUrl, payload, { timeout: 6000 })
|
||||||
return true
|
.then((response) => {
|
||||||
}).catch((error) => {
|
Logger.debug(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} response=`, response.data)
|
||||||
Logger.error(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} error=`, error)
|
return true
|
||||||
return false
|
})
|
||||||
})
|
.catch((error) => {
|
||||||
|
Logger.error(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} error=`, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = NotificationManager
|
module.exports = NotificationManager
|
||||||
|
@ -20,7 +20,7 @@ class NotificationSettings {
|
|||||||
construct(settings) {
|
construct(settings) {
|
||||||
this.appriseType = settings.appriseType
|
this.appriseType = settings.appriseType
|
||||||
this.appriseApiUrl = settings.appriseApiUrl || null
|
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.maxFailedAttempts = settings.maxFailedAttempts || 5
|
||||||
this.maxNotificationQueue = settings.maxNotificationQueue || 20
|
this.maxNotificationQueue = settings.maxNotificationQueue || 20
|
||||||
this.notificationDelay = settings.notificationDelay || 1000
|
this.notificationDelay = settings.notificationDelay || 1000
|
||||||
@ -31,7 +31,7 @@ class NotificationSettings {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
appriseType: this.appriseType,
|
appriseType: this.appriseType,
|
||||||
appriseApiUrl: this.appriseApiUrl,
|
appriseApiUrl: this.appriseApiUrl,
|
||||||
notifications: this.notifications.map(n => n.toJSON()),
|
notifications: this.notifications.map((n) => n.toJSON()),
|
||||||
maxFailedAttempts: this.maxFailedAttempts,
|
maxFailedAttempts: this.maxFailedAttempts,
|
||||||
maxNotificationQueue: this.maxNotificationQueue,
|
maxNotificationQueue: this.maxNotificationQueue,
|
||||||
notificationDelay: this.notificationDelay
|
notificationDelay: this.notificationDelay
|
||||||
@ -42,17 +42,29 @@ class NotificationSettings {
|
|||||||
return !!this.appriseApiUrl
|
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) {
|
getActiveNotificationsForEvent(eventName) {
|
||||||
return this.notifications.filter(n => n.eventName === eventName && n.enabled)
|
return this.notifications.filter((n) => n.eventName === eventName && n.enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
getNotification(id) {
|
getNotification(id) {
|
||||||
return this.notifications.find(n => n.id === id)
|
return this.notifications.find((n) => n.id === id)
|
||||||
}
|
}
|
||||||
|
|
||||||
removeNotification(id) {
|
removeNotification(id) {
|
||||||
if (this.notifications.some(n => n.id === id)) {
|
if (this.notifications.some((n) => n.id === id)) {
|
||||||
this.notifications = this.notifications.filter(n => n.id !== id)
|
this.notifications = this.notifications.filter((n) => n.id !== id)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -94,7 +106,7 @@ class NotificationSettings {
|
|||||||
|
|
||||||
updateNotification(payload) {
|
updateNotification(payload) {
|
||||||
if (!payload) return false
|
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) {
|
if (!notification) {
|
||||||
Logger.error(`[NotificationSettings] updateNotification: Notification not found ${payload.id}`)
|
Logger.error(`[NotificationSettings] updateNotification: Notification not found ${payload.id}`)
|
||||||
return false
|
return false
|
||||||
|
@ -27,6 +27,36 @@ module.exports.notificationData = {
|
|||||||
episodeDescription: 'Some description of the podcast episode.'
|
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',
|
name: 'onTest',
|
||||||
requiresLibrary: false,
|
requiresLibrary: false,
|
||||||
|
Loading…
Reference in New Issue
Block a user