2
0
Эх сурвалжийг харах

Merge pull request #3910 from barronpm/event-rewrite-1

Event Rewrite (Part 1)
Joshua M. Boniface 4 жил өмнө
parent
commit
cf6ef9958d
97 өөрчлөгдсөн 2070 нэмэгдсэн , 1082 устгасан
  1. 2 2
      Emby.Dlna/Eventing/DlnaEventManager.cs
  2. 1 1
      Emby.Dlna/IConnectionManager.cs
  3. 1 1
      Emby.Dlna/IContentDirectory.cs
  4. 1 1
      Emby.Dlna/IDlnaEventManager.cs
  5. 1 1
      Emby.Dlna/IMediaReceiverRegistrar.cs
  6. 1 1
      Emby.Dlna/PlayTo/PlayToController.cs
  7. 1 1
      Emby.Dlna/PlayTo/PlayToManager.cs
  8. 3 3
      Emby.Dlna/Service/BaseService.cs
  9. 1 1
      Emby.Dlna/Ssdp/DeviceDiscovery.cs
  10. 1 1
      Emby.Notifications/NotificationEntryPoint.cs
  11. 0 590
      Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
  12. 82 77
      Emby.Server.Implementations/ApplicationHost.cs
  13. 1 1
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  14. 1 1
      Emby.Server.Implementations/Devices/DeviceManager.cs
  15. 1 1
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  16. 1 1
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  17. 5 4
      Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
  18. 0 210
      Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs
  19. 1 1
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  20. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  21. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
  22. 1 1
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  23. 1 1
      Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  24. 1 1
      Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
  25. 61 56
      Emby.Server.Implementations/Session/SessionManager.cs
  26. 1 1
      Emby.Server.Implementations/Session/SessionWebSocketListener.cs
  27. 4 4
      Jellyfin.Api/Controllers/DlnaServerController.cs
  28. 1 1
      Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
  29. 1 1
      Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
  30. 8 8
      Jellyfin.Data/Events/GenericEventArgs.cs
  31. 11 0
      Jellyfin.Data/Events/System/PendingRestartEventArgs.cs
  32. 18 0
      Jellyfin.Data/Events/Users/UserCreatedEventArgs.cs
  33. 18 0
      Jellyfin.Data/Events/Users/UserDeletedEventArgs.cs
  34. 18 0
      Jellyfin.Data/Events/Users/UserLockedOutEventArgs.cs
  35. 18 0
      Jellyfin.Data/Events/Users/UserPasswordChangedEventArgs.cs
  36. 18 0
      Jellyfin.Data/Events/Users/UserUpdatedEventArgs.cs
  37. 1 1
      Jellyfin.Server.Implementations/Activity/ActivityManager.cs
  38. 102 0
      Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs
  39. 52 0
      Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs
  40. 49 0
      Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
  41. 104 0
      Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
  42. 106 0
      Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
  43. 54 0
      Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs
  44. 54 0
      Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs
  45. 31 0
      Jellyfin.Server.Implementations/Events/Consumers/System/PendingRestartNotifier.cs
  46. 158 0
      Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs
  47. 31 0
      Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs
  48. 31 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs
  49. 51 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs
  50. 31 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs
  51. 50 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs
  52. 31 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs
  53. 31 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs
  54. 45 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs
  55. 31 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs
  56. 51 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs
  57. 43 0
      Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs
  58. 44 0
      Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs
  59. 38 0
      Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs
  60. 47 0
      Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs
  61. 43 0
      Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs
  62. 41 0
      Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs
  63. 60 0
      Jellyfin.Server.Implementations/Events/EventManager.cs
  64. 72 0
      Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
  65. 1 1
      Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
  66. 13 18
      Jellyfin.Server.Implementations/Users/UserManager.cs
  67. 16 11
      Jellyfin.Server/CoreAppHost.cs
  68. 4 3
      Jellyfin.Server/Program.cs
  69. 1 2
      MediaBrowser.Common/IApplicationHost.cs
  70. 11 0
      MediaBrowser.Common/Plugins/BasePlugin.cs
  71. 13 0
      MediaBrowser.Common/Plugins/IPlugin.cs
  72. 2 1
      MediaBrowser.Common/Updates/InstallationEventArgs.cs
  73. 1 1
      MediaBrowser.Controller/Devices/IDeviceManager.cs
  74. 20 0
      MediaBrowser.Controller/Events/IEventConsumer.cs
  75. 28 0
      MediaBrowser.Controller/Events/IEventManager.cs
  76. 19 0
      MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs
  77. 19 0
      MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs
  78. 19 0
      MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs
  79. 19 0
      MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs
  80. 19 0
      MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs
  81. 19 0
      MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs
  82. 19 0
      MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs
  83. 9 7
      MediaBrowser.Controller/IServerApplicationHost.cs
  84. 1 21
      MediaBrowser.Controller/Library/IUserManager.cs
  85. 9 0
      MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs
  86. 1 1
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  87. 1 1
      MediaBrowser.Controller/Net/IHttpServer.cs
  88. 1 1
      MediaBrowser.Controller/Providers/IProviderManager.cs
  89. 1 1
      MediaBrowser.Controller/Session/ISessionManager.cs
  90. 0 29
      MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs
  91. 26 0
      MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs
  92. 1 1
      MediaBrowser.Model/Activity/IActivityManager.cs
  93. 1 1
      MediaBrowser.Model/Dlna/IDeviceDiscovery.cs
  94. 1 1
      MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs
  95. 1 1
      MediaBrowser.Model/Tasks/ITaskManager.cs
  96. 1 1
      MediaBrowser.Providers/Manager/ProviderManager.cs
  97. 4 3
      tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs

+ 2 - 2
Emby.Dlna/Eventing/EventManager.cs → Emby.Dlna/Eventing/DlnaEventManager.cs

@@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.Eventing
 {
-    public class EventManager : IEventManager
+    public class DlnaEventManager : IDlnaEventManager
     {
         private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions =
             new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
@@ -24,7 +24,7 @@ namespace Emby.Dlna.Eventing
 
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
-        public EventManager(ILogger logger, IHttpClient httpClient)
+        public DlnaEventManager(ILogger logger, IHttpClient httpClient)
         {
             _httpClient = httpClient;
             _logger = logger;

+ 1 - 1
Emby.Dlna/IConnectionManager.cs

@@ -2,7 +2,7 @@
 
 namespace Emby.Dlna
 {
-    public interface IConnectionManager : IEventManager, IUpnpService
+    public interface IConnectionManager : IDlnaEventManager, IUpnpService
     {
     }
 }

+ 1 - 1
Emby.Dlna/IContentDirectory.cs

@@ -2,7 +2,7 @@
 
 namespace Emby.Dlna
 {
-    public interface IContentDirectory : IEventManager, IUpnpService
+    public interface IContentDirectory : IDlnaEventManager, IUpnpService
     {
     }
 }

+ 1 - 1
Emby.Dlna/IEventManager.cs → Emby.Dlna/IDlnaEventManager.cs

@@ -2,7 +2,7 @@
 
 namespace Emby.Dlna
 {
-    public interface IEventManager
+    public interface IDlnaEventManager
     {
         /// <summary>
         /// Cancels the event subscription.

+ 1 - 1
Emby.Dlna/IMediaReceiverRegistrar.cs

@@ -2,7 +2,7 @@
 
 namespace Emby.Dlna
 {
-    public interface IMediaReceiverRegistrar : IEventManager, IUpnpService
+    public interface IMediaReceiverRegistrar : IDlnaEventManager, IUpnpService
     {
     }
 }

+ 1 - 1
Emby.Dlna/PlayTo/PlayToController.cs

@@ -8,6 +8,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Emby.Dlna.Didl;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
@@ -18,7 +19,6 @@ using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Session;
 using Microsoft.AspNetCore.WebUtilities;

+ 1 - 1
Emby.Dlna/PlayTo/PlayToManager.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Net;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
@@ -16,7 +17,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;

+ 3 - 3
Emby.Dlna/Service/BaseService.cs

@@ -6,17 +6,17 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.Service
 {
-    public class BaseService : IEventManager
+    public class BaseService : IDlnaEventManager
     {
         protected BaseService(ILogger<BaseService> logger, IHttpClient httpClient)
         {
             Logger = logger;
             HttpClient = httpClient;
 
-            EventManager = new EventManager(logger, HttpClient);
+            EventManager = new DlnaEventManager(logger, HttpClient);
         }
 
-        protected IEventManager EventManager { get; }
+        protected IDlnaEventManager EventManager { get; }
 
         protected IHttpClient HttpClient { get; }
 

+ 1 - 1
Emby.Dlna/Ssdp/DeviceDiscovery.cs

@@ -3,9 +3,9 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Events;
 using Rssdp;
 using Rssdp.Infrastructure;
 

+ 1 - 1
Emby.Notifications/NotificationEntryPoint.cs

@@ -4,6 +4,7 @@ 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;
@@ -13,7 +14,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Notifications;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Notifications;
 using Microsoft.Extensions.Logging;

+ 0 - 590
Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs

@@ -1,590 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Controller.Subtitles;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Notifications;
-using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Updates;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Activity
-{
-    /// <summary>
-    /// Entry point for the activity logger.
-    /// </summary>
-    public sealed class ActivityLogEntryPoint : IServerEntryPoint
-    {
-        private readonly ILogger<ActivityLogEntryPoint> _logger;
-        private readonly IInstallationManager _installationManager;
-        private readonly ISessionManager _sessionManager;
-        private readonly ITaskManager _taskManager;
-        private readonly IActivityManager _activityManager;
-        private readonly ILocalizationManager _localization;
-        private readonly ISubtitleManager _subManager;
-        private readonly IUserManager _userManager;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
-        /// </summary>
-        /// <param name="logger">The logger.</param>
-        /// <param name="sessionManager">The session manager.</param>
-        /// <param name="taskManager">The task manager.</param>
-        /// <param name="activityManager">The activity manager.</param>
-        /// <param name="localization">The localization manager.</param>
-        /// <param name="installationManager">The installation manager.</param>
-        /// <param name="subManager">The subtitle manager.</param>
-        /// <param name="userManager">The user manager.</param>
-        public ActivityLogEntryPoint(
-            ILogger<ActivityLogEntryPoint> logger,
-            ISessionManager sessionManager,
-            ITaskManager taskManager,
-            IActivityManager activityManager,
-            ILocalizationManager localization,
-            IInstallationManager installationManager,
-            ISubtitleManager subManager,
-            IUserManager userManager)
-        {
-            _logger = logger;
-            _sessionManager = sessionManager;
-            _taskManager = taskManager;
-            _activityManager = activityManager;
-            _localization = localization;
-            _installationManager = installationManager;
-            _subManager = subManager;
-            _userManager = userManager;
-        }
-
-        /// <inheritdoc />
-        public Task RunAsync()
-        {
-            _taskManager.TaskCompleted += OnTaskCompleted;
-
-            _installationManager.PluginInstalled += OnPluginInstalled;
-            _installationManager.PluginUninstalled += OnPluginUninstalled;
-            _installationManager.PluginUpdated += OnPluginUpdated;
-            _installationManager.PackageInstallationFailed += OnPackageInstallationFailed;
-
-            _sessionManager.SessionStarted += OnSessionStarted;
-            _sessionManager.AuthenticationFailed += OnAuthenticationFailed;
-            _sessionManager.AuthenticationSucceeded += OnAuthenticationSucceeded;
-            _sessionManager.SessionEnded += OnSessionEnded;
-            _sessionManager.PlaybackStart += OnPlaybackStart;
-            _sessionManager.PlaybackStopped += OnPlaybackStopped;
-
-            _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure;
-
-            _userManager.OnUserCreated += OnUserCreated;
-            _userManager.OnUserPasswordChanged += OnUserPasswordChanged;
-            _userManager.OnUserDeleted += OnUserDeleted;
-            _userManager.OnUserLockedOut += OnUserLockedOut;
-
-            return Task.CompletedTask;
-        }
-
-        private async void OnUserLockedOut(object sender, GenericEventArgs<User> e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                    string.Format(
-                        CultureInfo.InvariantCulture,
-                        _localization.GetLocalizedString("UserLockedOutWithName"),
-                        e.Argument.Username),
-                    NotificationType.UserLockedOut.ToString(),
-                    e.Argument.Id)
-            {
-                LogSeverity = LogLevel.Error
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
-                    e.Provider,
-                    Notifications.NotificationEntryPoint.GetItemName(e.Item)),
-                "SubtitleDownloadFailure",
-                Guid.Empty)
-            {
-                ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
-                ShortOverview = e.Exception.Message
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
-        {
-            var item = e.MediaInfo;
-
-            if (item == null)
-            {
-                _logger.LogWarning("PlaybackStopped reported with null media info.");
-                return;
-            }
-
-            if (e.Item != null && e.Item.IsThemeMedia)
-            {
-                // Don't report theme song or local trailer playback
-                return;
-            }
-
-            if (e.Users.Count == 0)
-            {
-                return;
-            }
-
-            var user = e.Users[0];
-
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
-                    user.Username,
-                    GetItemName(item),
-                    e.DeviceName),
-                GetPlaybackStoppedNotificationType(item.MediaType),
-                user.Id))
-                .ConfigureAwait(false);
-        }
-
-        private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
-        {
-            var item = e.MediaInfo;
-
-            if (item == null)
-            {
-                _logger.LogWarning("PlaybackStart reported with null media info.");
-                return;
-            }
-
-            if (e.Item != null && e.Item.IsThemeMedia)
-            {
-                // Don't report theme song or local trailer playback
-                return;
-            }
-
-            if (e.Users.Count == 0)
-            {
-                return;
-            }
-
-            var user = e.Users.First();
-
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
-                    user.Username,
-                    GetItemName(item),
-                    e.DeviceName),
-                GetPlaybackNotificationType(item.MediaType),
-                user.Id))
-                .ConfigureAwait(false);
-        }
-
-        private static string GetItemName(BaseItemDto item)
-        {
-            var name = item.Name;
-
-            if (!string.IsNullOrEmpty(item.SeriesName))
-            {
-                name = item.SeriesName + " - " + name;
-            }
-
-            if (item.Artists != null && item.Artists.Count > 0)
-            {
-                name = item.Artists[0] + " - " + name;
-            }
-
-            return name;
-        }
-
-        private static string GetPlaybackNotificationType(string mediaType)
-        {
-            if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
-            {
-                return NotificationType.AudioPlayback.ToString();
-            }
-
-            if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
-            {
-                return NotificationType.VideoPlayback.ToString();
-            }
-
-            return null;
-        }
-
-        private static string GetPlaybackStoppedNotificationType(string mediaType)
-        {
-            if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
-            {
-                return NotificationType.AudioPlaybackStopped.ToString();
-            }
-
-            if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
-            {
-                return NotificationType.VideoPlaybackStopped.ToString();
-            }
-
-            return null;
-        }
-
-        private async void OnSessionEnded(object sender, SessionEventArgs e)
-        {
-            var session = e.SessionInfo;
-
-            if (string.IsNullOrEmpty(session.UserName))
-            {
-                return;
-            }
-
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserOfflineFromDevice"),
-                    session.UserName,
-                    session.DeviceName),
-                "SessionEnded",
-                session.UserId)
-            {
-                ShortOverview = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("LabelIpAddressValue"),
-                    session.RemoteEndPoint),
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
-        {
-            var user = e.Argument.User;
-
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
-                    user.Name),
-                "AuthenticationSucceeded",
-                user.Id)
-            {
-                ShortOverview = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("LabelIpAddressValue"),
-                    e.Argument.SessionInfo.RemoteEndPoint),
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
-                    e.Argument.Username),
-                "AuthenticationFailed",
-                Guid.Empty)
-            {
-                LogSeverity = LogLevel.Error,
-                ShortOverview = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("LabelIpAddressValue"),
-                    e.Argument.RemoteEndPoint),
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserDeletedWithName"),
-                    e.Argument.Username),
-                "UserDeleted",
-                Guid.Empty))
-                .ConfigureAwait(false);
-        }
-
-        private async void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserPasswordChangedWithName"),
-                    e.Argument.Username),
-                "UserPasswordChanged",
-                e.Argument.Id))
-                .ConfigureAwait(false);
-        }
-
-        private async void OnUserCreated(object sender, GenericEventArgs<User> e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserCreatedWithName"),
-                    e.Argument.Username),
-                "UserCreated",
-                e.Argument.Id))
-                .ConfigureAwait(false);
-        }
-
-        private async void OnSessionStarted(object sender, SessionEventArgs e)
-        {
-            var session = e.SessionInfo;
-
-            if (string.IsNullOrEmpty(session.UserName))
-            {
-                return;
-            }
-
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserOnlineFromDevice"),
-                    session.UserName,
-                    session.DeviceName),
-                "SessionStarted",
-                session.UserId)
-            {
-                ShortOverview = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("LabelIpAddressValue"),
-                    session.RemoteEndPoint)
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnPluginUpdated(object sender, InstallationInfo e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("PluginUpdatedWithName"),
-                    e.Name),
-                NotificationType.PluginUpdateInstalled.ToString(),
-                Guid.Empty)
-            {
-                ShortOverview = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("VersionNumber"),
-                    e.Version),
-                Overview = e.Changelog
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnPluginUninstalled(object sender, IPlugin e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("PluginUninstalledWithName"),
-                    e.Name),
-                NotificationType.PluginUninstalled.ToString(),
-                Guid.Empty))
-                .ConfigureAwait(false);
-        }
-
-        private async void OnPluginInstalled(object sender, InstallationInfo e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("PluginInstalledWithName"),
-                    e.Name),
-                NotificationType.PluginInstalled.ToString(),
-                Guid.Empty)
-            {
-                ShortOverview = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("VersionNumber"),
-                    e.Version)
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
-        {
-            var installationInfo = e.InstallationInfo;
-
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("NameInstallFailed"),
-                    installationInfo.Name),
-                NotificationType.InstallationFailed.ToString(),
-                Guid.Empty)
-            {
-                ShortOverview = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("VersionNumber"),
-                    installationInfo.Version),
-                Overview = e.Exception.Message
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
-        {
-            var result = e.Result;
-            var task = e.Task;
-
-            if (task.ScheduledTask is IConfigurableScheduledTask activityTask
-                && !activityTask.IsLogged)
-            {
-                return;
-            }
-
-            var time = result.EndTimeUtc - result.StartTimeUtc;
-            var runningTime = string.Format(
-                CultureInfo.InvariantCulture,
-                _localization.GetLocalizedString("LabelRunningTimeValue"),
-                ToUserFriendlyString(time));
-
-            if (result.Status == TaskCompletionStatus.Failed)
-            {
-                var vals = new List<string>();
-
-                if (!string.IsNullOrEmpty(e.Result.ErrorMessage))
-                {
-                    vals.Add(e.Result.ErrorMessage);
-                }
-
-                if (!string.IsNullOrEmpty(e.Result.LongErrorMessage))
-                {
-                    vals.Add(e.Result.LongErrorMessage);
-                }
-
-                await CreateLogEntry(new ActivityLog(
-                    string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
-                    NotificationType.TaskFailed.ToString(),
-                    Guid.Empty)
-                {
-                    LogSeverity = LogLevel.Error,
-                    Overview = string.Join(Environment.NewLine, vals),
-                    ShortOverview = runningTime
-                }).ConfigureAwait(false);
-            }
-        }
-
-        private async Task CreateLogEntry(ActivityLog entry)
-            => await _activityManager.CreateAsync(entry).ConfigureAwait(false);
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            _taskManager.TaskCompleted -= OnTaskCompleted;
-
-            _installationManager.PluginInstalled -= OnPluginInstalled;
-            _installationManager.PluginUninstalled -= OnPluginUninstalled;
-            _installationManager.PluginUpdated -= OnPluginUpdated;
-            _installationManager.PackageInstallationFailed -= OnPackageInstallationFailed;
-
-            _sessionManager.SessionStarted -= OnSessionStarted;
-            _sessionManager.AuthenticationFailed -= OnAuthenticationFailed;
-            _sessionManager.AuthenticationSucceeded -= OnAuthenticationSucceeded;
-            _sessionManager.SessionEnded -= OnSessionEnded;
-
-            _sessionManager.PlaybackStart -= OnPlaybackStart;
-            _sessionManager.PlaybackStopped -= OnPlaybackStopped;
-
-            _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure;
-
-            _userManager.OnUserCreated -= OnUserCreated;
-            _userManager.OnUserPasswordChanged -= OnUserPasswordChanged;
-            _userManager.OnUserDeleted -= OnUserDeleted;
-            _userManager.OnUserLockedOut -= OnUserLockedOut;
-        }
-
-        /// <summary>
-        /// Constructs a user-friendly string for this TimeSpan instance.
-        /// </summary>
-        private static string ToUserFriendlyString(TimeSpan span)
-        {
-            const int DaysInYear = 365;
-            const int DaysInMonth = 30;
-
-            // Get each non-zero value from TimeSpan component
-            var values = new List<string>();
-
-            // Number of years
-            int days = span.Days;
-            if (days >= DaysInYear)
-            {
-                int years = days / DaysInYear;
-                values.Add(CreateValueString(years, "year"));
-                days %= DaysInYear;
-            }
-
-            // Number of months
-            if (days >= DaysInMonth)
-            {
-                int months = days / DaysInMonth;
-                values.Add(CreateValueString(months, "month"));
-                days = days % DaysInMonth;
-            }
-
-            // Number of days
-            if (days >= 1)
-            {
-                values.Add(CreateValueString(days, "day"));
-            }
-
-            // Number of hours
-            if (span.Hours >= 1)
-            {
-                values.Add(CreateValueString(span.Hours, "hour"));
-            }
-
-            // Number of minutes
-            if (span.Minutes >= 1)
-            {
-                values.Add(CreateValueString(span.Minutes, "minute"));
-            }
-
-            // Number of seconds (include when 0 if no other components included)
-            if (span.Seconds >= 1 || values.Count == 0)
-            {
-                values.Add(CreateValueString(span.Seconds, "second"));
-            }
-
-            // Combine values into string
-            var builder = new StringBuilder();
-            for (int i = 0; i < values.Count; i++)
-            {
-                if (builder.Length > 0)
-                {
-                    builder.Append(i == values.Count - 1 ? " and " : ", ");
-                }
-
-                builder.Append(values[i]);
-            }
-
-            // Return result
-            return builder.ToString();
-        }
-
-        /// <summary>
-        /// Constructs a string description of a time-span value.
-        /// </summary>
-        /// <param name="value">The value of this item.</param>
-        /// <param name="description">The name of this item (singular form).</param>
-        private static string CreateValueString(int value, string description)
-        {
-            return string.Format(
-                CultureInfo.InvariantCulture,
-                "{0:#,##0} {1}",
-                value,
-                value == 1 ? description : string.Format(CultureInfo.InvariantCulture, "{0}s", description));
-        }
-    }
-}

+ 82 - 77
Emby.Server.Implementations/ApplicationHost.cs

@@ -53,7 +53,6 @@ using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Collections;
@@ -173,6 +172,8 @@ namespace Emby.Server.Implementations
         /// </summary>
         protected ILogger<ApplicationHost> Logger { get; }
 
+        protected IServiceCollection ServiceCollection { get; }
+
         private IPlugin[] _plugins;
 
         /// <summary>
@@ -238,9 +239,11 @@ namespace Emby.Server.Implementations
             ILoggerFactory loggerFactory,
             IStartupOptions options,
             IFileSystem fileSystem,
-            INetworkManager networkManager)
+            INetworkManager networkManager,
+            IServiceCollection serviceCollection)
         {
             _xmlSerializer = new MyXmlSerializer();
+            ServiceCollection = serviceCollection;
 
             _networkManager = networkManager;
             networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
@@ -464,7 +467,7 @@ namespace Emby.Server.Implementations
         }
 
         /// <inheritdoc/>
-        public void Init(IServiceCollection serviceCollection)
+        public void Init()
         {
             HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber;
             HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber;
@@ -493,7 +496,7 @@ namespace Emby.Server.Implementations
 
             DiscoverTypes();
 
-            RegisterServices(serviceCollection);
+            RegisterServices();
         }
 
         public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
@@ -502,139 +505,139 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// Registers services/resources with the service collection that will be available via DI.
         /// </summary>
-        protected virtual void RegisterServices(IServiceCollection serviceCollection)
+        protected virtual void RegisterServices()
         {
-            serviceCollection.AddSingleton(_startupOptions);
+            ServiceCollection.AddSingleton(_startupOptions);
 
-            serviceCollection.AddMemoryCache();
+            ServiceCollection.AddMemoryCache();
 
-            serviceCollection.AddSingleton(ConfigurationManager);
-            serviceCollection.AddSingleton<IApplicationHost>(this);
+            ServiceCollection.AddSingleton(ConfigurationManager);
+            ServiceCollection.AddSingleton<IApplicationHost>(this);
 
-            serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
+            ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
 
-            serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
+            ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
 
-            serviceCollection.AddSingleton(_fileSystemManager);
-            serviceCollection.AddSingleton<TvdbClientManager>();
+            ServiceCollection.AddSingleton(_fileSystemManager);
+            ServiceCollection.AddSingleton<TvdbClientManager>();
 
-            serviceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
+            ServiceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
 
-            serviceCollection.AddSingleton(_networkManager);
+            ServiceCollection.AddSingleton(_networkManager);
 
-            serviceCollection.AddSingleton<IIsoManager, IsoManager>();
+            ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
 
-            serviceCollection.AddSingleton<ITaskManager, TaskManager>();
+            ServiceCollection.AddSingleton<ITaskManager, TaskManager>();
 
-            serviceCollection.AddSingleton(_xmlSerializer);
+            ServiceCollection.AddSingleton(_xmlSerializer);
 
-            serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
+            ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>();
 
-            serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
+            ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
 
-            serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
+            ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>();
 
-            serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
+            ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>();
 
-            serviceCollection.AddSingleton<IZipClient, ZipClient>();
+            ServiceCollection.AddSingleton<IZipClient, ZipClient>();
 
-            serviceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>();
+            ServiceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>();
 
-            serviceCollection.AddSingleton<IServerApplicationHost>(this);
-            serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
+            ServiceCollection.AddSingleton<IServerApplicationHost>(this);
+            ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
 
-            serviceCollection.AddSingleton(ServerConfigurationManager);
+            ServiceCollection.AddSingleton(ServerConfigurationManager);
 
-            serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
+            ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
 
-            serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
+            ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
 
-            serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
-            serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
+            ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
+            ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>();
 
-            serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
+            ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
 
-            serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
+            ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
 
             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
-            serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
+            ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
 
             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
-            serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
-            serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
+            ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
+            ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
 
             // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
-            serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
-            serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
-            serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
-            serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
+            ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
+            ServiceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
+            ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
+            ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>();
 
-            serviceCollection.AddSingleton<IMusicManager, MusicManager>();
+            ServiceCollection.AddSingleton<IMusicManager, MusicManager>();
 
-            serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
+            ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
 
-            serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
+            ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
 
-            serviceCollection.AddSingleton<ServiceController>();
-            serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
+            ServiceCollection.AddSingleton<ServiceController>();
+            ServiceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
 
-            serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
+            ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
 
-            serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
+            ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
 
-            serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
+            ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
 
-            serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
+            ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
 
-            serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
+            ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
 
-            serviceCollection.AddSingleton<IProviderManager, ProviderManager>();
+            ServiceCollection.AddSingleton<IProviderManager, ProviderManager>();
 
             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
-            serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
-            serviceCollection.AddSingleton<IDtoService, DtoService>();
+            ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
+            ServiceCollection.AddSingleton<IDtoService, DtoService>();
 
-            serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
+            ServiceCollection.AddSingleton<IChannelManager, ChannelManager>();
 
-            serviceCollection.AddSingleton<ISessionManager, SessionManager>();
+            ServiceCollection.AddSingleton<ISessionManager, SessionManager>();
 
-            serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
+            ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>();
 
-            serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
+            ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>();
 
-            serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
+            ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
 
-            serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
+            ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
 
-            serviceCollection.AddSingleton<LiveTvDtoService>();
-            serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
+            ServiceCollection.AddSingleton<LiveTvDtoService>();
+            ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
 
-            serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
+            ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>();
 
-            serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
+            ServiceCollection.AddSingleton<INotificationManager, NotificationManager>();
 
-            serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
+            ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
 
-            serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
+            ServiceCollection.AddSingleton<IChapterManager, ChapterManager>();
 
-            serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
+            ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
 
-            serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
-            serviceCollection.AddSingleton<ISessionContext, SessionContext>();
+            ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
+            ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
 
-            serviceCollection.AddSingleton<IAuthService, AuthService>();
+            ServiceCollection.AddSingleton<IAuthService, AuthService>();
 
-            serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
+            ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
 
-            serviceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
-            serviceCollection.AddSingleton<EncodingHelper>();
+            ServiceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
+            ServiceCollection.AddSingleton<EncodingHelper>();
 
-            serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
+            ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
 
-            serviceCollection.AddSingleton<TranscodingJobHelper>();
-            serviceCollection.AddScoped<MediaInfoHelper>();
-            serviceCollection.AddScoped<AudioHelper>();
-            serviceCollection.AddScoped<DynamicHlsHelper>();
+            ServiceCollection.AddSingleton<TranscodingJobHelper>();
+            ServiceCollection.AddScoped<MediaInfoHelper>();
+            ServiceCollection.AddScoped<AudioHelper>();
+            ServiceCollection.AddScoped<DynamicHlsHelper>();
         }
 
         /// <summary>
@@ -834,6 +837,8 @@ namespace Emby.Server.Implementations
                 {
                     hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
                 }
+
+                plugin.RegisterServices(ServiceCollection);
             }
             catch (Exception ex)
             {

+ 1 - 1
Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs

@@ -2,11 +2,11 @@ using System;
 using System.Globalization;
 using System.IO;
 using Emby.Server.Implementations.AppBase;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;

+ 1 - 1
Emby.Server.Implementations/Devices/DeviceManager.cs

@@ -7,13 +7,13 @@ using System.IO;
 using System.Linq;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Session;

+ 1 - 1
Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs

@@ -7,11 +7,11 @@ using System.Net;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Events;
 using Microsoft.Extensions.Logging;
 using Mono.Nat;
 

+ 1 - 1
Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

@@ -7,6 +7,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -15,7 +16,6 @@ using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints

+ 5 - 4
Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs

@@ -5,6 +5,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Plugins;
@@ -43,22 +44,22 @@ namespace Emby.Server.Implementations.EntryPoints
             return Task.CompletedTask;
         }
 
-        private async void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
         {
             await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false);
         }
 
-        private async void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
         {
             await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false);
         }
 
-        private async void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
         {
             await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false);
         }
 
-        private async void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
         {
             await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false);
         }

+ 0 - 210
Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs

@@ -1,210 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Updates;
-
-namespace Emby.Server.Implementations.EntryPoints
-{
-    /// <summary>
-    /// Class WebSocketEvents.
-    /// </summary>
-    public class ServerEventNotifier : IServerEntryPoint
-    {
-        /// <summary>
-        /// The user manager.
-        /// </summary>
-        private readonly IUserManager _userManager;
-
-        /// <summary>
-        /// The installation manager.
-        /// </summary>
-        private readonly IInstallationManager _installationManager;
-
-        /// <summary>
-        /// The kernel.
-        /// </summary>
-        private readonly IServerApplicationHost _appHost;
-
-        /// <summary>
-        /// The task manager.
-        /// </summary>
-        private readonly ITaskManager _taskManager;
-
-        private readonly ISessionManager _sessionManager;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ServerEventNotifier"/> class.
-        /// </summary>
-        /// <param name="appHost">The application host.</param>
-        /// <param name="userManager">The user manager.</param>
-        /// <param name="installationManager">The installation manager.</param>
-        /// <param name="taskManager">The task manager.</param>
-        /// <param name="sessionManager">The session manager.</param>
-        public ServerEventNotifier(
-            IServerApplicationHost appHost,
-            IUserManager userManager,
-            IInstallationManager installationManager,
-            ITaskManager taskManager,
-            ISessionManager sessionManager)
-        {
-            _userManager = userManager;
-            _installationManager = installationManager;
-            _appHost = appHost;
-            _taskManager = taskManager;
-            _sessionManager = sessionManager;
-        }
-
-        /// <inheritdoc />
-        public Task RunAsync()
-        {
-            _userManager.OnUserDeleted += OnUserDeleted;
-            _userManager.OnUserUpdated += OnUserUpdated;
-
-            _appHost.HasPendingRestartChanged += OnHasPendingRestartChanged;
-
-            _installationManager.PluginUninstalled += OnPluginUninstalled;
-            _installationManager.PackageInstalling += OnPackageInstalling;
-            _installationManager.PackageInstallationCancelled += OnPackageInstallationCancelled;
-            _installationManager.PackageInstallationCompleted += OnPackageInstallationCompleted;
-            _installationManager.PackageInstallationFailed += OnPackageInstallationFailed;
-
-            _taskManager.TaskCompleted += OnTaskCompleted;
-
-            return Task.CompletedTask;
-        }
-
-        private async void OnPackageInstalling(object sender, InstallationInfo e)
-        {
-            await SendMessageToAdminSessions("PackageInstalling", e).ConfigureAwait(false);
-        }
-
-        private async void OnPackageInstallationCancelled(object sender, InstallationInfo e)
-        {
-            await SendMessageToAdminSessions("PackageInstallationCancelled", e).ConfigureAwait(false);
-        }
-
-        private async void OnPackageInstallationCompleted(object sender, InstallationInfo e)
-        {
-            await SendMessageToAdminSessions("PackageInstallationCompleted", e).ConfigureAwait(false);
-        }
-
-        private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
-        {
-            await SendMessageToAdminSessions("PackageInstallationFailed", e.InstallationInfo).ConfigureAwait(false);
-        }
-
-        private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
-        {
-            await SendMessageToAdminSessions("ScheduledTaskEnded", e.Result).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Installations the manager_ plugin uninstalled.
-        /// </summary>
-        /// <param name="sender">The sender.</param>
-        /// <param name="e">The e.</param>
-        private async void OnPluginUninstalled(object sender, IPlugin e)
-        {
-            await SendMessageToAdminSessions("PluginUninstalled", e).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Handles the HasPendingRestartChanged event of the kernel control.
-        /// </summary>
-        /// <param name="sender">The source of the event.</param>
-        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
-        private async void OnHasPendingRestartChanged(object sender, EventArgs e)
-        {
-            await _sessionManager.SendRestartRequiredNotification(CancellationToken.None).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Users the manager_ user updated.
-        /// </summary>
-        /// <param name="sender">The sender.</param>
-        /// <param name="e">The e.</param>
-        private async void OnUserUpdated(object sender, GenericEventArgs<User> e)
-        {
-            var dto = _userManager.GetUserDto(e.Argument);
-
-            await SendMessageToUserSession(e.Argument, "UserUpdated", dto).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Users the manager_ user deleted.
-        /// </summary>
-        /// <param name="sender">The sender.</param>
-        /// <param name="e">The e.</param>
-        private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
-        {
-            await SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)).ConfigureAwait(false);
-        }
-
-        private async Task SendMessageToAdminSessions<T>(string name, T data)
-        {
-            try
-            {
-                await _sessionManager.SendMessageToAdminSessions(name, data, CancellationToken.None).ConfigureAwait(false);
-            }
-            catch (Exception)
-            {
-            }
-        }
-
-        private async Task SendMessageToUserSession<T>(User user, string name, T data)
-        {
-            try
-            {
-                await _sessionManager.SendMessageToUserSessions(
-                    new List<Guid> { user.Id },
-                    name,
-                    data,
-                    CancellationToken.None).ConfigureAwait(false);
-            }
-            catch (Exception)
-            {
-            }
-        }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources.
-        /// </summary>
-        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
-        protected virtual void Dispose(bool dispose)
-        {
-            if (dispose)
-            {
-                _userManager.OnUserDeleted -= OnUserDeleted;
-                _userManager.OnUserUpdated -= OnUserUpdated;
-
-                _installationManager.PluginUninstalled -= OnPluginUninstalled;
-                _installationManager.PackageInstalling -= OnPackageInstalling;
-                _installationManager.PackageInstallationCancelled -= OnPackageInstallationCancelled;
-                _installationManager.PackageInstallationCompleted -= OnPackageInstallationCompleted;
-                _installationManager.PackageInstallationFailed -= OnPackageInstallationFailed;
-
-                _appHost.HasPendingRestartChanged -= OnHasPendingRestartChanged;
-
-                _taskManager.TaskCompleted -= OnTaskCompleted;
-            }
-        }
-    }
-}

+ 1 - 1
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -12,13 +12,13 @@ using System.Threading;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.Services;
 using Emby.Server.Implementations.SocketSharp;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Services;

+ 1 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -13,6 +13,7 @@ using System.Threading.Tasks;
 using System.Xml;
 using Emby.Server.Implementations.Library;
 using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
@@ -29,7 +30,6 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.MediaInfo;

+ 1 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs

@@ -5,8 +5,8 @@ using System.Collections.Concurrent;
 using System.Globalization;
 using System.Linq;
 using System.Threading;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;

+ 1 - 1
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -10,6 +10,7 @@ using System.Threading.Tasks;
 using Emby.Server.Implementations.Library;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
@@ -24,7 +25,6 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;

+ 1 - 1
Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs

@@ -6,10 +6,10 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;

+ 1 - 1
Emby.Server.Implementations/ScheduledTasks/TaskManager.cs

@@ -5,8 +5,8 @@ using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;

+ 61 - 56
Emby.Server.Implementations/Session/SessionManager.cs

@@ -9,6 +9,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller;
@@ -17,6 +18,8 @@ using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Session;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
@@ -24,7 +27,6 @@ using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Devices;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Session;
@@ -40,25 +42,16 @@ namespace Emby.Server.Implementations.Session
     /// </summary>
     public class SessionManager : ISessionManager, IDisposable
     {
-        /// <summary>
-        /// The user data repository.
-        /// </summary>
         private readonly IUserDataManager _userDataManager;
-
-        /// <summary>
-        /// The logger.
-        /// </summary>
         private readonly ILogger<SessionManager> _logger;
-
+        private readonly IEventManager _eventManager;
         private readonly ILibraryManager _libraryManager;
         private readonly IUserManager _userManager;
         private readonly IMusicManager _musicManager;
         private readonly IDtoService _dtoService;
         private readonly IImageProcessor _imageProcessor;
         private readonly IMediaSourceManager _mediaSourceManager;
-
         private readonly IServerApplicationHost _appHost;
-
         private readonly IAuthenticationRepository _authRepo;
         private readonly IDeviceManager _deviceManager;
 
@@ -75,6 +68,7 @@ namespace Emby.Server.Implementations.Session
 
         public SessionManager(
             ILogger<SessionManager> logger,
+            IEventManager eventManager,
             IUserDataManager userDataManager,
             ILibraryManager libraryManager,
             IUserManager userManager,
@@ -87,6 +81,7 @@ namespace Emby.Server.Implementations.Session
             IMediaSourceManager mediaSourceManager)
         {
             _logger = logger;
+            _eventManager = eventManager;
             _userDataManager = userDataManager;
             _libraryManager = libraryManager;
             _userManager = userManager;
@@ -209,6 +204,8 @@ namespace Emby.Server.Implementations.Session
                 }
             }
 
+            _eventManager.Publish(new SessionStartedEventArgs(info));
+
             EventHelper.QueueEventIfNotNull(
                 SessionStarted,
                 this,
@@ -230,6 +227,8 @@ namespace Emby.Server.Implementations.Session
                 },
                 _logger);
 
+            _eventManager.Publish(new SessionEndedEventArgs(info));
+
             info.Dispose();
         }
 
@@ -667,22 +666,26 @@ namespace Emby.Server.Implementations.Session
                 }
             }
 
+            var eventArgs = new PlaybackProgressEventArgs
+            {
+                Item = libraryItem,
+                Users = users,
+                MediaSourceId = info.MediaSourceId,
+                MediaInfo = info.Item,
+                DeviceName = session.DeviceName,
+                ClientName = session.Client,
+                DeviceId = session.DeviceId,
+                Session = session
+            };
+
+            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
+
             // Nothing to save here
             // Fire events to inform plugins
             EventHelper.QueueEventIfNotNull(
                 PlaybackStart,
                 this,
-                new PlaybackProgressEventArgs
-                {
-                    Item = libraryItem,
-                    Users = users,
-                    MediaSourceId = info.MediaSourceId,
-                    MediaInfo = info.Item,
-                    DeviceName = session.DeviceName,
-                    ClientName = session.Client,
-                    DeviceId = session.DeviceId,
-                    Session = session
-                },
+                eventArgs,
                 _logger);
 
             StartIdleCheckTimer();
@@ -750,23 +753,25 @@ namespace Emby.Server.Implementations.Session
                 }
             }
 
-            PlaybackProgress?.Invoke(
-                this,
-                new PlaybackProgressEventArgs
-                {
-                    Item = libraryItem,
-                    Users = users,
-                    PlaybackPositionTicks = session.PlayState.PositionTicks,
-                    MediaSourceId = session.PlayState.MediaSourceId,
-                    MediaInfo = info.Item,
-                    DeviceName = session.DeviceName,
-                    ClientName = session.Client,
-                    DeviceId = session.DeviceId,
-                    IsPaused = info.IsPaused,
-                    PlaySessionId = info.PlaySessionId,
-                    IsAutomated = isAutomated,
-                    Session = session
-                });
+            var eventArgs = new PlaybackProgressEventArgs
+            {
+                Item = libraryItem,
+                Users = users,
+                PlaybackPositionTicks = session.PlayState.PositionTicks,
+                MediaSourceId = session.PlayState.MediaSourceId,
+                MediaInfo = info.Item,
+                DeviceName = session.DeviceName,
+                ClientName = session.Client,
+                DeviceId = session.DeviceId,
+                IsPaused = info.IsPaused,
+                PlaySessionId = info.PlaySessionId,
+                IsAutomated = isAutomated,
+                Session = session
+            };
+
+            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
+
+            PlaybackProgress?.Invoke(this, eventArgs);
 
             if (!isAutomated)
             {
@@ -943,23 +948,23 @@ namespace Emby.Server.Implementations.Session
                 }
             }
 
-            EventHelper.QueueEventIfNotNull(
-                PlaybackStopped,
-                this,
-                new PlaybackStopEventArgs
-                {
-                    Item = libraryItem,
-                    Users = users,
-                    PlaybackPositionTicks = info.PositionTicks,
-                    PlayedToCompletion = playedToCompletion,
-                    MediaSourceId = info.MediaSourceId,
-                    MediaInfo = info.Item,
-                    DeviceName = session.DeviceName,
-                    ClientName = session.Client,
-                    DeviceId = session.DeviceId,
-                    Session = session
-                },
-                _logger);
+            var eventArgs = new PlaybackStopEventArgs
+            {
+                Item = libraryItem,
+                Users = users,
+                PlaybackPositionTicks = info.PositionTicks,
+                PlayedToCompletion = playedToCompletion,
+                MediaSourceId = info.MediaSourceId,
+                MediaInfo = info.Item,
+                DeviceName = session.DeviceName,
+                ClientName = session.Client,
+                DeviceId = session.DeviceId,
+                Session = session
+            };
+
+            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
+
+            EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
         }
 
         private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)

+ 1 - 1
Emby.Server.Implementations/Session/SessionWebSocketListener.cs

@@ -4,9 +4,9 @@ using System.Linq;
 using System.Net.WebSockets;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Net;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;

+ 4 - 4
Jellyfin.Api/Controllers/DlnaServerController.cs

@@ -229,7 +229,7 @@ namespace Jellyfin.Api.Controllers
             });
         }
 
-        private EventSubscriptionResponse ProcessEventRequest(IEventManager eventManager)
+        private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager)
         {
             var subscriptionId = Request.Headers["SID"];
             if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase))
@@ -240,17 +240,17 @@ namespace Jellyfin.Api.Controllers
 
                 if (string.IsNullOrEmpty(notificationType))
                 {
-                    return eventManager.RenewEventSubscription(
+                    return dlnaEventManager.RenewEventSubscription(
                         subscriptionId,
                         notificationType,
                         timeoutString,
                         callback);
                 }
 
-                return eventManager.CreateEventSubscription(notificationType, timeoutString, callback);
+                return dlnaEventManager.CreateEventSubscription(notificationType, timeoutString, callback);
             }
 
-            return eventManager.CancelEventSubscription(subscriptionId);
+            return dlnaEventManager.CancelEventSubscription(subscriptionId);
         }
     }
 }

+ 1 - 1
Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs

@@ -1,8 +1,8 @@
 using System;
 using System.Threading.Tasks;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Events;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Api.WebSocketListeners

+ 1 - 1
Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs

@@ -1,8 +1,8 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
 

+ 8 - 8
MediaBrowser.Model/Events/GenericEventArgs.cs → Jellyfin.Data/Events/GenericEventArgs.cs

@@ -1,19 +1,13 @@
 using System;
 
-namespace MediaBrowser.Model.Events
+namespace Jellyfin.Data.Events
 {
     /// <summary>
     /// Provides a generic EventArgs subclass that can hold any kind of object.
     /// </summary>
-    /// <typeparam name="T"></typeparam>
+    /// <typeparam name="T">The type of this event.</typeparam>
     public class GenericEventArgs<T> : EventArgs
     {
-        /// <summary>
-        /// Gets or sets the argument.
-        /// </summary>
-        /// <value>The argument.</value>
-        public T Argument { get; set; }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="GenericEventArgs{T}"/> class.
         /// </summary>
@@ -22,5 +16,11 @@ namespace MediaBrowser.Model.Events
         {
             Argument = arg;
         }
+
+        /// <summary>
+        /// Gets the argument.
+        /// </summary>
+        /// <value>The argument.</value>
+        public T Argument { get; }
     }
 }

+ 11 - 0
Jellyfin.Data/Events/System/PendingRestartEventArgs.cs

@@ -0,0 +1,11 @@
+using System;
+
+namespace Jellyfin.Data.Events.System
+{
+    /// <summary>
+    /// An event that occurs when there is a pending restart.
+    /// </summary>
+    public class PendingRestartEventArgs : EventArgs
+    {
+    }
+}

+ 18 - 0
Jellyfin.Data/Events/Users/UserCreatedEventArgs.cs

@@ -0,0 +1,18 @@
+using Jellyfin.Data.Entities;
+
+namespace Jellyfin.Data.Events.Users
+{
+    /// <summary>
+    /// An event that occurs when a user is created.
+    /// </summary>
+    public class UserCreatedEventArgs : GenericEventArgs<User>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserCreatedEventArgs"/> class.
+        /// </summary>
+        /// <param name="arg">The user.</param>
+        public UserCreatedEventArgs(User arg) : base(arg)
+        {
+        }
+    }
+}

+ 18 - 0
Jellyfin.Data/Events/Users/UserDeletedEventArgs.cs

@@ -0,0 +1,18 @@
+using Jellyfin.Data.Entities;
+
+namespace Jellyfin.Data.Events.Users
+{
+    /// <summary>
+    /// An event that occurs when a user is deleted.
+    /// </summary>
+    public class UserDeletedEventArgs : GenericEventArgs<User>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserDeletedEventArgs"/> class.
+        /// </summary>
+        /// <param name="arg">The user.</param>
+        public UserDeletedEventArgs(User arg) : base(arg)
+        {
+        }
+    }
+}

+ 18 - 0
Jellyfin.Data/Events/Users/UserLockedOutEventArgs.cs

@@ -0,0 +1,18 @@
+using Jellyfin.Data.Entities;
+
+namespace Jellyfin.Data.Events.Users
+{
+    /// <summary>
+    /// An event that occurs when a user is locked out.
+    /// </summary>
+    public class UserLockedOutEventArgs : GenericEventArgs<User>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserLockedOutEventArgs"/> class.
+        /// </summary>
+        /// <param name="arg">The user.</param>
+        public UserLockedOutEventArgs(User arg) : base(arg)
+        {
+        }
+    }
+}

+ 18 - 0
Jellyfin.Data/Events/Users/UserPasswordChangedEventArgs.cs

@@ -0,0 +1,18 @@
+using Jellyfin.Data.Entities;
+
+namespace Jellyfin.Data.Events.Users
+{
+    /// <summary>
+    /// An event that occurs when a user's password has changed.
+    /// </summary>
+    public class UserPasswordChangedEventArgs : GenericEventArgs<User>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserPasswordChangedEventArgs"/> class.
+        /// </summary>
+        /// <param name="arg">The user.</param>
+        public UserPasswordChangedEventArgs(User arg) : base(arg)
+        {
+        }
+    }
+}

+ 18 - 0
Jellyfin.Data/Events/Users/UserUpdatedEventArgs.cs

@@ -0,0 +1,18 @@
+using Jellyfin.Data.Entities;
+
+namespace Jellyfin.Data.Events.Users
+{
+    /// <summary>
+    /// An event that occurs when a user is updated.
+    /// </summary>
+    public class UserUpdatedEventArgs : GenericEventArgs<User>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserUpdatedEventArgs"/> class.
+        /// </summary>
+        /// <param name="arg">The user.</param>
+        public UserUpdatedEventArgs(User arg) : base(arg)
+        {
+        }
+    }
+}

+ 1 - 1
Jellyfin.Server.Implementations/Activity/ActivityManager.cs

@@ -2,8 +2,8 @@ using System;
 using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events;
 using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Querying;
 
 namespace Jellyfin.Server.Implementations.Activity

+ 102 - 0
Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs

@@ -0,0 +1,102 @@
+using System;
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Subtitles;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Library
+{
+    /// <summary>
+    /// Creates an entry in the activity log whenever a subtitle download fails.
+    /// </summary>
+    public class SubtitleDownloadFailureLogger : IEventConsumer<SubtitleDownloadFailureEventArgs>
+    {
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SubtitleDownloadFailureLogger"/> class.
+        /// </summary>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public SubtitleDownloadFailureLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(SubtitleDownloadFailureEventArgs eventArgs)
+        {
+            await _activityManager.CreateAsync(new ActivityLog(
+                string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
+                    eventArgs.Provider,
+                    GetItemName(eventArgs.Item)),
+                "SubtitleDownloadFailure",
+                Guid.Empty)
+            {
+                ItemId = eventArgs.Item.Id.ToString("N", CultureInfo.InvariantCulture),
+                ShortOverview = eventArgs.Exception.Message
+            }).ConfigureAwait(false);
+        }
+
+        private 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;
+        }
+    }
+}

+ 52 - 0
Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs

@@ -0,0 +1,52 @@
+using System;
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Security
+{
+    /// <summary>
+    /// Creates an entry in the activity log when there is a failed login attempt.
+    /// </summary>
+    public class AuthenticationFailedLogger : IEventConsumer<GenericEventArgs<AuthenticationRequest>>
+    {
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AuthenticationFailedLogger"/> class.
+        /// </summary>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public AuthenticationFailedLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(GenericEventArgs<AuthenticationRequest> eventArgs)
+        {
+            await _activityManager.CreateAsync(new ActivityLog(
+                string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("FailedLoginAttemptWithUserName"),
+                    eventArgs.Argument.Username),
+                "AuthenticationFailed",
+                Guid.Empty)
+            {
+                LogSeverity = LogLevel.Error,
+                ShortOverview = string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("LabelIpAddressValue"),
+                    eventArgs.Argument.RemoteEndPoint),
+            }).ConfigureAwait(false);
+        }
+    }
+}

+ 49 - 0
Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs

@@ -0,0 +1,49 @@
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Security
+{
+    /// <summary>
+    /// Creates an entry in the activity log when there is a successful login attempt.
+    /// </summary>
+    public class AuthenticationSucceededLogger : IEventConsumer<GenericEventArgs<AuthenticationResult>>
+    {
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AuthenticationSucceededLogger"/> class.
+        /// </summary>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public AuthenticationSucceededLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(GenericEventArgs<AuthenticationResult> e)
+        {
+            await _activityManager.CreateAsync(new ActivityLog(
+                string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("AuthenticationSucceededWithUserName"),
+                    e.Argument.User.Name),
+                "AuthenticationSucceeded",
+                e.Argument.User.Id)
+            {
+                ShortOverview = string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("LabelIpAddressValue"),
+                    e.Argument.SessionInfo.RemoteEndPoint),
+            }).ConfigureAwait(false);
+        }
+    }
+}

+ 104 - 0
Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs

@@ -0,0 +1,104 @@
+using System;
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Notifications;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Session
+{
+    /// <summary>
+    /// Creates an entry in the activity log whenever a user starts playback.
+    /// </summary>
+    public class PlaybackStartLogger : IEventConsumer<PlaybackStartEventArgs>
+    {
+        private readonly ILogger<PlaybackStartLogger> _logger;
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PlaybackStartLogger"/> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public PlaybackStartLogger(ILogger<PlaybackStartLogger> logger, ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _logger = logger;
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(PlaybackStartEventArgs eventArgs)
+        {
+            if (eventArgs.MediaInfo == null)
+            {
+                _logger.LogWarning("PlaybackStart reported with null media info.");
+                return;
+            }
+
+            if (eventArgs.Item != null && eventArgs.Item.IsThemeMedia)
+            {
+                // Don't report theme song or local trailer playback
+                return;
+            }
+
+            if (eventArgs.Users.Count == 0)
+            {
+                return;
+            }
+
+            var user = eventArgs.Users[0];
+
+            await _activityManager.CreateAsync(new ActivityLog(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        _localizationManager.GetLocalizedString("UserStartedPlayingItemWithValues"),
+                        user.Username,
+                        GetItemName(eventArgs.MediaInfo),
+                        eventArgs.DeviceName),
+                    GetPlaybackNotificationType(eventArgs.MediaInfo.MediaType),
+                    user.Id))
+                .ConfigureAwait(false);
+        }
+
+        private static string GetItemName(BaseItemDto item)
+        {
+            var name = item.Name;
+
+            if (!string.IsNullOrEmpty(item.SeriesName))
+            {
+                name = item.SeriesName + " - " + name;
+            }
+
+            if (item.Artists != null && item.Artists.Count > 0)
+            {
+                name = item.Artists[0] + " - " + name;
+            }
+
+            return name;
+        }
+
+        private static string GetPlaybackNotificationType(string mediaType)
+        {
+            if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+            {
+                return NotificationType.AudioPlayback.ToString();
+            }
+
+            if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+            {
+                return NotificationType.VideoPlayback.ToString();
+            }
+
+            return null;
+        }
+    }
+}

+ 106 - 0
Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs

@@ -0,0 +1,106 @@
+using System;
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Notifications;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Session
+{
+    /// <summary>
+    /// Creates an activity log entry whenever a user stops playback.
+    /// </summary>
+    public class PlaybackStopLogger : IEventConsumer<PlaybackStopEventArgs>
+    {
+        private readonly ILogger<PlaybackStopLogger> _logger;
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PlaybackStopLogger"/> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public PlaybackStopLogger(ILogger<PlaybackStopLogger> logger, ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _logger = logger;
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(PlaybackStopEventArgs eventArgs)
+        {
+            var item = eventArgs.MediaInfo;
+
+            if (item == null)
+            {
+                _logger.LogWarning("PlaybackStopped reported with null media info.");
+                return;
+            }
+
+            if (eventArgs.Item != null && eventArgs.Item.IsThemeMedia)
+            {
+                // Don't report theme song or local trailer playback
+                return;
+            }
+
+            if (eventArgs.Users.Count == 0)
+            {
+                return;
+            }
+
+            var user = eventArgs.Users[0];
+
+            await _activityManager.CreateAsync(new ActivityLog(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        _localizationManager.GetLocalizedString("UserStoppedPlayingItemWithValues"),
+                        user.Username,
+                        GetItemName(item),
+                        eventArgs.DeviceName),
+                    GetPlaybackStoppedNotificationType(item.MediaType),
+                    user.Id))
+                .ConfigureAwait(false);
+        }
+
+        private static string GetItemName(BaseItemDto item)
+        {
+            var name = item.Name;
+
+            if (!string.IsNullOrEmpty(item.SeriesName))
+            {
+                name = item.SeriesName + " - " + name;
+            }
+
+            if (item.Artists != null && item.Artists.Count > 0)
+            {
+                name = item.Artists[0] + " - " + name;
+            }
+
+            return name;
+        }
+
+        private static string GetPlaybackStoppedNotificationType(string mediaType)
+        {
+            if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+            {
+                return NotificationType.AudioPlaybackStopped.ToString();
+            }
+
+            if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+            {
+                return NotificationType.VideoPlaybackStopped.ToString();
+            }
+
+            return null;
+        }
+    }
+}

+ 54 - 0
Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs

@@ -0,0 +1,54 @@
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Session;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Session
+{
+    /// <summary>
+    /// Creates an entry in the activity log whenever a session ends.
+    /// </summary>
+    public class SessionEndedLogger : IEventConsumer<SessionEndedEventArgs>
+    {
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SessionEndedLogger"/> class.
+        /// </summary>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public SessionEndedLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(SessionEndedEventArgs eventArgs)
+        {
+            if (string.IsNullOrEmpty(eventArgs.Argument.UserName))
+            {
+                return;
+            }
+
+            await _activityManager.CreateAsync(new ActivityLog(
+                string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("UserOfflineFromDevice"),
+                    eventArgs.Argument.UserName,
+                    eventArgs.Argument.DeviceName),
+                "SessionEnded",
+                eventArgs.Argument.UserId)
+            {
+                ShortOverview = string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("LabelIpAddressValue"),
+                    eventArgs.Argument.RemoteEndPoint),
+            }).ConfigureAwait(false);
+        }
+    }
+}

+ 54 - 0
Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs

@@ -0,0 +1,54 @@
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Session;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Session
+{
+    /// <summary>
+    /// Creates an entry in the activity log when a session is started.
+    /// </summary>
+    public class SessionStartedLogger : IEventConsumer<SessionStartedEventArgs>
+    {
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SessionStartedLogger"/> class.
+        /// </summary>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public SessionStartedLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(SessionStartedEventArgs eventArgs)
+        {
+            if (string.IsNullOrEmpty(eventArgs.Argument.UserName))
+            {
+                return;
+            }
+
+            await _activityManager.CreateAsync(new ActivityLog(
+                string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("UserOnlineFromDevice"),
+                    eventArgs.Argument.UserName,
+                    eventArgs.Argument.DeviceName),
+                "SessionStarted",
+                eventArgs.Argument.UserId)
+            {
+                ShortOverview = string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("LabelIpAddressValue"),
+                    eventArgs.Argument.RemoteEndPoint)
+            }).ConfigureAwait(false);
+        }
+    }
+}

+ 31 - 0
Jellyfin.Server.Implementations/Events/Consumers/System/PendingRestartNotifier.cs

@@ -0,0 +1,31 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Events.System;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Session;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.System
+{
+    /// <summary>
+    /// Notifies users when there is a pending restart.
+    /// </summary>
+    public class PendingRestartNotifier : IEventConsumer<PendingRestartEventArgs>
+    {
+        private readonly ISessionManager _sessionManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PendingRestartNotifier"/> class.
+        /// </summary>
+        /// <param name="sessionManager">The session manager.</param>
+        public PendingRestartNotifier(ISessionManager sessionManager)
+        {
+            _sessionManager = sessionManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(PendingRestartEventArgs eventArgs)
+        {
+            await _sessionManager.SendRestartRequiredNotification(CancellationToken.None).ConfigureAwait(false);
+        }
+    }
+}

+ 158 - 0
Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs

@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Notifications;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.System
+{
+    /// <summary>
+    /// Creates an activity log entry whenever a task is completed.
+    /// </summary>
+    public class TaskCompletedLogger : IEventConsumer<TaskCompletionEventArgs>
+    {
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TaskCompletedLogger"/> class.
+        /// </summary>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public TaskCompletedLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(TaskCompletionEventArgs e)
+        {
+            var result = e.Result;
+            var task = e.Task;
+
+            if (task.ScheduledTask is IConfigurableScheduledTask activityTask
+                && !activityTask.IsLogged)
+            {
+                return;
+            }
+
+            var time = result.EndTimeUtc - result.StartTimeUtc;
+            var runningTime = string.Format(
+                CultureInfo.InvariantCulture,
+                _localizationManager.GetLocalizedString("LabelRunningTimeValue"),
+                ToUserFriendlyString(time));
+
+            if (result.Status == TaskCompletionStatus.Failed)
+            {
+                var vals = new List<string>();
+
+                if (!string.IsNullOrEmpty(e.Result.ErrorMessage))
+                {
+                    vals.Add(e.Result.ErrorMessage);
+                }
+
+                if (!string.IsNullOrEmpty(e.Result.LongErrorMessage))
+                {
+                    vals.Add(e.Result.LongErrorMessage);
+                }
+
+                await _activityManager.CreateAsync(new ActivityLog(
+                    string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
+                    NotificationType.TaskFailed.ToString(),
+                    Guid.Empty)
+                {
+                    LogSeverity = LogLevel.Error,
+                    Overview = string.Join(Environment.NewLine, vals),
+                    ShortOverview = runningTime
+                }).ConfigureAwait(false);
+            }
+        }
+
+        private static string ToUserFriendlyString(TimeSpan span)
+        {
+            const int DaysInYear = 365;
+            const int DaysInMonth = 30;
+
+            // Get each non-zero value from TimeSpan component
+            var values = new List<string>();
+
+            // Number of years
+            int days = span.Days;
+            if (days >= DaysInYear)
+            {
+                int years = days / DaysInYear;
+                values.Add(CreateValueString(years, "year"));
+                days %= DaysInYear;
+            }
+
+            // Number of months
+            if (days >= DaysInMonth)
+            {
+                int months = days / DaysInMonth;
+                values.Add(CreateValueString(months, "month"));
+                days = days % DaysInMonth;
+            }
+
+            // Number of days
+            if (days >= 1)
+            {
+                values.Add(CreateValueString(days, "day"));
+            }
+
+            // Number of hours
+            if (span.Hours >= 1)
+            {
+                values.Add(CreateValueString(span.Hours, "hour"));
+            }
+
+            // Number of minutes
+            if (span.Minutes >= 1)
+            {
+                values.Add(CreateValueString(span.Minutes, "minute"));
+            }
+
+            // Number of seconds (include when 0 if no other components included)
+            if (span.Seconds >= 1 || values.Count == 0)
+            {
+                values.Add(CreateValueString(span.Seconds, "second"));
+            }
+
+            // Combine values into string
+            var builder = new StringBuilder();
+            for (int i = 0; i < values.Count; i++)
+            {
+                if (builder.Length > 0)
+                {
+                    builder.Append(i == values.Count - 1 ? " and " : ", ");
+                }
+
+                builder.Append(values[i]);
+            }
+
+            // Return result
+            return builder.ToString();
+        }
+
+        /// <summary>
+        /// Constructs a string description of a time-span value.
+        /// </summary>
+        /// <param name="value">The value of this item.</param>
+        /// <param name="description">The name of this item (singular form).</param>
+        private static string CreateValueString(int value, string description)
+        {
+            return string.Format(
+                CultureInfo.InvariantCulture,
+                "{0:#,##0} {1}",
+                value,
+                value == 1 ? description : string.Format(CultureInfo.InvariantCulture, "{0}s", description));
+        }
+    }
+}

+ 31 - 0
Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs

@@ -0,0 +1,31 @@
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Tasks;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.System
+{
+    /// <summary>
+    /// Notifies admin users when a task is completed.
+    /// </summary>
+    public class TaskCompletedNotifier : IEventConsumer<TaskCompletionEventArgs>
+    {
+        private readonly ISessionManager _sessionManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TaskCompletedNotifier"/> class.
+        /// </summary>
+        /// <param name="sessionManager">The session manager.</param>
+        public TaskCompletedNotifier(ISessionManager sessionManager)
+        {
+            _sessionManager = sessionManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(TaskCompletionEventArgs eventArgs)
+        {
+            await _sessionManager.SendMessageToAdminSessions("ScheduledTaskEnded", eventArgs.Result, CancellationToken.None).ConfigureAwait(false);
+        }
+    }
+}

+ 31 - 0
Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs

@@ -0,0 +1,31 @@
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Updates;
+using MediaBrowser.Controller.Session;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
+{
+    /// <summary>
+    /// Notifies admin users when a plugin installation is cancelled.
+    /// </summary>
+    public class PluginInstallationCancelledNotifier : IEventConsumer<PluginInstallationCancelledEventArgs>
+    {
+        private readonly ISessionManager _sessionManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PluginInstallationCancelledNotifier"/> class.
+        /// </summary>
+        /// <param name="sessionManager">The session manager.</param>
+        public PluginInstallationCancelledNotifier(ISessionManager sessionManager)
+        {
+            _sessionManager = sessionManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(PluginInstallationCancelledEventArgs eventArgs)
+        {
+            await _sessionManager.SendMessageToAdminSessions("PackageInstallationCancelled", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
+        }
+    }
+}

+ 51 - 0
Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs

@@ -0,0 +1,51 @@
+using System;
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Notifications;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
+{
+    /// <summary>
+    /// Creates an entry in the activity log when a package installation fails.
+    /// </summary>
+    public class PluginInstallationFailedLogger : IEventConsumer<InstallationFailedEventArgs>
+    {
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PluginInstallationFailedLogger"/> class.
+        /// </summary>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public PluginInstallationFailedLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(InstallationFailedEventArgs eventArgs)
+        {
+            await _activityManager.CreateAsync(new ActivityLog(
+                string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("NameInstallFailed"),
+                    eventArgs.InstallationInfo.Name),
+                NotificationType.InstallationFailed.ToString(),
+                Guid.Empty)
+            {
+                ShortOverview = string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("VersionNumber"),
+                    eventArgs.InstallationInfo.Version),
+                Overview = eventArgs.Exception.Message
+            }).ConfigureAwait(false);
+        }
+    }
+}

+ 31 - 0
Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs

@@ -0,0 +1,31 @@
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Session;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
+{
+    /// <summary>
+    /// Notifies admin users when a plugin installation fails.
+    /// </summary>
+    public class PluginInstallationFailedNotifier : IEventConsumer<InstallationFailedEventArgs>
+    {
+        private readonly ISessionManager _sessionManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PluginInstallationFailedNotifier"/> class.
+        /// </summary>
+        /// <param name="sessionManager">The session manager.</param>
+        public PluginInstallationFailedNotifier(ISessionManager sessionManager)
+        {
+            _sessionManager = sessionManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(InstallationFailedEventArgs eventArgs)
+        {
+            await _sessionManager.SendMessageToAdminSessions("PackageInstallationFailed", eventArgs.InstallationInfo, CancellationToken.None).ConfigureAwait(false);
+        }
+    }
+}

+ 50 - 0
Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs

@@ -0,0 +1,50 @@
+using System;
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Updates;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Notifications;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
+{
+    /// <summary>
+    /// Creates an entry in the activity log when a plugin is installed.
+    /// </summary>
+    public class PluginInstalledLogger : IEventConsumer<PluginInstalledEventArgs>
+    {
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PluginInstalledLogger"/> class.
+        /// </summary>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public PluginInstalledLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(PluginInstalledEventArgs eventArgs)
+        {
+            await _activityManager.CreateAsync(new ActivityLog(
+                string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("PluginInstalledWithName"),
+                    eventArgs.Argument.Name),
+                NotificationType.PluginInstalled.ToString(),
+                Guid.Empty)
+            {
+                ShortOverview = string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("VersionNumber"),
+                    eventArgs.Argument.Version)
+            }).ConfigureAwait(false);
+        }
+    }
+}

+ 31 - 0
Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs

@@ -0,0 +1,31 @@
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Updates;
+using MediaBrowser.Controller.Session;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
+{
+    /// <summary>
+    /// Notifies admin users when a plugin is installed.
+    /// </summary>
+    public class PluginInstalledNotifier : IEventConsumer<PluginInstalledEventArgs>
+    {
+        private readonly ISessionManager _sessionManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PluginInstalledNotifier"/> class.
+        /// </summary>
+        /// <param name="sessionManager">The session manager.</param>
+        public PluginInstalledNotifier(ISessionManager sessionManager)
+        {
+            _sessionManager = sessionManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(PluginInstalledEventArgs eventArgs)
+        {
+            await _sessionManager.SendMessageToAdminSessions("PackageInstallationCompleted", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
+        }
+    }
+}

+ 31 - 0
Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs

@@ -0,0 +1,31 @@
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Updates;
+using MediaBrowser.Controller.Session;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
+{
+    /// <summary>
+    /// Notifies admin users when a plugin is being installed.
+    /// </summary>
+    public class PluginInstallingNotifier : IEventConsumer<PluginInstallingEventArgs>
+    {
+        private readonly ISessionManager _sessionManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PluginInstallingNotifier"/> class.
+        /// </summary>
+        /// <param name="sessionManager">The session manager.</param>
+        public PluginInstallingNotifier(ISessionManager sessionManager)
+        {
+            _sessionManager = sessionManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(PluginInstallingEventArgs eventArgs)
+        {
+            await _sessionManager.SendMessageToAdminSessions("PackageInstalling", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
+        }
+    }
+}

+ 45 - 0
Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs

@@ -0,0 +1,45 @@
+using System;
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Updates;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Notifications;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
+{
+    /// <summary>
+    /// Creates an entry in the activity log when a plugin is uninstalled.
+    /// </summary>
+    public class PluginUninstalledLogger : IEventConsumer<PluginUninstalledEventArgs>
+    {
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PluginUninstalledLogger"/> class.
+        /// </summary>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public PluginUninstalledLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(PluginUninstalledEventArgs e)
+        {
+            await _activityManager.CreateAsync(new ActivityLog(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        _localizationManager.GetLocalizedString("PluginUninstalledWithName"),
+                        e.Argument.Name),
+                    NotificationType.PluginUninstalled.ToString(),
+                    Guid.Empty))
+                .ConfigureAwait(false);
+        }
+    }
+}

+ 31 - 0
Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs

@@ -0,0 +1,31 @@
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Updates;
+using MediaBrowser.Controller.Session;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
+{
+    /// <summary>
+    /// Notifies admin users when a plugin is uninstalled.
+    /// </summary>
+    public class PluginUninstalledNotifier : IEventConsumer<PluginUninstalledEventArgs>
+    {
+        private readonly ISessionManager _sessionManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PluginUninstalledNotifier"/> class.
+        /// </summary>
+        /// <param name="sessionManager">The session manager.</param>
+        public PluginUninstalledNotifier(ISessionManager sessionManager)
+        {
+            _sessionManager = sessionManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(PluginUninstalledEventArgs eventArgs)
+        {
+            await _sessionManager.SendMessageToAdminSessions("PluginUninstalled", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
+        }
+    }
+}

+ 51 - 0
Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs

@@ -0,0 +1,51 @@
+using System;
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Updates;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Notifications;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
+{
+    /// <summary>
+    /// Creates an entry in the activity log when a plugin is updated.
+    /// </summary>
+    public class PluginUpdatedLogger : IEventConsumer<PluginUpdatedEventArgs>
+    {
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PluginUpdatedLogger"/> class.
+        /// </summary>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public PluginUpdatedLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(PluginUpdatedEventArgs eventArgs)
+        {
+            await _activityManager.CreateAsync(new ActivityLog(
+                string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("PluginUpdatedWithName"),
+                    eventArgs.Argument.Name),
+                NotificationType.PluginUpdateInstalled.ToString(),
+                Guid.Empty)
+            {
+                ShortOverview = string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("VersionNumber"),
+                    eventArgs.Argument.Version),
+                Overview = eventArgs.Argument.Changelog
+            }).ConfigureAwait(false);
+        }
+    }
+}

+ 43 - 0
Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs

@@ -0,0 +1,43 @@
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events.Users;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Users
+{
+    /// <summary>
+    /// Creates an entry in the activity log when a user is created.
+    /// </summary>
+    public class UserCreatedLogger : IEventConsumer<UserCreatedEventArgs>
+    {
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserCreatedLogger"/> class.
+        /// </summary>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public UserCreatedLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(UserCreatedEventArgs eventArgs)
+        {
+            await _activityManager.CreateAsync(new ActivityLog(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        _localizationManager.GetLocalizedString("UserCreatedWithName"),
+                        eventArgs.Argument.Username),
+                    "UserCreated",
+                    eventArgs.Argument.Id))
+                .ConfigureAwait(false);
+        }
+    }
+}

+ 44 - 0
Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events.Users;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Users
+{
+    /// <summary>
+    /// Adds an entry to the activity log when a user is deleted.
+    /// </summary>
+    public class UserDeletedLogger : IEventConsumer<UserDeletedEventArgs>
+    {
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserDeletedLogger"/> class.
+        /// </summary>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public UserDeletedLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(UserDeletedEventArgs eventArgs)
+        {
+            await _activityManager.CreateAsync(new ActivityLog(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        _localizationManager.GetLocalizedString("UserDeletedWithName"),
+                        eventArgs.Argument.Username),
+                    "UserDeleted",
+                    Guid.Empty))
+                .ConfigureAwait(false);
+        }
+    }
+}

+ 38 - 0
Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Events.Users;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Session;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Users
+{
+    /// <summary>
+    /// Notifies the user's sessions when a user is deleted.
+    /// </summary>
+    public class UserDeletedNotifier : IEventConsumer<UserDeletedEventArgs>
+    {
+        private readonly ISessionManager _sessionManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserDeletedNotifier"/> class.
+        /// </summary>
+        /// <param name="sessionManager">The session manager.</param>
+        public UserDeletedNotifier(ISessionManager sessionManager)
+        {
+            _sessionManager = sessionManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(UserDeletedEventArgs eventArgs)
+        {
+            await _sessionManager.SendMessageToUserSessions(
+                new List<Guid> { eventArgs.Argument.Id },
+                "UserDeleted",
+                eventArgs.Argument.Id.ToString("N", CultureInfo.InvariantCulture),
+                CancellationToken.None).ConfigureAwait(false);
+        }
+    }
+}

+ 47 - 0
Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs

@@ -0,0 +1,47 @@
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events.Users;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Notifications;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Users
+{
+    /// <summary>
+    /// Creates an entry in the activity log when a user is locked out.
+    /// </summary>
+    public class UserLockedOutLogger : IEventConsumer<UserLockedOutEventArgs>
+    {
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserLockedOutLogger"/> class.
+        /// </summary>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public UserLockedOutLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(UserLockedOutEventArgs eventArgs)
+        {
+            await _activityManager.CreateAsync(new ActivityLog(
+                string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("UserLockedOutWithName"),
+                    eventArgs.Argument.Username),
+                NotificationType.UserLockedOut.ToString(),
+                eventArgs.Argument.Id)
+            {
+                LogSeverity = LogLevel.Error
+            }).ConfigureAwait(false);
+        }
+    }
+}

+ 43 - 0
Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs

@@ -0,0 +1,43 @@
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events.Users;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Users
+{
+    /// <summary>
+    /// Creates an entry in the activity log when a user's password is changed.
+    /// </summary>
+    public class UserPasswordChangedLogger : IEventConsumer<UserPasswordChangedEventArgs>
+    {
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserPasswordChangedLogger"/> class.
+        /// </summary>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        public UserPasswordChangedLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
+        {
+            _localizationManager = localizationManager;
+            _activityManager = activityManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(UserPasswordChangedEventArgs eventArgs)
+        {
+            await _activityManager.CreateAsync(new ActivityLog(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        _localizationManager.GetLocalizedString("UserPasswordChangedWithName"),
+                        eventArgs.Argument.Username),
+                    "UserPasswordChanged",
+                    eventArgs.Argument.Id))
+                .ConfigureAwait(false);
+        }
+    }
+}

+ 41 - 0
Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs

@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Events.Users;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Session;
+
+namespace Jellyfin.Server.Implementations.Events.Consumers.Users
+{
+    /// <summary>
+    /// Notifies a user when their account has been updated.
+    /// </summary>
+    public class UserUpdatedNotifier : IEventConsumer<UserUpdatedEventArgs>
+    {
+        private readonly IUserManager _userManager;
+        private readonly ISessionManager _sessionManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserUpdatedNotifier"/> class.
+        /// </summary>
+        /// <param name="userManager">The user manager.</param>
+        /// <param name="sessionManager">The session manager.</param>
+        public UserUpdatedNotifier(IUserManager userManager, ISessionManager sessionManager)
+        {
+            _userManager = userManager;
+            _sessionManager = sessionManager;
+        }
+
+        /// <inheritdoc />
+        public async Task OnEvent(UserUpdatedEventArgs e)
+        {
+            await _sessionManager.SendMessageToUserSessions(
+                new List<Guid> { e.Argument.Id },
+                "UserUpdated",
+                _userManager.GetUserDto(e.Argument),
+                CancellationToken.None).ConfigureAwait(false);
+        }
+    }
+}

+ 60 - 0
Jellyfin.Server.Implementations/Events/EventManager.cs

@@ -0,0 +1,60 @@
+using System;
+using System.Threading.Tasks;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Events;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Events
+{
+    /// <summary>
+    /// Handles the firing of events.
+    /// </summary>
+    public class EventManager : IEventManager
+    {
+        private readonly ILogger<EventManager> _logger;
+        private readonly IServerApplicationHost _appHost;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="EventManager"/> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        /// <param name="appHost">The application host.</param>
+        public EventManager(ILogger<EventManager> logger, IServerApplicationHost appHost)
+        {
+            _logger = logger;
+            _appHost = appHost;
+        }
+
+        /// <inheritdoc />
+        public void Publish<T>(T eventArgs)
+            where T : EventArgs
+        {
+            Task.WaitAll(PublishInternal(eventArgs));
+        }
+
+        /// <inheritdoc />
+        public async Task PublishAsync<T>(T eventArgs)
+            where T : EventArgs
+        {
+            await PublishInternal(eventArgs).ConfigureAwait(false);
+        }
+
+        private async Task PublishInternal<T>(T eventArgs)
+            where T : EventArgs
+        {
+            using var scope = _appHost.ServiceProvider.CreateScope();
+            foreach (var service in scope.ServiceProvider.GetServices<IEventConsumer<T>>())
+            {
+                try
+                {
+                    await service.OnEvent(eventArgs).ConfigureAwait(false);
+                }
+                catch (Exception e)
+                {
+                    _logger.LogError(e, "Uncaught exception in EventConsumer {type}: ", service.GetType());
+                }
+            }
+        }
+    }
+}

+ 72 - 0
Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs

@@ -0,0 +1,72 @@
+using Jellyfin.Data.Events;
+using Jellyfin.Data.Events.System;
+using Jellyfin.Data.Events.Users;
+using Jellyfin.Server.Implementations.Events.Consumers.Library;
+using Jellyfin.Server.Implementations.Events.Consumers.Security;
+using Jellyfin.Server.Implementations.Events.Consumers.Session;
+using Jellyfin.Server.Implementations.Events.Consumers.System;
+using Jellyfin.Server.Implementations.Events.Consumers.Updates;
+using Jellyfin.Server.Implementations.Events.Consumers.Users;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Session;
+using MediaBrowser.Controller.Events.Updates;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.Subtitles;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Jellyfin.Server.Implementations.Events
+{
+    /// <summary>
+    /// A class containing extensions to <see cref="IServiceCollection"/> for eventing.
+    /// </summary>
+    public static class EventingServiceCollectionExtensions
+    {
+        /// <summary>
+        /// Adds the event services to the service collection.
+        /// </summary>
+        /// <param name="collection">The service collection.</param>
+        public static void AddEventServices(this IServiceCollection collection)
+        {
+            // Library consumers
+            collection.AddScoped<IEventConsumer<SubtitleDownloadFailureEventArgs>, SubtitleDownloadFailureLogger>();
+
+            // Security consumers
+            collection.AddScoped<IEventConsumer<GenericEventArgs<AuthenticationRequest>>, AuthenticationFailedLogger>();
+            collection.AddScoped<IEventConsumer<GenericEventArgs<AuthenticationResult>>, AuthenticationSucceededLogger>();
+
+            // Session consumers
+            collection.AddScoped<IEventConsumer<PlaybackStartEventArgs>, PlaybackStartLogger>();
+            collection.AddScoped<IEventConsumer<PlaybackStopEventArgs>, PlaybackStopLogger>();
+            collection.AddScoped<IEventConsumer<SessionEndedEventArgs>, SessionEndedLogger>();
+            collection.AddScoped<IEventConsumer<SessionStartedEventArgs>, SessionStartedLogger>();
+
+            // System consumers
+            collection.AddScoped<IEventConsumer<PendingRestartEventArgs>, PendingRestartNotifier>();
+            collection.AddScoped<IEventConsumer<TaskCompletionEventArgs>, TaskCompletedLogger>();
+            collection.AddScoped<IEventConsumer<TaskCompletionEventArgs>, TaskCompletedNotifier>();
+
+            // Update consumers
+            collection.AddScoped<IEventConsumer<PluginInstallationCancelledEventArgs>, PluginInstallationCancelledNotifier>();
+            collection.AddScoped<IEventConsumer<InstallationFailedEventArgs>, PluginInstallationFailedLogger>();
+            collection.AddScoped<IEventConsumer<InstallationFailedEventArgs>, PluginInstallationFailedNotifier>();
+            collection.AddScoped<IEventConsumer<PluginInstalledEventArgs>, PluginInstalledLogger>();
+            collection.AddScoped<IEventConsumer<PluginInstalledEventArgs>, PluginInstalledNotifier>();
+            collection.AddScoped<IEventConsumer<PluginInstallingEventArgs>, PluginInstallingNotifier>();
+            collection.AddScoped<IEventConsumer<PluginUninstalledEventArgs>, PluginUninstalledLogger>();
+            collection.AddScoped<IEventConsumer<PluginUninstalledEventArgs>, PluginUninstalledNotifier>();
+            collection.AddScoped<IEventConsumer<PluginUpdatedEventArgs>, PluginUpdatedLogger>();
+
+            // User consumers
+            collection.AddScoped<IEventConsumer<UserCreatedEventArgs>, UserCreatedLogger>();
+            collection.AddScoped<IEventConsumer<UserDeletedEventArgs>, UserDeletedLogger>();
+            collection.AddScoped<IEventConsumer<UserDeletedEventArgs>, UserDeletedNotifier>();
+            collection.AddScoped<IEventConsumer<UserLockedOutEventArgs>, UserLockedOutLogger>();
+            collection.AddScoped<IEventConsumer<UserPasswordChangedEventArgs>, UserPasswordChangedLogger>();
+            collection.AddScoped<IEventConsumer<UserUpdatedEventArgs>, UserUpdatedNotifier>();
+        }
+    }
+}

+ 1 - 1
Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs

@@ -4,12 +4,12 @@
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Events;
 
 namespace Jellyfin.Server.Implementations.Users
 {

+ 13 - 18
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -10,18 +10,20 @@ using System.Text.RegularExpressions;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
+using Jellyfin.Data.Events.Users;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Cryptography;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Cryptography;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Users;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Logging;
@@ -34,6 +36,7 @@ namespace Jellyfin.Server.Implementations.Users
     public class UserManager : IUserManager
     {
         private readonly JellyfinDbProvider _dbProvider;
+        private readonly IEventManager _eventManager;
         private readonly ICryptoProvider _cryptoProvider;
         private readonly INetworkManager _networkManager;
         private readonly IApplicationHost _appHost;
@@ -49,6 +52,7 @@ namespace Jellyfin.Server.Implementations.Users
         /// Initializes a new instance of the <see cref="UserManager"/> class.
         /// </summary>
         /// <param name="dbProvider">The database provider.</param>
+        /// <param name="eventManager">The event manager.</param>
         /// <param name="cryptoProvider">The cryptography provider.</param>
         /// <param name="networkManager">The network manager.</param>
         /// <param name="appHost">The application host.</param>
@@ -56,6 +60,7 @@ namespace Jellyfin.Server.Implementations.Users
         /// <param name="logger">The logger.</param>
         public UserManager(
             JellyfinDbProvider dbProvider,
+            IEventManager eventManager,
             ICryptoProvider cryptoProvider,
             INetworkManager networkManager,
             IApplicationHost appHost,
@@ -63,6 +68,7 @@ namespace Jellyfin.Server.Implementations.Users
             ILogger<UserManager> logger)
         {
             _dbProvider = dbProvider;
+            _eventManager = eventManager;
             _cryptoProvider = cryptoProvider;
             _networkManager = networkManager;
             _appHost = appHost;
@@ -77,21 +83,9 @@ namespace Jellyfin.Server.Implementations.Users
             _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
         }
 
-        /// <inheritdoc/>
-        public event EventHandler<GenericEventArgs<User>>? OnUserPasswordChanged;
-
         /// <inheritdoc/>
         public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
 
-        /// <inheritdoc/>
-        public event EventHandler<GenericEventArgs<User>>? OnUserCreated;
-
-        /// <inheritdoc/>
-        public event EventHandler<GenericEventArgs<User>>? OnUserDeleted;
-
-        /// <inheritdoc/>
-        public event EventHandler<GenericEventArgs<User>>? OnUserLockedOut;
-
         /// <inheritdoc/>
         public IEnumerable<User> Users
         {
@@ -234,7 +228,7 @@ namespace Jellyfin.Server.Implementations.Users
             dbContext.Users.Add(newUser);
             await dbContext.SaveChangesAsync().ConfigureAwait(false);
 
-            OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
+            await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false);
 
             return newUser;
         }
@@ -293,7 +287,8 @@ namespace Jellyfin.Server.Implementations.Users
             dbContext.RemoveRange(user.AccessSchedules);
             dbContext.Users.Remove(user);
             dbContext.SaveChanges();
-            OnUserDeleted?.Invoke(this, new GenericEventArgs<User>(user));
+
+            _eventManager.Publish(new UserDeletedEventArgs(user));
         }
 
         /// <inheritdoc/>
@@ -319,7 +314,7 @@ namespace Jellyfin.Server.Implementations.Users
             await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
             await UpdateUserAsync(user).ConfigureAwait(false);
 
-            OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
+            await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false);
         }
 
         /// <inheritdoc/>
@@ -338,7 +333,7 @@ namespace Jellyfin.Server.Implementations.Users
             user.EasyPassword = newPasswordSha1;
             UpdateUser(user);
 
-            OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
+            _eventManager.Publish(new UserPasswordChangedEventArgs(user));
         }
 
         /// <inheritdoc/>
@@ -901,7 +896,7 @@ namespace Jellyfin.Server.Implementations.Users
             if (maxInvalidLogins.HasValue && user.InvalidLoginAttemptCount >= maxInvalidLogins)
             {
                 user.SetPermission(PermissionKind.IsDisabled, true);
-                OnUserLockedOut?.Invoke(this, new GenericEventArgs<User>(user));
+                await _eventManager.PublishAsync(new UserLockedOutEventArgs(user)).ConfigureAwait(false);
                 _logger.LogWarning(
                     "Disabling user {Username} due to {Attempts} unsuccessful login attempts.",
                     user.Username,

+ 16 - 11
Jellyfin.Server/CoreAppHost.cs

@@ -1,20 +1,20 @@
 using System;
 using System.Collections.Generic;
-using System.IO;
 using System.Reflection;
 using Emby.Drawing;
 using Emby.Server.Implementations;
 using Jellyfin.Drawing.Skia;
 using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Implementations.Activity;
+using Jellyfin.Server.Implementations.Events;
 using Jellyfin.Server.Implementations.Users;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.IO;
-using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 
@@ -33,30 +33,33 @@ namespace Jellyfin.Server
         /// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
+        /// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
         public CoreAppHost(
             IServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
             IStartupOptions options,
             IFileSystem fileSystem,
-            INetworkManager networkManager)
+            INetworkManager networkManager,
+            IServiceCollection collection)
             : base(
                 applicationPaths,
                 loggerFactory,
                 options,
                 fileSystem,
-                networkManager)
+                networkManager,
+                collection)
         {
         }
 
         /// <inheritdoc/>
-        protected override void RegisterServices(IServiceCollection serviceCollection)
+        protected override void RegisterServices()
         {
             // Register an image encoder
             bool useSkiaEncoder = SkiaEncoder.IsNativeLibAvailable();
             Type imageEncoderType = useSkiaEncoder
                 ? typeof(SkiaEncoder)
                 : typeof(NullImageEncoder);
-            serviceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType);
+            ServiceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType);
 
             // Log a warning if the Skia encoder could not be used
             if (!useSkiaEncoder)
@@ -71,13 +74,15 @@ namespace Jellyfin.Server
             //         .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
             //     ServiceLifetime.Transient);
 
-            serviceCollection.AddSingleton<JellyfinDbProvider>();
+            ServiceCollection.AddEventServices();
+            ServiceCollection.AddSingleton<IEventManager, EventManager>();
+            ServiceCollection.AddSingleton<JellyfinDbProvider>();
 
-            serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
-            serviceCollection.AddSingleton<IUserManager, UserManager>();
-            serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
+            ServiceCollection.AddSingleton<IActivityManager, ActivityManager>();
+            ServiceCollection.AddSingleton<IUserManager, UserManager>();
+            ServiceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
 
-            base.RegisterServices(serviceCollection);
+            base.RegisterServices();
         }
 
         /// <inheritdoc />

+ 4 - 3
Jellyfin.Server/Program.cs

@@ -154,13 +154,15 @@ namespace Jellyfin.Server
             ApplicationHost.LogEnvironmentInfo(_logger, appPaths);
 
             PerformStaticInitialization();
+            var serviceCollection = new ServiceCollection();
 
             var appHost = new CoreAppHost(
                 appPaths,
                 _loggerFactory,
                 options,
                 new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
-                new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()));
+                new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()),
+                serviceCollection);
 
             try
             {
@@ -178,8 +180,7 @@ namespace Jellyfin.Server
                     }
                 }
 
-                ServiceCollection serviceCollection = new ServiceCollection();
-                appHost.Init(serviceCollection);
+                appHost.Init();
 
                 var webHost = new WebHostBuilder().ConfigureWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build();
 

+ 1 - 2
MediaBrowser.Common/IApplicationHost.cs

@@ -116,8 +116,7 @@ namespace MediaBrowser.Common
         /// <summary>
         /// Initializes this instance.
         /// </summary>
-        /// <param name="serviceCollection">The service collection.</param>
-        void Init(IServiceCollection serviceCollection);
+        void Init();
 
         /// <summary>
         /// Creates the instance.

+ 11 - 0
MediaBrowser.Common/Plugins/BasePlugin.cs

@@ -6,6 +6,7 @@ using System.Reflection;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.Plugins;
 using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.DependencyInjection;
 
 namespace MediaBrowser.Common.Plugins
 {
@@ -81,6 +82,16 @@ namespace MediaBrowser.Common.Plugins
         {
         }
 
+        /// <inheritdoc />
+        public virtual void RegisterServices(IServiceCollection serviceCollection)
+        {
+        }
+
+        /// <inheritdoc />
+        public virtual void UnregisterServices(IServiceCollection serviceCollection)
+        {
+        }
+
         /// <inheritdoc />
         public void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion)
         {

+ 13 - 0
MediaBrowser.Common/Plugins/IPlugin.cs

@@ -2,6 +2,7 @@
 
 using System;
 using MediaBrowser.Model.Plugins;
+using Microsoft.Extensions.DependencyInjection;
 
 namespace MediaBrowser.Common.Plugins
 {
@@ -61,6 +62,18 @@ namespace MediaBrowser.Common.Plugins
         /// Called when just before the plugin is uninstalled from the server.
         /// </summary>
         void OnUninstalling();
+
+        /// <summary>
+        /// Registers the plugin's services to the service collection.
+        /// </summary>
+        /// <param name="serviceCollection">The service collection.</param>
+        void RegisterServices(IServiceCollection serviceCollection);
+
+        /// <summary>
+        /// Unregisters the plugin's services from the service collection.
+        /// </summary>
+        /// <param name="serviceCollection">The service collection.</param>
+        void UnregisterServices(IServiceCollection serviceCollection);
     }
 
     public interface IHasPluginConfiguration

+ 2 - 1
MediaBrowser.Common/Updates/InstallationEventArgs.cs

@@ -1,10 +1,11 @@
 #pragma warning disable CS1591
 
+using System;
 using MediaBrowser.Model.Updates;
 
 namespace MediaBrowser.Common.Updates
 {
-    public class InstallationEventArgs
+    public class InstallationEventArgs : EventArgs
     {
         public InstallationInfo InstallationInfo { get; set; }
 

+ 1 - 1
MediaBrowser.Controller/Devices/IDeviceManager.cs

@@ -2,8 +2,8 @@
 
 using System;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events;
 using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Session;
 

+ 20 - 0
MediaBrowser.Controller/Events/IEventConsumer.cs

@@ -0,0 +1,20 @@
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Events
+{
+    /// <summary>
+    /// An interface representing a type that consumes events of type <c>T</c>.
+    /// </summary>
+    /// <typeparam name="T">The type of events this consumes.</typeparam>
+    public interface IEventConsumer<in T>
+        where T : EventArgs
+    {
+        /// <summary>
+        /// A method that is called when an event of type <c>T</c> is fired.
+        /// </summary>
+        /// <param name="eventArgs">The event.</param>
+        /// <returns>A task representing the consumption of the event.</returns>
+        Task OnEvent(T eventArgs);
+    }
+}

+ 28 - 0
MediaBrowser.Controller/Events/IEventManager.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Events
+{
+    /// <summary>
+    /// An interface that handles eventing.
+    /// </summary>
+    public interface IEventManager
+    {
+        /// <summary>
+        /// Publishes an event.
+        /// </summary>
+        /// <param name="eventArgs">the event arguments.</param>
+        /// <typeparam name="T">The type of event.</typeparam>
+        void Publish<T>(T eventArgs)
+            where T : EventArgs;
+
+        /// <summary>
+        /// Publishes an event asynchronously.
+        /// </summary>
+        /// <param name="eventArgs">The event arguments.</param>
+        /// <typeparam name="T">The type of event.</typeparam>
+        /// <returns>A task representing the publishing of the event.</returns>
+        Task PublishAsync<T>(T eventArgs)
+            where T : EventArgs;
+    }
+}

+ 19 - 0
MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs

@@ -0,0 +1,19 @@
+using Jellyfin.Data.Events;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.Events.Session
+{
+    /// <summary>
+    /// An event that fires when a session is ended.
+    /// </summary>
+    public class SessionEndedEventArgs : GenericEventArgs<SessionInfo>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SessionEndedEventArgs"/> class.
+        /// </summary>
+        /// <param name="arg">The session info.</param>
+        public SessionEndedEventArgs(SessionInfo arg) : base(arg)
+        {
+        }
+    }
+}

+ 19 - 0
MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs

@@ -0,0 +1,19 @@
+using Jellyfin.Data.Events;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.Events.Session
+{
+    /// <summary>
+    /// An event that fires when a session is started.
+    /// </summary>
+    public class SessionStartedEventArgs : GenericEventArgs<SessionInfo>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SessionStartedEventArgs"/> class.
+        /// </summary>
+        /// <param name="arg">The session info.</param>
+        public SessionStartedEventArgs(SessionInfo arg) : base(arg)
+        {
+        }
+    }
+}

+ 19 - 0
MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs

@@ -0,0 +1,19 @@
+using Jellyfin.Data.Events;
+using MediaBrowser.Model.Updates;
+
+namespace MediaBrowser.Controller.Events.Updates
+{
+    /// <summary>
+    /// An event that occurs when a plugin installation is cancelled.
+    /// </summary>
+    public class PluginInstallationCancelledEventArgs : GenericEventArgs<InstallationInfo>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PluginInstallationCancelledEventArgs"/> class.
+        /// </summary>
+        /// <param name="arg">The installation info.</param>
+        public PluginInstallationCancelledEventArgs(InstallationInfo arg) : base(arg)
+        {
+        }
+    }
+}

+ 19 - 0
MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs

@@ -0,0 +1,19 @@
+using Jellyfin.Data.Events;
+using MediaBrowser.Model.Updates;
+
+namespace MediaBrowser.Controller.Events.Updates
+{
+    /// <summary>
+    /// An event that occurs when a plugin is installed.
+    /// </summary>
+    public class PluginInstalledEventArgs : GenericEventArgs<InstallationInfo>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PluginInstalledEventArgs"/> class.
+        /// </summary>
+        /// <param name="arg">The installation info.</param>
+        public PluginInstalledEventArgs(InstallationInfo arg) : base(arg)
+        {
+        }
+    }
+}

+ 19 - 0
MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs

@@ -0,0 +1,19 @@
+using Jellyfin.Data.Events;
+using MediaBrowser.Model.Updates;
+
+namespace MediaBrowser.Controller.Events.Updates
+{
+    /// <summary>
+    /// An event that occurs when a plugin is installing.
+    /// </summary>
+    public class PluginInstallingEventArgs : GenericEventArgs<InstallationInfo>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PluginInstallingEventArgs"/> class.
+        /// </summary>
+        /// <param name="arg">The installation info.</param>
+        public PluginInstallingEventArgs(InstallationInfo arg) : base(arg)
+        {
+        }
+    }
+}

+ 19 - 0
MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs

@@ -0,0 +1,19 @@
+using Jellyfin.Data.Events;
+using MediaBrowser.Common.Plugins;
+
+namespace MediaBrowser.Controller.Events.Updates
+{
+    /// <summary>
+    /// An event that occurs when a plugin is uninstalled.
+    /// </summary>
+    public class PluginUninstalledEventArgs : GenericEventArgs<IPlugin>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PluginUninstalledEventArgs"/> class.
+        /// </summary>
+        /// <param name="arg">The plugin.</param>
+        public PluginUninstalledEventArgs(IPlugin arg) : base(arg)
+        {
+        }
+    }
+}

+ 19 - 0
MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs

@@ -0,0 +1,19 @@
+using Jellyfin.Data.Events;
+using MediaBrowser.Model.Updates;
+
+namespace MediaBrowser.Controller.Events.Updates
+{
+    /// <summary>
+    /// An event that occurs when a plugin is updated.
+    /// </summary>
+    public class PluginUpdatedEventArgs : GenericEventArgs<InstallationInfo>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PluginUpdatedEventArgs"/> class.
+        /// </summary>
+        /// <param name="arg">The installation info.</param>
+        public PluginUpdatedEventArgs(InstallationInfo arg) : base(arg)
+        {
+        }
+    }
+}

+ 9 - 7
MediaBrowser.Controller/IServerApplicationHost.cs

@@ -18,13 +18,7 @@ namespace MediaBrowser.Controller
     {
         event EventHandler HasUpdateAvailableChanged;
 
-        /// <summary>
-        /// Gets the system info.
-        /// </summary>
-        /// <returns>SystemInfo.</returns>
-        Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken);
-
-        Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken);
+        IServiceProvider ServiceProvider { get; }
 
         bool CanLaunchWebBrowser { get; }
 
@@ -57,6 +51,14 @@ namespace MediaBrowser.Controller
         /// <value>The name of the friendly.</value>
         string FriendlyName { get; }
 
+        /// <summary>
+        /// Gets the system info.
+        /// </summary>
+        /// <returns>SystemInfo.</returns>
+        Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken);
+
+        Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken);
+
         /// <summary>
         /// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request
         /// to the API that should exist at the address.

+ 1 - 21
MediaBrowser.Controller/Library/IUserManager.cs

@@ -4,9 +4,9 @@ using System;
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Library
@@ -21,26 +21,6 @@ namespace MediaBrowser.Controller.Library
         /// </summary>
         event EventHandler<GenericEventArgs<User>> OnUserUpdated;
 
-        /// <summary>
-        /// Occurs when a user is created.
-        /// </summary>
-        event EventHandler<GenericEventArgs<User>> OnUserCreated;
-
-        /// <summary>
-        /// Occurs when a user is deleted.
-        /// </summary>
-        event EventHandler<GenericEventArgs<User>> OnUserDeleted;
-
-        /// <summary>
-        /// Occurs when a user's password is changed.
-        /// </summary>
-        event EventHandler<GenericEventArgs<User>> OnUserPasswordChanged;
-
-        /// <summary>
-        /// Occurs when a user is locked out.
-        /// </summary>
-        event EventHandler<GenericEventArgs<User>> OnUserLockedOut;
-
         /// <summary>
         /// Gets the users.
         /// </summary>

+ 9 - 0
MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs

@@ -0,0 +1,9 @@
+namespace MediaBrowser.Controller.Library
+{
+    /// <summary>
+    /// An event that occurs when playback is started.
+    /// </summary>
+    public class PlaybackStartEventArgs : PlaybackProgressEventArgs
+    {
+    }
+}

+ 1 - 1
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -5,11 +5,11 @@ using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Querying;
 

+ 1 - 1
MediaBrowser.Controller/Net/IHttpServer.cs

@@ -1,7 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Threading.Tasks;
-using MediaBrowser.Model.Events;
+using Jellyfin.Data.Events;
 using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
 

+ 1 - 1
MediaBrowser.Controller/Providers/IProviderManager.cs

@@ -7,12 +7,12 @@ using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Providers;
 
 namespace MediaBrowser.Controller.Providers

+ 1 - 1
MediaBrowser.Controller/Session/ISessionManager.cs

@@ -4,11 +4,11 @@ using System;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.SyncPlay;
 

+ 0 - 29
MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs

@@ -1,29 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Controller.Entities;
-
-namespace MediaBrowser.Controller.Subtitles
-{
-    public class SubtitleDownloadEventArgs
-    {
-        public BaseItem Item { get; set; }
-
-        public string Format { get; set; }
-
-        public string Language { get; set; }
-
-        public bool IsForced { get; set; }
-
-        public string Provider { get; set; }
-    }
-
-    public class SubtitleDownloadFailureEventArgs
-    {
-        public BaseItem Item { get; set; }
-
-        public string Provider { get; set; }
-
-        public Exception Exception { get; set; }
-    }
-}

+ 26 - 0
MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs

@@ -0,0 +1,26 @@
+using System;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Subtitles
+{
+    /// <summary>
+    /// An event that occurs when subtitle downloading fails.
+    /// </summary>
+    public class SubtitleDownloadFailureEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Gets or sets the item.
+        /// </summary>
+        public BaseItem Item { get; set; }
+
+        /// <summary>
+        /// Gets or sets the provider.
+        /// </summary>
+        public string Provider { get; set; }
+
+        /// <summary>
+        /// Gets or sets the exception.
+        /// </summary>
+        public Exception Exception { get; set; }
+    }
+}

+ 1 - 1
MediaBrowser.Model/Activity/IActivityManager.cs

@@ -4,7 +4,7 @@ using System;
 using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
-using MediaBrowser.Model.Events;
+using Jellyfin.Data.Events;
 using MediaBrowser.Model.Querying;
 
 namespace MediaBrowser.Model.Activity

+ 1 - 1
MediaBrowser.Model/Dlna/IDeviceDiscovery.cs

@@ -1,7 +1,7 @@
 #pragma warning disable CS1591
 
 using System;
-using MediaBrowser.Model.Events;
+using Jellyfin.Data.Events;
 
 namespace MediaBrowser.Model.Dlna
 {

+ 1 - 1
MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs

@@ -1,6 +1,6 @@
 #nullable disable
 using System;
-using MediaBrowser.Model.Events;
+using Jellyfin.Data.Events;
 
 namespace MediaBrowser.Model.Tasks
 {

+ 1 - 1
MediaBrowser.Model/Tasks/ITaskManager.cs

@@ -3,7 +3,7 @@
 using System;
 using System.Collections.Generic;
 using System.Threading.Tasks;
-using MediaBrowser.Model.Events;
+using Jellyfin.Data.Events;
 
 namespace MediaBrowser.Model.Tasks
 {

+ 1 - 1
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -10,6 +10,7 @@ using System.Net.Mime;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller;
@@ -23,7 +24,6 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Providers;

+ 4 - 3
tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs

@@ -72,6 +72,7 @@ namespace MediaBrowser.Api.Tests
             var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths);
 
             ILoggerFactory loggerFactory = new SerilogLoggerFactory();
+            var serviceCollection = new ServiceCollection();
             _disposableComponents.Add(loggerFactory);
 
             // Create the app host and initialize it
@@ -80,10 +81,10 @@ namespace MediaBrowser.Api.Tests
                 loggerFactory,
                 commandLineOpts,
                 new ManagedFileSystem(loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
-                new NetworkManager(loggerFactory.CreateLogger<NetworkManager>()));
+                new NetworkManager(loggerFactory.CreateLogger<NetworkManager>()),
+                serviceCollection);
             _disposableComponents.Add(appHost);
-            var serviceCollection = new ServiceCollection();
-            appHost.Init(serviceCollection);
+            appHost.Init();
 
             // Configure the web host builder
             Program.ConfigureWebHostBuilder(builder, appHost, serviceCollection, commandLineOpts, startupConfig, appPaths);