using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Notifications; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Notifications; using Microsoft.Extensions.Logging; namespace Emby.Notifications { /// /// Creates notifications for various system events. /// public class NotificationEntryPoint : IServerEntryPoint { private readonly ILogger _logger; private readonly IActivityManager _activityManager; private readonly ILocalizationManager _localization; private readonly INotificationManager _notificationManager; private readonly ILibraryManager _libraryManager; private readonly IServerApplicationHost _appHost; private readonly IConfigurationManager _config; private readonly object _libraryChangedSyncLock = new object(); private readonly List _itemsAdded = new List(); private Timer? _libraryUpdateTimer; private string[] _coreNotificationTypes; private bool _disposed = false; /// /// Initializes a new instance of the class. /// /// The logger. /// The activity manager. /// The localization manager. /// The notification manager. /// The library manager. /// The application host. /// The configuration manager. public NotificationEntryPoint( ILogger logger, IActivityManager activityManager, ILocalizationManager localization, INotificationManager notificationManager, ILibraryManager libraryManager, IServerApplicationHost appHost, IConfigurationManager config) { _logger = logger; _activityManager = activityManager; _localization = localization; _notificationManager = notificationManager; _libraryManager = libraryManager; _appHost = appHost; _config = config; _coreNotificationTypes = new CoreNotificationTypes(localization).GetNotificationTypes().Select(i => i.Type).ToArray(); } /// public Task RunAsync() { _libraryManager.ItemAdded += OnLibraryManagerItemAdded; _appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged; _appHost.HasUpdateAvailableChanged += OnAppHostHasUpdateAvailableChanged; _activityManager.EntryCreated += OnActivityManagerEntryCreated; return Task.CompletedTask; } private async void OnAppHostHasPendingRestartChanged(object? sender, EventArgs e) { var type = NotificationType.ServerRestartRequired.ToString(); var notification = new NotificationRequest { NotificationType = type, Name = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("ServerNameNeedsToBeRestarted"), _appHost.Name) }; await SendNotification(notification, null).ConfigureAwait(false); } private async void OnActivityManagerEntryCreated(object? sender, GenericEventArgs e) { var entry = e.Argument; var type = entry.Type; if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparer.OrdinalIgnoreCase)) { return; } var userId = e.Argument.UserId; if (!userId.Equals(Guid.Empty) && !GetOptions().IsEnabledToMonitorUser(type, userId)) { return; } var notification = new NotificationRequest { NotificationType = type, Name = entry.Name, Description = entry.Overview }; await SendNotification(notification, null).ConfigureAwait(false); } private NotificationOptions GetOptions() { return _config.GetConfiguration("notifications"); } private async void OnAppHostHasUpdateAvailableChanged(object? sender, EventArgs e) { if (!_appHost.HasUpdateAvailable) { return; } var type = NotificationType.ApplicationUpdateAvailable.ToString(); var notification = new NotificationRequest { Description = "Please see jellyfin.org for details.", NotificationType = type, Name = _localization.GetLocalizedString("NewVersionIsAvailable") }; await SendNotification(notification, null).ConfigureAwait(false); } private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e) { if (!FilterItem(e.Item)) { return; } lock (_libraryChangedSyncLock) { if (_libraryUpdateTimer == null) { _libraryUpdateTimer = new Timer( LibraryUpdateTimerCallback, null, 5000, Timeout.Infinite); } else { _libraryUpdateTimer.Change(5000, Timeout.Infinite); } _itemsAdded.Add(e.Item); } } private bool FilterItem(BaseItem item) { if (item.IsFolder) { return false; } if (!item.HasPathProtocol) { return false; } if (item is IItemByName) { return false; } return item.SourceType == SourceType.Library; } private async void LibraryUpdateTimerCallback(object? state) { List items; lock (_libraryChangedSyncLock) { items = _itemsAdded.ToList(); _itemsAdded.Clear(); _libraryUpdateTimer!.Dispose(); // Shouldn't be null as it just set off this callback _libraryUpdateTimer = null; } if (items.Count > 10) { items = items.GetRange(0, 10); } foreach (var item in items) { var notification = new NotificationRequest { NotificationType = NotificationType.NewLibraryContent.ToString(), Name = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("ValueHasBeenAddedToLibrary"), GetItemName(item)), Description = item.Overview }; await SendNotification(notification, item).ConfigureAwait(false); } } /// /// Creates a human readable name for the item. /// /// The item. /// A human readable name for the item. public static string GetItemName(BaseItem item) { var name = item.Name; if (item is Episode episode) { if (episode.IndexNumber.HasValue) { name = string.Format( CultureInfo.InvariantCulture, "Ep{0} - {1}", episode.IndexNumber.Value, name); } if (episode.ParentIndexNumber.HasValue) { name = string.Format( CultureInfo.InvariantCulture, "S{0}, {1}", episode.ParentIndexNumber.Value, name); } } if (item is IHasSeries hasSeries) { name = hasSeries.SeriesName + " - " + name; } if (item is IHasAlbumArtist hasAlbumArtist) { var artists = hasAlbumArtist.AlbumArtists; if (artists.Count > 0) { name = artists[0] + " - " + name; } } else if (item is IHasArtist hasArtist) { var artists = hasArtist.Artists; if (artists.Count > 0) { name = artists[0] + " - " + name; } } return name; } private async Task SendNotification(NotificationRequest notification, BaseItem? relatedItem) { try { await _notificationManager.SendNotification(notification, relatedItem, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "Error sending notification"); } } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and optionally managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (_disposed) { return; } if (disposing) { _libraryUpdateTimer?.Dispose(); } _libraryUpdateTimer = null; _libraryManager.ItemAdded -= OnLibraryManagerItemAdded; _appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged; _appHost.HasUpdateAvailableChanged -= OnAppHostHasUpdateAvailableChanged; _activityManager.EntryCreated -= OnActivityManagerEntryCreated; _disposed = true; } } }