diff --git a/Jellyfin.Plugin.Webhook/Configuration/Web/config.js b/Jellyfin.Plugin.Webhook/Configuration/Web/config.js index f42bd07..8eeb515 100644 --- a/Jellyfin.Plugin.Webhook/Configuration/Web/config.js +++ b/Jellyfin.Plugin.Webhook/Configuration/Web/config.js @@ -40,6 +40,7 @@ export default function (view) { template: document.querySelector("#template-notification-type"), values: { "ItemAdded": "Item Added", + "ItemDeleted": "Item Deleted", "PlaybackStart": "Playback Start", "PlaybackProgress": "Playback Progress", "PlaybackStop": "Playback Stop", diff --git a/Jellyfin.Plugin.Webhook/Destinations/NotificationType.cs b/Jellyfin.Plugin.Webhook/Destinations/NotificationType.cs index 27fce7f..b647b0b 100644 --- a/Jellyfin.Plugin.Webhook/Destinations/NotificationType.cs +++ b/Jellyfin.Plugin.Webhook/Destinations/NotificationType.cs @@ -123,5 +123,10 @@ public enum NotificationType /// /// User data saved. /// - UserDataSaved = 23 + UserDataSaved = 23, + + /// + /// Item Deleted notification. + /// + ItemDeleted = 24 } diff --git a/Jellyfin.Plugin.Webhook/Notifiers/ItemDeletedNotifier/IItemDeletedManager.cs b/Jellyfin.Plugin.Webhook/Notifiers/ItemDeletedNotifier/IItemDeletedManager.cs new file mode 100644 index 0000000..b80e569 --- /dev/null +++ b/Jellyfin.Plugin.Webhook/Notifiers/ItemDeletedNotifier/IItemDeletedManager.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; + +namespace Jellyfin.Plugin.Webhook.Notifiers.ItemDeletedNotifier; + +/// +/// Item deleted manager interface. +/// +public interface IItemDeletedManager +{ + /// + /// Process the current queue. + /// + /// A representing the asynchronous operation. + public Task ProcessItemsAsync(); + + /// + /// Add item to process queue. + /// + /// The deleted item. + public void AddItem(BaseItem item); +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Webhook/Notifiers/ItemDeletedNotifier/ItemDeletedManager.cs b/Jellyfin.Plugin.Webhook/Notifiers/ItemDeletedNotifier/ItemDeletedManager.cs new file mode 100644 index 0000000..b47f4f2 --- /dev/null +++ b/Jellyfin.Plugin.Webhook/Notifiers/ItemDeletedNotifier/ItemDeletedManager.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using Jellyfin.Plugin.Webhook.Destinations; +using Jellyfin.Plugin.Webhook.Helpers; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.Webhook.Notifiers.ItemDeletedNotifier; + +/// +public class ItemDeletedManager : IItemDeletedManager +{ + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly IServerApplicationHost _applicationHost; + private readonly ConcurrentDictionary _itemProcessQueue; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ItemDeletedManager( + ILogger logger, + ILibraryManager libraryManager, + IServerApplicationHost applicationHost) + { + _logger = logger; + _libraryManager = libraryManager; + _applicationHost = applicationHost; + _itemProcessQueue = new ConcurrentDictionary(); + } + + /// + public async Task ProcessItemsAsync() + { + _logger.LogDebug("ProcessItemsAsync"); + // Attempt to process all items in queue. + if (!_itemProcessQueue.IsEmpty) + { + var scope = _applicationHost.ServiceProvider!.CreateAsyncScope(); + await using (scope.ConfigureAwait(false)) + { + var webhookSender = scope.ServiceProvider.GetRequiredService(); + foreach (var (key, item) in _itemProcessQueue) + { + if (item != null) + { + _logger.LogDebug("Item {ItemName}", item.Name); + + // Skip notification if item type is Studio + if (item.GetType().Name == "Studio") + { + _logger.LogDebug("Skipping notification for item type Studio"); + _itemProcessQueue.TryRemove(key, out _); + continue; + } + + _logger.LogDebug("Notifying for {ItemName}", item.Name); + + // Send notification to each configured destination. + var dataObject = DataObjectHelpers + .GetBaseDataObject(_applicationHost, NotificationType.ItemDeleted) + .AddBaseItemData(item); + + var itemType = item.GetType(); + await webhookSender.SendNotification(NotificationType.ItemDeleted, dataObject, itemType) + .ConfigureAwait(false); + + // Remove item from queue. + _itemProcessQueue.TryRemove(key, out _); + } + } + } + } + } + + /// + public void AddItem(BaseItem item) + { + _itemProcessQueue.TryAdd(item.Id, item); + _logger.LogDebug("Queued {ItemName} for notification", item.Name); + } +} diff --git a/Jellyfin.Plugin.Webhook/Notifiers/ItemDeletedNotifier/ItemDeletedNotifierEntryPoint.cs b/Jellyfin.Plugin.Webhook/Notifiers/ItemDeletedNotifier/ItemDeletedNotifierEntryPoint.cs new file mode 100644 index 0000000..f68245b --- /dev/null +++ b/Jellyfin.Plugin.Webhook/Notifiers/ItemDeletedNotifier/ItemDeletedNotifierEntryPoint.cs @@ -0,0 +1,53 @@ +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Library; +using Microsoft.Extensions.Hosting; + +namespace Jellyfin.Plugin.Webhook.Notifiers.ItemDeletedNotifier; + +/// +/// Notifier when a library item is deleted. +/// +public class ItemDeletedNotifierEntryPoint : IHostedService +{ + private readonly IItemDeletedManager _itemDeletedManager; + private readonly ILibraryManager _libraryManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public ItemDeletedNotifierEntryPoint( + IItemDeletedManager itemDeletedManager, + ILibraryManager libraryManager) + { + _itemDeletedManager = itemDeletedManager; + _libraryManager = libraryManager; + } + + private void ItemDeletedHandler(object? sender, ItemChangeEventArgs itemChangeEventArgs) + { + // Never notify on virtual items. + if (itemChangeEventArgs.Item.IsVirtualItem) + { + return; + } + + _itemDeletedManager.AddItem(itemChangeEventArgs.Item); + } + + /// + public Task StartAsync(CancellationToken cancellationToken) + { + _libraryManager.ItemRemoved += ItemDeletedHandler; + return Task.CompletedTask; + } + + /// + public Task StopAsync(CancellationToken cancellationToken) + { + _libraryManager.ItemRemoved -= ItemDeletedHandler; + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Webhook/Notifiers/ItemDeletedNotifier/ItemDeletedScheduledTask.cs b/Jellyfin.Plugin.Webhook/Notifiers/ItemDeletedNotifier/ItemDeletedScheduledTask.cs new file mode 100644 index 0000000..4e88336 --- /dev/null +++ b/Jellyfin.Plugin.Webhook/Notifiers/ItemDeletedNotifier/ItemDeletedScheduledTask.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Tasks; + +namespace Jellyfin.Plugin.Webhook.Notifiers.ItemDeletedNotifier; + +/// +/// Scheduled task that processes item deleted events. +/// +public class ItemDeletedScheduledTask : IScheduledTask, IConfigurableScheduledTask +{ + private const int RecheckIntervalSec = 30; + private readonly IItemDeletedManager _itemDeletedManager; + private readonly ILocalizationManager _localizationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public ItemDeletedScheduledTask( + IItemDeletedManager itemDeletedManager, + ILocalizationManager localizationManager) + { + _itemDeletedManager = itemDeletedManager; + _localizationManager = localizationManager; + } + + /// + public string Name => "Webhook Item Deleted Notifier"; + + /// + public string Key => "WebhookItemDeleted"; + + /// + public string Description => "Processes item deleted queue"; + + /// + public string Category => _localizationManager.GetLocalizedString("TasksLibraryCategory"); + + /// + public bool IsHidden => false; + + /// + public bool IsEnabled => true; + + /// + public bool IsLogged => false; + + /// + public Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + { + return _itemDeletedManager.ProcessItemsAsync(); + } + + /// + public IEnumerable GetDefaultTriggers() + { + return new[] + { + new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerInterval, + IntervalTicks = TimeSpan.FromSeconds(RecheckIntervalSec).Ticks + } + }; + } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Webhook/PluginServiceRegistrator.cs b/Jellyfin.Plugin.Webhook/PluginServiceRegistrator.cs index 131f790..9550a38 100644 --- a/Jellyfin.Plugin.Webhook/PluginServiceRegistrator.cs +++ b/Jellyfin.Plugin.Webhook/PluginServiceRegistrator.cs @@ -13,6 +13,7 @@ using Jellyfin.Plugin.Webhook.Helpers; using Jellyfin.Plugin.Webhook.Notifiers; using Jellyfin.Plugin.Webhook.Notifiers.ItemAddedNotifier; +using Jellyfin.Plugin.Webhook.Notifiers.ItemDeletedNotifier; using Jellyfin.Plugin.Webhook.Notifiers.UserDataSavedNotifier; using MediaBrowser.Common.Updates; using MediaBrowser.Controller; @@ -58,6 +59,7 @@ public void RegisterServices(IServiceCollection serviceCollection, IServerApplic // Library consumers. serviceCollection.AddScoped, SubtitleDownloadFailureNotifier>(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); // Security consumers. serviceCollection.AddScoped, AuthenticationFailureNotifier>(); @@ -90,6 +92,7 @@ public void RegisterServices(IServiceCollection serviceCollection, IServerApplic serviceCollection.AddHostedService(); serviceCollection.AddHostedService(); + serviceCollection.AddHostedService(); serviceCollection.AddHostedService(); } }