Browse Source

Merge remote-tracking branch 'upstream/master' into 3.1.7

crobibero 4 years ago
parent
commit
bd66fd25df
100 changed files with 2010 additions and 1479 deletions
  1. 27 4
      .ci/azure-pipelines-package.yml
  2. 2 2
      Emby.Dlna/Eventing/DlnaEventManager.cs
  3. 1 1
      Emby.Dlna/IConnectionManager.cs
  4. 1 1
      Emby.Dlna/IContentDirectory.cs
  5. 1 1
      Emby.Dlna/IDlnaEventManager.cs
  6. 1 1
      Emby.Dlna/IMediaReceiverRegistrar.cs
  7. 1 1
      Emby.Dlna/PlayTo/PlayToController.cs
  8. 1 1
      Emby.Dlna/PlayTo/PlayToManager.cs
  9. 3 3
      Emby.Dlna/Service/BaseService.cs
  10. 1 1
      Emby.Dlna/Ssdp/DeviceDiscovery.cs
  11. 2 1
      Emby.Naming/Emby.Naming.csproj
  12. 1 1
      Emby.Notifications/NotificationEntryPoint.cs
  13. 0 590
      Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
  14. 82 77
      Emby.Server.Implementations/ApplicationHost.cs
  15. 1 1
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  16. 0 3
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  17. 1 1
      Emby.Server.Implementations/Devices/DeviceManager.cs
  18. 1 1
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  19. 1 1
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  20. 5 4
      Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
  21. 0 210
      Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs
  22. 1 1
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  23. 7 1
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  24. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  25. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
  26. 1 1
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  27. 1 1
      Emby.Server.Implementations/Localization/Core/id.json
  28. 4 1
      Emby.Server.Implementations/Localization/Core/th.json
  29. 1 1
      Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  30. 1 1
      Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
  31. 61 56
      Emby.Server.Implementations/Session/SessionManager.cs
  32. 1 1
      Emby.Server.Implementations/Session/SessionWebSocketListener.cs
  33. 4 4
      Jellyfin.Api/Controllers/DlnaServerController.cs
  34. 1 1
      Jellyfin.Api/Controllers/DynamicHlsController.cs
  35. 5 5
      Jellyfin.Api/Controllers/LibraryController.cs
  36. 6 2
      Jellyfin.Api/Controllers/PluginsController.cs
  37. 5 1
      Jellyfin.Api/Helpers/MediaInfoHelper.cs
  38. 1 1
      Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
  39. 1 1
      Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
  40. 8 8
      Jellyfin.Data/Events/GenericEventArgs.cs
  41. 11 0
      Jellyfin.Data/Events/System/PendingRestartEventArgs.cs
  42. 18 0
      Jellyfin.Data/Events/Users/UserCreatedEventArgs.cs
  43. 18 0
      Jellyfin.Data/Events/Users/UserDeletedEventArgs.cs
  44. 18 0
      Jellyfin.Data/Events/Users/UserLockedOutEventArgs.cs
  45. 18 0
      Jellyfin.Data/Events/Users/UserPasswordChangedEventArgs.cs
  46. 18 0
      Jellyfin.Data/Events/Users/UserUpdatedEventArgs.cs
  47. 8 0
      Jellyfin.Data/Jellyfin.Data.csproj
  48. 1 11
      Jellyfin.Server.Implementations/Activity/ActivityManager.cs
  49. 102 0
      Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs
  50. 52 0
      Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs
  51. 49 0
      Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
  52. 104 0
      Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
  53. 106 0
      Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
  54. 54 0
      Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs
  55. 54 0
      Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs
  56. 31 0
      Jellyfin.Server.Implementations/Events/Consumers/System/PendingRestartNotifier.cs
  57. 158 0
      Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs
  58. 31 0
      Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs
  59. 31 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs
  60. 51 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs
  61. 31 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs
  62. 50 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs
  63. 31 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs
  64. 31 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs
  65. 45 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs
  66. 31 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs
  67. 51 0
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs
  68. 43 0
      Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs
  69. 44 0
      Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs
  70. 38 0
      Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs
  71. 47 0
      Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs
  72. 43 0
      Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs
  73. 41 0
      Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs
  74. 60 0
      Jellyfin.Server.Implementations/Events/EventManager.cs
  75. 72 0
      Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
  76. 1 1
      Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
  77. 19 24
      Jellyfin.Server.Implementations/Users/UserManager.cs
  78. 16 11
      Jellyfin.Server/CoreAppHost.cs
  79. 2 1
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  80. 5 0
      Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
  81. 10 3
      Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
  82. 4 3
      Jellyfin.Server/Program.cs
  83. 1 2
      MediaBrowser.Common/IApplicationHost.cs
  84. 0 56
      MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs
  85. 0 40
      MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs
  86. 0 56
      MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs
  87. 0 82
      MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs
  88. 0 59
      MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs
  89. 0 55
      MediaBrowser.Common/Json/Converters/JsonNullableInt32Converter.cs
  90. 0 70
      MediaBrowser.Common/Json/Converters/JsonNullableInt64Converter.cs
  91. 2 7
      MediaBrowser.Common/Json/JsonDefaults.cs
  92. 2 1
      MediaBrowser.Common/MediaBrowser.Common.csproj
  93. 11 0
      MediaBrowser.Common/Plugins/BasePlugin.cs
  94. 13 0
      MediaBrowser.Common/Plugins/IPlugin.cs
  95. 2 1
      MediaBrowser.Common/Updates/InstallationEventArgs.cs
  96. 1 1
      MediaBrowser.Controller/Devices/IDeviceManager.cs
  97. 20 0
      MediaBrowser.Controller/Events/IEventConsumer.cs
  98. 28 0
      MediaBrowser.Controller/Events/IEventManager.cs
  99. 19 0
      MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs
  100. 19 0
      MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs

+ 27 - 4
.ci/azure-pipelines-package.yml

@@ -147,19 +147,42 @@ jobs:
   displayName: 'Publish NuGet packages'
   dependsOn:
   - BuildPackage
-  condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags'))
+  condition: succeeded('BuildPackage')
 
   pool:
     vmImage: 'ubuntu-latest'
 
   steps:
-  - task: NuGetCommand@2
+  - task: DotNetCoreCLI@2
+    displayName: 'Build Stable Nuget packages'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
     inputs:
       command: 'pack'
-      packagesToPack: Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj
-      packDestination: '$(Build.ArtifactStagingDirectory)'
+      packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj'
+      versioningScheme: 'off'
+
+  - task: DotNetCoreCLI@2
+    displayName: 'Build Unstable Nuget packages'
+    inputs:
+      command: 'custom'
+      projects: |
+        Jellyfin.Data/Jellyfin.Data.csproj
+        MediaBrowser.Common/MediaBrowser.Common.csproj
+        MediaBrowser.Controller/MediaBrowser.Controller.csproj
+        MediaBrowser.Model/MediaBrowser.Model.csproj
+        Emby.Naming/Emby.Naming.csproj
+      custom: 'pack'
+      arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory)'
+
+  - task: PublishBuildArtifacts@1
+    displayName: 'Publish Nuget packages'
+    inputs:
+      pathToPublish: $(Build.ArtifactStagingDirectory)
+      artifactName: Jellyfin Nuget Packages
 
   - task: NuGetCommand@2
+    displayName: 'Push Nuget packages to feed'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
     inputs:
       command: 'push'
       packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'

+ 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;
 

+ 2 - 1
Emby.Naming/Emby.Naming.csproj

@@ -23,8 +23,9 @@
   <PropertyGroup>
     <Authors>Jellyfin Contributors</Authors>
     <PackageId>Jellyfin.Naming</PackageId>
-    <PackageLicenseUrl>https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</PackageLicenseUrl>
+    <VersionPrefix>10.7.0</VersionPrefix>
     <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
+    <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
   </PropertyGroup>
 
   <!-- Code Analyzers-->

+ 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;

+ 0 - 3
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -90,9 +90,6 @@ namespace Emby.Server.Implementations.Data
             _typeMapper = new TypeMapper();
             _jsonOptions = JsonDefaults.GetOptions();
 
-            // GetItem throws NotSupportedException with this enabled, so hardcode false.
-            _jsonOptions.IgnoreNullValues = false;
-
             DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
         }
 

+ 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;

+ 7 - 1
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -179,7 +179,7 @@ namespace Emby.Server.Implementations.HttpServer
                 return;
             }
 
-            WebSocketMessage<object> stub;
+            WebSocketMessage<object>? stub;
             try
             {
 
@@ -209,6 +209,12 @@ namespace Emby.Server.Implementations.HttpServer
                 return;
             }
 
+            if (stub == null)
+            {
+                _logger.LogError("Error processing web socket message");
+                return;
+            }
+
             // Tell the PipeReader how much of the buffer we have consumed
             reader.AdvanceTo(buffer.End);
 

+ 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/Localization/Core/id.json

@@ -22,7 +22,7 @@
     "HeaderContinueWatching": "Lanjutkan Menonton",
     "HeaderCameraUploads": "Unggahan Kamera",
     "HeaderAlbumArtists": "Album Artis",
-    "Genres": "Genre",
+    "Genres": "Aliran",
     "Folders": "Folder",
     "Favorites": "Favorit",
     "Collections": "Koleksi",

+ 4 - 1
Emby.Server.Implementations/Localization/Core/th.json

@@ -69,5 +69,8 @@
     "AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
     "Albums": "อัลบั้ม",
     "ScheduledTaskStartedWithName": "{0} เริ่มต้น",
-    "ScheduledTaskFailedWithName": "{0} ล้มเหลว"
+    "ScheduledTaskFailedWithName": "{0} ล้มเหลว",
+    "Songs": "เพลง",
+    "Shows": "แสดง",
+    "ServerNameNeedsToBeRestarted": "{0} ต้องการรีสตาร์ท"
 }

+ 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/Controllers/DynamicHlsController.cs

@@ -1356,7 +1356,7 @@ namespace Jellyfin.Api.Controllers
 
             return string.Format(
                 CultureInfo.InvariantCulture,
-                "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"",
+                "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size 2048 -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"",
                 inputModifier,
                 _encodingHelper.GetInputArgument(state, encodingOptions),
                 threads,

+ 5 - 5
Jellyfin.Api/Controllers/LibraryController.cs

@@ -619,7 +619,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.Download)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult GetDownload([FromRoute] Guid itemId)
+        public async Task<ActionResult> GetDownload([FromRoute] Guid itemId)
         {
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)
@@ -648,7 +648,7 @@ namespace Jellyfin.Api.Controllers
 
             if (user != null)
             {
-                LogDownload(item, user, auth);
+                await LogDownloadAsync(item, user, auth).ConfigureAwait(false);
             }
 
             var path = item.Path;
@@ -861,17 +861,17 @@ namespace Jellyfin.Api.Controllers
                 : item;
         }
 
-        private void LogDownload(BaseItem item, User user, AuthorizationInfo auth)
+        private async Task LogDownloadAsync(BaseItem item, User user, AuthorizationInfo auth)
         {
             try
             {
-                _activityManager.Create(new ActivityLog(
+                await _activityManager.CreateAsync(new ActivityLog(
                     string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
                     "UserDownloadingContent",
                     auth.UserId)
                 {
                     ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device),
-                });
+                }).ConfigureAwait(false);
             }
             catch
             {

+ 6 - 2
Jellyfin.Api/Controllers/PluginsController.cs

@@ -120,10 +120,14 @@ namespace Jellyfin.Api.Controllers
                 return NotFound();
             }
 
-            var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions)
+            var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions)
                 .ConfigureAwait(false);
 
-            plugin.UpdateConfiguration(configuration);
+            if (configuration != null)
+            {
+                plugin.UpdateConfiguration(configuration);
+            }
+
             return NoContent();
         }
 

+ 5 - 1
Jellyfin.Api/Helpers/MediaInfoHelper.cs

@@ -127,7 +127,11 @@ namespace Jellyfin.Api.Helpers
             {
                 // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
                 // Should we move this directly into MediaSourceManager?
-                result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
+                var mediaSourcesClone = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
+                if (mediaSourcesClone != null)
+                {
+                    result.MediaSources = mediaSourcesClone;
+                }
 
                 result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
             }

+ 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)
+        {
+        }
+    }
+}

+ 8 - 0
Jellyfin.Data/Jellyfin.Data.csproj

@@ -7,6 +7,14 @@
     <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>
   </PropertyGroup>
 
+  <PropertyGroup>
+    <Authors>Jellyfin Contributors</Authors>
+    <PackageId>Jellyfin.Data</PackageId>
+    <VersionPrefix>10.7.0</VersionPrefix>
+    <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
+    <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
+  </PropertyGroup>
+
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
     <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>

+ 1 - 11
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
@@ -27,16 +27,6 @@ namespace Jellyfin.Server.Implementations.Activity
         /// <inheritdoc/>
         public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
 
-        /// <inheritdoc/>
-        public void Create(ActivityLog entry)
-        {
-            using var dbContext = _provider.CreateContext();
-            dbContext.ActivityLogs.Add(entry);
-            dbContext.SaveChanges();
-
-            EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
-        }
-
         /// <inheritdoc/>
         public async Task CreateAsync(ActivityLog entry)
         {

+ 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
 {

+ 19 - 24
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/>
@@ -407,13 +402,13 @@ namespace Jellyfin.Server.Implementations.Users
                     EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
                     AccessSchedules = user.AccessSchedules.ToArray(),
                     BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
-                    EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels),
+                    EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels)?.Select(Guid.Parse).ToArray(),
                     EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
-                    EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders),
+                    EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders)?.Select(Guid.Parse).ToArray(),
                     EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders),
                     SyncPlayAccess = user.SyncPlayAccess,
-                    BlockedChannels = user.GetPreference(PreferenceKind.BlockedChannels),
-                    BlockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders),
+                    BlockedChannels = user.GetPreference(PreferenceKind.BlockedChannels)?.Select(Guid.Parse).ToArray(),
+                    BlockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders)?.Select(Guid.Parse).ToArray(),
                     BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems).Select(Enum.Parse<UnratedItem>).ToArray()
                 }
             };
@@ -740,9 +735,9 @@ namespace Jellyfin.Server.Implementations.Users
                 PreferenceKind.BlockUnratedItems,
                 policy.BlockUnratedItems?.Select(i => i.ToString()).ToArray() ?? Array.Empty<string>());
             user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
-            user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
+            user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
             user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
-            user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
+            user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
             user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
 
             dbContext.Update(user);
@@ -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 />

+ 2 - 1
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -168,7 +168,8 @@ namespace Jellyfin.Server.Extensions
                     // From JsonDefaults
                     options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling;
                     options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
-                    options.JsonSerializerOptions.IgnoreNullValues = jsonOptions.IgnoreNullValues;
+                    options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
+                    options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
 
                     options.JsonSerializerOptions.Converters.Clear();
                     foreach (var converter in jsonOptions.Converters)

+ 5 - 0
Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs

@@ -81,6 +81,11 @@ namespace Jellyfin.Server.Migrations.Routines
                 foreach (var result in results)
                 {
                     var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result[3].ToString(), _jsonOptions);
+                    if (dto == null)
+                    {
+                        continue;
+                    }
+
                     var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version)
                         ? chromecastDict[version]
                         : ChromecastVersion.Stable;

+ 10 - 3
Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs

@@ -1,5 +1,7 @@
 using System;
+using System.Globalization;
 using System.IO;
+using System.Linq;
 using Emby.Server.Implementations.Data;
 using Emby.Server.Implementations.Serialization;
 using Jellyfin.Data.Entities;
@@ -74,7 +76,12 @@ namespace Jellyfin.Server.Migrations.Routines
 
                 foreach (var entry in queryResult)
                 {
-                    UserMockup mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.GetOptions());
+                    UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.GetOptions());
+                    if (mockup == null)
+                    {
+                        continue;
+                    }
+
                     var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name);
 
                     var config = File.Exists(Path.Combine(userDataDir, "config.xml"))
@@ -161,9 +168,9 @@ namespace Jellyfin.Server.Migrations.Routines
                     }
 
                     user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
-                    user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
+                    user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
                     user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
-                    user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
+                    user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
                     user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
                     user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
                     user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);

+ 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.

+ 0 - 56
MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs

@@ -1,56 +0,0 @@
-using System;
-using System.Buffers;
-using System.Buffers.Text;
-using System.Globalization;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace MediaBrowser.Common.Json.Converters
-{
-    /// <summary>
-    /// Double to String JSON converter.
-    /// Web client send quoted doubles.
-    /// </summary>
-    public class JsonDoubleConverter : JsonConverter<double>
-    {
-        /// <summary>
-        /// Read JSON string as double.
-        /// </summary>
-        /// <param name="reader"><see cref="Utf8JsonReader"/>.</param>
-        /// <param name="typeToConvert">Type.</param>
-        /// <param name="options">Options.</param>
-        /// <returns>Parsed value.</returns>
-        public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-        {
-            if (reader.TokenType == JsonTokenType.String)
-            {
-                // try to parse number directly from bytes
-                var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
-                if (Utf8Parser.TryParse(span, out double number, out var bytesConsumed) && span.Length == bytesConsumed)
-                {
-                    return number;
-                }
-
-                // try to parse from a string if the above failed, this covers cases with other escaped/UTF characters
-                if (double.TryParse(reader.GetString(), out number))
-                {
-                    return number;
-                }
-            }
-
-            // fallback to default handling
-            return reader.GetDouble();
-        }
-
-        /// <summary>
-        /// Write double to JSON string.
-        /// </summary>
-        /// <param name="writer"><see cref="Utf8JsonWriter"/>.</param>
-        /// <param name="value">Value to write.</param>
-        /// <param name="options">Options.</param>
-        public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options)
-        {
-            writer.WriteNumberValue(value);
-        }
-    }
-}

+ 0 - 40
MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs

@@ -1,40 +0,0 @@
-using System;
-using System.Buffers;
-using System.Buffers.Text;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace MediaBrowser.Common.Json.Converters
-{
-    /// <summary>
-    /// Converts a int32 object or value to/from JSON.
-    /// </summary>
-    public class JsonInt32Converter : JsonConverter<int>
-    {
-        /// <inheritdoc />
-        public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-        {
-            if (reader.TokenType == JsonTokenType.String)
-            {
-                ReadOnlySpan<byte> span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
-                if (Utf8Parser.TryParse(span, out int number, out int bytesConsumed) && span.Length == bytesConsumed)
-                {
-                    return number;
-                }
-
-                if (int.TryParse(reader.GetString(), out number))
-                {
-                    return number;
-                }
-            }
-
-            return reader.GetInt32();
-        }
-
-        /// <inheritdoc />
-        public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
-        {
-            writer.WriteNumberValue(value);
-        }
-    }
-}

+ 0 - 56
MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs

@@ -1,56 +0,0 @@
-using System;
-using System.Buffers;
-using System.Buffers.Text;
-using System.Globalization;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace MediaBrowser.Common.Json.Converters
-{
-    /// <summary>
-    /// Parse JSON string as long.
-    /// Javascript does not support 64-bit integers.
-    /// </summary>
-    public class JsonInt64Converter : JsonConverter<long>
-    {
-        /// <summary>
-        /// Read JSON string as int64.
-        /// </summary>
-        /// <param name="reader"><see cref="Utf8JsonReader"/>.</param>
-        /// <param name="type">Type.</param>
-        /// <param name="options">Options.</param>
-        /// <returns>Parsed value.</returns>
-        public override long Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
-        {
-            if (reader.TokenType == JsonTokenType.String)
-            {
-                // try to parse number directly from bytes
-                var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
-                if (Utf8Parser.TryParse(span, out long number, out var bytesConsumed) && span.Length == bytesConsumed)
-                {
-                    return number;
-                }
-
-                // try to parse from a string if the above failed, this covers cases with other escaped/UTF characters
-                if (long.TryParse(reader.GetString(), out number))
-                {
-                    return number;
-                }
-            }
-
-            // fallback to default handling
-            return reader.GetInt64();
-        }
-
-        /// <summary>
-        /// Write long to JSON long.
-        /// </summary>
-        /// <param name="writer"><see cref="Utf8JsonWriter"/>.</param>
-        /// <param name="value">Value to write.</param>
-        /// <param name="options">Options.</param>
-        public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
-        {
-            writer.WriteNumberValue(value);
-        }
-    }
-}

+ 0 - 82
MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs

@@ -1,82 +0,0 @@
-#nullable enable
-
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Reflection;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace MediaBrowser.Common.Json.Converters
-{
-    /// <summary>
-    /// Converter for Dictionaries without string key.
-    /// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys.
-    /// </summary>
-    /// <typeparam name="TKey">Type of key.</typeparam>
-    /// <typeparam name="TValue">Type of value.</typeparam>
-    internal sealed class JsonNonStringKeyDictionaryConverter<TKey, TValue> : JsonConverter<IDictionary<TKey, TValue>>
-    {
-        /// <summary>
-        /// Read JSON.
-        /// </summary>
-        /// <param name="reader">The Utf8JsonReader.</param>
-        /// <param name="typeToConvert">The type to convert.</param>
-        /// <param name="options">The json serializer options.</param>
-        /// <returns>Typed dictionary.</returns>
-        /// <exception cref="NotSupportedException">Dictionary key type not supported.</exception>
-        public override IDictionary<TKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-        {
-            var convertedType = typeof(Dictionary<,>).MakeGenericType(typeof(string), typeToConvert.GenericTypeArguments[1]);
-            var value = JsonSerializer.Deserialize(ref reader, convertedType, options);
-            var instance = (Dictionary<TKey, TValue>)Activator.CreateInstance(
-                typeToConvert,
-                BindingFlags.Instance | BindingFlags.Public,
-                null,
-                null,
-                CultureInfo.CurrentCulture);
-            var enumerator = (IEnumerator)convertedType.GetMethod("GetEnumerator")!.Invoke(value, null);
-            var parse = typeof(TKey).GetMethod(
-                "Parse",
-                0,
-                BindingFlags.Public | BindingFlags.Static,
-                null,
-                CallingConventions.Any,
-                new[] { typeof(string) },
-                null);
-            if (parse == null)
-            {
-                throw new NotSupportedException($"{typeof(TKey)} as TKey in IDictionary<TKey, TValue> is not supported.");
-            }
-
-            while (enumerator.MoveNext())
-            {
-                var element = (KeyValuePair<string?, TValue>)enumerator.Current;
-                instance.Add((TKey)parse.Invoke(null, new[] { (object?)element.Key }), element.Value);
-            }
-
-            return instance;
-        }
-
-        /// <summary>
-        /// Write dictionary as Json.
-        /// </summary>
-        /// <param name="writer">The Utf8JsonWriter.</param>
-        /// <param name="value">The dictionary value.</param>
-        /// <param name="options">The Json serializer options.</param>
-        public override void Write(Utf8JsonWriter writer, IDictionary<TKey, TValue> value, JsonSerializerOptions options)
-        {
-            var convertedDictionary = new Dictionary<string?, TValue>(value.Count);
-            foreach (var (k, v) in value)
-            {
-                if (k != null)
-                {
-                    convertedDictionary[k.ToString()] = v;
-                }
-            }
-
-            JsonSerializer.Serialize(writer, convertedDictionary, options);
-        }
-    }
-}

+ 0 - 59
MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs

@@ -1,59 +0,0 @@
-#nullable enable
-
-using System;
-using System.Collections;
-using System.Globalization;
-using System.Reflection;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace MediaBrowser.Common.Json.Converters
-{
-    /// <summary>
-    /// https://github.com/dotnet/runtime/issues/30524#issuecomment-524619972.
-    /// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys.
-    /// </summary>
-    internal sealed class JsonNonStringKeyDictionaryConverterFactory : JsonConverterFactory
-    {
-        /// <summary>
-        /// Only convert objects that implement IDictionary and do not have string keys.
-        /// </summary>
-        /// <param name="typeToConvert">Type convert.</param>
-        /// <returns>Conversion ability.</returns>
-        public override bool CanConvert(Type typeToConvert)
-        {
-            if (!typeToConvert.IsGenericType)
-            {
-                return false;
-            }
-
-            // Let built in converter handle string keys
-            if (typeToConvert.GenericTypeArguments[0] == typeof(string))
-            {
-                return false;
-            }
-
-            // Only support objects that implement IDictionary
-            return typeToConvert.GetInterface(nameof(IDictionary)) != null;
-        }
-
-        /// <summary>
-        /// Create converter for generic dictionary type.
-        /// </summary>
-        /// <param name="typeToConvert">Type to convert.</param>
-        /// <param name="options">Json serializer options.</param>
-        /// <returns>JsonConverter for given type.</returns>
-        public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
-        {
-            var converterType = typeof(JsonNonStringKeyDictionaryConverter<,>)
-                .MakeGenericType(typeToConvert.GenericTypeArguments[0], typeToConvert.GenericTypeArguments[1]);
-            var converter = (JsonConverter)Activator.CreateInstance(
-                converterType,
-                BindingFlags.Instance | BindingFlags.Public,
-                null,
-                null,
-                CultureInfo.CurrentCulture);
-            return converter;
-        }
-    }
-}

+ 0 - 55
MediaBrowser.Common/Json/Converters/JsonNullableInt32Converter.cs

@@ -1,55 +0,0 @@
-using System;
-using System.Buffers;
-using System.Buffers.Text;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace MediaBrowser.Common.Json.Converters
-{
-    /// <summary>
-    /// Converts a nullable int32 object or value to/from JSON.
-    /// </summary>
-    public class JsonNullableInt32Converter : JsonConverter<int?>
-    {
-        /// <inheritdoc />
-        public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-        {
-            if (reader.TokenType == JsonTokenType.String)
-            {
-                ReadOnlySpan<byte> span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
-                if (Utf8Parser.TryParse(span, out int number, out int bytesConsumed) && span.Length == bytesConsumed)
-                {
-                    return number;
-                }
-
-                var stringValue = reader.GetString().AsSpan();
-
-                // value is null or empty, just return null.
-                if (stringValue.IsEmpty)
-                {
-                    return null;
-                }
-
-                if (int.TryParse(stringValue, out number))
-                {
-                    return number;
-                }
-            }
-
-            return reader.GetInt32();
-        }
-
-        /// <inheritdoc />
-        public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
-        {
-            if (value is null)
-            {
-                writer.WriteNullValue();
-            }
-            else
-            {
-                writer.WriteNumberValue(value.Value);
-            }
-        }
-    }
-}

+ 0 - 70
MediaBrowser.Common/Json/Converters/JsonNullableInt64Converter.cs

@@ -1,70 +0,0 @@
-using System;
-using System.Buffers;
-using System.Buffers.Text;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace MediaBrowser.Common.Json.Converters
-{
-    /// <summary>
-    /// Parse JSON string as nullable long.
-    /// Javascript does not support 64-bit integers.
-    /// </summary>
-    public class JsonNullableInt64Converter : JsonConverter<long?>
-    {
-        /// <summary>
-        /// Read JSON string as int64.
-        /// </summary>
-        /// <param name="reader"><see cref="Utf8JsonReader"/>.</param>
-        /// <param name="type">Type.</param>
-        /// <param name="options">Options.</param>
-        /// <returns>Parsed value.</returns>
-        public override long? Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
-        {
-            if (reader.TokenType == JsonTokenType.String)
-            {
-                // try to parse number directly from bytes
-                var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
-                if (Utf8Parser.TryParse(span, out long number, out var bytesConsumed) && span.Length == bytesConsumed)
-                {
-                    return number;
-                }
-
-                var stringValue = reader.GetString().AsSpan();
-
-                // value is null or empty, just return null.
-                if (stringValue.IsEmpty)
-                {
-                    return null;
-                }
-
-                // try to parse from a string if the above failed, this covers cases with other escaped/UTF characters
-                if (long.TryParse(stringValue, out number))
-                {
-                    return number;
-                }
-            }
-
-            // fallback to default handling
-            return reader.GetInt64();
-        }
-
-        /// <summary>
-        /// Write long to JSON long.
-        /// </summary>
-        /// <param name="writer"><see cref="Utf8JsonWriter"/>.</param>
-        /// <param name="value">Value to write.</param>
-        /// <param name="options">Options.</param>
-        public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
-        {
-            if (value is null)
-            {
-                writer.WriteNullValue();
-            }
-            else
-            {
-                writer.WriteNumberValue(value.Value);
-            }
-        }
-    }
-}

+ 2 - 7
MediaBrowser.Common/Json/JsonDefaults.cs

@@ -25,17 +25,12 @@ namespace MediaBrowser.Common.Json
             {
                 ReadCommentHandling = JsonCommentHandling.Disallow,
                 WriteIndented = false,
-                IgnoreNullValues = true
+                DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+                NumberHandling = JsonNumberHandling.AllowReadingFromString
             };
 
             options.Converters.Add(new JsonGuidConverter());
-            options.Converters.Add(new JsonInt32Converter());
-            options.Converters.Add(new JsonNullableInt32Converter());
             options.Converters.Add(new JsonStringEnumConverter());
-            options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory());
-            options.Converters.Add(new JsonInt64Converter());
-            options.Converters.Add(new JsonNullableInt64Converter());
-            options.Converters.Add(new JsonDoubleConverter());
 
             return options;
         }

+ 2 - 1
MediaBrowser.Common/MediaBrowser.Common.csproj

@@ -8,8 +8,9 @@
   <PropertyGroup>
     <Authors>Jellyfin Contributors</Authors>
     <PackageId>Jellyfin.Common</PackageId>
-    <PackageLicenseUrl>https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</PackageLicenseUrl>
+    <VersionPrefix>10.7.0</VersionPrefix>
     <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
+    <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
   </PropertyGroup>
 
   <ItemGroup>

+ 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)
+        {
+        }
+    }
+}

Some files were not shown because too many files changed in this diff