Преглед изворни кода

Merge branch 'master' into userdb-efcore

# Conflicts:
#	Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
#	Emby.Server.Implementations/ApplicationHost.cs
#	Emby.Server.Implementations/Devices/DeviceManager.cs
#	Jellyfin.Server/Jellyfin.Server.csproj
#	Jellyfin.Server/Migrations/MigrationRunner.cs
#	MediaBrowser.Controller/Devices/IDeviceManager.cs
Patrick Barron пре 5 година
родитељ
комит
aca7e221d8
87 измењених фајлова са 986 додато и 1992 уклоњено
  1. 4 1
      Emby.Dlna/PlayTo/PlayToController.cs
  2. 1 1
      Emby.Naming/Video/VideoListResolver.cs
  3. 28 82
      Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
  4. 35 84
      Emby.Server.Implementations/ApplicationHost.cs
  5. 4 4
      Emby.Server.Implementations/Browser/BrowserLauncher.cs
  6. 0 6
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  7. 2 288
      Emby.Server.Implementations/Devices/DeviceManager.cs
  8. 1 2
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  9. 2 2
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  10. 109 113
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  11. 0 39
      Emby.Server.Implementations/HttpServer/IHttpListener.cs
  12. 15 4
      Emby.Server.Implementations/HttpServer/ResponseFilter.cs
  13. 124 155
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  14. 2 2
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
  15. 3 3
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  16. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  17. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
  18. 2 2
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  19. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  20. 3 1
      Emby.Server.Implementations/Localization/Core/bn.json
  21. 9 1
      Emby.Server.Implementations/Localization/Core/he.json
  22. 5 1
      Emby.Server.Implementations/Localization/Core/nb.json
  23. 0 39
      Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs
  24. 0 48
      Emby.Server.Implementations/Net/IWebSocket.cs
  25. 0 29
      Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs
  26. 0 191
      Emby.Server.Implementations/Session/HttpSessionController.cs
  27. 7 8
      Emby.Server.Implementations/Session/SessionManager.cs
  28. 19 19
      Emby.Server.Implementations/Session/SessionWebSocketListener.cs
  29. 51 35
      Emby.Server.Implementations/Session/WebSocketController.cs
  30. 0 105
      Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs
  31. 0 135
      Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
  32. 0 10
      Emby.Server.Implementations/WebSockets/WebSocketHandler.cs
  33. 0 102
      Emby.Server.Implementations/WebSockets/WebSocketManager.cs
  34. 60 59
      Jellyfin.Data/Entities/ActivityLog.cs
  35. 1 5
      Jellyfin.Data/Jellyfin.Data.csproj
  36. 8 9
      Jellyfin.Server.Implementations/Activity/ActivityManager.cs
  37. 8 0
      Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
  38. 3 2
      Jellyfin.Server.Implementations/JellyfinDb.cs
  39. 1 1
      Jellyfin.Server.Implementations/JellyfinDbProvider.cs
  40. 4 5
      Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs
  41. 5 5
      Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs
  42. 0 3
      Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs
  43. 1 3
      Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
  44. 14 0
      Jellyfin.Server/CoreAppHost.cs
  45. 1 7
      Jellyfin.Server/Jellyfin.Server.csproj
  46. 1 0
      Jellyfin.Server/Migrations/MigrationRunner.cs
  47. 50 37
      Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
  48. 79 0
      Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
  49. 2 5
      Jellyfin.Server/Program.cs
  50. 0 1
      Jellyfin.Server/Startup.cs
  51. 2 2
      MediaBrowser.Api/BaseApiService.cs
  52. 0 36
      MediaBrowser.Api/Devices/DeviceService.cs
  53. 6 4
      MediaBrowser.Api/Library/LibraryService.cs
  54. 1 1
      MediaBrowser.Api/Movies/MoviesService.cs
  55. 9 3
      MediaBrowser.Api/Movies/TrailersService.cs
  56. 1 1
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  57. 1 1
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  58. 1 1
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  59. 1 1
      MediaBrowser.Api/Playback/MediaInfoService.cs
  60. 1 1
      MediaBrowser.Api/Playback/Progressive/AudioService.cs
  61. 1 1
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  62. 6 3
      MediaBrowser.Api/Playback/UniversalAudioService.cs
  63. 22 21
      MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs
  64. 6 1
      MediaBrowser.Api/System/ActivityLogService.cs
  65. 8 8
      MediaBrowser.Api/System/ActivityLogWebSocketListener.cs
  66. 1 1
      MediaBrowser.Api/UserLibrary/ArtistsService.cs
  67. 1 1
      MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
  68. 1 1
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  69. 0 23
      MediaBrowser.Controller/Devices/IDeviceManager.cs
  70. 35 22
      MediaBrowser.Controller/IServerApplicationHost.cs
  71. 5 7
      MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
  72. 8 19
      MediaBrowser.Controller/Net/IHttpServer.cs
  73. 9 32
      MediaBrowser.Controller/Net/IWebSocketConnection.cs
  74. 2 1
      MediaBrowser.Controller/Session/ISessionController.cs
  75. 24 50
      MediaBrowser.Controller/Session/SessionInfo.cs
  76. 13 2
      MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
  77. 1 2
      MediaBrowser.Model/Activity/IActivityManager.cs
  78. 26 6
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  79. 9 0
      MediaBrowser.Model/Devices/DeviceOptions.cs
  80. 0 23
      MediaBrowser.Model/Devices/DevicesOptions.cs
  81. 6 2
      MediaBrowser.Model/Net/WebSocketMessage.cs
  82. 0 18
      MediaBrowser.Model/System/SystemInfo.cs
  83. 14 10
      MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
  84. 9 6
      MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
  85. 53 21
      MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs
  86. 34 1
      MediaBrowser.Providers/Tmdb/TmdbUtils.cs
  87. 2 2
      MediaBrowser.sln

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

@@ -914,7 +914,8 @@ namespace Emby.Dlna.PlayTo
             return 0;
         }
 
-        public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
+        /// <inheritdoc />
+        public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
         {
             if (_disposed)
             {
@@ -930,10 +931,12 @@ namespace Emby.Dlna.PlayTo
             {
                 return SendPlayCommand(data as PlayRequest, cancellationToken);
             }
+
             if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
             {
                 return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
             }
+
             if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
             {
                 return SendGeneralCommand(data as GeneralCommand, cancellationToken);

+ 1 - 1
Emby.Naming/Video/VideoListResolver.cs

@@ -227,7 +227,7 @@ namespace Emby.Naming.Video
             }
 
             return remainingFiles
-                .Where(i => i.ExtraType == null)
+                .Where(i => i.ExtraType != null)
                 .Where(i => baseNames.Any(b =>
                     i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
                 .ToList();

+ 28 - 82
Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs

@@ -8,7 +8,6 @@ using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
@@ -30,7 +29,7 @@ namespace Emby.Server.Implementations.Activity
     /// </summary>
     public sealed class ActivityLogEntryPoint : IServerEntryPoint
     {
-        private readonly ILogger _logger;
+        private readonly ILogger<ActivityLogEntryPoint> _logger;
         private readonly IInstallationManager _installationManager;
         private readonly ISessionManager _sessionManager;
         private readonly ITaskManager _taskManager;
@@ -38,14 +37,12 @@ namespace Emby.Server.Implementations.Activity
         private readonly ILocalizationManager _localization;
         private readonly ISubtitleManager _subManager;
         private readonly IUserManager _userManager;
-        private readonly IDeviceManager _deviceManager;
 
         /// <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="deviceManager">The device manager.</param>
         /// <param name="taskManager">The task manager.</param>
         /// <param name="activityManager">The activity manager.</param>
         /// <param name="localization">The localization manager.</param>
@@ -55,7 +52,6 @@ namespace Emby.Server.Implementations.Activity
         public ActivityLogEntryPoint(
             ILogger<ActivityLogEntryPoint> logger,
             ISessionManager sessionManager,
-            IDeviceManager deviceManager,
             ITaskManager taskManager,
             IActivityManager activityManager,
             ILocalizationManager localization,
@@ -65,7 +61,6 @@ namespace Emby.Server.Implementations.Activity
         {
             _logger = logger;
             _sessionManager = sessionManager;
-            _deviceManager = deviceManager;
             _taskManager = taskManager;
             _activityManager = activityManager;
             _localization = localization;
@@ -98,36 +93,18 @@ namespace Emby.Server.Implementations.Activity
             _userManager.OnUserDeleted += OnUserDeleted;
             _userManager.OnUserLockedOut += OnUserLockedOut;
 
-            _deviceManager.CameraImageUploaded += OnCameraImageUploaded;
-
             return Task.CompletedTask;
         }
 
-        private async void OnCameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("CameraImageUploadedFrom"),
-                    e.Argument.Device.Name),
-                NotificationType.CameraImageUploaded.ToString(),
-                Guid.Empty,
-                DateTime.UtcNow,
-                LogLevel.Trace))
-                .ConfigureAwait(false);
-        }
-
         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,
-                DateTime.UtcNow,
-                LogLevel.Trace))
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        _localization.GetLocalizedString("UserLockedOutWithName"),
+                        e.Argument.Username),
+                    NotificationType.UserLockedOut.ToString(),
+                    e.Argument.Id))
                 .ConfigureAwait(false);
         }
 
@@ -138,11 +115,9 @@ namespace Emby.Server.Implementations.Activity
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
                     e.Provider,
-                    Emby.Notifications.NotificationEntryPoint.GetItemName(e.Item)),
+                    Notifications.NotificationEntryPoint.GetItemName(e.Item)),
                 "SubtitleDownloadFailure",
-                Guid.Empty,
-                DateTime.UtcNow,
-                LogLevel.Trace)
+                Guid.Empty)
             {
                 ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
                 ShortOverview = e.Exception.Message
@@ -180,9 +155,7 @@ namespace Emby.Server.Implementations.Activity
                     GetItemName(item),
                     e.DeviceName),
                 GetPlaybackStoppedNotificationType(item.MediaType),
-                user.Id,
-                DateTime.UtcNow,
-                LogLevel.Trace))
+                user.Id))
                 .ConfigureAwait(false);
         }
 
@@ -217,9 +190,7 @@ namespace Emby.Server.Implementations.Activity
                     GetItemName(item),
                     e.DeviceName),
                 GetPlaybackNotificationType(item.MediaType),
-                user.Id,
-                DateTime.UtcNow,
-                LogLevel.Trace))
+                user.Id))
                 .ConfigureAwait(false);
         }
 
@@ -286,9 +257,7 @@ namespace Emby.Server.Implementations.Activity
                     session.UserName,
                     session.DeviceName),
                 "SessionEnded",
-                session.UserId,
-                DateTime.UtcNow,
-                LogLevel.Trace)
+                session.UserId)
             {
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
@@ -307,9 +276,7 @@ namespace Emby.Server.Implementations.Activity
                     _localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
                     user.Name),
                 "AuthenticationSucceeded",
-                user.Id,
-                DateTime.UtcNow,
-                LogLevel.Trace)
+                user.Id)
             {
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
@@ -326,10 +293,9 @@ namespace Emby.Server.Implementations.Activity
                     _localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
                     e.Argument.Username),
                 "AuthenticationFailed",
-                Guid.Empty,
-                DateTime.UtcNow,
-                LogLevel.Error)
+                Guid.Empty)
             {
+                LogSeverity = LogLevel.Error,
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("LabelIpAddressValue"),
@@ -345,9 +311,7 @@ namespace Emby.Server.Implementations.Activity
                     _localization.GetLocalizedString("UserPolicyUpdatedWithName"),
                     e.Argument.Username),
                 "UserPolicyUpdated",
-                e.Argument.Id,
-                DateTime.UtcNow,
-                LogLevel.Trace))
+                e.Argument.Id))
                 .ConfigureAwait(false);
         }
 
@@ -359,9 +323,7 @@ namespace Emby.Server.Implementations.Activity
                     _localization.GetLocalizedString("UserDeletedWithName"),
                     e.Argument.Username),
                 "UserDeleted",
-                Guid.Empty,
-                DateTime.UtcNow,
-                LogLevel.Trace))
+                Guid.Empty))
                 .ConfigureAwait(false);
         }
 
@@ -373,9 +335,8 @@ namespace Emby.Server.Implementations.Activity
                     _localization.GetLocalizedString("UserPasswordChangedWithName"),
                     e.Argument.Username),
                 "UserPasswordChanged",
-                e.Argument.Id,
-                DateTime.UtcNow,
-                LogLevel.Trace)).ConfigureAwait(false);
+                e.Argument.Id))
+                .ConfigureAwait(false);
         }
 
         private async void OnUserCreated(object sender, GenericEventArgs<User> e)
@@ -386,9 +347,7 @@ namespace Emby.Server.Implementations.Activity
                     _localization.GetLocalizedString("UserCreatedWithName"),
                     e.Argument.Username),
                 "UserCreated",
-                e.Argument.Id,
-                DateTime.UtcNow,
-                LogLevel.Trace))
+                e.Argument.Id))
                 .ConfigureAwait(false);
         }
 
@@ -408,9 +367,7 @@ namespace Emby.Server.Implementations.Activity
                     session.UserName,
                     session.DeviceName),
                 "SessionStarted",
-                session.UserId,
-                DateTime.UtcNow,
-                LogLevel.Trace)
+                session.UserId)
             {
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
@@ -427,9 +384,7 @@ namespace Emby.Server.Implementations.Activity
                     _localization.GetLocalizedString("PluginUpdatedWithName"),
                     e.Argument.Item1.Name),
                 NotificationType.PluginUpdateInstalled.ToString(),
-                Guid.Empty,
-                DateTime.UtcNow,
-                LogLevel.Trace)
+                Guid.Empty)
             {
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
@@ -447,9 +402,7 @@ namespace Emby.Server.Implementations.Activity
                     _localization.GetLocalizedString("PluginUninstalledWithName"),
                     e.Argument.Name),
                 NotificationType.PluginUninstalled.ToString(),
-                Guid.Empty,
-                DateTime.UtcNow,
-                LogLevel.Trace))
+                Guid.Empty))
                 .ConfigureAwait(false);
         }
 
@@ -461,9 +414,7 @@ namespace Emby.Server.Implementations.Activity
                     _localization.GetLocalizedString("PluginInstalledWithName"),
                     e.Argument.name),
                 NotificationType.PluginInstalled.ToString(),
-                Guid.Empty,
-                DateTime.UtcNow,
-                LogLevel.Trace)
+                Guid.Empty)
             {
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
@@ -482,9 +433,7 @@ namespace Emby.Server.Implementations.Activity
                     _localization.GetLocalizedString("NameInstallFailed"),
                     installationInfo.Name),
                 NotificationType.InstallationFailed.ToString(),
-                Guid.Empty,
-                DateTime.UtcNow,
-                LogLevel.Trace)
+                Guid.Empty)
             {
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
@@ -528,10 +477,9 @@ namespace Emby.Server.Implementations.Activity
                 await CreateLogEntry(new ActivityLog(
                     string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
                     NotificationType.TaskFailed.ToString(),
-                    Guid.Empty,
-                    DateTime.UtcNow,
-                    LogLevel.Error)
+                    Guid.Empty)
                 {
+                    LogSeverity = LogLevel.Error,
                     Overview = string.Join(Environment.NewLine, vals),
                     ShortOverview = runningTime
                 }).ConfigureAwait(false);
@@ -565,8 +513,6 @@ namespace Emby.Server.Implementations.Activity
             _userManager.OnUserPasswordChanged -= OnUserPasswordChanged;
             _userManager.OnUserDeleted -= OnUserDeleted;
             _userManager.OnUserLockedOut -= OnUserLockedOut;
-
-            _deviceManager.CameraImageUploaded -= OnCameraImageUploaded;
         }
 
         /// <summary>
@@ -586,7 +532,7 @@ namespace Emby.Server.Implementations.Activity
             {
                 int years = days / DaysInYear;
                 values.Add(CreateValueString(years, "year"));
-                days = days % DaysInYear;
+                days %= DaysInYear;
             }
 
             // Number of months

+ 35 - 84
Emby.Server.Implementations/ApplicationHost.cs

@@ -43,12 +43,8 @@ using Emby.Server.Implementations.Security;
 using Emby.Server.Implementations.Serialization;
 using Emby.Server.Implementations.Services;
 using Emby.Server.Implementations.Session;
-using Emby.Server.Implementations.SocketSharp;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.Updates;
-using Jellyfin.Server.Implementations;
-using Jellyfin.Server.Implementations.Activity;
-using Jellyfin.Server.Implementations.User;
 using MediaBrowser.Api;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
@@ -84,7 +80,6 @@ using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Controller.TV;
 using MediaBrowser.LocalMetadata.Savers;
 using MediaBrowser.MediaEncoding.BdInfo;
-using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Cryptography;
 using MediaBrowser.Model.Dlna;
@@ -103,12 +98,10 @@ using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.WebDashboard.Api;
 using MediaBrowser.XbmcMetadata.Providers;
 using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
-using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
 using Prometheus.DotNetRuntime;
+using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
 
 namespace Emby.Server.Implementations
 {
@@ -505,32 +498,8 @@ namespace Emby.Server.Implementations
             RegisterServices(serviceCollection);
         }
 
-        public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next)
-        {
-            if (!context.WebSockets.IsWebSocketRequest)
-            {
-                await next().ConfigureAwait(false);
-                return;
-            }
-
-            await _httpServer.ProcessWebSocketRequest(context).ConfigureAwait(false);
-        }
-
-        public async Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
-        {
-            if (context.WebSockets.IsWebSocketRequest)
-            {
-                await next().ConfigureAwait(false);
-                return;
-            }
-
-            var request = context.Request;
-            var response = context.Response;
-            var localPath = context.Request.Path.ToString();
-
-            var req = new WebSocketSharpRequest(request, response, request.Path, LoggerFactory.CreateLogger<WebSocketSharpRequest>());
-            await _httpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false);
-        }
+        public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
+            => _httpServer.RequestHandler(context);
 
         /// <summary>
         /// Registers services/resources with the service collection that will be available via DI.
@@ -548,20 +517,6 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
 
-            // TODO: Remove support for injecting ILogger completely
-            serviceCollection.AddSingleton((provider) =>
-            {
-                Logger.LogWarning("Injecting ILogger directly is deprecated and should be replaced with ILogger<T>");
-                return Logger;
-            });
-
-            // TODO: properly set up scoping and switch to AddDbContextPool
-            serviceCollection.AddDbContext<JellyfinDb>(
-                options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
-                ServiceLifetime.Transient);
-
-            serviceCollection.AddSingleton<JellyfinDbProvider>();
-
             serviceCollection.AddSingleton(_fileSystemManager);
             serviceCollection.AddSingleton<TvdbClientManager>();
 
@@ -599,6 +554,8 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
             serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
 
+            serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
+
             serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
 
             serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
@@ -626,7 +583,6 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
 
             serviceCollection.AddSingleton<ServiceController>();
-            serviceCollection.AddSingleton<IHttpListener, WebSocketSharpListener>();
             serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
 
             serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
@@ -668,8 +624,6 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
 
-            serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
-
             serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
             serviceCollection.AddSingleton<ISessionContext, SessionContext>();
 
@@ -697,6 +651,7 @@ namespace Emby.Server.Implementations
             _httpServer = Resolve<IHttpServer>();
             _httpClient = Resolve<IHttpClient>();
 
+            ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
 
             SetStaticProperties();
@@ -1147,9 +1102,6 @@ namespace Emby.Server.Implementations
                 ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
                 InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
                 CachePath = ApplicationPaths.CachePath,
-                HttpServerPortNumber = HttpPort,
-                SupportsHttps = SupportsHttps,
-                HttpsPortNumber = HttpsPort,
                 OperatingSystem = OperatingSystem.Id.ToString(),
                 OperatingSystemDisplayName = OperatingSystem.Name,
                 CanSelfRestart = CanSelfRestart,
@@ -1185,23 +1137,22 @@ namespace Emby.Server.Implementations
             };
         }
 
-        public bool EnableHttps => SupportsHttps && ServerConfigurationManager.Configuration.EnableHttps;
-
-        public bool SupportsHttps => Certificate != null || ServerConfigurationManager.Configuration.IsBehindProxy;
+        /// <inheritdoc/>
+        public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps;
 
-        public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken, bool forceHttp = false)
+        /// <inheritdoc/>
+        public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken)
         {
             try
             {
                 // Return the first matched address, if found, or the first known local address
                 var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false);
-
-                foreach (var address in addresses)
+                if (addresses.Count == 0)
                 {
-                    return GetLocalApiUrl(address, forceHttp);
+                    return null;
                 }
 
-                return null;
+                return GetLocalApiUrl(addresses.First());
             }
             catch (Exception ex)
             {
@@ -1228,7 +1179,7 @@ namespace Emby.Server.Implementations
         }
 
         /// <inheritdoc />
-        public string GetLocalApiUrl(IPAddress ipAddress, bool forceHttp = false)
+        public string GetLocalApiUrl(IPAddress ipAddress)
         {
             if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
             {
@@ -1238,29 +1189,30 @@ namespace Emby.Server.Implementations
                 str.CopyTo(span.Slice(1));
                 span[^1] = ']';
 
-                return GetLocalApiUrl(span, forceHttp);
+                return GetLocalApiUrl(span);
             }
 
-            return GetLocalApiUrl(ipAddress.ToString(), forceHttp);
+            return GetLocalApiUrl(ipAddress.ToString());
         }
 
-        /// <inheritdoc />
-        public string GetLocalApiUrl(ReadOnlySpan<char> host, bool forceHttp = false)
+        /// <inheritdoc/>
+        public string GetLoopbackHttpApiUrl()
         {
-            var url = new StringBuilder(64);
-            bool useHttps = EnableHttps && !forceHttp;
-            url.Append(useHttps ? "https://" : "http://")
-                .Append(host)
-                .Append(':')
-                .Append(useHttps ? HttpsPort : HttpPort);
-
-            string baseUrl = ServerConfigurationManager.Configuration.BaseUrl;
-            if (baseUrl.Length != 0)
-            {
-                url.Append(baseUrl);
-            }
+            return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
+        }
 
-            return url.ToString();
+        /// <inheritdoc/>
+        public string GetLocalApiUrl(ReadOnlySpan<char> host, string scheme = null, int? port = null)
+        {
+            // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
+            // not. For consistency, always trim the trailing slash.
+            return new UriBuilder
+            {
+                Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
+                Host = host.ToString(),
+                Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
+                Path = ServerConfigurationManager.Configuration.BaseUrl
+            }.ToString().TrimEnd('/');
         }
 
         public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken)
@@ -1294,7 +1246,7 @@ namespace Emby.Server.Implementations
                     }
                 }
 
-                var valid = await IsIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
+                var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
                 if (valid)
                 {
                     resultList.Add(address);
@@ -1328,7 +1280,7 @@ namespace Emby.Server.Implementations
 
         private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
 
-        private async Task<bool> IsIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
+        private async Task<bool> IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
         {
             if (address.Equals(IPAddress.Loopback)
                 || address.Equals(IPAddress.IPv6Loopback))
@@ -1336,8 +1288,7 @@ namespace Emby.Server.Implementations
                 return true;
             }
 
-            var apiUrl = GetLocalApiUrl(address);
-            apiUrl += "/system/ping";
+            var apiUrl = GetLocalApiUrl(address) + "/system/ping";
 
             if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult))
             {

+ 4 - 4
Emby.Server.Implementations/Browser/BrowserLauncher.cs

@@ -31,18 +31,18 @@ namespace Emby.Server.Implementations.Browser
         /// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored.
         /// </summary>
         /// <param name="appHost">The application host.</param>
-        /// <param name="url">The URL.</param>
-        private static void TryOpenUrl(IServerApplicationHost appHost, string url)
+        /// <param name="relativeUrl">The URL to open, relative to the server base URL.</param>
+        private static void TryOpenUrl(IServerApplicationHost appHost, string relativeUrl)
         {
             try
             {
                 string baseUrl = appHost.GetLocalApiUrl("localhost");
-                appHost.LaunchUrl(baseUrl + url);
+                appHost.LaunchUrl(baseUrl + relativeUrl);
             }
             catch (Exception ex)
             {
                 var logger = appHost.Resolve<ILogger>();
-                logger?.LogError(ex, "Failed to open browser window with URL {URL}", url);
+                logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl);
             }
         }
     }

+ 0 - 6
Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs

@@ -193,12 +193,6 @@ namespace Emby.Server.Implementations.Configuration
                 changed = true;
             }
 
-            if (!config.CameraUploadUpgraded)
-            {
-                config.CameraUploadUpgraded = true;
-                changed = true;
-            }
-
             if (!config.CollectionsUpgraded)
             {
                 config.CollectionsUpgraded = true;

+ 2 - 288
Emby.Server.Implementations/Devices/DeviceManager.cs

@@ -7,26 +7,19 @@ using System.IO;
 using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Configuration;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Security;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Users;
-using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Devices
 {
@@ -34,38 +27,23 @@ namespace Emby.Server.Implementations.Devices
     {
         private readonly IJsonSerializer _json;
         private readonly IUserManager _userManager;
-        private readonly IFileSystem _fileSystem;
-        private readonly ILibraryMonitor _libraryMonitor;
         private readonly IServerConfigurationManager _config;
-        private readonly ILibraryManager _libraryManager;
-        private readonly ILocalizationManager _localizationManager;
         private readonly IAuthenticationRepository _authRepo;
         private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
 
         public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
 
-        public event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
-
-        private readonly object _cameraUploadSyncLock = new object();
         private readonly object _capabilitiesSyncLock = new object();
 
         public DeviceManager(
             IAuthenticationRepository authRepo,
             IJsonSerializer json,
-            ILibraryManager libraryManager,
-            ILocalizationManager localizationManager,
             IUserManager userManager,
-            IFileSystem fileSystem,
-            ILibraryMonitor libraryMonitor,
             IServerConfigurationManager config)
         {
             _json = json;
             _userManager = userManager;
-            _fileSystem = fileSystem;
-            _libraryMonitor = libraryMonitor;
             _config = config;
-            _libraryManager = libraryManager;
-            _localizationManager = localizationManager;
             _authRepo = authRepo;
             _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
         }
@@ -195,173 +173,7 @@ namespace Emby.Server.Implementations.Devices
             return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N", CultureInfo.InvariantCulture));
         }
 
-        public ContentUploadHistory GetCameraUploadHistory(string deviceId)
-        {
-            var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json");
-
-            lock (_cameraUploadSyncLock)
-            {
-                try
-                {
-                    return _json.DeserializeFromFile<ContentUploadHistory>(path);
-                }
-                catch (IOException)
-                {
-                    return new ContentUploadHistory
-                    {
-                        DeviceId = deviceId
-                    };
-                }
-            }
-        }
-
-        public async Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file)
-        {
-            var device = GetDevice(deviceId, false);
-            var uploadPathInfo = GetUploadPath(device);
-
-            var path = uploadPathInfo.Item1;
-
-            if (!string.IsNullOrWhiteSpace(file.Album))
-            {
-                path = Path.Combine(path, _fileSystem.GetValidFilename(file.Album));
-            }
-
-            path = Path.Combine(path, file.Name);
-            path = Path.ChangeExtension(path, MimeTypes.ToExtension(file.MimeType) ?? "jpg");
-
-            Directory.CreateDirectory(Path.GetDirectoryName(path));
-
-            await EnsureLibraryFolder(uploadPathInfo.Item2, uploadPathInfo.Item3).ConfigureAwait(false);
-
-            _libraryMonitor.ReportFileSystemChangeBeginning(path);
-
-            try
-            {
-                using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
-                {
-                    await stream.CopyToAsync(fs).ConfigureAwait(false);
-                }
-
-                AddCameraUpload(deviceId, file);
-            }
-            finally
-            {
-                _libraryMonitor.ReportFileSystemChangeComplete(path, true);
-            }
-
-            if (CameraImageUploaded != null)
-            {
-                CameraImageUploaded?.Invoke(this, new GenericEventArgs<CameraImageUploadInfo>
-                {
-                    Argument = new CameraImageUploadInfo
-                    {
-                        Device = device,
-                        FileInfo = file
-                    }
-                });
-            }
-        }
-
-        private void AddCameraUpload(string deviceId, LocalFileInfo file)
-        {
-            var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json");
-            Directory.CreateDirectory(Path.GetDirectoryName(path));
-
-            lock (_cameraUploadSyncLock)
-            {
-                ContentUploadHistory history;
-
-                try
-                {
-                    history = _json.DeserializeFromFile<ContentUploadHistory>(path);
-                }
-                catch (IOException)
-                {
-                    history = new ContentUploadHistory
-                    {
-                        DeviceId = deviceId
-                    };
-                }
-
-                history.DeviceId = deviceId;
-
-                var list = history.FilesUploaded.ToList();
-                list.Add(file);
-                history.FilesUploaded = list.ToArray();
-
-                _json.SerializeToFile(history, path);
-            }
-        }
-
-        internal Task EnsureLibraryFolder(string path, string name)
-        {
-            var existingFolders = _libraryManager
-                .RootFolder
-                .Children
-                .OfType<Folder>()
-                .Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path))
-                .ToList();
-
-            if (existingFolders.Count > 0)
-            {
-                return Task.CompletedTask;
-            }
-
-            Directory.CreateDirectory(path);
-
-            var libraryOptions = new LibraryOptions
-            {
-                PathInfos = new[] { new MediaPathInfo { Path = path } },
-                EnablePhotos = true,
-                EnableRealtimeMonitor = false,
-                SaveLocalMetadata = true
-            };
-
-            if (string.IsNullOrWhiteSpace(name))
-            {
-                name = _localizationManager.GetLocalizedString("HeaderCameraUploads");
-            }
-
-            return _libraryManager.AddVirtualFolder(name, CollectionType.HomeVideos, libraryOptions, true);
-        }
-
-        private Tuple<string, string, string> GetUploadPath(DeviceInfo device)
-        {
-            var config = _config.GetUploadOptions();
-            var path = config.CameraUploadPath;
-
-            if (string.IsNullOrWhiteSpace(path))
-            {
-                path = DefaultCameraUploadsPath;
-            }
-
-            var topLibraryPath = path;
-
-            if (config.EnableCameraUploadSubfolders)
-            {
-                path = Path.Combine(path, _fileSystem.GetValidFilename(device.Name));
-            }
-
-            return new Tuple<string, string, string>(path, topLibraryPath, null);
-        }
-
-        internal string GetUploadsPath()
-        {
-            var config = _config.GetUploadOptions();
-            var path = config.CameraUploadPath;
-
-            if (string.IsNullOrWhiteSpace(path))
-            {
-                path = DefaultCameraUploadsPath;
-            }
-
-            return path;
-        }
-
-        private string DefaultCameraUploadsPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "camerauploads");
-
-        public bool CanAccessDevice(Jellyfin.Data.Entities.User user, string deviceId)
+        public bool CanAccessDevice(User user, string deviceId)
         {
             if (user == null)
             {
@@ -391,102 +203,4 @@ namespace Emby.Server.Implementations.Devices
             return true;
         }
     }
-
-    public class DeviceManagerEntryPoint : IServerEntryPoint
-    {
-        private readonly DeviceManager _deviceManager;
-        private readonly IServerConfigurationManager _config;
-        private ILogger _logger;
-
-        public DeviceManagerEntryPoint(
-            IDeviceManager deviceManager,
-            IServerConfigurationManager config,
-            ILogger<DeviceManagerEntryPoint> logger)
-        {
-            _deviceManager = (DeviceManager)deviceManager;
-            _config = config;
-            _logger = logger;
-        }
-
-        public async Task RunAsync()
-        {
-            if (!_config.Configuration.CameraUploadUpgraded && _config.Configuration.IsStartupWizardCompleted)
-            {
-                var path = _deviceManager.GetUploadsPath();
-
-                if (Directory.Exists(path))
-                {
-                    try
-                    {
-                        await _deviceManager.EnsureLibraryFolder(path, null).ConfigureAwait(false);
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "Error creating camera uploads library");
-                    }
-
-                    _config.Configuration.CameraUploadUpgraded = true;
-                    _config.SaveConfiguration();
-                }
-            }
-        }
-
-        #region IDisposable Support
-        private bool disposedValue = false; // To detect redundant calls
-
-        protected virtual void Dispose(bool disposing)
-        {
-            if (!disposedValue)
-            {
-                if (disposing)
-                {
-                    // TODO: dispose managed state (managed objects).
-                }
-
-                // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
-                // TODO: set large fields to null.
-
-                disposedValue = true;
-            }
-        }
-
-        // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
-        // ~DeviceManagerEntryPoint() {
-        //   // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
-        //   Dispose(false);
-        // }
-
-        // This code added to correctly implement the disposable pattern.
-        public void Dispose()
-        {
-            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
-            Dispose(true);
-            // TODO: uncomment the following line if the finalizer is overridden above.
-            // GC.SuppressFinalize(this);
-        }
-        #endregion
-    }
-
-    public class DevicesConfigStore : IConfigurationFactory
-    {
-        public IEnumerable<ConfigurationStore> GetConfigurations()
-        {
-            return new ConfigurationStore[]
-            {
-                new ConfigurationStore
-                {
-                     Key = "devices",
-                     ConfigurationType = typeof(DevicesOptions)
-                }
-            };
-        }
-    }
-
-    public static class UploadConfigExtension
-    {
-        public static DevicesOptions GetUploadOptions(this IConfigurationManager config)
-        {
-            return config.GetConfiguration<DevicesOptions>("devices");
-        }
-    }
 }

+ 1 - 2
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -9,7 +9,6 @@
     <ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
     <ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
     <ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
-    <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
     <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
@@ -51,7 +50,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp3.1</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

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

@@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.EntryPoints
                 .Append(config.PublicHttpsPort).Append(Separator)
                 .Append(_appHost.HttpPort).Append(Separator)
                 .Append(_appHost.HttpsPort).Append(Separator)
-                .Append(_appHost.EnableHttps).Append(Separator)
+                .Append(_appHost.ListenWithHttps).Append(Separator)
                 .Append(config.EnableRemoteAccess).Append(Separator)
                 .ToString();
         }
@@ -158,7 +158,7 @@ namespace Emby.Server.Implementations.EntryPoints
         {
             yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort);
 
-            if (_appHost.EnableHttps)
+            if (_appHost.ListenWithHttps)
             {
                 yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort);
             }

+ 109 - 113
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -6,11 +6,12 @@ using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Net.Sockets;
+using System.Net.WebSockets;
 using System.Reflection;
 using System.Threading;
 using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
 using Emby.Server.Implementations.Services;
+using Emby.Server.Implementations.SocketSharp;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
@@ -22,15 +23,17 @@ using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
 using Microsoft.AspNetCore.WebUtilities;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
 using ServiceStack.Text.Jsv;
 
 namespace Emby.Server.Implementations.HttpServer
 {
-    public class HttpListenerHost : IHttpServer, IDisposable
+    public class HttpListenerHost : IHttpServer
     {
         /// <summary>
         /// The key for a setting that specifies the default redirect path
@@ -39,17 +42,17 @@ namespace Emby.Server.Implementations.HttpServer
         public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
 
         private readonly ILogger _logger;
+        private readonly ILoggerFactory _loggerFactory;
         private readonly IServerConfigurationManager _config;
         private readonly INetworkManager _networkManager;
         private readonly IServerApplicationHost _appHost;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IXmlSerializer _xmlSerializer;
-        private readonly IHttpListener _socketListener;
         private readonly Func<Type, Func<string, object>> _funcParseFn;
         private readonly string _defaultRedirectPath;
         private readonly string _baseUrlPrefix;
+
         private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
-        private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>();
         private readonly IHostEnvironment _hostEnvironment;
 
         private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
@@ -63,10 +66,10 @@ namespace Emby.Server.Implementations.HttpServer
             INetworkManager networkManager,
             IJsonSerializer jsonSerializer,
             IXmlSerializer xmlSerializer,
-            IHttpListener socketListener,
             ILocalizationManager localizationManager,
             ServiceController serviceController,
-            IHostEnvironment hostEnvironment)
+            IHostEnvironment hostEnvironment,
+            ILoggerFactory loggerFactory)
         {
             _appHost = applicationHost;
             _logger = logger;
@@ -76,11 +79,9 @@ namespace Emby.Server.Implementations.HttpServer
             _networkManager = networkManager;
             _jsonSerializer = jsonSerializer;
             _xmlSerializer = xmlSerializer;
-            _socketListener = socketListener;
             ServiceController = serviceController;
-
-            _socketListener.WebSocketConnected = OnWebSocketConnected;
             _hostEnvironment = hostEnvironment;
+            _loggerFactory = loggerFactory;
 
             _funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
 
@@ -172,38 +173,6 @@ namespace Emby.Server.Implementations.HttpServer
             return attributes;
         }
 
-        private void OnWebSocketConnected(WebSocketConnectEventArgs e)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger)
-            {
-                OnReceive = ProcessWebSocketMessageReceived,
-                Url = e.Url,
-                QueryString = e.QueryString
-            };
-
-            connection.Closed += OnConnectionClosed;
-
-            lock (_webSocketConnections)
-            {
-                _webSocketConnections.Add(connection);
-            }
-
-            WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
-        }
-
-        private void OnConnectionClosed(object sender, EventArgs e)
-        {
-            lock (_webSocketConnections)
-            {
-                _webSocketConnections.Remove((IWebSocketConnection)sender);
-            }
-        }
-
         private static Exception GetActualException(Exception ex)
         {
             if (ex is AggregateException agg)
@@ -289,32 +258,6 @@ namespace Emby.Server.Implementations.HttpServer
                 .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
         }
 
-        /// <summary>
-        /// Shut down the Web Service
-        /// </summary>
-        public void Stop()
-        {
-            List<IWebSocketConnection> connections;
-
-            lock (_webSocketConnections)
-            {
-                connections = _webSocketConnections.ToList();
-                _webSocketConnections.Clear();
-            }
-
-            foreach (var connection in connections)
-            {
-                try
-                {
-                    connection.Dispose();
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error disposing connection");
-                }
-            }
-        }
-
         public static string RemoveQueryStringByKey(string url, string key)
         {
             var uri = new Uri(url);
@@ -424,33 +367,52 @@ namespace Emby.Server.Implementations.HttpServer
             return true;
         }
 
+        /// <summary>
+        /// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required.
+        /// </summary>
+        /// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns>
         private bool ValidateSsl(string remoteIp, string urlString)
         {
-            if (_config.Configuration.RequireHttps && _appHost.EnableHttps && !_config.Configuration.IsBehindProxy)
+            if (_config.Configuration.RequireHttps
+                && _appHost.ListenWithHttps
+                && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
             {
-                if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1)
+                // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
+                if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
+                    || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
                 {
-                    // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
-                    if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
-                        || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
-                    {
-                        return true;
-                    }
+                    return true;
+                }
 
-                    if (!_networkManager.IsInLocalNetwork(remoteIp))
-                    {
-                        return false;
-                    }
+                if (!_networkManager.IsInLocalNetwork(remoteIp))
+                {
+                    return false;
                 }
             }
 
             return true;
         }
 
+        /// <inheritdoc />
+        public Task RequestHandler(HttpContext context)
+        {
+            if (context.WebSockets.IsWebSocketRequest)
+            {
+                return WebSocketRequestHandler(context);
+            }
+
+            var request = context.Request;
+            var response = context.Response;
+            var localPath = context.Request.Path.ToString();
+
+            var req = new WebSocketSharpRequest(request, response, request.Path, _logger);
+            return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
+        }
+
         /// <summary>
         /// Overridable method that can be used to implement a custom handler.
         /// </summary>
-        public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
+        private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
         {
             var stopWatch = new Stopwatch();
             stopWatch.Start();
@@ -493,9 +455,10 @@ namespace Emby.Server.Implementations.HttpServer
                 if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
                 {
                     httpRes.StatusCode = 200;
-                    httpRes.Headers.Add("Access-Control-Allow-Origin", "*");
-                    httpRes.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
-                    httpRes.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization");
+                    foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
+                    {
+                        httpRes.Headers.Add(key, value);
+                    }
                     httpRes.ContentType = "text/plain";
                     await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
                     return;
@@ -578,6 +541,68 @@ namespace Emby.Server.Implementations.HttpServer
             }
         }
 
+        private async Task WebSocketRequestHandler(HttpContext context)
+        {
+            if (_disposed)
+            {
+                return;
+            }
+
+            try
+            {
+                _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
+
+                WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
+
+                var connection = new WebSocketConnection(
+                    _loggerFactory.CreateLogger<WebSocketConnection>(),
+                    webSocket,
+                    context.Connection.RemoteIpAddress,
+                    context.Request.Query)
+                {
+                    OnReceive = ProcessWebSocketMessageReceived
+                };
+
+                WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
+
+                await connection.ProcessAsync().ConfigureAwait(false);
+                _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
+            }
+            catch (Exception ex) // Otherwise ASP.Net will ignore the exception
+            {
+                _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
+                if (!context.Response.HasStarted)
+                {
+                    context.Response.StatusCode = 500;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Get the default CORS headers
+        /// </summary>
+        /// <param name="req"></param>
+        /// <returns></returns>
+        public IDictionary<string, string> GetDefaultCorsHeaders(IRequest req)
+        {
+            var origin = req.Headers["Origin"];
+            if (origin == StringValues.Empty)
+            {
+                origin = req.Headers["Host"];
+                if (origin == StringValues.Empty)
+                {
+                    origin = "*";
+                }
+            }
+
+            var headers = new Dictionary<string, string>();
+            headers.Add("Access-Control-Allow-Origin", origin);
+            headers.Add("Access-Control-Allow-Credentials", "true");
+            headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
+            headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie");
+            return headers;
+        }
+
         // Entry point for HttpListener
         public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
         {
@@ -624,7 +649,7 @@ namespace Emby.Server.Implementations.HttpServer
 
             ResponseFilters = new Action<IRequest, HttpResponse, object>[]
             {
-                new ResponseFilter(_logger).FilterResponse
+                new ResponseFilter(this, _logger).FilterResponse
             };
         }
 
@@ -685,11 +710,6 @@ namespace Emby.Server.Implementations.HttpServer
             return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
         }
 
-        public Task ProcessWebSocketRequest(HttpContext context)
-        {
-            return _socketListener.ProcessWebSocketRequest(context);
-        }
-
         private string NormalizeEmbyRoutePath(string path)
         {
             _logger.LogDebug("Normalizing /emby route");
@@ -708,28 +728,6 @@ namespace Emby.Server.Implementations.HttpServer
             return _baseUrlPrefix + NormalizeUrlPath(path);
         }
 
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        protected virtual void Dispose(bool disposing)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            if (disposing)
-            {
-                Stop();
-            }
-
-            _disposed = true;
-        }
-
         /// <summary>
         /// Processes the web socket message received.
         /// </summary>
@@ -741,8 +739,6 @@ namespace Emby.Server.Implementations.HttpServer
                 return Task.CompletedTask;
             }
 
-            _logger.LogDebug("Websocket message received: {0}", result.MessageType);
-
             IEnumerable<Task> GetTasks()
             {
                 foreach (var x in _webSocketListeners)

+ 0 - 39
Emby.Server.Implementations/HttpServer/IHttpListener.cs

@@ -1,39 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    public interface IHttpListener : IDisposable
-    {
-        /// <summary>
-        /// Gets or sets the error handler.
-        /// </summary>
-        /// <value>The error handler.</value>
-        Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
-
-        /// <summary>
-        /// Gets or sets the request handler.
-        /// </summary>
-        /// <value>The request handler.</value>
-        Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
-
-        /// <summary>
-        /// Gets or sets the web socket handler.
-        /// </summary>
-        /// <value>The web socket handler.</value>
-        Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
-
-        /// <summary>
-        /// Stops this instance.
-        /// </summary>
-        Task Stop();
-
-        Task ProcessWebSocketRequest(HttpContext ctx);
-    }
-}

+ 15 - 4
Emby.Server.Implementations/HttpServer/ResponseFilter.cs

@@ -1,6 +1,8 @@
 using System;
+using System.Collections.Generic;
 using System.Globalization;
 using System.Text;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
@@ -13,14 +15,17 @@ namespace Emby.Server.Implementations.HttpServer
     /// </summary>
     public class ResponseFilter
     {
+        private readonly IHttpServer _server;
         private readonly ILogger _logger;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ResponseFilter"/> class.
         /// </summary>
+        /// <param name="server">The HTTP server.</param>
         /// <param name="logger">The logger.</param>
-        public ResponseFilter(ILogger logger)
+        public ResponseFilter(IHttpServer server, ILogger logger)
         {
+            _server = server;
             _logger = logger;
         }
 
@@ -32,10 +37,16 @@ namespace Emby.Server.Implementations.HttpServer
         /// <param name="dto">The dto.</param>
         public void FilterResponse(IRequest req, HttpResponse res, object dto)
         {
+            foreach(var (key, value) in _server.GetDefaultCorsHeaders(req))
+            {
+                res.Headers.Add(key, value);
+            }
             // Try to prevent compatibility view
-            res.Headers.Add("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization");
-            res.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
-            res.Headers.Add("Access-Control-Allow-Origin", "*");
+            res.Headers["Access-Control-Allow-Headers"] = ("Accept, Accept-Language, Authorization, Cache-Control, " +
+                "Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " +
+                "Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " +
+                "Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " +
+                "X-Emby-Authorization");
 
             if (dto is Exception exception)
             {

+ 124 - 155
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -1,15 +1,18 @@
-using System;
+#nullable enable
+
+using System;
+using System.Buffers;
+using System.IO.Pipelines;
+using System.Net;
 using System.Net.WebSockets;
-using System.Text;
+using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
+using MediaBrowser.Common.Json;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
-using UtfUnknown;
 
 namespace Emby.Server.Implementations.HttpServer
 {
@@ -24,69 +27,50 @@ namespace Emby.Server.Implementations.HttpServer
         private readonly ILogger _logger;
 
         /// <summary>
-        /// The json serializer.
+        /// The json serializer options.
         /// </summary>
-        private readonly IJsonSerializer _jsonSerializer;
+        private readonly JsonSerializerOptions _jsonOptions;
 
         /// <summary>
         /// The socket.
         /// </summary>
-        private readonly IWebSocket _socket;
+        private readonly WebSocket _socket;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
         /// </summary>
+        /// <param name="logger">The logger.</param>
         /// <param name="socket">The socket.</param>
         /// <param name="remoteEndPoint">The remote end point.</param>
-        /// <param name="jsonSerializer">The json serializer.</param>
-        /// <param name="logger">The logger.</param>
-        /// <exception cref="ArgumentNullException">socket</exception>
-        public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger)
+        /// <param name="query">The query.</param>
+        public WebSocketConnection(
+            ILogger<WebSocketConnection> logger,
+            WebSocket socket,
+            IPAddress? remoteEndPoint,
+            IQueryCollection query)
         {
-            if (socket == null)
-            {
-                throw new ArgumentNullException(nameof(socket));
-            }
-
-            if (string.IsNullOrEmpty(remoteEndPoint))
-            {
-                throw new ArgumentNullException(nameof(remoteEndPoint));
-            }
-
-            if (jsonSerializer == null)
-            {
-                throw new ArgumentNullException(nameof(jsonSerializer));
-            }
-
-            if (logger == null)
-            {
-                throw new ArgumentNullException(nameof(logger));
-            }
-
-            Id = Guid.NewGuid();
-            _jsonSerializer = jsonSerializer;
+            _logger = logger;
             _socket = socket;
-            _socket.OnReceiveBytes = OnReceiveInternal;
-
             RemoteEndPoint = remoteEndPoint;
-            _logger = logger;
+            QueryString = query;
 
-            socket.Closed += OnSocketClosed;
+            _jsonOptions = JsonDefaults.GetOptions();
+            LastActivityDate = DateTime.Now;
         }
 
         /// <inheritdoc />
-        public event EventHandler<EventArgs> Closed;
+        public event EventHandler<EventArgs>? Closed;
 
         /// <summary>
         /// Gets or sets the remote end point.
         /// </summary>
-        public string RemoteEndPoint { get; private set; }
+        public IPAddress? RemoteEndPoint { get; }
 
         /// <summary>
         /// Gets or sets the receive action.
         /// </summary>
         /// <value>The receive action.</value>
-        public Func<WebSocketMessageInfo, Task> OnReceive { get; set; }
+        public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
 
         /// <summary>
         /// Gets the last activity date.
@@ -94,23 +78,11 @@ namespace Emby.Server.Implementations.HttpServer
         /// <value>The last activity date.</value>
         public DateTime LastActivityDate { get; private set; }
 
-        /// <summary>
-        /// Gets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        public Guid Id { get; private set; }
-
-        /// <summary>
-        /// Gets or sets the URL.
-        /// </summary>
-        /// <value>The URL.</value>
-        public string Url { get; set; }
-
         /// <summary>
         /// Gets or sets the query string.
         /// </summary>
         /// <value>The query string.</value>
-        public IQueryCollection QueryString { get; set; }
+        public IQueryCollection QueryString { get; }
 
         /// <summary>
         /// Gets the state.
@@ -118,138 +90,135 @@ namespace Emby.Server.Implementations.HttpServer
         /// <value>The state.</value>
         public WebSocketState State => _socket.State;
 
-        void OnSocketClosed(object sender, EventArgs e)
+        /// <summary>
+        /// Sends a message asynchronously.
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <param name="message">The message.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
         {
-            Closed?.Invoke(this, EventArgs.Empty);
+            var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
+            return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
         }
 
-        /// <summary>
-        /// Called when [receive].
-        /// </summary>
-        /// <param name="bytes">The bytes.</param>
-        private void OnReceiveInternal(byte[] bytes)
+        /// <inheritdoc />
+        public async Task ProcessAsync(CancellationToken cancellationToken = default)
         {
-            LastActivityDate = DateTime.UtcNow;
+            var pipe = new Pipe();
+            var writer = pipe.Writer;
 
-            if (OnReceive == null)
+            ValueWebSocketReceiveResult receiveresult;
+            do
             {
-                return;
-            }
-            var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName;
+                // Allocate at least 512 bytes from the PipeWriter
+                Memory<byte> memory = writer.GetMemory(512);
+                try
+                {
+                    receiveresult = await _socket.ReceiveAsync(memory, cancellationToken);
+                }
+                catch (WebSocketException ex)
+                {
+                    _logger.LogWarning("WS {IP} error receiving data: {Message}", RemoteEndPoint, ex.Message);
+                    break;
+                }
 
-            if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase))
-            {
-                OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length));
-            }
-            else
+                int bytesRead = receiveresult.Count;
+                if (bytesRead == 0)
+                {
+                    break;
+                }
+
+                // Tell the PipeWriter how much was read from the Socket
+                writer.Advance(bytesRead);
+
+                // Make the data available to the PipeReader
+                FlushResult flushResult = await writer.FlushAsync();
+                if (flushResult.IsCompleted)
+                {
+                    // The PipeReader stopped reading
+                    break;
+                }
+
+                LastActivityDate = DateTime.UtcNow;
+
+                if (receiveresult.EndOfMessage)
+                {
+                    await ProcessInternal(pipe.Reader).ConfigureAwait(false);
+                }
+            } while (
+                (_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
+                && receiveresult.MessageType != WebSocketMessageType.Close);
+
+            Closed?.Invoke(this, EventArgs.Empty);
+
+            if (_socket.State == WebSocketState.Open
+                || _socket.State == WebSocketState.CloseReceived
+                || _socket.State == WebSocketState.CloseSent)
             {
-                OnReceiveInternal(Encoding.ASCII.GetString(bytes, 0, bytes.Length));
+                await _socket.CloseAsync(
+                    WebSocketCloseStatus.NormalClosure,
+                    string.Empty,
+                    cancellationToken).ConfigureAwait(false);
             }
         }
 
-        private void OnReceiveInternal(string message)
+        private async Task ProcessInternal(PipeReader reader)
         {
-            LastActivityDate = DateTime.UtcNow;
-
-            if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
-            {
-                // This info is useful sometimes but also clogs up the log
-                _logger.LogDebug("Received web socket message that is not a json structure: {message}", message);
-                return;
-            }
+            ReadResult result = await reader.ReadAsync().ConfigureAwait(false);
+            ReadOnlySequence<byte> buffer = result.Buffer;
 
             if (OnReceive == null)
             {
+                // Tell the PipeReader how much of the buffer we have consumed
+                reader.AdvanceTo(buffer.End);
                 return;
             }
 
+            WebSocketMessage<object> stub;
             try
             {
-                var stub = (WebSocketMessage<object>)_jsonSerializer.DeserializeFromString(message, typeof(WebSocketMessage<object>));
 
-                var info = new WebSocketMessageInfo
+                if (buffer.IsSingleSegment)
                 {
-                    MessageType = stub.MessageType,
-                    Data = stub.Data?.ToString(),
-                    Connection = this
-                };
-
-                OnReceive(info);
+                    stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buffer.FirstSpan, _jsonOptions);
+                }
+                else
+                {
+                    var buf = ArrayPool<byte>.Shared.Rent(Convert.ToInt32(buffer.Length));
+                    try
+                    {
+                        buffer.CopyTo(buf);
+                        stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buf, _jsonOptions);
+                    }
+                    finally
+                    {
+                        ArrayPool<byte>.Shared.Return(buf);
+                    }
+                }
             }
-            catch (Exception ex)
+            catch (JsonException ex)
             {
+                // Tell the PipeReader how much of the buffer we have consumed
+                reader.AdvanceTo(buffer.End);
                 _logger.LogError(ex, "Error processing web socket message");
-            }
-        }
-
-        /// <summary>
-        /// Sends a message asynchronously.
-        /// </summary>
-        /// <typeparam name="T"></typeparam>
-        /// <param name="message">The message.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="ArgumentNullException">message</exception>
-        public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
-        {
-            if (message == null)
-            {
-                throw new ArgumentNullException(nameof(message));
-            }
-
-            var json = _jsonSerializer.SerializeToString(message);
-
-            return SendAsync(json, cancellationToken);
-        }
-
-        /// <summary>
-        /// Sends a message asynchronously.
-        /// </summary>
-        /// <param name="buffer">The buffer.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task SendAsync(byte[] buffer, CancellationToken cancellationToken)
-        {
-            if (buffer == null)
-            {
-                throw new ArgumentNullException(nameof(buffer));
+                return;
             }
 
-            cancellationToken.ThrowIfCancellationRequested();
+            // Tell the PipeReader how much of the buffer we have consumed
+            reader.AdvanceTo(buffer.End);
 
-            return _socket.SendAsync(buffer, true, cancellationToken);
-        }
+            _logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub);
 
-        /// <inheritdoc />
-        public Task SendAsync(string text, CancellationToken cancellationToken)
-        {
-            if (string.IsNullOrEmpty(text))
+            var info = new WebSocketMessageInfo
             {
-                throw new ArgumentNullException(nameof(text));
-            }
+                MessageType = stub.MessageType,
+                Data = stub.Data?.ToString(), // Data can be null
+                Connection = this
+            };
 
-            cancellationToken.ThrowIfCancellationRequested();
-
-            return _socket.SendAsync(text, true, cancellationToken);
-        }
-
-        /// <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)
-            {
-                _socket.Dispose();
-            }
+            await OnReceive(info).ConfigureAwait(false);
         }
     }
 }

+ 2 - 2
Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs

@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
     /// </summary>
     public class MusicAlbumResolver : ItemResolver<MusicAlbum>
     {
-        private readonly ILogger _logger;
+        private readonly ILogger<MusicAlbumResolver> _logger;
         private readonly IFileSystem _fileSystem;
         private readonly ILibraryManager _libraryManager;
 
@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         /// <param name="logger">The logger.</param>
         /// <param name="fileSystem">The file system.</param>
         /// <param name="libraryManager">The library manager.</param>
-        public MusicAlbumResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager)
+        public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager)
         {
             _logger = logger;
             _fileSystem = fileSystem;

+ 3 - 3
Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs

@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
     /// </summary>
     public class MusicArtistResolver : ItemResolver<MusicArtist>
     {
-        private readonly ILogger _logger;
+        private readonly ILogger<MusicAlbumResolver> _logger;
         private readonly IFileSystem _fileSystem;
         private readonly ILibraryManager _libraryManager;
         private readonly IServerConfigurationManager _config;
@@ -23,12 +23,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         /// <summary>
         /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
         /// </summary>
-        /// <param name="logger">The logger.</param>
+        /// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param>
         /// <param name="fileSystem">The file system.</param>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="config">The configuration manager.</param>
         public MusicArtistResolver(
-            ILogger<MusicArtistResolver> logger,
+            ILogger<MusicAlbumResolver> logger,
             IFileSystem fileSystem,
             ILibraryManager libraryManager,
             IServerConfigurationManager config)

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

@@ -1059,7 +1059,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
             var stream = new MediaSourceInfo
             {
-                EncoderPath = _appHost.GetLocalApiUrl("127.0.0.1", true) + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
+                EncoderPath = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
                 EncoderProtocol = MediaProtocol.Http,
                 Path = info.Path,
                 Protocol = MediaProtocol.File,

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs

@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             //OpenedMediaSource.Path = tempFile;
             //OpenedMediaSource.ReadAtNativeFramerate = true;
 
-            MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1", true) + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
+            MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
             MediaSource.Protocol = MediaProtocol.Http;
             //OpenedMediaSource.SupportsDirectPlay = false;
             //OpenedMediaSource.SupportsDirectStream = true;

+ 2 - 2
Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         public M3UTunerHost(
             IServerConfigurationManager config,
             IMediaSourceManager mediaSourceManager,
-            ILogger logger,
+            ILogger<M3UTunerHost> logger,
             IJsonSerializer jsonSerializer,
             IFileSystem fileSystem,
             IHttpClient httpClient,
@@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             return Task.FromResult(list);
         }
 
-        private static readonly string[] _disallowedSharedStreamExtensions = new string[]
+        private static readonly string[] _disallowedSharedStreamExtensions =
         {
             ".mkv",
             ".mp4",

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs

@@ -106,7 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             //OpenedMediaSource.Path = tempFile;
             //OpenedMediaSource.ReadAtNativeFramerate = true;
 
-            MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1", true) + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
+            MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
             MediaSource.Protocol = MediaProtocol.Http;
 
             //OpenedMediaSource.Path = TempFilePath;

+ 3 - 1
Emby.Server.Implementations/Localization/Core/bn.json

@@ -91,5 +91,7 @@
     "HeaderNextUp": "এরপরে আসছে",
     "HeaderLiveTV": "লাইভ টিভি",
     "HeaderFavoriteSongs": "প্রিয় গানগুলো",
-    "HeaderFavoriteShows": "প্রিয় শোগুলো"
+    "HeaderFavoriteShows": "প্রিয় শোগুলো",
+    "TasksLibraryCategory": "গ্রন্থাগার",
+    "TasksMaintenanceCategory": "রক্ষণাবেক্ষণ"
 }

+ 9 - 1
Emby.Server.Implementations/Localization/Core/he.json

@@ -99,5 +99,13 @@
     "TaskCleanCache": "נקה תיקיית מטמון",
     "TasksApplicationCategory": "יישום",
     "TasksLibraryCategory": "ספרייה",
-    "TasksMaintenanceCategory": "תחזוקה"
+    "TasksMaintenanceCategory": "תחזוקה",
+    "TaskUpdatePlugins": "עדכן תוספים",
+    "TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.",
+    "TaskRefreshPeople": "רענן אנשים",
+    "TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
+    "TaskCleanLogs": "נקה תיקיית יומן",
+    "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
+    "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
+    "TasksChannelsCategory": "ערוצי אינטרנט"
 }

+ 5 - 1
Emby.Server.Implementations/Localization/Core/nb.json

@@ -97,5 +97,9 @@
     "TasksApplicationCategory": "Applikasjon",
     "TasksLibraryCategory": "Bibliotek",
     "TasksMaintenanceCategory": "Vedlikehold",
-    "TaskCleanCache": "Tøm buffer katalog"
+    "TaskCleanCache": "Tøm buffer katalog",
+    "TaskRefreshLibrary": "Skann mediebibliotek",
+    "TaskRefreshChapterImagesDescription": "Lager forhåndsvisningsbilder for videoer som har kapitler.",
+    "TaskRefreshChapterImages": "Trekk ut Kapittelbilder",
+    "TaskCleanCacheDescription": "Sletter mellomlagrede filer som ikke lengre trengs av systemet."
 }

+ 0 - 39
Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs

@@ -1,39 +0,0 @@
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using WebSocketManager = Emby.Server.Implementations.WebSockets.WebSocketManager;
-
-namespace Emby.Server.Implementations.Middleware
-{
-    public class WebSocketMiddleware
-    {
-        private readonly RequestDelegate _next;
-        private readonly ILogger<WebSocketMiddleware> _logger;
-        private readonly WebSocketManager _webSocketManager;
-
-        public WebSocketMiddleware(RequestDelegate next, ILogger<WebSocketMiddleware> logger, WebSocketManager webSocketManager)
-        {
-            _next = next;
-            _logger = logger;
-            _webSocketManager = webSocketManager;
-        }
-
-        public async Task Invoke(HttpContext httpContext)
-        {
-            _logger.LogInformation("Handling request: " + httpContext.Request.Path);
-
-            if (httpContext.WebSockets.IsWebSocketRequest)
-            {
-                var webSocketContext = await httpContext.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
-                if (webSocketContext != null)
-                {
-                    await _webSocketManager.OnWebSocketConnected(webSocketContext).ConfigureAwait(false);
-                }
-            }
-            else
-            {
-                await _next.Invoke(httpContext).ConfigureAwait(false);
-            }
-        }
-    }
-}

+ 0 - 48
Emby.Server.Implementations/Net/IWebSocket.cs

@@ -1,48 +0,0 @@
-using System;
-using System.Net.WebSockets;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Emby.Server.Implementations.Net
-{
-    /// <summary>
-    /// Interface IWebSocket
-    /// </summary>
-    public interface IWebSocket : IDisposable
-    {
-        /// <summary>
-        /// Occurs when [closed].
-        /// </summary>
-        event EventHandler<EventArgs> Closed;
-
-        /// <summary>
-        /// Gets or sets the state.
-        /// </summary>
-        /// <value>The state.</value>
-        WebSocketState State { get; }
-
-        /// <summary>
-        /// Gets or sets the receive action.
-        /// </summary>
-        /// <value>The receive action.</value>
-        Action<byte[]> OnReceiveBytes { get; set; }
-
-        /// <summary>
-        /// Sends the async.
-        /// </summary>
-        /// <param name="bytes">The bytes.</param>
-        /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Sends the asynchronous.
-        /// </summary>
-        /// <param name="text">The text.</param>
-        /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken);
-    }
-}

+ 0 - 29
Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs

@@ -1,29 +0,0 @@
-using System;
-using Microsoft.AspNetCore.Http;
-
-namespace Emby.Server.Implementations.Net
-{
-    public class WebSocketConnectEventArgs : EventArgs
-    {
-        /// <summary>
-        /// Gets or sets the URL.
-        /// </summary>
-        /// <value>The URL.</value>
-        public string Url { get; set; }
-        /// <summary>
-        /// Gets or sets the query string.
-        /// </summary>
-        /// <value>The query string.</value>
-        public IQueryCollection QueryString { get; set; }
-        /// <summary>
-        /// Gets or sets the web socket.
-        /// </summary>
-        /// <value>The web socket.</value>
-        public IWebSocket WebSocket { get; set; }
-        /// <summary>
-        /// Gets or sets the endpoint.
-        /// </summary>
-        /// <value>The endpoint.</value>
-        public string Endpoint { get; set; }
-    }
-}

+ 0 - 191
Emby.Server.Implementations/Session/HttpSessionController.cs

@@ -1,191 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Session;
-
-namespace Emby.Server.Implementations.Session
-{
-    public class HttpSessionController : ISessionController
-    {
-        private readonly IHttpClient _httpClient;
-        private readonly IJsonSerializer _json;
-        private readonly ISessionManager _sessionManager;
-
-        public SessionInfo Session { get; private set; }
-
-        private readonly string _postUrl;
-
-        public HttpSessionController(IHttpClient httpClient,
-            IJsonSerializer json,
-            SessionInfo session,
-            string postUrl, ISessionManager sessionManager)
-        {
-            _httpClient = httpClient;
-            _json = json;
-            Session = session;
-            _postUrl = postUrl;
-            _sessionManager = sessionManager;
-        }
-
-        private string PostUrl => string.Format("http://{0}{1}", Session.RemoteEndPoint, _postUrl);
-
-        public bool IsSessionActive => (DateTime.UtcNow - Session.LastActivityDate).TotalMinutes <= 5;
-
-        public bool SupportsMediaControl => true;
-
-        private Task SendMessage(string name, string messageId, CancellationToken cancellationToken)
-        {
-            return SendMessage(name, messageId, new Dictionary<string, string>(), cancellationToken);
-        }
-
-        private Task SendMessage(string name, string messageId, Dictionary<string, string> args, CancellationToken cancellationToken)
-        {
-            args["messageId"] = messageId;
-            var url = PostUrl + "/" + name + ToQueryString(args);
-
-            return SendRequest(new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = cancellationToken,
-                BufferContent = false
-            });
-        }
-
-        private Task SendPlayCommand(PlayRequest command, string messageId, CancellationToken cancellationToken)
-        {
-            var dict = new Dictionary<string, string>();
-
-            dict["ItemIds"] = string.Join(",", command.ItemIds.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
-
-            if (command.StartPositionTicks.HasValue)
-            {
-                dict["StartPositionTicks"] = command.StartPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
-            }
-            if (command.AudioStreamIndex.HasValue)
-            {
-                dict["AudioStreamIndex"] = command.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture);
-            }
-            if (command.SubtitleStreamIndex.HasValue)
-            {
-                dict["SubtitleStreamIndex"] = command.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture);
-            }
-            if (command.StartIndex.HasValue)
-            {
-                dict["StartIndex"] = command.StartIndex.Value.ToString(CultureInfo.InvariantCulture);
-            }
-            if (!string.IsNullOrEmpty(command.MediaSourceId))
-            {
-                dict["MediaSourceId"] = command.MediaSourceId;
-            }
-
-            return SendMessage(command.PlayCommand.ToString(), messageId, dict, cancellationToken);
-        }
-
-        private Task SendPlaystateCommand(PlaystateRequest command, string messageId, CancellationToken cancellationToken)
-        {
-            var args = new Dictionary<string, string>();
-
-            if (command.Command == PlaystateCommand.Seek)
-            {
-                if (!command.SeekPositionTicks.HasValue)
-                {
-                    throw new ArgumentException("SeekPositionTicks cannot be null");
-                }
-
-                args["SeekPositionTicks"] = command.SeekPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
-            }
-
-            return SendMessage(command.Command.ToString(), messageId, args, cancellationToken);
-        }
-
-        private string[] _supportedMessages = Array.Empty<string>();
-        public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
-        {
-            if (!IsSessionActive)
-            {
-                return Task.CompletedTask;
-            }
-
-            if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
-            {
-                return SendPlayCommand(data as PlayRequest, messageId, cancellationToken);
-            }
-            if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
-            {
-                return SendPlaystateCommand(data as PlaystateRequest, messageId, cancellationToken);
-            }
-            if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
-            {
-                var command = data as GeneralCommand;
-                return SendMessage(command.Name, messageId, command.Arguments, cancellationToken);
-            }
-
-            if (!_supportedMessages.Contains(name, StringComparer.OrdinalIgnoreCase))
-            {
-                return Task.CompletedTask;
-            }
-
-            var url = PostUrl + "/" + name;
-
-            url += "?messageId=" + messageId;
-
-            var options = new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = cancellationToken,
-                BufferContent = false
-            };
-
-            if (data != null)
-            {
-                if (typeof(T) == typeof(string))
-                {
-                    var str = data as string;
-                    if (!string.IsNullOrEmpty(str))
-                    {
-                        options.RequestContent = str;
-                        options.RequestContentType = "application/json";
-                    }
-                }
-                else
-                {
-                    options.RequestContent = _json.SerializeToString(data);
-                    options.RequestContentType = "application/json";
-                }
-            }
-
-            return SendRequest(options);
-        }
-
-        private async Task SendRequest(HttpRequestOptions options)
-        {
-            using (var response = await _httpClient.Post(options).ConfigureAwait(false))
-            {
-
-            }
-        }
-
-        private static string ToQueryString(Dictionary<string, string> nvc)
-        {
-            var array = (from item in nvc
-                         select string.Format("{0}={1}", WebUtility.UrlEncode(item.Key), WebUtility.UrlEncode(item.Value)))
-                .ToArray();
-
-            var args = string.Join("&", array);
-
-            if (string.IsNullOrEmpty(args))
-            {
-                return args;
-            }
-
-            return "?" + args;
-        }
-    }
-}

+ 7 - 8
Emby.Server.Implementations/Session/SessionManager.cs

@@ -490,8 +490,7 @@ namespace Emby.Server.Implementations.Session
                 Client = appName,
                 DeviceId = deviceId,
                 ApplicationVersion = appVersion,
-                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
-                ServerId = _appHost.SystemId
+                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture)
             };
 
             var username = user?.Username;
@@ -1052,12 +1051,12 @@ namespace Emby.Server.Implementations.Session
 
         private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken)
         {
-            var controllers = session.SessionControllers.ToArray();
-            var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+            var controllers = session.SessionControllers;
+            var messageId = Guid.NewGuid();
 
             foreach (var controller in controllers)
             {
-                await controller.SendMessage(name, messageId, data, controllers, cancellationToken).ConfigureAwait(false);
+                await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
             }
         }
 
@@ -1065,13 +1064,13 @@ namespace Emby.Server.Implementations.Session
         {
             IEnumerable<Task> GetTasks()
             {
-                var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+                var messageId = Guid.NewGuid();
                 foreach (var session in sessions)
                 {
                     var controllers = session.SessionControllers;
                     foreach (var controller in controllers)
                     {
-                        yield return controller.SendMessage(name, messageId, data, controllers, cancellationToken);
+                        yield return controller.SendMessage(name, messageId, data, cancellationToken);
                     }
                 }
             }
@@ -1772,7 +1771,7 @@ namespace Emby.Server.Implementations.Session
                 throw new ArgumentNullException(nameof(info));
             }
 
-            var user = info.UserId.Equals(Guid.Empty)
+            var user = info.UserId == Guid.Empty
                 ? null
                 : _userManager.GetUserById(info.UserId);
 

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

@@ -3,7 +3,6 @@ using System.Threading.Tasks;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Serialization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 
@@ -12,7 +11,7 @@ namespace Emby.Server.Implementations.Session
     /// <summary>
     /// Class SessionWebSocketListener
     /// </summary>
-    public class SessionWebSocketListener : IWebSocketListener, IDisposable
+    public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable
     {
         /// <summary>
         /// The _session manager
@@ -23,42 +22,41 @@ namespace Emby.Server.Implementations.Session
         /// The _logger
         /// </summary>
         private readonly ILogger _logger;
-
-        /// <summary>
-        /// The _dto service
-        /// </summary>
-        private readonly IJsonSerializer _json;
+        private readonly ILoggerFactory _loggerFactory;
 
         private readonly IHttpServer _httpServer;
 
-
         /// <summary>
         /// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
         /// </summary>
+        /// <param name="logger">The logger.</param>
         /// <param name="sessionManager">The session manager.</param>
         /// <param name="loggerFactory">The logger factory.</param>
-        /// <param name="json">The json.</param>
         /// <param name="httpServer">The HTTP server.</param>
-        public SessionWebSocketListener(ISessionManager sessionManager, ILoggerFactory loggerFactory, IJsonSerializer json, IHttpServer httpServer)
+        public SessionWebSocketListener(
+            ILogger<SessionWebSocketListener> logger,
+            ISessionManager sessionManager,
+            ILoggerFactory loggerFactory,
+            IHttpServer httpServer)
         {
+            _logger = logger;
             _sessionManager = sessionManager;
-            _logger = loggerFactory.CreateLogger(GetType().Name);
-            _json = json;
+            _loggerFactory = loggerFactory;
             _httpServer = httpServer;
-            httpServer.WebSocketConnected += _serverManager_WebSocketConnected;
+
+            httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
         }
 
-        void _serverManager_WebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
+        private void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
         {
-            var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint);
-
+            var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint.ToString());
             if (session != null)
             {
                 EnsureController(session, e.Argument);
             }
             else
             {
-                _logger.LogWarning("Unable to determine session based on url: {0}", e.Argument.Url);
+                _logger.LogWarning("Unable to determine session based on query string: {0}", e.Argument.QueryString);
             }
         }
 
@@ -79,9 +77,10 @@ namespace Emby.Server.Implementations.Session
             return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint);
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
-            _httpServer.WebSocketConnected -= _serverManager_WebSocketConnected;
+            _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
         }
 
         /// <summary>
@@ -94,7 +93,8 @@ namespace Emby.Server.Implementations.Session
 
         private void EnsureController(SessionInfo session, IWebSocketConnection connection)
         {
-            var controllerInfo = session.EnsureController<WebSocketController>(s => new WebSocketController(s, _logger, _sessionManager));
+            var controllerInfo = session.EnsureController<WebSocketController>(
+                s => new WebSocketController(_loggerFactory.CreateLogger<WebSocketController>(), s, _sessionManager));
 
             var controller = (WebSocketController)controllerInfo.Item1;
             controller.AddWebSocket(connection);

+ 51 - 35
Emby.Server.Implementations/Session/WebSocketController.cs

@@ -1,3 +1,7 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+#nullable enable
+
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -11,60 +15,63 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Session
 {
-    public class WebSocketController : ISessionController, IDisposable
+    public sealed class WebSocketController : ISessionController, IDisposable
     {
-        public SessionInfo Session { get; private set; }
-        public IReadOnlyList<IWebSocketConnection> Sockets { get; private set; }
-
         private readonly ILogger _logger;
-
         private readonly ISessionManager _sessionManager;
+        private readonly SessionInfo _session;
 
-        public WebSocketController(SessionInfo session, ILogger logger, ISessionManager sessionManager)
+        private readonly List<IWebSocketConnection> _sockets;
+        private bool _disposed = false;
+
+        public WebSocketController(
+            ILogger<WebSocketController> logger,
+            SessionInfo session,
+            ISessionManager sessionManager)
         {
-            Session = session;
             _logger = logger;
+            _session = session;
             _sessionManager = sessionManager;
-            Sockets = new List<IWebSocketConnection>();
+            _sockets = new List<IWebSocketConnection>();
         }
 
         private bool HasOpenSockets => GetActiveSockets().Any();
 
+        /// <inheritdoc />
         public bool SupportsMediaControl => HasOpenSockets;
 
+        /// <inheritdoc />
         public bool IsSessionActive => HasOpenSockets;
 
         private IEnumerable<IWebSocketConnection> GetActiveSockets()
-        {
-            return Sockets
-                .OrderByDescending(i => i.LastActivityDate)
-                .Where(i => i.State == WebSocketState.Open);
-        }
+            => _sockets.Where(i => i.State == WebSocketState.Open);
 
         public void AddWebSocket(IWebSocketConnection connection)
         {
-            var sockets = Sockets.ToList();
-            sockets.Add(connection);
+            _logger.LogDebug("Adding websocket to session {Session}", _session.Id);
+            _sockets.Add(connection);
 
-            Sockets = sockets;
-
-            connection.Closed += connection_Closed;
+            connection.Closed += OnConnectionClosed;
         }
 
-        void connection_Closed(object sender, EventArgs e)
+        private void OnConnectionClosed(object sender, EventArgs e)
         {
             var connection = (IWebSocketConnection)sender;
-            var sockets = Sockets.ToList();
-            sockets.Remove(connection);
-
-            Sockets = sockets;
-
-            _sessionManager.CloseIfNeeded(Session);
+            _logger.LogDebug("Removing websocket from session {Session}", _session.Id);
+            _sockets.Remove(connection);
+            connection.Closed -= OnConnectionClosed;
+            _sessionManager.CloseIfNeeded(_session);
         }
 
-        public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
+        /// <inheritdoc />
+        public Task SendMessage<T>(
+            string name,
+            Guid messageId,
+            T data,
+            CancellationToken cancellationToken)
         {
             var socket = GetActiveSockets()
+                .OrderByDescending(i => i.LastActivityDate)
                 .FirstOrDefault();
 
             if (socket == null)
@@ -72,21 +79,30 @@ namespace Emby.Server.Implementations.Session
                 return Task.CompletedTask;
             }
 
-            return socket.SendAsync(new WebSocketMessage<T>
-            {
-                Data = data,
-                MessageType = name,
-                MessageId = messageId
-
-            }, cancellationToken);
+            return socket.SendAsync(
+                new WebSocketMessage<T>
+                {
+                    Data = data,
+                    MessageType = name,
+                    MessageId = messageId
+                },
+                cancellationToken);
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
-            foreach (var socket in Sockets.ToList())
+            if (_disposed)
             {
-                socket.Closed -= connection_Closed;
+                return;
             }
+
+            foreach (var socket in _sockets)
+            {
+                socket.Closed -= OnConnectionClosed;
+            }
+
+            _disposed = true;
         }
     }
 }

+ 0 - 105
Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs

@@ -1,105 +0,0 @@
-using System;
-using System.Net.WebSockets;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
-    public class SharpWebSocket : IWebSocket
-    {
-        /// <summary>
-        /// The logger
-        /// </summary>
-        private readonly ILogger _logger;
-
-        public event EventHandler<EventArgs> Closed;
-
-        /// <summary>
-        /// Gets or sets the web socket.
-        /// </summary>
-        /// <value>The web socket.</value>
-        private readonly WebSocket _webSocket;
-
-        private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
-        private bool _disposed;
-
-        public SharpWebSocket(WebSocket socket, ILogger logger)
-        {
-            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
-            _webSocket = socket ?? throw new ArgumentNullException(nameof(socket));
-        }
-
-        /// <summary>
-        /// Gets the state.
-        /// </summary>
-        /// <value>The state.</value>
-        public WebSocketState State => _webSocket.State;
-
-        /// <summary>
-        /// Sends the async.
-        /// </summary>
-        /// <param name="bytes">The bytes.</param>
-        /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken)
-        {
-            return _webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Binary, endOfMessage, cancellationToken);
-        }
-
-        /// <summary>
-        /// Sends the asynchronous.
-        /// </summary>
-        /// <param name="text">The text.</param>
-        /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken)
-        {
-            return _webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(text)), WebSocketMessageType.Text, endOfMessage, cancellationToken);
-        }
-
-        /// <summary>
-        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
-        /// </summary>
-        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 (_disposed)
-            {
-                return;
-            }
-
-            if (dispose)
-            {
-                _cancellationTokenSource.Cancel();
-                if (_webSocket.State == WebSocketState.Open)
-                {
-                    _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client",
-                        CancellationToken.None);
-                }
-                Closed?.Invoke(this, EventArgs.Empty);
-            }
-
-            _disposed = true;
-        }
-
-        /// <summary>
-        /// Gets or sets the receive action.
-        /// </summary>
-        /// <value>The receive action.</value>
-        public Action<byte[]> OnReceiveBytes { get; set; }
-    }
-}

+ 0 - 135
Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs

@@ -1,135 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.WebSockets;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using Emby.Server.Implementations.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
-    public class WebSocketSharpListener : IHttpListener
-    {
-        private readonly ILogger _logger;
-
-        private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
-        private CancellationToken _disposeCancellationToken;
-
-        public WebSocketSharpListener(ILogger<WebSocketSharpListener> logger)
-        {
-            _logger = logger;
-            _disposeCancellationToken = _disposeCancellationTokenSource.Token;
-        }
-
-        public Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
-
-        public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
-
-        public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
-
-        private static void LogRequest(ILogger logger, HttpRequest request)
-        {
-            var url = request.GetDisplayUrl();
-
-            logger.LogInformation("WS {Url}. UserAgent: {UserAgent}", url, request.Headers[HeaderNames.UserAgent].ToString());
-        }
-
-        public async Task ProcessWebSocketRequest(HttpContext ctx)
-        {
-            try
-            {
-                LogRequest(_logger, ctx.Request);
-                var endpoint = ctx.Connection.RemoteIpAddress.ToString();
-                var url = ctx.Request.GetDisplayUrl();
-
-                var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
-                var socket = new SharpWebSocket(webSocketContext, _logger);
-
-                WebSocketConnected(new WebSocketConnectEventArgs
-                {
-                    Url = url,
-                    QueryString = ctx.Request.Query,
-                    WebSocket = socket,
-                    Endpoint = endpoint
-                });
-
-                WebSocketReceiveResult result;
-                var message = new List<byte>();
-
-                do
-                {
-                    var buffer = WebSocket.CreateServerBuffer(4096);
-                    result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken);
-                    message.AddRange(buffer.Array.Take(result.Count));
-
-                    if (result.EndOfMessage)
-                    {
-                        socket.OnReceiveBytes(message.ToArray());
-                        message.Clear();
-                    }
-                } while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close);
-
-
-                if (webSocketContext.State == WebSocketState.Open)
-                {
-                    await webSocketContext.CloseAsync(
-                        result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
-                        result.CloseStatusDescription,
-                        _disposeCancellationToken).ConfigureAwait(false);
-                }
-
-                socket.Dispose();
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "AcceptWebSocketAsync error");
-                if (!ctx.Response.HasStarted)
-                {
-                    ctx.Response.StatusCode = 500;
-                }
-            }
-        }
-
-        public Task Stop()
-        {
-            _disposeCancellationTokenSource.Cancel();
-            return Task.CompletedTask;
-        }
-
-        /// <summary>
-        /// Releases the unmanaged resources and disposes of the managed resources used.
-        /// </summary>
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        private bool _disposed;
-
-        /// <summary>
-        /// Releases the unmanaged resources and disposes of the managed resources used.
-        /// </summary>
-        /// <param name="disposing">Whether or not the managed resources should be disposed.</param>
-        protected virtual void Dispose(bool disposing)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            if (disposing)
-            {
-                Stop().GetAwaiter().GetResult();
-            }
-
-            _disposed = true;
-        }
-    }
-}

+ 0 - 10
Emby.Server.Implementations/WebSockets/WebSocketHandler.cs

@@ -1,10 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
-
-namespace Emby.Server.Implementations.WebSockets
-{
-    public interface IWebSocketHandler
-    {
-        Task ProcessMessage(WebSocketMessage<object> message, TaskCompletionSource<bool> taskCompletionSource);
-    }
-}

+ 0 - 102
Emby.Server.Implementations/WebSockets/WebSocketManager.cs

@@ -1,102 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.WebSockets;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
-using UtfUnknown;
-
-namespace Emby.Server.Implementations.WebSockets
-{
-    public class WebSocketManager
-    {
-        private readonly IWebSocketHandler[] _webSocketHandlers;
-        private readonly IJsonSerializer _jsonSerializer;
-        private readonly ILogger<WebSocketManager> _logger;
-        private const int BufferSize = 4096;
-
-        public WebSocketManager(IWebSocketHandler[] webSocketHandlers, IJsonSerializer jsonSerializer, ILogger<WebSocketManager> logger)
-        {
-            _webSocketHandlers = webSocketHandlers;
-            _jsonSerializer = jsonSerializer;
-            _logger = logger;
-        }
-
-        public async Task OnWebSocketConnected(WebSocket webSocket)
-        {
-            var taskCompletionSource = new TaskCompletionSource<bool>();
-            var cancellationToken = new CancellationTokenSource().Token;
-            WebSocketReceiveResult result;
-            var message = new List<byte>();
-
-            // Keep listening for incoming messages, otherwise the socket closes automatically
-            do
-            {
-                var buffer = WebSocket.CreateServerBuffer(BufferSize);
-                result = await webSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false);
-                message.AddRange(buffer.Array.Take(result.Count));
-
-                if (result.EndOfMessage)
-                {
-                    await ProcessMessage(message.ToArray(), taskCompletionSource).ConfigureAwait(false);
-                    message.Clear();
-                }
-            } while (!taskCompletionSource.Task.IsCompleted &&
-                     webSocket.State == WebSocketState.Open &&
-                     result.MessageType != WebSocketMessageType.Close);
-
-            if (webSocket.State == WebSocketState.Open)
-            {
-                await webSocket.CloseAsync(
-                    result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
-                    result.CloseStatusDescription,
-                    cancellationToken).ConfigureAwait(false);
-            }
-        }
-
-        private async Task ProcessMessage(byte[] messageBytes, TaskCompletionSource<bool> taskCompletionSource)
-        {
-            var charset = CharsetDetector.DetectFromBytes(messageBytes).Detected?.EncodingName;
-            var message = string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase)
-                ? Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length)
-                : Encoding.ASCII.GetString(messageBytes, 0, messageBytes.Length);
-
-            // All messages are expected to be valid JSON objects
-            if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
-            {
-                _logger.LogDebug("Received web socket message that is not a json structure: {Message}", message);
-                return;
-            }
-
-            try
-            {
-                var info = _jsonSerializer.DeserializeFromString<WebSocketMessage<object>>(message);
-
-                _logger.LogDebug("Websocket message received: {0}", info.MessageType);
-
-                var tasks = _webSocketHandlers.Select(handler => Task.Run(() =>
-                {
-                    try
-                    {
-                        handler.ProcessMessage(info, taskCompletionSource).ConfigureAwait(false);
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "{HandlerType} failed processing WebSocket message {MessageType}",
-                            handler.GetType().Name, info.MessageType ?? string.Empty);
-                    }
-                }));
-
-                await Task.WhenAll(tasks);
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error processing web socket message");
-            }
-        }
-    }
-}

+ 60 - 59
Jellyfin.Data/Entities/ActivityLog.cs

@@ -1,72 +1,62 @@
 using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.ComponentModel;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
-using System.Linq;
-using System.Runtime.CompilerServices;
+using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Data.Entities
 {
-    [Table("ActivityLog")]
-    public partial class ActivityLog
+    /// <summary>
+    /// An entity referencing an activity log entry.
+    /// </summary>
+    public partial class ActivityLog : ISavingChanges
     {
-        partial void Init();
-
         /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// Initializes a new instance of the <see cref="ActivityLog"/> class.
+        /// Public constructor with required data.
         /// </summary>
-        protected ActivityLog()
+        /// <param name="name">The name.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="userId">The user id.</param>
+        public ActivityLog(string name, string type, Guid userId)
         {
-            Init();
-        }
+            if (string.IsNullOrEmpty(name))
+            {
+                throw new ArgumentNullException(nameof(name));
+            }
 
-        /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static ActivityLog CreateActivityLogUnsafe()
-        {
-            return new ActivityLog();
-        }
+            if (string.IsNullOrEmpty(type))
+            {
+                throw new ArgumentNullException(nameof(type));
+            }
 
-        /// <summary>
-        /// Public constructor with required data
-        /// </summary>
-        /// <param name="name"></param>
-        /// <param name="type"></param>
-        /// <param name="userid"></param>
-        /// <param name="datecreated"></param>
-        /// <param name="logseverity"></param>
-        public ActivityLog(string name, string type, Guid userid, DateTime datecreated, Microsoft.Extensions.Logging.LogLevel logseverity)
-        {
-            if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
             this.Name = name;
-
-            if (string.IsNullOrEmpty(type)) throw new ArgumentNullException(nameof(type));
             this.Type = type;
+            this.UserId = userId;
+            this.DateCreated = DateTime.UtcNow;
+            this.LogSeverity = LogLevel.Trace;
 
-            this.UserId = userid;
-
-            this.DateCreated = datecreated;
-
-            this.LogSeverity = logseverity;
-
+            Init();
+        }
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ActivityLog"/> class.
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected ActivityLog()
+        {
             Init();
         }
 
         /// <summary>
         /// Static create function (for use in LINQ queries, etc.)
         /// </summary>
-        /// <param name="name"></param>
-        /// <param name="type"></param>
-        /// <param name="userid"></param>
-        /// <param name="datecreated"></param>
-        /// <param name="logseverity"></param>
-        public static ActivityLog Create(string name, string type, Guid userid, DateTime datecreated, Microsoft.Extensions.Logging.LogLevel logseverity)
+        /// <param name="name">The name.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="userId">The user's id.</param>
+        /// <returns>The new <see cref="ActivityLog"/> instance.</returns>
+        public static ActivityLog Create(string name, string type, Guid userId)
         {
-            return new ActivityLog(name, type, userid, datecreated, logseverity);
+            return new ActivityLog(name, type, userId);
         }
 
         /*************************************************************************
@@ -74,7 +64,8 @@ namespace Jellyfin.Data.Entities
          *************************************************************************/
 
         /// <summary>
-        /// Identity, Indexed, Required
+        /// Gets or sets the identity of this instance.
+        /// This is the key in the backing database.
         /// </summary>
         [Key]
         [Required]
@@ -82,7 +73,8 @@ namespace Jellyfin.Data.Entities
         public int Id { get; protected set; }
 
         /// <summary>
-        /// Required, Max length = 512
+        /// Gets or sets the name.
+        /// Required, Max length = 512.
         /// </summary>
         [Required]
         [MaxLength(512)]
@@ -90,21 +82,24 @@ namespace Jellyfin.Data.Entities
         public string Name { get; set; }
 
         /// <summary>
-        /// Max length = 512
+        /// Gets or sets the overview.
+        /// Max length = 512.
         /// </summary>
         [MaxLength(512)]
         [StringLength(512)]
         public string Overview { get; set; }
 
         /// <summary>
-        /// Max length = 512
+        /// Gets or sets the short overview.
+        /// Max length = 512.
         /// </summary>
         [MaxLength(512)]
         [StringLength(512)]
         public string ShortOverview { get; set; }
 
         /// <summary>
-        /// Required, Max length = 256
+        /// Gets or sets the type.
+        /// Required, Max length = 256.
         /// </summary>
         [Required]
         [MaxLength(256)]
@@ -112,42 +107,48 @@ namespace Jellyfin.Data.Entities
         public string Type { get; set; }
 
         /// <summary>
-        /// Required
+        /// Gets or sets the user id.
+        /// Required.
         /// </summary>
         [Required]
         public Guid UserId { get; set; }
 
         /// <summary>
-        /// Max length = 256
+        /// Gets or sets the item id.
+        /// Max length = 256.
         /// </summary>
         [MaxLength(256)]
         [StringLength(256)]
         public string ItemId { get; set; }
 
         /// <summary>
-        /// Required
+        /// Gets or sets the date created. This should be in UTC.
+        /// Required.
         /// </summary>
         [Required]
         public DateTime DateCreated { get; set; }
 
         /// <summary>
-        /// Required
+        /// Gets or sets the log severity. Default is <see cref="LogLevel.Trace"/>.
+        /// Required.
         /// </summary>
         [Required]
-        public Microsoft.Extensions.Logging.LogLevel LogSeverity { get; set; }
+        public LogLevel LogSeverity { get; set; }
 
         /// <summary>
-        /// Required, ConcurrenyToken
+        /// Gets or sets the row version.
+        /// Required, ConcurrencyToken.
         /// </summary>
         [ConcurrencyCheck]
         [Required]
         public uint RowVersion { get; set; }
 
+        partial void Init();
+
+        /// <inheritdoc />
         public void OnSavingChanges()
         {
             RowVersion++;
         }
-
     }
 }
-

+ 1 - 5
Jellyfin.Data/Jellyfin.Data.csproj

@@ -17,14 +17,10 @@
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
   </ItemGroup>
-  
+
   <ItemGroup>
     <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.3" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.3" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.3">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
-    </PackageReference>
   </ItemGroup>
 
 </Project>

+ 8 - 9
Jellyfin.Server.Implementations/Activity/ActivityManager.cs

@@ -1,5 +1,4 @@
 using System;
-using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
@@ -14,7 +13,7 @@ namespace Jellyfin.Server.Implementations.Activity
     /// </summary>
     public class ActivityManager : IActivityManager
     {
-        private JellyfinDbProvider _provider;
+        private readonly JellyfinDbProvider _provider;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ActivityManager"/> class.
@@ -50,31 +49,31 @@ namespace Jellyfin.Server.Implementations.Activity
 
         /// <inheritdoc/>
         public QueryResult<ActivityLogEntry> GetPagedResult(
-            Func<IQueryable<ActivityLog>, IEnumerable<ActivityLog>> func,
+            Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>> func,
             int? startIndex,
             int? limit)
         {
             using var dbContext = _provider.CreateContext();
 
-            var result = func.Invoke(dbContext.ActivityLogs).AsQueryable();
+            var query = func(dbContext.ActivityLogs.OrderByDescending(entry => entry.DateCreated));
 
             if (startIndex.HasValue)
             {
-                result = result.Where(entry => entry.Id >= startIndex.Value);
+                query = query.Skip(startIndex.Value);
             }
 
             if (limit.HasValue)
             {
-                result = result.OrderByDescending(entry => entry.DateCreated).Take(limit.Value);
+                query = query.Take(limit.Value);
             }
 
             // This converts the objects from the new database model to the old for compatibility with the existing API.
-            var list = result.Select(entry => ConvertToOldModel(entry)).ToList();
+            var list = query.Select(ConvertToOldModel).ToList();
 
-            return new QueryResult<ActivityLogEntry>()
+            return new QueryResult<ActivityLogEntry>
             {
                 Items = list,
-                TotalRecordCount = list.Count
+                TotalRecordCount = func(dbContext.ActivityLogs).Count()
             };
         }
 

+ 8 - 0
Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj

@@ -27,6 +27,14 @@
     <Compile Remove="Migrations\20200504195424_UserSchema.Designer.cs" />
   </ItemGroup>
 
+  <ItemGroup>
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.3">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
+  </ItemGroup>
+
   <ItemGroup>
     <ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" />
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />

+ 3 - 2
Jellyfin.Server.Implementations/JellyfinDb.cs

@@ -107,9 +107,10 @@ namespace Jellyfin.Server.Implementations
 
         public override int SaveChanges()
         {
-            foreach (var entity in ChangeTracker.Entries().Where(e => e.State == EntityState.Modified))
+            foreach (var saveEntity in ChangeTracker.Entries()
+                .Where(e => e.State == EntityState.Modified)
+                .OfType<ISavingChanges>())
             {
-                var saveEntity = entity.Entity as ISavingChanges;
                 saveEntity.OnSavingChanges();
             }
 

+ 1 - 1
Jellyfin.Server.Implementations/JellyfinDbProvider.cs

@@ -27,7 +27,7 @@ namespace Jellyfin.Server.Implementations
         /// <returns>The newly created context.</returns>
         public JellyfinDb CreateContext()
         {
-            return _serviceProvider.GetService<JellyfinDb>();
+            return _serviceProvider.GetRequiredService<JellyfinDb>();
         }
     }
 }

+ 4 - 5
Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.Designer.cs → Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs

@@ -1,5 +1,4 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1601
+#pragma warning disable CS1591
 
 // <auto-generated />
 using System;
@@ -12,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 namespace Jellyfin.Server.Implementations.Migrations
 {
     [DbContext(typeof(JellyfinDb))]
-    [Migration("20200502231229_InitialSchema")]
-    partial class InitialSchema
+    [Migration("20200514181226_AddActivityLog")]
+    partial class AddActivityLog
     {
         protected override void BuildTargetModel(ModelBuilder modelBuilder)
         {
@@ -65,7 +64,7 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasKey("Id");
 
-                    b.ToTable("ActivityLog");
+                    b.ToTable("ActivityLogs");
                 });
 #pragma warning restore 612, 618
         }

+ 5 - 5
Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.cs → Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs

@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
 #pragma warning disable SA1601
 
 using System;
@@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
 
 namespace Jellyfin.Server.Implementations.Migrations
 {
-    public partial class InitialSchema : Migration
+    public partial class AddActivityLog : Migration
     {
         protected override void Up(MigrationBuilder migrationBuilder)
         {
@@ -14,7 +14,7 @@ namespace Jellyfin.Server.Implementations.Migrations
                 name: "jellyfin");
 
             migrationBuilder.CreateTable(
-                name: "ActivityLog",
+                name: "ActivityLogs",
                 schema: "jellyfin",
                 columns: table => new
                 {
@@ -32,14 +32,14 @@ namespace Jellyfin.Server.Implementations.Migrations
                 },
                 constraints: table =>
                 {
-                    table.PrimaryKey("PK_ActivityLog", x => x.Id);
+                    table.PrimaryKey("PK_ActivityLogs", x => x.Id);
                 });
         }
 
         protected override void Down(MigrationBuilder migrationBuilder)
         {
             migrationBuilder.DropTable(
-                name: "ActivityLog",
+                name: "ActivityLogs",
                 schema: "jellyfin");
         }
     }

+ 0 - 3
Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs

@@ -1,6 +1,3 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1601
-
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Design;
 

+ 1 - 3
Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs

@@ -1,9 +1,7 @@
 // <auto-generated />
 using System;
-using Jellyfin.Server.Implementations;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 
 namespace Jellyfin.Server.Implementations.Migrations
 {
@@ -60,7 +58,7 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasKey("Id");
 
-                    b.ToTable("ActivityLog");
+                    b.ToTable("ActivityLogs");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Group", b =>

+ 14 - 0
Jellyfin.Server/CoreAppHost.cs

@@ -1,12 +1,17 @@
 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 MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.IO;
+using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 
@@ -56,6 +61,15 @@ namespace Jellyfin.Server
                 Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
             }
 
+            // TODO: Set up scoping and use AddDbContextPool
+            serviceCollection.AddDbContext<JellyfinDb>(
+                    options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
+                    ServiceLifetime.Transient);
+
+            serviceCollection.AddSingleton<JellyfinDbProvider>();
+
+            serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
+
             base.RegisterServices(serviceCollection);
         }
 

+ 1 - 7
Jellyfin.Server/Jellyfin.Server.csproj

@@ -13,9 +13,6 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <Nullable>enable</Nullable>
-
-    <!-- Used for generating migrations for EF Core -->
-    <GenerateRuntimeConfigurationFiles>True</GenerateRuntimeConfigurationFiles>
   </PropertyGroup>
 
   <ItemGroup>
@@ -45,10 +42,6 @@
   <ItemGroup>
     <PackageReference Include="CommandLineParser" Version="2.7.82" />
     <PackageReference Include="Json.Net" Version="1.0.22" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
-    </PackageReference>
     <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.3" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" />
     <PackageReference Include="prometheus-net" Version="3.5.0" />
@@ -68,6 +61,7 @@
     <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
     <ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
     <ProjectReference Include="..\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj" />
+    <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
   </ItemGroup>
 
 </Project>

+ 1 - 0
Jellyfin.Server/Migrations/MigrationRunner.cs

@@ -19,6 +19,7 @@ namespace Jellyfin.Server.Migrations
             typeof(Routines.DisableTranscodingThrottling),
             typeof(Routines.CreateUserLoggingConfigFile),
             typeof(Routines.MigrateActivityLogDb),
+            typeof(Routines.RemoveDuplicateExtras),
             typeof(Routines.MigrateUserDb)
         };
 

+ 50 - 37
Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs

@@ -1,7 +1,7 @@
-#pragma warning disable CS1591
-
 using System;
+using System.Collections.Generic;
 using System.IO;
+using System.Linq;
 using Emby.Server.Implementations.Data;
 using Jellyfin.Data.Entities;
 using Jellyfin.Server.Implementations;
@@ -12,6 +12,9 @@ using SQLitePCL.pretty;
 
 namespace Jellyfin.Server.Migrations.Routines
 {
+    /// <summary>
+    /// The migration routine for migrating the activity log database to EF Core.
+    /// </summary>
     public class MigrateActivityLogDb : IMigrationRoutine
     {
         private const string DbFilename = "activitylog.db";
@@ -20,6 +23,12 @@ namespace Jellyfin.Server.Migrations.Routines
         private readonly JellyfinDbProvider _provider;
         private readonly IServerApplicationPaths _paths;
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MigrateActivityLogDb"/> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        /// <param name="paths">The server application paths.</param>
+        /// <param name="provider">The database provider.</param>
         public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider)
         {
             _logger = logger;
@@ -27,19 +36,35 @@ namespace Jellyfin.Server.Migrations.Routines
             _paths = paths;
         }
 
+        /// <inheritdoc/>
         public Guid Id => Guid.Parse("3793eb59-bc8c-456c-8b9f-bd5a62a42978");
 
+        /// <inheritdoc/>
         public string Name => "MigrateActivityLogDatabase";
 
+        /// <inheritdoc/>
         public void Perform()
         {
+            var logLevelDictionary = new Dictionary<string, LogLevel>(StringComparer.OrdinalIgnoreCase)
+            {
+                { "None", LogLevel.None },
+                { "Trace", LogLevel.Trace },
+                { "Debug", LogLevel.Debug },
+                { "Information", LogLevel.Information },
+                { "Info", LogLevel.Information },
+                { "Warn", LogLevel.Warning },
+                { "Warning", LogLevel.Warning },
+                { "Error", LogLevel.Error },
+                { "Critical", LogLevel.Critical }
+            };
+
             var dataPath = _paths.DataPath;
             using (var connection = SQLite3.Open(
                 Path.Combine(dataPath, DbFilename),
                 ConnectionFlags.ReadOnly,
                 null))
             {
-                _logger.LogInformation("Migrating the database may take a while, do not stop Jellyfin.");
+                _logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin.");
                 using var dbContext = _provider.CreateContext();
 
                 var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id ASC");
@@ -51,14 +76,21 @@ namespace Jellyfin.Server.Migrations.Routines
                 dbContext.Database.ExecuteSqlRaw("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'ActivityLog';");
                 dbContext.SaveChanges();
 
-                foreach (var entry in queryResult)
+                var newEntries = queryResult.Select(entry =>
                 {
+                    if (!logLevelDictionary.TryGetValue(entry[8].ToString(), out var severity))
+                    {
+                        severity = LogLevel.Trace;
+                    }
+
                     var newEntry = new ActivityLog(
                         entry[1].ToString(),
                         entry[4].ToString(),
-                        entry[6].SQLiteType == SQLiteType.Null ? Guid.Empty : Guid.Parse(entry[6].ToString()),
-                        entry[7].ReadDateTime(),
-                        ParseLogLevel(entry[8].ToString()));
+                        entry[6].SQLiteType == SQLiteType.Null ? Guid.Empty : Guid.Parse(entry[6].ToString()))
+                    {
+                        DateCreated = entry[7].ReadDateTime(),
+                        LogSeverity = severity
+                    };
 
                     if (entry[2].SQLiteType != SQLiteType.Null)
                     {
@@ -75,46 +107,27 @@ namespace Jellyfin.Server.Migrations.Routines
                         newEntry.ItemId = entry[5].ToString();
                     }
 
-                    dbContext.ActivityLogs.Add(newEntry);
-                    dbContext.SaveChanges();
-                }
+                    return newEntry;
+                });
+
+                dbContext.ActivityLogs.AddRange(newEntries);
+                dbContext.SaveChanges();
             }
 
             try
             {
                 File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old"));
+
+                var journalPath = Path.Combine(dataPath, DbFilename + "-journal");
+                if (File.Exists(journalPath))
+                {
+                    File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal"));
+                }
             }
             catch (IOException e)
             {
                 _logger.LogError(e, "Error renaming legacy activity log database to 'activitylog.db.old'");
             }
         }
-
-        private LogLevel ParseLogLevel(string entry)
-        {
-            if (string.Equals(entry, "Debug", StringComparison.OrdinalIgnoreCase))
-            {
-                return LogLevel.Debug;
-            }
-
-            if (string.Equals(entry, "Information", StringComparison.OrdinalIgnoreCase)
-                || string.Equals(entry, "Info", StringComparison.OrdinalIgnoreCase))
-            {
-                return LogLevel.Information;
-            }
-
-            if (string.Equals(entry, "Warning", StringComparison.OrdinalIgnoreCase)
-                || string.Equals(entry, "Warn", StringComparison.OrdinalIgnoreCase))
-            {
-                return LogLevel.Warning;
-            }
-
-            if (string.Equals(entry, "Error", StringComparison.OrdinalIgnoreCase))
-            {
-                return LogLevel.Error;
-            }
-
-            return LogLevel.Trace;
-        }
     }
 }

+ 79 - 0
Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs

@@ -0,0 +1,79 @@
+using System;
+using System.Globalization;
+using System.IO;
+
+using MediaBrowser.Controller;
+using Microsoft.Extensions.Logging;
+using SQLitePCL.pretty;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+    /// <summary>
+    /// Remove duplicate entries which were caused by a bug where a file was considered to be an "Extra" to itself.
+    /// </summary>
+    internal class RemoveDuplicateExtras : IMigrationRoutine
+    {
+        private const string DbFilename = "library.db";
+        private readonly ILogger _logger;
+        private readonly IServerApplicationPaths _paths;
+
+        public RemoveDuplicateExtras(ILogger<RemoveDuplicateExtras> logger, IServerApplicationPaths paths)
+        {
+            _logger = logger;
+            _paths = paths;
+        }
+
+        /// <inheritdoc/>
+        public Guid Id => Guid.Parse("{ACBE17B7-8435-4A83-8B64-6FCF162CB9BD}");
+
+        /// <inheritdoc/>
+        public string Name => "RemoveDuplicateExtras";
+
+        /// <inheritdoc/>
+        public void Perform()
+        {
+            var dataPath = _paths.DataPath;
+            var dbPath = Path.Combine(dataPath, DbFilename);
+            using (var connection = SQLite3.Open(
+                dbPath,
+                ConnectionFlags.ReadWrite,
+                null))
+            {
+                // Query the database for the ids of duplicate extras
+                var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'");
+                var bads = string.Join(", ", queryResult.SelectScalarString());
+
+                // Do nothing if no duplicate extras were detected
+                if (bads.Length == 0)
+                {
+                    _logger.LogInformation("No duplicate extras detected, skipping migration.");
+                    return;
+                }
+
+                // Back up the database before deleting any entries
+                for (int i = 1; ; i++)
+                {
+                    var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
+                    if (!File.Exists(bakPath))
+                    {
+                        try
+                        {
+                            File.Copy(dbPath, bakPath);
+                            _logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
+                            break;
+                        }
+                        catch (Exception ex)
+                        {
+                            _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
+                            throw;
+                        }
+                    }
+                }
+
+                // Delete all duplicate extras
+                _logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads);
+                connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')");
+            }
+        }
+    }
+}

+ 2 - 5
Jellyfin.Server/Program.cs

@@ -10,14 +10,11 @@ using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
 using CommandLine;
-using Emby.Drawing;
 using Emby.Server.Implementations;
 using Emby.Server.Implementations.HttpServer;
 using Emby.Server.Implementations.IO;
 using Emby.Server.Implementations.Networking;
-using Jellyfin.Drawing.Skia;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.WebDashboard.Api;
 using Microsoft.AspNetCore.Hosting;
@@ -297,7 +294,7 @@ namespace Jellyfin.Server
                         {
                             _logger.LogInformation("Kestrel listening on {IpAddress}", address);
                             options.Listen(address, appHost.HttpPort);
-                            if (appHost.EnableHttps && appHost.Certificate != null)
+                            if (appHost.ListenWithHttps)
                             {
                                 options.Listen(address, appHost.HttpsPort, listenOptions =>
                                 {
@@ -327,7 +324,7 @@ namespace Jellyfin.Server
                         _logger.LogInformation("Kestrel listening on all interfaces");
                         options.ListenAnyIP(appHost.HttpPort);
 
-                        if (appHost.EnableHttps && appHost.Certificate != null)
+                        if (appHost.ListenWithHttps)
                         {
                             options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
                             {

+ 0 - 1
Jellyfin.Server/Startup.cs

@@ -64,7 +64,6 @@ namespace Jellyfin.Server
             app.UseResponseCompression();
 
             // TODO app.UseMiddleware<WebSocketMiddleware>();
-            app.Use(serverApplicationHost.ExecuteWebsocketHandlerAsync);
 
             // TODO use when old API is removed: app.UseAuthentication();
             app.UseJellyfinApiSwagger();

+ 2 - 2
MediaBrowser.Api/BaseApiService.cs

@@ -22,7 +22,7 @@ namespace MediaBrowser.Api
     public abstract class BaseApiService : IService, IRequiresRequest
     {
         public BaseApiService(
-            ILogger logger,
+            ILogger<BaseApiService> logger,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory)
         {
@@ -35,7 +35,7 @@ namespace MediaBrowser.Api
         /// Gets the logger.
         /// </summary>
         /// <value>The logger.</value>
-        protected ILogger Logger { get; }
+        protected ILogger<BaseApiService> Logger { get; }
 
         /// <summary>
         /// Gets or sets the server configuration manager.

+ 0 - 36
MediaBrowser.Api/Devices/DeviceService.cs

@@ -1,5 +1,4 @@
 using System.IO;
-using System.Threading.Tasks;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Net;
@@ -116,11 +115,6 @@ namespace MediaBrowser.Api.Devices
             return _deviceManager.GetDeviceOptions(request.Id);
         }
 
-        public object Get(GetCameraUploads request)
-        {
-            return ToOptimizedResult(_deviceManager.GetCameraUploadHistory(request.DeviceId));
-        }
-
         public void Delete(DeleteDevice request)
         {
             var sessions = _authRepo.Get(new AuthenticationInfoQuery
@@ -134,35 +128,5 @@ namespace MediaBrowser.Api.Devices
                 _sessionManager.Logout(session);
             }
         }
-
-        public Task Post(PostCameraUpload request)
-        {
-            var deviceId = Request.QueryString["DeviceId"];
-            var album = Request.QueryString["Album"];
-            var id = Request.QueryString["Id"];
-            var name = Request.QueryString["Name"];
-            var req = Request.Response.HttpContext.Request;
-
-            if (req.HasFormContentType)
-            {
-                var file = req.Form.Files.Count == 0 ? null : req.Form.Files[0];
-
-                return _deviceManager.AcceptCameraUpload(deviceId, file.OpenReadStream(), new LocalFileInfo
-                {
-                    MimeType = file.ContentType,
-                    Album = album,
-                    Name = name,
-                    Id = id
-                });
-            }
-
-            return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo
-            {
-                MimeType = Request.ContentType,
-                Album = album,
-                Name = name,
-                Id = id
-            });
-        }
     }
 }

+ 6 - 4
MediaBrowser.Api/Library/LibraryService.cs

@@ -319,11 +319,14 @@ namespace MediaBrowser.Api.Library
         private readonly ILocalizationManager _localization;
         private readonly ILibraryMonitor _libraryMonitor;
 
+        private readonly ILogger<MoviesService> _moviesServiceLogger;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="LibraryService" /> class.
         /// </summary>
         public LibraryService(
             ILogger<LibraryService> logger,
+            ILogger<MoviesService> moviesServiceLogger,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
             IProviderManager providerManager,
@@ -344,6 +347,7 @@ namespace MediaBrowser.Api.Library
             _activityManager = activityManager;
             _localization = localization;
             _libraryMonitor = libraryMonitor;
+            _moviesServiceLogger = moviesServiceLogger;
         }
 
         private string[] GetRepresentativeItemTypes(string contentType)
@@ -543,7 +547,7 @@ namespace MediaBrowser.Api.Library
             if (item is Movie || (program != null && program.IsMovie) || item is Trailer)
             {
                 return new MoviesService(
-                    Logger,
+                    _moviesServiceLogger,
                     ServerConfigurationManager,
                     ResultFactory,
                     _userManager,
@@ -762,9 +766,7 @@ namespace MediaBrowser.Api.Library
                 _activityManager.Create(new Jellyfin.Data.Entities.ActivityLog(
                     string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
                     "UserDownloadingContent",
-                    auth.UserId,
-                    DateTime.UtcNow,
-                    LogLevel.Trace)
+                    auth.UserId)
                 {
                     ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device),
                 });

+ 1 - 1
MediaBrowser.Api/Movies/MoviesService.cs

@@ -82,7 +82,7 @@ namespace MediaBrowser.Api.Movies
         /// Initializes a new instance of the <see cref="MoviesService" /> class.
         /// </summary>
         public MoviesService(
-            ILogger logger,
+            ILogger<MoviesService> logger,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
             IUserManager userManager,

+ 9 - 3
MediaBrowser.Api/Movies/TrailersService.cs

@@ -33,13 +33,18 @@ namespace MediaBrowser.Api.Movies
         /// </summary>
         private readonly ILibraryManager _libraryManager;
 
+        /// <summary>
+        /// The logger for the created <see cref="ItemsService"/> instances.
+        /// </summary>
+        private readonly ILogger<ItemsService> _logger;
+
         private readonly IDtoService _dtoService;
         private readonly ILocalizationManager _localizationManager;
         private readonly IJsonSerializer _json;
         private readonly IAuthorizationContext _authContext;
 
         public TrailersService(
-            ILogger<TrailersService> logger,
+            ILoggerFactory loggerFactory,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
             IUserManager userManager,
@@ -48,7 +53,7 @@ namespace MediaBrowser.Api.Movies
             ILocalizationManager localizationManager,
             IJsonSerializer json,
             IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
+            : base(loggerFactory.CreateLogger<TrailersService>(), serverConfigurationManager, httpResultFactory)
         {
             _userManager = userManager;
             _libraryManager = libraryManager;
@@ -56,6 +61,7 @@ namespace MediaBrowser.Api.Movies
             _localizationManager = localizationManager;
             _json = json;
             _authContext = authContext;
+            _logger = loggerFactory.CreateLogger<ItemsService>();
         }
 
         public object Get(Getrailers request)
@@ -66,7 +72,7 @@ namespace MediaBrowser.Api.Movies
             getItems.IncludeItemTypes = "Trailer";
 
             return new ItemsService(
-                Logger,
+                _logger,
                 ServerConfigurationManager,
                 ResultFactory,
                 _userManager,

+ 1 - 1
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -82,7 +82,7 @@ namespace MediaBrowser.Api.Playback
         /// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
         /// </summary>
         protected BaseStreamingService(
-            ILogger logger,
+            ILogger<BaseStreamingService> logger,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
             IUserManager userManager,

+ 1 - 1
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -25,7 +25,7 @@ namespace MediaBrowser.Api.Playback.Hls
     public abstract class BaseHlsService : BaseStreamingService
     {
         public BaseHlsService(
-            ILogger logger,
+            ILogger<BaseHlsService> logger,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
             IUserManager userManager,

+ 1 - 1
MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

@@ -94,7 +94,7 @@ namespace MediaBrowser.Api.Playback.Hls
     public class DynamicHlsService : BaseHlsService
     {
         public DynamicHlsService(
-            ILogger logger,
+            ILogger<DynamicHlsService> logger,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
             IUserManager userManager,

+ 1 - 1
MediaBrowser.Api/Playback/MediaInfoService.cs

@@ -80,7 +80,7 @@ namespace MediaBrowser.Api.Playback
         private readonly IAuthorizationContext _authContext;
 
         public MediaInfoService(
-            ILogger logger,
+            ILogger<MediaInfoService> logger,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
             IMediaSourceManager mediaSourceManager,

+ 1 - 1
MediaBrowser.Api/Playback/Progressive/AudioService.cs

@@ -33,7 +33,7 @@ namespace MediaBrowser.Api.Playback.Progressive
     public class AudioService : BaseProgressiveStreamingService
     {
         public AudioService(
-            ILogger logger,
+            ILogger<AudioService> logger,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
             IHttpClient httpClient,

+ 1 - 1
MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs

@@ -28,7 +28,7 @@ namespace MediaBrowser.Api.Playback.Progressive
         protected IHttpClient HttpClient { get; private set; }
 
         public BaseProgressiveStreamingService(
-            ILogger logger,
+            ILogger<BaseProgressiveStreamingService> logger,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
             IHttpClient httpClient,

+ 6 - 3
MediaBrowser.Api/Playback/UniversalAudioService.cs

@@ -75,9 +75,11 @@ namespace MediaBrowser.Api.Playback
     public class UniversalAudioService : BaseApiService
     {
         private readonly EncodingHelper _encodingHelper;
+        private readonly ILoggerFactory _loggerFactory;
 
         public UniversalAudioService(
             ILogger<UniversalAudioService> logger,
+            ILoggerFactory loggerFactory,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
             IHttpClient httpClient,
@@ -108,6 +110,7 @@ namespace MediaBrowser.Api.Playback
             AuthorizationContext = authorizationContext;
             NetworkManager = networkManager;
             _encodingHelper = encodingHelper;
+            _loggerFactory = loggerFactory;
         }
 
         protected IHttpClient HttpClient { get; private set; }
@@ -233,7 +236,7 @@ namespace MediaBrowser.Api.Playback
             AuthorizationContext.GetAuthorizationInfo(Request).DeviceId = request.DeviceId;
 
             var mediaInfoService = new MediaInfoService(
-                Logger,
+                _loggerFactory.CreateLogger<MediaInfoService>(),
                 ServerConfigurationManager,
                 ResultFactory,
                 MediaSourceManager,
@@ -277,7 +280,7 @@ namespace MediaBrowser.Api.Playback
             if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
             {
                 var service = new DynamicHlsService(
-                    Logger,
+                    _loggerFactory.CreateLogger<DynamicHlsService>(),
                     ServerConfigurationManager,
                     ResultFactory,
                     UserManager,
@@ -331,7 +334,7 @@ namespace MediaBrowser.Api.Playback
             else
             {
                 var service = new AudioService(
-                    Logger,
+                    _loggerFactory.CreateLogger<AudioService>(),
                     ServerConfigurationManager,
                     ResultFactory,
                     HttpClient,

+ 22 - 21
MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs

@@ -31,46 +31,46 @@ namespace MediaBrowser.Api.Sessions
         {
             _sessionManager = sessionManager;
 
-            _sessionManager.SessionStarted += _sessionManager_SessionStarted;
-            _sessionManager.SessionEnded += _sessionManager_SessionEnded;
-            _sessionManager.PlaybackStart += _sessionManager_PlaybackStart;
-            _sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped;
-            _sessionManager.PlaybackProgress += _sessionManager_PlaybackProgress;
-            _sessionManager.CapabilitiesChanged += _sessionManager_CapabilitiesChanged;
-            _sessionManager.SessionActivity += _sessionManager_SessionActivity;
+            _sessionManager.SessionStarted += OnSessionManagerSessionStarted;
+            _sessionManager.SessionEnded += OnSessionManagerSessionEnded;
+            _sessionManager.PlaybackStart += OnSessionManagerPlaybackStart;
+            _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped;
+            _sessionManager.PlaybackProgress += OnSessionManagerPlaybackProgress;
+            _sessionManager.CapabilitiesChanged += OnSessionManagerCapabilitiesChanged;
+            _sessionManager.SessionActivity += OnSessionManagerSessionActivity;
         }
 
-        void _sessionManager_SessionActivity(object sender, SessionEventArgs e)
+        private void OnSessionManagerSessionActivity(object sender, SessionEventArgs e)
         {
             SendData(false);
         }
 
-        void _sessionManager_CapabilitiesChanged(object sender, SessionEventArgs e)
+        private void OnSessionManagerCapabilitiesChanged(object sender, SessionEventArgs e)
         {
             SendData(true);
         }
 
-        void _sessionManager_PlaybackProgress(object sender, PlaybackProgressEventArgs e)
+        private void OnSessionManagerPlaybackProgress(object sender, PlaybackProgressEventArgs e)
         {
             SendData(!e.IsAutomated);
         }
 
-        void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e)
+        private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
         {
             SendData(true);
         }
 
-        void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e)
+        private void OnSessionManagerPlaybackStart(object sender, PlaybackProgressEventArgs e)
         {
             SendData(true);
         }
 
-        void _sessionManager_SessionEnded(object sender, SessionEventArgs e)
+        private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
         {
             SendData(true);
         }
 
-        void _sessionManager_SessionStarted(object sender, SessionEventArgs e)
+        private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
         {
             SendData(true);
         }
@@ -84,15 +84,16 @@ namespace MediaBrowser.Api.Sessions
             return Task.FromResult(_sessionManager.Sessions);
         }
 
+        /// <inheritdoc />
         protected override void Dispose(bool dispose)
         {
-            _sessionManager.SessionStarted -= _sessionManager_SessionStarted;
-            _sessionManager.SessionEnded -= _sessionManager_SessionEnded;
-            _sessionManager.PlaybackStart -= _sessionManager_PlaybackStart;
-            _sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped;
-            _sessionManager.PlaybackProgress -= _sessionManager_PlaybackProgress;
-            _sessionManager.CapabilitiesChanged -= _sessionManager_CapabilitiesChanged;
-            _sessionManager.SessionActivity -= _sessionManager_SessionActivity;
+            _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
+            _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
+            _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
+            _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
+            _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
+            _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
+            _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
 
             base.Dispose(dispose);
         }

+ 6 - 1
MediaBrowser.Api/System/ActivityLogService.cs

@@ -1,5 +1,7 @@
 using System;
 using System.Globalization;
+using System.Linq;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Activity;
@@ -53,7 +55,10 @@ namespace MediaBrowser.Api.System
                 (DateTime?)null :
                 DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
 
-            var result = _activityManager.GetPagedResult(request.StartIndex, request.Limit);
+            var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
+                entries => entries.Where(entry => entry.DateCreated >= minDate));
+
+            var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit);
 
             return ToOptimizedResult(result);
         }

+ 8 - 8
MediaBrowser.Api/System/ActivityLogWebSocketListener.cs

@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Activity;
@@ -10,7 +10,7 @@ namespace MediaBrowser.Api.System
     /// <summary>
     /// Class SessionInfoWebSocketListener
     /// </summary>
-    public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<List<ActivityLogEntry>, WebSocketListenerState>
+    public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
     {
         /// <summary>
         /// Gets the name.
@@ -26,10 +26,10 @@ namespace MediaBrowser.Api.System
         public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager) : base(logger)
         {
             _activityManager = activityManager;
-            _activityManager.EntryCreated += _activityManager_EntryCreated;
+            _activityManager.EntryCreated += OnEntryCreated;
         }
 
-        void _activityManager_EntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
+        private void OnEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
         {
             SendData(true);
         }
@@ -38,15 +38,15 @@ namespace MediaBrowser.Api.System
         /// Gets the data to send.
         /// </summary>
         /// <returns>Task{SystemInfo}.</returns>
-        protected override Task<List<ActivityLogEntry>> GetDataToSend()
+        protected override Task<ActivityLogEntry[]> GetDataToSend()
         {
-            return Task.FromResult(new List<ActivityLogEntry>());
+            return Task.FromResult(Array.Empty<ActivityLogEntry>());
         }
 
-
+        /// <inheritdoc />
         protected override void Dispose(bool dispose)
         {
-            _activityManager.EntryCreated -= _activityManager_EntryCreated;
+            _activityManager.EntryCreated -= OnEntryCreated;
 
             base.Dispose(dispose);
         }

+ 1 - 1
MediaBrowser.Api/UserLibrary/ArtistsService.cs

@@ -51,7 +51,7 @@ namespace MediaBrowser.Api.UserLibrary
     public class ArtistsService : BaseItemsByNameService<MusicArtist>
     {
         public ArtistsService(
-            ILogger<GenresService> logger,
+            ILogger<ArtistsService> logger,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
             IUserManager userManager,

+ 1 - 1
MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs

@@ -28,7 +28,7 @@ namespace MediaBrowser.Api.UserLibrary
         /// <param name="userDataRepository">The user data repository.</param>
         /// <param name="dtoService">The dto service.</param>
         protected BaseItemsByNameService(
-            ILogger logger,
+            ILogger<BaseItemsByNameService<TItemType>> logger,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
             IUserManager userManager,

+ 1 - 1
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -60,7 +60,7 @@ namespace MediaBrowser.Api.UserLibrary
         /// <param name="localization">The localization.</param>
         /// <param name="dtoService">The dto service.</param>
         public ItemsService(
-            ILogger logger,
+            ILogger<ItemsService> logger,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
             IUserManager userManager,

+ 0 - 23
MediaBrowser.Controller/Devices/IDeviceManager.cs

@@ -1,6 +1,4 @@
 using System;
-using System.IO;
-using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Model.Devices;
 using MediaBrowser.Model.Events;
@@ -11,11 +9,6 @@ namespace MediaBrowser.Controller.Devices
 {
     public interface IDeviceManager
     {
-        /// <summary>
-        /// Occurs when [camera image uploaded].
-        /// </summary>
-        event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
-
         /// <summary>
         /// Saves the capabilities.
         /// </summary>
@@ -45,22 +38,6 @@ namespace MediaBrowser.Controller.Devices
         /// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
         QueryResult<DeviceInfo> GetDevices(DeviceQuery query);
 
-        /// <summary>
-        /// Gets the upload history.
-        /// </summary>
-        /// <param name="deviceId">The device identifier.</param>
-        /// <returns>ContentUploadHistory.</returns>
-        ContentUploadHistory GetCameraUploadHistory(string deviceId);
-
-        /// <summary>
-        /// Accepts the upload.
-        /// </summary>
-        /// <param name="deviceId">The device identifier.</param>
-        /// <param name="stream">The stream.</param>
-        /// <param name="file">The file.</param>
-        /// <returns>Task.</returns>
-        Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file);
-
         /// <summary>
         /// Determines whether this instance [can access device] the specified user identifier.
         /// </summary>

+ 35 - 22
MediaBrowser.Controller/IServerApplicationHost.cs

@@ -39,10 +39,9 @@ namespace MediaBrowser.Controller
         int HttpsPort { get; }
 
         /// <summary>
-        /// Gets a value indicating whether [supports HTTPS].
+        /// Gets a value indicating whether the server should listen on an HTTPS port.
         /// </summary>
-        /// <value><c>true</c> if [supports HTTPS]; otherwise, <c>false</c>.</value>
-        bool EnableHttps { get; }
+        bool ListenWithHttps { get; }
 
         /// <summary>
         /// Gets a value indicating whether this instance has update available.
@@ -57,34 +56,50 @@ namespace MediaBrowser.Controller
         string FriendlyName { get; }
 
         /// <summary>
-        /// Gets the local ip address.
+        /// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request
+        /// to the API that should exist at the address.
         /// </summary>
-        /// <value>The local ip address.</value>
+        /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
+        /// <returns>A list containing all the local IP addresses of the server.</returns>
         Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken);
 
         /// <summary>
-        /// Gets the local API URL.
+        /// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured
+        /// IP address that can be found via <see cref="GetLocalIpAddresses"/>. HTTPS will be preferred when available.
         /// </summary>
-        /// <param name="cancellationToken">Token to cancel the request if needed.</param>
-        /// <param name="forceHttp">Whether to force usage of plain HTTP protocol.</param>
-        /// <value>The local API URL.</value>
-        Task<string> GetLocalApiUrl(CancellationToken cancellationToken, bool forceHttp = false);
+        /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
+        /// <returns>The server URL.</returns>
+        Task<string> GetLocalApiUrl(CancellationToken cancellationToken);
 
         /// <summary>
-        /// Gets the local API URL.
+        /// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1)
+        /// over HTTP (not HTTPS).
         /// </summary>
-        /// <param name="hostname">The hostname.</param>
-        /// <param name="forceHttp">Whether to force usage of plain HTTP protocol.</param>
-        /// <returns>The local API URL.</returns>
-        string GetLocalApiUrl(ReadOnlySpan<char> hostname, bool forceHttp = false);
+        /// <returns>The API URL.</returns>
+        string GetLoopbackHttpApiUrl();
 
         /// <summary>
-        /// Gets the local API URL.
+        /// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available.
         /// </summary>
-        /// <param name="address">The IP address.</param>
-        /// <param name="forceHttp">Whether to force usage of plain HTTP protocol.</param>
-        /// <returns>The local API URL.</returns>
-        string GetLocalApiUrl(IPAddress address, bool forceHttp = false);
+        /// <param name="address">The IP address to use as the hostname in the URL.</param>
+        /// <returns>The API URL.</returns>
+        string GetLocalApiUrl(IPAddress address);
+
+        /// <summary>
+        /// Gets a local (LAN) URL that can be used to access the API.
+        /// Note: if passing non-null scheme or port it is up to the caller to ensure they form the correct pair.
+        /// </summary>
+        /// <param name="hostname">The hostname to use in the URL.</param>
+        /// <param name="scheme">
+        /// The scheme to use for the URL. If null, the scheme will be selected automatically,
+        /// preferring HTTPS, if available.
+        /// </param>
+        /// <param name="port">
+        /// The port to use for the URL. If null, the port will be selected automatically,
+        /// preferring the HTTPS port, if available.
+        /// </param>
+        /// <returns>The API URL.</returns>
+        string GetLocalApiUrl(ReadOnlySpan<char> hostname, string scheme = null, int? port = null);
 
         /// <summary>
         /// Open a URL in an external browser window.
@@ -101,7 +116,5 @@ namespace MediaBrowser.Controller
         string ReverseVirtualPath(string path);
 
         Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next);
-
-        Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next);
     }
 }

+ 5 - 7
MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs

@@ -77,8 +77,6 @@ namespace MediaBrowser.Controller.Net
             return Task.CompletedTask;
         }
 
-        protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
         /// <summary>
         /// Starts sending messages over a web socket
         /// </summary>
@@ -87,12 +85,12 @@ namespace MediaBrowser.Controller.Net
         {
             var vals = message.Data.Split(',');
 
-            var dueTimeMs = long.Parse(vals[0], UsCulture);
-            var periodMs = long.Parse(vals[1], UsCulture);
+            var dueTimeMs = long.Parse(vals[0], CultureInfo.InvariantCulture);
+            var periodMs = long.Parse(vals[1], CultureInfo.InvariantCulture);
 
             var cancellationTokenSource = new CancellationTokenSource();
 
-            Logger.LogDebug("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name);
+            Logger.LogDebug("WS {1} begin transmitting to {0}", message.Connection.RemoteEndPoint, GetType().Name);
 
             var state = new TStateType
             {
@@ -154,7 +152,6 @@ namespace MediaBrowser.Controller.Net
                     {
                         MessageType = Name,
                         Data = data
-
                     }, cancellationToken).ConfigureAwait(false);
 
                     state.DateLastSendUtc = DateTime.UtcNow;
@@ -197,7 +194,7 @@ namespace MediaBrowser.Controller.Net
         /// <param name="connection">The connection.</param>
         private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> connection)
         {
-            Logger.LogDebug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
+            Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
 
             // TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really...
             // connection.Item1.Dispose();
@@ -242,6 +239,7 @@ namespace MediaBrowser.Controller.Net
         public void Dispose()
         {
             Dispose(true);
+            GC.SuppressFinalize(this);
         }
     }
 

+ 8 - 19
MediaBrowser.Controller/Net/IHttpServer.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Services;
@@ -9,9 +8,9 @@ using Microsoft.AspNetCore.Http;
 namespace MediaBrowser.Controller.Net
 {
     /// <summary>
-    /// Interface IHttpServer
+    /// Interface IHttpServer.
     /// </summary>
-    public interface IHttpServer : IDisposable
+    public interface IHttpServer
     {
         /// <summary>
         /// Gets the URL prefix.
@@ -19,11 +18,6 @@ namespace MediaBrowser.Controller.Net
         /// <value>The URL prefix.</value>
         string[] UrlPrefixes { get; }
 
-        /// <summary>
-        /// Stops this instance.
-        /// </summary>
-        void Stop();
-
         /// <summary>
         /// Occurs when [web socket connected].
         /// </summary>
@@ -40,22 +34,17 @@ namespace MediaBrowser.Controller.Net
         string GlobalResponse { get; set; }
 
         /// <summary>
-        /// Sends the http context to the socket listener
+        /// The HTTP request handler
         /// </summary>
-        /// <param name="ctx"></param>
+        /// <param name="context"></param>
         /// <returns></returns>
-        Task ProcessWebSocketRequest(HttpContext ctx);
+        Task RequestHandler(HttpContext context);
 
         /// <summary>
-        /// The HTTP request handler
+        /// Get the default CORS headers
         /// </summary>
-        /// <param name="httpReq"></param>
-        /// <param name="urlString"></param>
-        /// <param name="host"></param>
-        /// <param name="localPath"></param>
-        /// <param name="cancellationToken"></param>
+        /// <param name="req"></param>
         /// <returns></returns>
-        Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath,
-            CancellationToken cancellationToken);
+        IDictionary<string, string> GetDefaultCorsHeaders(IRequest req);
     }
 }

+ 9 - 32
MediaBrowser.Controller/Net/IWebSocketConnection.cs

@@ -1,4 +1,7 @@
+#nullable enable
+
 using System;
+using System.Net;
 using System.Net.WebSockets;
 using System.Threading;
 using System.Threading.Tasks;
@@ -7,18 +10,12 @@ using Microsoft.AspNetCore.Http;
 
 namespace MediaBrowser.Controller.Net
 {
-    public interface IWebSocketConnection : IDisposable
+    public interface IWebSocketConnection
     {
         /// <summary>
         /// Occurs when [closed].
         /// </summary>
-        event EventHandler<EventArgs> Closed;
-
-        /// <summary>
-        /// Gets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        Guid Id { get; }
+        event EventHandler<EventArgs>? Closed;
 
         /// <summary>
         /// Gets the last activity date.
@@ -26,22 +23,17 @@ namespace MediaBrowser.Controller.Net
         /// <value>The last activity date.</value>
         DateTime LastActivityDate { get; }
 
-        /// <summary>
-        /// Gets or sets the URL.
-        /// </summary>
-        /// <value>The URL.</value>
-        string Url { get; set; }
         /// <summary>
         /// Gets or sets the query string.
         /// </summary>
         /// <value>The query string.</value>
-        IQueryCollection QueryString { get; set; }
+        IQueryCollection QueryString { get; }
 
         /// <summary>
         /// Gets or sets the receive action.
         /// </summary>
         /// <value>The receive action.</value>
-        Func<WebSocketMessageInfo, Task> OnReceive { get; set; }
+        Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
 
         /// <summary>
         /// Gets the state.
@@ -53,7 +45,7 @@ namespace MediaBrowser.Controller.Net
         /// Gets the remote end point.
         /// </summary>
         /// <value>The remote end point.</value>
-        string RemoteEndPoint { get; }
+        IPAddress? RemoteEndPoint { get; }
 
         /// <summary>
         /// Sends a message asynchronously.
@@ -65,21 +57,6 @@ namespace MediaBrowser.Controller.Net
         /// <exception cref="ArgumentNullException">message</exception>
         Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken);
 
-        /// <summary>
-        /// Sends a message asynchronously.
-        /// </summary>
-        /// <param name="buffer">The buffer.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        Task SendAsync(byte[] buffer, CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Sends a message asynchronously.
-        /// </summary>
-        /// <param name="text">The text.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="ArgumentNullException">buffer</exception>
-        Task SendAsync(string text, CancellationToken cancellationToken);
+        Task ProcessAsync(CancellationToken cancellationToken = default);
     }
 }

+ 2 - 1
MediaBrowser.Controller/Session/ISessionController.cs

@@ -1,3 +1,4 @@
+using System;
 using System.Threading;
 using System.Threading.Tasks;
 
@@ -20,6 +21,6 @@ namespace MediaBrowser.Controller.Session
         /// <summary>
         /// Sends the message.
         /// </summary>
-        Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken);
+        Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken);
     }
 }

+ 24 - 50
MediaBrowser.Controller/Session/SessionInfo.cs

@@ -10,13 +10,23 @@ using Microsoft.Extensions.Logging;
 namespace MediaBrowser.Controller.Session
 {
     /// <summary>
-    /// Class SessionInfo
+    /// Class SessionInfo.
     /// </summary>
-    public class SessionInfo : IDisposable
+    public sealed class SessionInfo : IDisposable
     {
-        private ISessionManager _sessionManager;
+        // 1 second
+        private const long ProgressIncrement = 10000000;
+
+        private readonly ISessionManager _sessionManager;
         private readonly ILogger _logger;
 
+
+        private readonly object _progressLock = new object();
+        private Timer _progressTimer;
+        private PlaybackProgressInfo _lastProgressInfo;
+
+        private bool _disposed = false;
+
         public SessionInfo(ISessionManager sessionManager, ILogger logger)
         {
             _sessionManager = sessionManager;
@@ -97,8 +107,6 @@ namespace MediaBrowser.Controller.Session
         /// <value>The name of the device.</value>
         public string DeviceName { get; set; }
 
-        public string DeviceType { get; set; }
-
         /// <summary>
         /// Gets or sets the now playing item.
         /// </summary>
@@ -128,22 +136,6 @@ namespace MediaBrowser.Controller.Session
         [JsonIgnore]
         public ISessionController[] SessionControllers { get; set; }
 
-        /// <summary>
-        /// Gets or sets the supported commands.
-        /// </summary>
-        /// <value>The supported commands.</value>
-        public string[] SupportedCommands
-        {
-            get
-            {
-                if (Capabilities == null)
-                {
-                    return new string[] { };
-                }
-                return Capabilities.SupportedCommands;
-            }
-        }
-
         public TranscodingInfo TranscodingInfo { get; set; }
 
         /// <summary>
@@ -215,6 +207,14 @@ namespace MediaBrowser.Controller.Session
             }
         }
 
+        public QueueItem[] NowPlayingQueue { get; set; }
+
+        public bool HasCustomDeviceName { get; set; }
+
+        public string PlaylistItemId { get; set; }
+
+        public string UserPrimaryImageTag { get; set; }
+
         public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
         {
             var controllers = SessionControllers.ToList();
@@ -258,10 +258,6 @@ namespace MediaBrowser.Controller.Session
             return false;
         }
 
-        private readonly object _progressLock = new object();
-        private Timer _progressTimer;
-        private PlaybackProgressInfo _lastProgressInfo;
-
         public void StartAutomaticProgress(PlaybackProgressInfo progressInfo)
         {
             if (_disposed)
@@ -284,9 +280,6 @@ namespace MediaBrowser.Controller.Session
             }
         }
 
-        // 1 second
-        private const long ProgressIncrement = 10000000;
-
         private async void OnProgressTimerCallback(object state)
         {
             if (_disposed)
@@ -345,8 +338,7 @@ namespace MediaBrowser.Controller.Session
             }
         }
 
-        private bool _disposed = false;
-
+        /// <inheritdoc />
         public void Dispose()
         {
             _disposed = true;
@@ -358,30 +350,12 @@ namespace MediaBrowser.Controller.Session
 
             foreach (var controller in controllers)
             {
-                var disposable = controller as IDisposable;
-
-                if (disposable != null)
+                if (controller is IDisposable disposable)
                 {
                     _logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name);
-
-                    try
-                    {
-                        disposable.Dispose();
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "Error disposing session controller");
-                    }
+                    disposable.Dispose();
                 }
             }
-
-            _sessionManager = null;
         }
-
-        public QueueItem[] NowPlayingQueue { get; set; }
-        public bool HasCustomDeviceName { get; set; }
-        public string PlaylistItemId { get; set; }
-        public string ServerId { get; set; }
-        public string UserPrimaryImageTag { get; set; }
     }
 }

+ 13 - 2
MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs

@@ -7,14 +7,25 @@ using MediaBrowser.Model.MediaInfo;
 
 namespace MediaBrowser.MediaEncoding.Subtitles
 {
+    /// <summary>
+    /// Subtitle writer for the WebVTT format.
+    /// </summary>
     public class VttWriter : ISubtitleWriter
     {
+        /// <inheritdoc />
         public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
         {
             using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
             {
                 writer.WriteLine("WEBVTT");
                 writer.WriteLine(string.Empty);
+                writer.WriteLine("REGION");
+                writer.WriteLine("id:subtitle");
+                writer.WriteLine("width:80%");
+                writer.WriteLine("lines:3");
+                writer.WriteLine("regionanchor:50%,100%");
+                writer.WriteLine("viewportanchor:50%,90%");
+                writer.WriteLine(string.Empty);
                 foreach (var trackEvent in info.TrackEvents)
                 {
                     cancellationToken.ThrowIfCancellationRequested();
@@ -22,13 +33,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                     var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks);
                     var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks);
 
-                    // make sure the start and end times are different and seqential
+                    // make sure the start and end times are different and sequential
                     if (endTime.TotalMilliseconds <= startTime.TotalMilliseconds)
                     {
                         endTime = startTime.Add(TimeSpan.FromMilliseconds(1));
                     }
 
-                    writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff}", startTime, endTime);
+                    writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff} region:subtitle", startTime, endTime);
 
                     var text = trackEvent.Text;
 

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

@@ -1,7 +1,6 @@
 #pragma warning disable CS1591
 
 using System;
-using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
@@ -21,7 +20,7 @@ namespace MediaBrowser.Model.Activity
         QueryResult<ActivityLogEntry> GetPagedResult(int? startIndex, int? limit);
 
         QueryResult<ActivityLogEntry> GetPagedResult(
-            Func<IQueryable<ActivityLog>, IEnumerable<ActivityLog>> func,
+            Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>> func,
             int? startIndex,
             int? limit);
     }

+ 26 - 6
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -49,17 +49,24 @@ namespace MediaBrowser.Model.Configuration
         public int HttpsPortNumber { get; set; }
 
         /// <summary>
-        /// Gets or sets a value indicating whether [use HTTPS].
+        /// Gets or sets a value indicating whether to use HTTPS.
         /// </summary>
-        /// <value><c>true</c> if [use HTTPS]; otherwise, <c>false</c>.</value>
+        /// <remarks>
+        /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
+        /// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>.
+        /// </remarks>
         public bool EnableHttps { get; set; }
+
         public bool EnableNormalizedItemByNameIds { get; set; }
 
         /// <summary>
-        /// Gets or sets the value pointing to the file system where the ssl certificate is located..
+        /// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
         /// </summary>
-        /// <value>The value pointing to the file system where the ssl certificate is located..</value>
         public string CertificatePath { get; set; }
+
+        /// <summary>
+        /// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
+        /// </summary>
         public string CertificatePassword { get; set; }
 
         /// <summary>
@@ -69,8 +76,9 @@ namespace MediaBrowser.Model.Configuration
         public bool IsPortAuthorized { get; set; }
 
         public bool AutoRunWebApp { get; set; }
+
         public bool EnableRemoteAccess { get; set; }
-        public bool CameraUploadUpgraded { get; set; }
+
         public bool CollectionsUpgraded { get; set; }
 
         /// <summary>
@@ -86,6 +94,7 @@ namespace MediaBrowser.Model.Configuration
         /// </summary>
         /// <value>The metadata path.</value>
         public string MetadataPath { get; set; }
+
         public string MetadataNetworkPath { get; set; }
 
         /// <summary>
@@ -208,15 +217,26 @@ namespace MediaBrowser.Model.Configuration
         public int RemoteClientBitrateLimit { get; set; }
 
         public bool EnableFolderView { get; set; }
+
         public bool EnableGroupingIntoCollections { get; set; }
+
         public bool DisplaySpecialsWithinSeasons { get; set; }
+
         public string[] LocalNetworkSubnets { get; set; }
+
         public string[] LocalNetworkAddresses { get; set; }
+
         public string[] CodecsUsed { get; set; }
+
         public bool IgnoreVirtualInterfaces { get; set; }
+
         public bool EnableExternalContentInSuggestions { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the server should force connections over HTTPS.
+        /// </summary>
         public bool RequireHttps { get; set; }
-        public bool IsBehindProxy { get; set; }
+
         public bool EnableNewOmdbSupport { get; set; }
 
         public string[] RemoteIPFilter { get; set; }

+ 9 - 0
MediaBrowser.Model/Devices/DeviceOptions.cs

@@ -0,0 +1,9 @@
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Model.Devices
+{
+    public class DeviceOptions
+    {
+        public string CustomName { get; set; }
+    }
+}

+ 0 - 23
MediaBrowser.Model/Devices/DevicesOptions.cs

@@ -1,23 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Devices
-{
-    public class DevicesOptions
-    {
-        public string[] EnabledCameraUploadDevices { get; set; }
-        public string CameraUploadPath { get; set; }
-        public bool EnableCameraUploadSubfolders { get; set; }
-
-        public DevicesOptions()
-        {
-            EnabledCameraUploadDevices = Array.Empty<string>();
-        }
-    }
-
-    public class DeviceOptions
-    {
-        public string CustomName { get; set; }
-    }
-}

+ 6 - 2
MediaBrowser.Model/Net/WebSocketMessage.cs

@@ -1,5 +1,8 @@
+
 #pragma warning disable CS1591
 
+using System;
+
 namespace MediaBrowser.Model.Net
 {
     /// <summary>
@@ -13,7 +16,9 @@ namespace MediaBrowser.Model.Net
         /// </summary>
         /// <value>The type of the message.</value>
         public string MessageType { get; set; }
-        public string MessageId { get; set; }
+
+        public Guid MessageId { get; set; }
+
         public string ServerId { get; set; }
 
         /// <summary>
@@ -22,5 +27,4 @@ namespace MediaBrowser.Model.Net
         /// <value>The data.</value>
         public T Data { get; set; }
     }
-
 }

+ 0 - 18
MediaBrowser.Model/System/SystemInfo.cs

@@ -115,24 +115,6 @@ namespace MediaBrowser.Model.System
         /// <value>The transcode path.</value>
         public string TranscodingTempPath { get; set; }
 
-        /// <summary>
-        /// Gets or sets the HTTP server port number.
-        /// </summary>
-        /// <value>The HTTP server port number.</value>
-        public int HttpServerPortNumber { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [enable HTTPS].
-        /// </summary>
-        /// <value><c>true</c> if [enable HTTPS]; otherwise, <c>false</c>.</value>
-        public bool SupportsHttps { get; set; }
-
-        /// <summary>
-        /// Gets or sets the HTTPS server port number.
-        /// </summary>
-        /// <value>The HTTPS server port number.</value>
-        public int HttpsPortNumber { get; set; }
-
         /// <summary>
         /// Gets or sets a value indicating whether this instance has update available.
         /// </summary>

+ 14 - 10
MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs

@@ -11,13 +11,10 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Providers.Plugins.Omdb
 {
-    public class OmdbEpisodeProvider :
-            IRemoteMetadataProvider<Episode, EpisodeInfo>,
-            IHasOrder
+    public class OmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
     {
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IHttpClient _httpClient;
@@ -26,16 +23,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IApplicationHost _appHost;
 
-        public OmdbEpisodeProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
+        public OmdbEpisodeProvider(
+            IJsonSerializer jsonSerializer,
+            IApplicationHost appHost,
+            IHttpClient httpClient,
+            ILibraryManager libraryManager,
+            IFileSystem fileSystem,
+            IServerConfigurationManager configurationManager)
         {
             _jsonSerializer = jsonSerializer;
             _httpClient = httpClient;
             _fileSystem = fileSystem;
             _configurationManager = configurationManager;
             _appHost = appHost;
-            _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, logger, libraryManager, fileSystem, configurationManager);
+            _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, libraryManager, fileSystem, configurationManager);
         }
 
+        // After TheTvDb
+        public int Order => 1;
+
+        public string Name => "The Open Movie Database";
+
         public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
         {
             return _itemProvider.GetSearchResults(searchInfo, "episode", cancellationToken);
@@ -66,10 +74,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb
 
             return result;
         }
-        // After TheTvDb
-        public int Order => 1;
-
-        public string Name => "The Open Movie Database";
 
         public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
         {

+ 9 - 6
MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs

@@ -17,7 +17,6 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Providers.Plugins.Omdb
 {
@@ -26,22 +25,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb
     {
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IHttpClient _httpClient;
-        private readonly ILogger _logger;
         private readonly ILibraryManager _libraryManager;
         private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IApplicationHost _appHost;
 
-        public OmdbItemProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
+        public OmdbItemProvider(
+            IJsonSerializer jsonSerializer,
+            IApplicationHost appHost,
+            IHttpClient httpClient,
+            ILibraryManager libraryManager,
+            IFileSystem fileSystem,
+            IServerConfigurationManager configurationManager)
         {
             _jsonSerializer = jsonSerializer;
             _httpClient = httpClient;
-            _logger = logger;
             _libraryManager = libraryManager;
             _fileSystem = fileSystem;
             _configurationManager = configurationManager;
             _appHost = appHost;
         }
+
         // After primary option
         public int Order => 2;
 
@@ -80,7 +84,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
                 var parsedName = _libraryManager.ParseName(name);
                 var yearInName = parsedName.Year;
                 name = parsedName.Name;
-                year = year ?? yearInName;
+                year ??= yearInName;
             }
 
             if (string.IsNullOrWhiteSpace(imdbId))
@@ -312,6 +316,5 @@ namespace MediaBrowser.Providers.Plugins.Omdb
             /// <value>The results.</value>
             public List<SearchResult> Search { get; set; }
         }
-
     }
 }

+ 53 - 21
MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs

@@ -27,9 +27,6 @@ namespace MediaBrowser.Providers.Tmdb.TV
     public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
     {
         private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings";
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
-        internal static TmdbSeriesProvider Current { get; private set; }
 
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IFileSystem _fileSystem;
@@ -39,6 +36,10 @@ namespace MediaBrowser.Providers.Tmdb.TV
         private readonly IHttpClient _httpClient;
         private readonly ILibraryManager _libraryManager;
 
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+        internal static TmdbSeriesProvider Current { get; private set; }
+
         public TmdbSeriesProvider(
             IJsonSerializer jsonSerializer,
             IFileSystem fileSystem,
@@ -217,10 +218,9 @@ namespace MediaBrowser.Providers.Tmdb.TV
             var series = seriesResult.Item;
 
             series.Name = seriesInfo.Name;
+            series.OriginalTitle = seriesInfo.Original_Name;
             series.SetProviderId(MetadataProviders.Tmdb, seriesInfo.Id.ToString(_usCulture));
 
-            //series.VoteCount = seriesInfo.vote_count;
-
             string voteAvg = seriesInfo.Vote_Average.ToString(CultureInfo.InvariantCulture);
 
             if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out float rating))
@@ -240,7 +240,7 @@ namespace MediaBrowser.Providers.Tmdb.TV
                 series.Genres = seriesInfo.Genres.Select(i => i.Name).ToArray();
             }
 
-            //series.HomePageUrl = seriesInfo.homepage;
+            series.HomePageUrl = seriesInfo.Homepage;
 
             series.RunTimeTicks = seriesInfo.Episode_Run_Time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
 
@@ -308,29 +308,61 @@ namespace MediaBrowser.Providers.Tmdb.TV
             seriesResult.ResetPeople();
             var tmdbImageUrl = settings.images.GetImageUrl("original");
 
-            if (seriesInfo.Credits != null && seriesInfo.Credits.Cast != null)
+            if (seriesInfo.Credits != null)
             {
-                foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order))
+                if (seriesInfo.Credits.Cast != null)
                 {
-                    var personInfo = new PersonInfo
+                    foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order))
                     {
-                        Name = actor.Name.Trim(),
-                        Role = actor.Character,
-                        Type = PersonType.Actor,
-                        SortOrder = actor.Order
-                    };
+                        var personInfo = new PersonInfo
+                        {
+                            Name = actor.Name.Trim(),
+                            Role = actor.Character,
+                            Type = PersonType.Actor,
+                            SortOrder = actor.Order
+                        };
 
-                    if (!string.IsNullOrWhiteSpace(actor.Profile_Path))
-                    {
-                        personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path;
+                        if (!string.IsNullOrWhiteSpace(actor.Profile_Path))
+                        {
+                            personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path;
+                        }
+
+                        if (actor.Id > 0)
+                        {
+                            personInfo.SetProviderId(MetadataProviders.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
+                        }
+
+                        seriesResult.AddPerson(personInfo);
                     }
+                }
 
-                    if (actor.Id > 0)
+                if (seriesInfo.Credits.Crew != null)
+                {
+                    var keepTypes = new[]
                     {
-                        personInfo.SetProviderId(MetadataProviders.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
-                    }
+                        PersonType.Director,
+                        PersonType.Writer,
+                        PersonType.Producer
+                    };
 
-                    seriesResult.AddPerson(personInfo);
+                    foreach (var person in seriesInfo.Credits.Crew)
+                    {
+                        // Normalize this
+                        var type = TmdbUtils.MapCrewToPersonType(person);
+
+                        if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
+                            && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+                        {
+                            continue;
+                        }
+
+                        seriesResult.AddPerson(new PersonInfo
+                        {
+                            Name = person.Name.Trim(),
+                            Role = person.Job,
+                            Type = type
+                        });
+                    }
                 }
             }
         }

+ 34 - 1
MediaBrowser.Providers/Tmdb/TmdbUtils.cs

@@ -4,18 +4,51 @@ using MediaBrowser.Providers.Tmdb.Models.General;
 
 namespace MediaBrowser.Providers.Tmdb
 {
+    /// <summary>
+    /// Utilities for the TMDb provider
+    /// </summary>
     public static class TmdbUtils
     {
+        /// <summary>
+        /// URL of the TMDB instance to use.
+        /// </summary>
         public const string BaseTmdbUrl = "https://www.themoviedb.org/";
+
+        /// <summary>
+        /// URL of the TMDB API instance to use.
+        /// </summary>
         public const string BaseTmdbApiUrl = "https://api.themoviedb.org/";
+
+        /// <summary>
+        /// Name of the provider.
+        /// </summary>
         public const string ProviderName = "TheMovieDb";
+
+        /// <summary>
+        /// API key to use when performing an API call.
+        /// </summary>
         public const string ApiKey = "4219e299c89411838049ab0dab19ebd5";
+
+        /// <summary>
+        /// Value of the Accept header for requests to the provider.
+        /// </summary>
         public const string AcceptHeader = "application/json,image/*";
 
+        /// <summary>
+        /// Maps the TMDB provided roles for crew members to Jellyfin roles.
+        /// </summary>
+        /// <param name="crew">Crew member to map against the Jellyfin person types.</param>
+        /// <returns>The Jellyfin person type.</returns>
         public static string MapCrewToPersonType(Crew crew)
         {
             if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase)
-                && crew.Job.IndexOf("producer", StringComparison.InvariantCultureIgnoreCase) != -1)
+                && crew.Job.Contains("director", StringComparison.InvariantCultureIgnoreCase))
+            {
+                return PersonType.Director;
+            }
+
+            if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase)
+                && crew.Job.Contains("producer", StringComparison.InvariantCultureIgnoreCase))
             {
                 return PersonType.Producer;
             }

+ 2 - 2
MediaBrowser.sln

@@ -1,6 +1,6 @@
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.30011.22
+# Visual Studio 15
+VisualStudioVersion = 15.0.26730.3
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}"
 EndProject