From aad2d9a017c10b16ba29312dbbfb48ffa65c5f14 Mon Sep 17 00:00:00 2001
From: matt <matt@starfish.reviews>
Date: Fri, 27 Sep 2024 10:54:41 -0500
Subject: [PATCH 1/2] Added notification support for new items being added to a
 library. Supports the client upload method (one or more items), or folder
 changes.

---
 docs/objects/Notification.yaml         |  2 +-
 docs/openapi.json                      |  1 +
 server/managers/NotificationManager.js | 26 ++++++++++++++++++++++++++
 server/scanner/LibraryScanner.js       |  8 ++++++--
 server/utils/notifications.js          | 22 ++++++++++++++++++++++
 5 files changed, 56 insertions(+), 3 deletions(-)

diff --git a/docs/objects/Notification.yaml b/docs/objects/Notification.yaml
index 50299ec8be..05c7d93764 100644
--- a/docs/objects/Notification.yaml
+++ b/docs/objects/Notification.yaml
@@ -22,7 +22,7 @@ components:
     notificationEventName:
       type: string
       description: The name of the event the notification will fire on.
-      enum: ['onPodcastEpisodeDownloaded', 'onBackupCompleted', 'onBackupFailed', 'onTest']
+      enum: ['onItemsAdded', 'onPodcastEpisodeDownloaded', 'onBackupCompleted', 'onBackupFailed', 'onTest']
     urls:
       type: array
       items:
diff --git a/docs/openapi.json b/docs/openapi.json
index 48f30ecfb0..bab29e157d 100644
--- a/docs/openapi.json
+++ b/docs/openapi.json
@@ -3225,6 +3225,7 @@
         "type": "string",
         "description": "The name of the event the notification will fire on.",
         "enum": [
+          "onItemsAdded",
           "onPodcastEpisodeDownloaded",
           "onBackupCompleted",
           "onBackupFailed",
diff --git a/server/managers/NotificationManager.js b/server/managers/NotificationManager.js
index a59c128154..dffc642f39 100644
--- a/server/managers/NotificationManager.js
+++ b/server/managers/NotificationManager.js
@@ -14,6 +14,32 @@ class NotificationManager {
     return notificationData
   }
 
+  async onItemsAdded(LibraryItems) {
+    if (!Database.notificationSettings.isUseable) return
+
+    if (!Database.notificationSettings.getHasActiveNotificationsForEvent('onItemsAdded')) {
+      Logger.debug(`[NotificationManager] onItemsAdded: No active notifications`)
+      return
+    }
+
+    for (const item of LibraryItems) {
+      Logger.debug(`[NotificationManager] onItemsAdded: Item "${item.media.metadata.title}"`)
+      const library = await Database.libraryModel.findByPk(item.libraryId)
+      const eventData = {
+        libraryItemId: item.id,
+        libraryId: item.libraryId,
+        libraryName: library?.name || 'Unknown',
+        tags: (item.media.tags || []).join(', ') || 'None',
+        title: item.media.metadata.title,
+        authors: (item.media.metadata.authors.map(a => a.name) || []).join(', ') || '',
+        description: item.media.metadata.description || '',
+        genres: (item.media.metadata.genres || []).join(', ') || 'None',
+        publishedYear: item.media.metadata.publishedYear || '',
+      }
+      this.triggerNotification('onItemsAdded', eventData)
+    }
+  }
+
   async onPodcastEpisodeDownloaded(libraryItem, episode) {
     if (!Database.notificationSettings.isUseable) return
 
diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js
index bd0bb310f5..02e261f399 100644
--- a/server/scanner/LibraryScanner.js
+++ b/server/scanner/LibraryScanner.js
@@ -16,7 +16,8 @@ const LibraryItemScanData = require('./LibraryItemScanData')
 const Task = require('../objects/Task')
 
 class LibraryScanner {
-  constructor() {
+  constructor(notificationManager) {
+    this.notificationManager = notificationManager
     this.cancelLibraryScan = {}
     /** @type {string[]} - library ids */
     this.librariesScanning = []
@@ -284,6 +285,7 @@ class LibraryScanner {
             'items_added',
             newOldLibraryItems.map((li) => li.toJSONExpanded())
           )
+          this.notificationManager.onItemsAdded(newOldLibraryItems)
           newOldLibraryItems = []
         }
 
@@ -296,6 +298,7 @@ class LibraryScanner {
           'items_added',
           newOldLibraryItems.map((li) => li.toJSONExpanded())
         )
+        this.notificationManager.onItemsAdded(newOldLibraryItems)
       }
     }
 
@@ -647,6 +650,7 @@ class LibraryScanner {
       if (newLibraryItem) {
         const oldNewLibraryItem = Database.libraryItemModel.getOldLibraryItem(newLibraryItem)
         SocketAuthority.emitter('item_added', oldNewLibraryItem.toJSONExpanded())
+        this.notificationManager.onItemsAdded([oldNewLibraryItem])
       }
       itemGroupingResults[itemDir] = newLibraryItem ? ScanResult.ADDED : ScanResult.NOTHING
     }
@@ -654,7 +658,7 @@ class LibraryScanner {
     return itemGroupingResults
   }
 }
-module.exports = new LibraryScanner()
+module.exports = new LibraryScanner(new NotificationManager())
 
 function ItemToFileInoMatch(libraryItem1, libraryItem2) {
   return libraryItem1.isFile && libraryItem2.libraryFiles.some((lf) => lf.ino === libraryItem1.ino)
diff --git a/server/utils/notifications.js b/server/utils/notifications.js
index 96e8ddf8cd..a8e0e58661 100644
--- a/server/utils/notifications.js
+++ b/server/utils/notifications.js
@@ -2,6 +2,28 @@ const { version } = require('../../package.json')
 
 module.exports.notificationData = {
   events: [
+    {
+      name: 'onItemsAdded',
+      requiresLibrary: true,
+      libraryMediaType: 'item',
+      description: 'Triggered when an item is added to the library',
+      variables: ['libraryItemId', 'libraryId', 'libraryName', 'tags', 'title', 'authors', 'description', 'genres', 'publishedYear'],
+      defaults: {
+        title: 'New Book!',
+        body: '{{title}} has been added to {{libraryName}} library.'
+      },
+      testData: {
+        libraryItemId: 'li_notification_test',
+        libraryId: 'lib_test',
+        libraryName: 'My Library',
+        tags: 'TestTag1, TestTag2',
+        title: 'ABS Test Book',
+        authors: 'Author1, Author2',
+        description: 'Description of the Abs Test Book belongs here.',
+        genres: 'TestGenre1, TestGenre2',
+        publishedYear: '2020'
+      }
+    },
     {
       name: 'onPodcastEpisodeDownloaded',
       requiresLibrary: true,

From 9513580043045d028a83b985ebc49f79da5e30dc Mon Sep 17 00:00:00 2001
From: matt <matt@starfish.reviews>
Date: Fri, 27 Sep 2024 20:23:58 -0500
Subject: [PATCH 2/2] Updated to use new NotificationManager singleton.

---
 server/scanner/LibraryScanner.js | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js
index 02e261f399..78ec422f27 100644
--- a/server/scanner/LibraryScanner.js
+++ b/server/scanner/LibraryScanner.js
@@ -14,10 +14,10 @@ const LibraryItemScanner = require('./LibraryItemScanner')
 const LibraryScan = require('./LibraryScan')
 const LibraryItemScanData = require('./LibraryItemScanData')
 const Task = require('../objects/Task')
+const NotificationManager = require('../managers/NotificationManager')
 
 class LibraryScanner {
-  constructor(notificationManager) {
-    this.notificationManager = notificationManager
+  constructor() {
     this.cancelLibraryScan = {}
     /** @type {string[]} - library ids */
     this.librariesScanning = []
@@ -285,7 +285,7 @@ class LibraryScanner {
             'items_added',
             newOldLibraryItems.map((li) => li.toJSONExpanded())
           )
-          this.notificationManager.onItemsAdded(newOldLibraryItems)
+          NotificationManager.onItemsAdded(newOldLibraryItems)
           newOldLibraryItems = []
         }
 
@@ -298,7 +298,7 @@ class LibraryScanner {
           'items_added',
           newOldLibraryItems.map((li) => li.toJSONExpanded())
         )
-        this.notificationManager.onItemsAdded(newOldLibraryItems)
+        NotificationManager.onItemsAdded(newOldLibraryItems)
       }
     }
 
@@ -650,7 +650,7 @@ class LibraryScanner {
       if (newLibraryItem) {
         const oldNewLibraryItem = Database.libraryItemModel.getOldLibraryItem(newLibraryItem)
         SocketAuthority.emitter('item_added', oldNewLibraryItem.toJSONExpanded())
-        this.notificationManager.onItemsAdded([oldNewLibraryItem])
+        NotificationManager.onItemsAdded([oldNewLibraryItem])
       }
       itemGroupingResults[itemDir] = newLibraryItem ? ScanResult.ADDED : ScanResult.NOTHING
     }
@@ -658,7 +658,7 @@ class LibraryScanner {
     return itemGroupingResults
   }
 }
-module.exports = new LibraryScanner(new NotificationManager())
+module.exports = new LibraryScanner()
 
 function ItemToFileInoMatch(libraryItem1, libraryItem2) {
   return libraryItem1.isFile && libraryItem2.libraryFiles.some((lf) => lf.ino === libraryItem1.ino)