Browse Source

Merge remote-tracking branch 'upstream/master' into output-formatters

crobibero 4 years ago
parent
commit
a523ff840c
100 changed files with 1638 additions and 7421 deletions
  1. 0 3
      .ci/azure-pipelines-abi.yml
  2. 62 14
      .ci/azure-pipelines-package.yml
  3. 10 4
      .ci/azure-pipelines.yml
  4. 1 0
      CONTRIBUTORS.md
  5. 2 2
      Emby.Dlna/Eventing/DlnaEventManager.cs
  6. 1 1
      Emby.Dlna/IConnectionManager.cs
  7. 1 1
      Emby.Dlna/IContentDirectory.cs
  8. 1 1
      Emby.Dlna/IDlnaEventManager.cs
  9. 1 1
      Emby.Dlna/IMediaReceiverRegistrar.cs
  10. 1 1
      Emby.Dlna/PlayTo/PlayToController.cs
  11. 1 1
      Emby.Dlna/PlayTo/PlayToManager.cs
  12. 3 3
      Emby.Dlna/Service/BaseService.cs
  13. 1 1
      Emby.Dlna/Ssdp/DeviceDiscovery.cs
  14. 15 1
      Emby.Naming/Emby.Naming.csproj
  15. 1 1
      Emby.Notifications/NotificationEntryPoint.cs
  16. 0 590
      Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
  17. 109 91
      Emby.Server.Implementations/ApplicationHost.cs
  18. 1 1
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  19. 1 1
      Emby.Server.Implementations/ConfigurationOptions.cs
  20. 1 1
      Emby.Server.Implementations/Devices/DeviceManager.cs
  21. 6 6
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  22. 1 1
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  23. 1 1
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  24. 5 4
      Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
  25. 0 210
      Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs
  26. 0 250
      Emby.Server.Implementations/HttpServer/FileWriter.cs
  27. 0 766
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  28. 0 721
      Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
  29. 0 212
      Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
  30. 0 113
      Emby.Server.Implementations/HttpServer/ResponseFilter.cs
  31. 1 212
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  32. 9 15
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  33. 7 13
      Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
  34. 0 120
      Emby.Server.Implementations/HttpServer/StreamWriter.cs
  35. 7 1
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  36. 102 0
      Emby.Server.Implementations/HttpServer/WebSocketManager.cs
  37. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  38. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
  39. 1 1
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  40. 4 1
      Emby.Server.Implementations/Localization/Core/es_DO.json
  41. 1 1
      Emby.Server.Implementations/Localization/Core/id.json
  42. 1 1
      Emby.Server.Implementations/Localization/Core/nb.json
  43. 104 60
      Emby.Server.Implementations/Localization/Core/th.json
  44. 285 0
      Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
  45. 1 1
      Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  46. 1 1
      Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
  47. 0 64
      Emby.Server.Implementations/Services/HttpResult.cs
  48. 0 51
      Emby.Server.Implementations/Services/RequestHelper.cs
  49. 0 141
      Emby.Server.Implementations/Services/ResponseHelper.cs
  50. 0 202
      Emby.Server.Implementations/Services/ServiceController.cs
  51. 0 230
      Emby.Server.Implementations/Services/ServiceExec.cs
  52. 0 212
      Emby.Server.Implementations/Services/ServiceHandler.cs
  53. 0 20
      Emby.Server.Implementations/Services/ServiceMethod.cs
  54. 0 550
      Emby.Server.Implementations/Services/ServicePath.cs
  55. 0 118
      Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
  56. 0 27
      Emby.Server.Implementations/Services/UrlExtensions.cs
  57. 79 56
      Emby.Server.Implementations/Session/SessionManager.cs
  58. 7 7
      Emby.Server.Implementations/Session/SessionWebSocketListener.cs
  59. 0 248
      Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
  60. 4 4
      Jellyfin.Api/Controllers/DlnaServerController.cs
  61. 1 1
      Jellyfin.Api/Controllers/DynamicHlsController.cs
  62. 2 2
      Jellyfin.Api/Controllers/ImageController.cs
  63. 4 2
      Jellyfin.Api/Controllers/ItemsController.cs
  64. 6 7
      Jellyfin.Api/Controllers/LibraryController.cs
  65. 5 7
      Jellyfin.Api/Controllers/LibraryStructureController.cs
  66. 2 1
      Jellyfin.Api/Controllers/MoviesController.cs
  67. 6 2
      Jellyfin.Api/Controllers/PluginsController.cs
  68. 154 0
      Jellyfin.Api/Controllers/QuickConnectController.cs
  69. 34 0
      Jellyfin.Api/Controllers/UserController.cs
  70. 11 8
      Jellyfin.Api/Controllers/VideosController.cs
  71. 5 1
      Jellyfin.Api/Helpers/MediaInfoHelper.cs
  72. 36 29
      Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
  73. 2 2
      Jellyfin.Api/Jellyfin.Api.csproj
  74. 3 3
      Jellyfin.Api/Models/LibraryStructureDto/AddVirtualFolderDto.cs
  75. 21 0
      Jellyfin.Api/Models/LibraryStructureDto/UpdateLibraryOptionsDto.cs
  76. 16 0
      Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
  77. 1 1
      Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
  78. 1 1
      Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
  79. 2 1
      Jellyfin.Data/Entities/ActivityLog.cs
  80. 0 210
      Jellyfin.Data/Entities/Artwork.cs
  81. 0 72
      Jellyfin.Data/Entities/Book.cs
  82. 0 125
      Jellyfin.Data/Entities/BookMetadata.cs
  83. 0 277
      Jellyfin.Data/Entities/Chapter.cs
  84. 0 123
      Jellyfin.Data/Entities/Collection.cs
  85. 0 156
      Jellyfin.Data/Entities/CollectionItem.cs
  86. 0 159
      Jellyfin.Data/Entities/Company.cs
  87. 0 236
      Jellyfin.Data/Entities/CompanyMetadata.cs
  88. 0 71
      Jellyfin.Data/Entities/CustomItem.cs
  89. 0 83
      Jellyfin.Data/Entities/CustomItemMetadata.cs
  90. 0 118
      Jellyfin.Data/Entities/Episode.cs
  91. 0 198
      Jellyfin.Data/Entities/EpisodeMetadata.cs
  92. 0 162
      Jellyfin.Data/Entities/Genre.cs
  93. 2 1
      Jellyfin.Data/Entities/Group.cs
  94. 81 0
      Jellyfin.Data/Entities/Libraries/Artwork.cs
  95. 28 0
      Jellyfin.Data/Entities/Libraries/Book.cs
  96. 55 0
      Jellyfin.Data/Entities/Libraries/BookMetadata.cs
  97. 102 0
      Jellyfin.Data/Entities/Libraries/Chapter.cs
  98. 55 0
      Jellyfin.Data/Entities/Libraries/Collection.cs
  99. 94 0
      Jellyfin.Data/Entities/Libraries/CollectionItem.cs
  100. 67 0
      Jellyfin.Data/Entities/Libraries/Company.cs

+ 0 - 3
.ci/azure-pipelines-abi.yml

@@ -62,7 +62,6 @@ jobs:
 
       - task: DownloadPipelineArtifact@2
         displayName: 'Download Reference Assembly Build Artifact'
-        enabled: false
         inputs:
           source: "specific"
           artifact: "$(NugetPackageName)"
@@ -74,7 +73,6 @@ jobs:
 
       - task: CopyFiles@2
         displayName: 'Copy Reference Assembly Build Artifact'
-        enabled: false
         inputs:
           sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
           contents: '**/*.dll'
@@ -85,7 +83,6 @@ jobs:
 
       - task: DotNetCoreCLI@2
         displayName: 'Execute ABI Compatibility Check Tool'
-        enabled: false
         inputs:
           command: custom
           custom: compat

+ 62 - 14
.ci/azure-pipelines-package.yml

@@ -42,7 +42,7 @@ jobs:
 
   - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
     displayName: 'Run Dockerfile (stable)'
-    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 
   - task: PublishPipelineArtifact@1
     displayName: 'Publish Release'
@@ -87,7 +87,7 @@ jobs:
   steps:
   - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
     displayName: Set release version (stable)
-    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 
   - task: Docker@2
     displayName: 'Push Unstable Image'
@@ -104,7 +104,7 @@ jobs:
 
   - task: Docker@2
     displayName: 'Push Stable Image'
-    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
     inputs:
       repository: 'jellyfin/jellyfin-server'
       command: buildAndPush
@@ -116,8 +116,9 @@ jobs:
         $(JellyfinVersion)-$(BuildConfiguration)
 
 - job: CollectArtifacts
-  timeoutInMinutes: 10
+  timeoutInMinutes: 20
   displayName: 'Collect Artifacts'
+  continueOnError: true
   dependsOn:
   - BuildPackage
   - BuildDocker
@@ -129,38 +130,85 @@ jobs:
   steps:
   - task: SSH@0
     displayName: 'Update Unstable Repository'
+    continueOnError: true
     condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
     inputs:
       sshEndpoint: repository
       runOptions: 'commands'
-      commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
+      commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
 
   - task: SSH@0
     displayName: 'Update Stable Repository'
-    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+    continueOnError: true
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
     inputs:
       sshEndpoint: repository
       runOptions: 'commands'
-      commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
-      
+      commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
+
 - job: PublishNuget
   displayName: 'Publish NuGet packages'
   dependsOn:
   - BuildPackage
-  condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags'))
+  condition: succeeded('BuildPackage')
 
   pool:
     vmImage: 'ubuntu-latest'
 
   steps:
-  - task: NuGetCommand@2
+  - task: DotNetCoreCLI@2
+    displayName: 'Build Stable Nuget packages'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
     inputs:
       command: 'pack'
-      packagesToPack: Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj
-      packDestination: '$(Build.ArtifactStagingDirectory)'
+      packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj'
+      versioningScheme: 'off'
+
+  - task: DotNetCoreCLI@2
+    displayName: 'Build Unstable Nuget packages'
+    inputs:
+      command: 'custom'
+      projects: |
+        Jellyfin.Data/Jellyfin.Data.csproj
+        MediaBrowser.Common/MediaBrowser.Common.csproj
+        MediaBrowser.Controller/MediaBrowser.Controller.csproj
+        MediaBrowser.Model/MediaBrowser.Model.csproj
+        Emby.Naming/Emby.Naming.csproj
+      custom: 'pack'
+      arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
+
+  - task: PublishBuildArtifacts@1
+    displayName: 'Publish Nuget packages'
+    inputs:
+      pathToPublish: $(Build.ArtifactStagingDirectory)
+      artifactName: Jellyfin Nuget Packages
+
+  - task: NuGetAuthenticate@0
+    displayName: 'Authenticate to stable Nuget feed'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+    inputs:
+      nuGetServiceConnections: 'NugetOrg'
 
   - task: NuGetCommand@2
+    displayName: 'Push Nuget packages to stable feed'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+    inputs:
+      command: 'push'
+      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg'
+      nuGetFeedType: 'external'
+      publishFeedCredentials: 'NugetOrg'
+      allowPackageConflicts: true # This ignores an error if the version already exists
+
+  - task: NuGetAuthenticate@0
+    displayName: 'Authenticate to unstable Nuget feed'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+
+  - task: NuGetCommand@2
+    displayName: 'Push Nuget packages to unstable feed'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
     inputs:
       command: 'push'
-      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
-      includeNugetOrg: 'true'
+      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' # No symbols since Azure Artifact does not support it
+      nuGetFeedType: 'internal'
+      publishVstsFeed: '7cce6c46-d610-45e3-9fb7-65a6bfd1b671/a5746b79-f369-42db-93ff-59cd066f9327'
+      allowPackageConflicts: true # This ignores an error if the version already exists

+ 10 - 4
.ci/azure-pipelines.yml

@@ -13,15 +13,21 @@ pr:
 
 trigger:
   batch: true
+  branches:
+    include:
+      - '*'
+  tags:
+    include:
+      - 'v*'
 
 jobs:
-- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
+- ${{ if not(startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) }}:
   - template: azure-pipelines-main.yml
     parameters:
       LinuxImage: 'ubuntu-latest'
       RestoreBuildProjects: $(RestoreBuildProjects)
 
-- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
   - template: azure-pipelines-test.yml
     parameters:
       ImageNames:
@@ -29,7 +35,7 @@ jobs:
         Windows: 'windows-latest'
         macOS: 'macos-latest'
 
-- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
   - template: azure-pipelines-abi.yml
     parameters:
       Packages:
@@ -47,5 +53,5 @@ jobs:
           AssemblyFileName: MediaBrowser.Common.dll
       LinuxImage: 'ubuntu-latest'
 
-- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
+- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
   - template: azure-pipelines-package.yml

+ 1 - 0
CONTRIBUTORS.md

@@ -16,6 +16,7 @@
  - [bugfixin](https://github.com/bugfixin)
  - [chaosinnovator](https://github.com/chaosinnovator)
  - [ckcr4lyf](https://github.com/ckcr4lyf)
+ - [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
  - [crankdoofus](https://github.com/crankdoofus)
  - [crobibero](https://github.com/crobibero)
  - [cromefire](https://github.com/cromefire)

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

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

+ 1 - 1
Emby.Dlna/IConnectionManager.cs

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

+ 1 - 1
Emby.Dlna/IContentDirectory.cs

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

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

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

+ 1 - 1
Emby.Dlna/IMediaReceiverRegistrar.cs

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

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

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

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

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

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

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

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

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

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

@@ -10,6 +10,15 @@
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <PublishRepositoryUrl>true</PublishRepositoryUrl>
+    <EmbedUntrackedSources>true</EmbedUntrackedSources>
+    <IncludeSymbols>true</IncludeSymbols>
+    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
+  </PropertyGroup>
+
+  <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
+    <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
+    <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
   </PropertyGroup>
 
   <ItemGroup>
@@ -23,10 +32,15 @@
   <PropertyGroup>
     <Authors>Jellyfin Contributors</Authors>
     <PackageId>Jellyfin.Naming</PackageId>
-    <PackageLicenseUrl>https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</PackageLicenseUrl>
+    <VersionPrefix>10.7.0</VersionPrefix>
     <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
+    <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
   </PropertyGroup>
 
+  <ItemGroup>
+    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
+  </ItemGroup>
+
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
     <!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->

+ 1 - 1
Emby.Notifications/NotificationEntryPoint.cs

@@ -4,6 +4,7 @@ using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
@@ -13,7 +14,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Notifications;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Notifications;
 using Microsoft.Extensions.Logging;

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

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

+ 109 - 91
Emby.Server.Implementations/ApplicationHost.cs

@@ -37,10 +37,10 @@ using Emby.Server.Implementations.LiveTv;
 using Emby.Server.Implementations.Localization;
 using Emby.Server.Implementations.Net;
 using Emby.Server.Implementations.Playlists;
+using Emby.Server.Implementations.QuickConnect;
 using Emby.Server.Implementations.ScheduledTasks;
 using Emby.Server.Implementations.Security;
 using Emby.Server.Implementations.Serialization;
-using Emby.Server.Implementations.Services;
 using Emby.Server.Implementations.Session;
 using Emby.Server.Implementations.SyncPlay;
 using Emby.Server.Implementations.TV;
@@ -53,7 +53,6 @@ using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Collections;
@@ -72,6 +71,7 @@ using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.QuickConnect;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
@@ -89,7 +89,6 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Providers.Chapters;
@@ -97,11 +96,12 @@ using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Plugins.TheTvdb;
 using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.XbmcMetadata.Providers;
-using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Prometheus.DotNetRuntime;
 using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
+using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
 
 namespace Emby.Server.Implementations
 {
@@ -122,14 +122,18 @@ namespace Emby.Server.Implementations
 
         private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
-        private IHttpServer _httpServer;
+        private IWebSocketManager _webSocketManager;
         private IHttpClient _httpClient;
 
+        private string[] _urlPrefixes;
+
         /// <summary>
         /// Gets a value indicating whether this instance can self restart.
         /// </summary>
         public bool CanSelfRestart => _startupOptions.RestartPath != null;
 
+        public bool CoreStartupHasCompleted { get; private set; }
+
         public virtual bool CanLaunchWebBrowser
         {
             get
@@ -173,6 +177,8 @@ namespace Emby.Server.Implementations
         /// </summary>
         protected ILogger<ApplicationHost> Logger { get; }
 
+        protected IServiceCollection ServiceCollection { get; }
+
         private IPlugin[] _plugins;
 
         /// <summary>
@@ -238,9 +244,11 @@ namespace Emby.Server.Implementations
             ILoggerFactory loggerFactory,
             IStartupOptions options,
             IFileSystem fileSystem,
-            INetworkManager networkManager)
+            INetworkManager networkManager,
+            IServiceCollection serviceCollection)
         {
             _xmlSerializer = new MyXmlSerializer();
+            ServiceCollection = serviceCollection;
 
             _networkManager = networkManager;
             networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
@@ -440,8 +448,7 @@ namespace Emby.Server.Implementations
             Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
 
             Logger.LogInformation("Core startup complete");
-            _httpServer.GlobalResponse = null;
-
+            CoreStartupHasCompleted = true;
             stopWatch.Restart();
             await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
             Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
@@ -464,7 +471,7 @@ namespace Emby.Server.Implementations
         }
 
         /// <inheritdoc/>
-        public void Init(IServiceCollection serviceCollection)
+        public void Init()
         {
             HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber;
             HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber;
@@ -493,148 +500,143 @@ namespace Emby.Server.Implementations
 
             DiscoverTypes();
 
-            RegisterServices(serviceCollection);
+            RegisterServices();
         }
 
-        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.
         /// </summary>
-        protected virtual void RegisterServices(IServiceCollection serviceCollection)
+        protected virtual void RegisterServices()
         {
-            serviceCollection.AddSingleton(_startupOptions);
-
-            serviceCollection.AddMemoryCache();
+            ServiceCollection.AddSingleton(_startupOptions);
 
-            serviceCollection.AddSingleton(ConfigurationManager);
-            serviceCollection.AddSingleton<IApplicationHost>(this);
+            ServiceCollection.AddMemoryCache();
 
-            serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
+            ServiceCollection.AddSingleton(ConfigurationManager);
+            ServiceCollection.AddSingleton<IApplicationHost>(this);
 
-            serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
+            ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
 
-            serviceCollection.AddSingleton(_fileSystemManager);
-            serviceCollection.AddSingleton<TvdbClientManager>();
+            ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
 
-            serviceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
+            ServiceCollection.AddSingleton(_fileSystemManager);
+            ServiceCollection.AddSingleton<TvdbClientManager>();
 
-            serviceCollection.AddSingleton(_networkManager);
+            ServiceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
 
-            serviceCollection.AddSingleton<IIsoManager, IsoManager>();
+            ServiceCollection.AddSingleton(_networkManager);
 
-            serviceCollection.AddSingleton<ITaskManager, TaskManager>();
+            ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
 
-            serviceCollection.AddSingleton(_xmlSerializer);
+            ServiceCollection.AddSingleton<ITaskManager, TaskManager>();
 
-            serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
+            ServiceCollection.AddSingleton(_xmlSerializer);
 
-            serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
+            ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>();
 
-            serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
+            ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
 
-            serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
+            ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>();
 
-            serviceCollection.AddSingleton<IZipClient, ZipClient>();
+            ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>();
 
-            serviceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>();
+            ServiceCollection.AddSingleton<IZipClient, ZipClient>();
 
-            serviceCollection.AddSingleton<IServerApplicationHost>(this);
-            serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
+            ServiceCollection.AddSingleton<IServerApplicationHost>(this);
+            ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
 
-            serviceCollection.AddSingleton(ServerConfigurationManager);
+            ServiceCollection.AddSingleton(ServerConfigurationManager);
 
-            serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
+            ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
 
-            serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
+            ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
 
-            serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
-            serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
+            ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
+            ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>();
 
-            serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
+            ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
 
-            serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
+            ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
 
             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
-            serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
+            ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
 
             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
-            serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
-            serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
+            ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
+            ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
 
             // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
-            serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
-            serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
-            serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
-            serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
+            ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
+            ServiceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
+            ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
+            ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>();
 
-            serviceCollection.AddSingleton<IMusicManager, MusicManager>();
+            ServiceCollection.AddSingleton<IMusicManager, MusicManager>();
 
-            serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
+            ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
 
-            serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
+            ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
 
-            serviceCollection.AddSingleton<ServiceController>();
-            serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
+            ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
 
-            serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
+            ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
 
-            serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
+            ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
 
-            serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
+            ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
 
-            serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
+            ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
 
-            serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
+            ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
 
-            serviceCollection.AddSingleton<IProviderManager, ProviderManager>();
+            ServiceCollection.AddSingleton<IProviderManager, ProviderManager>();
 
             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
-            serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
-            serviceCollection.AddSingleton<IDtoService, DtoService>();
+            ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
+            ServiceCollection.AddSingleton<IDtoService, DtoService>();
 
-            serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
+            ServiceCollection.AddSingleton<IChannelManager, ChannelManager>();
 
-            serviceCollection.AddSingleton<ISessionManager, SessionManager>();
+            ServiceCollection.AddSingleton<ISessionManager, SessionManager>();
 
-            serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
+            ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>();
 
-            serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
+            ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>();
 
-            serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
+            ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
 
-            serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
+            ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
 
-            serviceCollection.AddSingleton<LiveTvDtoService>();
-            serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
+            ServiceCollection.AddSingleton<LiveTvDtoService>();
+            ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
 
-            serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
+            ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>();
 
-            serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
+            ServiceCollection.AddSingleton<INotificationManager, NotificationManager>();
 
-            serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
+            ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
 
-            serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
+            ServiceCollection.AddSingleton<IChapterManager, ChapterManager>();
 
-            serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
+            ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
 
-            serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
-            serviceCollection.AddSingleton<ISessionContext, SessionContext>();
+            ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
+            ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
 
-            serviceCollection.AddSingleton<IAuthService, AuthService>();
+            ServiceCollection.AddSingleton<IAuthService, AuthService>();
+            ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
 
-            serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
+            ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
 
-            serviceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
-            serviceCollection.AddSingleton<EncodingHelper>();
+            ServiceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
+            ServiceCollection.AddSingleton<EncodingHelper>();
 
-            serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
+            ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
 
-            serviceCollection.AddSingleton<TranscodingJobHelper>();
-            serviceCollection.AddScoped<MediaInfoHelper>();
-            serviceCollection.AddScoped<AudioHelper>();
-            serviceCollection.AddScoped<DynamicHlsHelper>();
+            ServiceCollection.AddSingleton<TranscodingJobHelper>();
+            ServiceCollection.AddScoped<MediaInfoHelper>();
+            ServiceCollection.AddScoped<AudioHelper>();
+            ServiceCollection.AddScoped<DynamicHlsHelper>();
         }
 
         /// <summary>
@@ -648,7 +650,7 @@ namespace Emby.Server.Implementations
 
             _mediaEncoder = Resolve<IMediaEncoder>();
             _sessionManager = Resolve<ISessionManager>();
-            _httpServer = Resolve<IHttpServer>();
+            _webSocketManager = Resolve<IWebSocketManager>();
             _httpClient = Resolve<IHttpClient>();
 
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
@@ -750,7 +752,6 @@ namespace Emby.Server.Implementations
             CollectionFolder.XmlSerializer = _xmlSerializer;
             CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
             CollectionFolder.ApplicationHost = this;
-            AuthenticatedAttribute.AuthService = Resolve<IAuthService>();
         }
 
         /// <summary>
@@ -770,7 +771,8 @@ namespace Emby.Server.Implementations
                         .Where(i => i != null)
                         .ToArray();
 
-            _httpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes());
+            _urlPrefixes = GetUrlPrefixes().ToArray();
+            _webSocketManager.Init(GetExports<IWebSocketListener>());
 
             Resolve<ILibraryManager>().AddParts(
                 GetExports<IResolverIgnoreRule>(),
@@ -834,6 +836,8 @@ namespace Emby.Server.Implementations
                 {
                     hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
                 }
+
+                plugin.RegisterServices(ServiceCollection);
             }
             catch (Exception ex)
             {
@@ -934,7 +938,7 @@ namespace Emby.Server.Implementations
                 }
             }
 
-            if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
+            if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
             {
                 requiresRestart = true;
             }
@@ -1388,6 +1392,20 @@ namespace Emby.Server.Implementations
             _plugins = list.ToArray();
         }
 
+        public IEnumerable<Assembly> GetApiPluginAssemblies()
+        {
+            var assemblies = _allConcreteTypes
+                .Where(i => typeof(ControllerBase).IsAssignableFrom(i))
+                .Select(i => i.Assembly)
+                .Distinct();
+
+            foreach (var assembly in assemblies)
+            {
+                Logger.LogDebug("Found API endpoints in plugin {name}", assembly.FullName);
+                yield return assembly;
+            }
+        }
+
         public virtual void LaunchUrl(string url)
         {
             if (!CanLaunchWebBrowser)

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

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

+ 1 - 1
Emby.Server.Implementations/ConfigurationOptions.cs

@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations
         public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string>
         {
             { HostWebClientKey, bool.TrueString },
-            { HttpListenerHost.DefaultRedirectKey, "web/index.html" },
+            { DefaultRedirectKey, "web/index.html" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegAnalyzeDurationKey, "200M" },
             { PlaylistsAllowDuplicatesKey, bool.TrueString },

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

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

+ 6 - 6
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -22,7 +22,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="IPNetwork2" Version="2.5.211" />
+    <PackageReference Include="IPNetwork2" Version="2.5.224" />
     <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
@@ -32,12 +32,12 @@
     <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
-    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.7" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.7" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
+    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.7" />
     <PackageReference Include="Mono.Nat" Version="2.0.2" />
-    <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
+    <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
     <PackageReference Include="sharpcompress" Version="0.26.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />

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

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

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

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

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

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

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

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

+ 0 - 250
Emby.Server.Implementations/HttpServer/FileWriter.cs

@@ -1,250 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Runtime.InteropServices;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    public class FileWriter : IHttpResult
-    {
-        private static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
-
-        private static readonly string[] _skipLogExtensions = {
-            ".js",
-            ".html",
-            ".css"
-        };
-
-        private readonly IStreamHelper _streamHelper;
-        private readonly ILogger _logger;
-
-        /// <summary>
-        /// The _options.
-        /// </summary>
-        private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
-
-        /// <summary>
-        /// The _requested ranges.
-        /// </summary>
-        private List<KeyValuePair<long, long?>> _requestedRanges;
-
-        public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem, IStreamHelper streamHelper)
-        {
-            if (string.IsNullOrEmpty(contentType))
-            {
-                throw new ArgumentNullException(nameof(contentType));
-            }
-
-            _streamHelper = streamHelper;
-
-            Path = path;
-            _logger = logger;
-            RangeHeader = rangeHeader;
-
-            Headers[HeaderNames.ContentType] = contentType;
-
-            TotalContentLength = fileSystem.GetFileInfo(path).Length;
-            Headers[HeaderNames.AcceptRanges] = "bytes";
-
-            if (string.IsNullOrWhiteSpace(rangeHeader))
-            {
-                Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture);
-                StatusCode = HttpStatusCode.OK;
-            }
-            else
-            {
-                StatusCode = HttpStatusCode.PartialContent;
-                SetRangeValues();
-            }
-
-            FileShare = FileShare.Read;
-            Cookies = new List<Cookie>();
-        }
-
-        private string RangeHeader { get; set; }
-
-        private bool IsHeadRequest { get; set; }
-
-        private long RangeStart { get; set; }
-
-        private long RangeEnd { get; set; }
-
-        private long RangeLength { get; set; }
-
-        public long TotalContentLength { get; set; }
-
-        public Action OnComplete { get; set; }
-
-        public Action OnError { get; set; }
-
-        public List<Cookie> Cookies { get; private set; }
-
-        public FileShare FileShare { get; set; }
-
-        /// <summary>
-        /// Gets the options.
-        /// </summary>
-        /// <value>The options.</value>
-        public IDictionary<string, string> Headers => _options;
-
-        public string Path { get; set; }
-
-        /// <summary>
-        /// Gets the requested ranges.
-        /// </summary>
-        /// <value>The requested ranges.</value>
-        protected List<KeyValuePair<long, long?>> RequestedRanges
-        {
-            get
-            {
-                if (_requestedRanges == null)
-                {
-                    _requestedRanges = new List<KeyValuePair<long, long?>>();
-
-                    // Example: bytes=0-,32-63
-                    var ranges = RangeHeader.Split('=')[1].Split(',');
-
-                    foreach (var range in ranges)
-                    {
-                        var vals = range.Split('-');
-
-                        long start = 0;
-                        long? end = null;
-
-                        if (!string.IsNullOrEmpty(vals[0]))
-                        {
-                            start = long.Parse(vals[0], UsCulture);
-                        }
-
-                        if (!string.IsNullOrEmpty(vals[1]))
-                        {
-                            end = long.Parse(vals[1], UsCulture);
-                        }
-
-                        _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
-                    }
-                }
-
-                return _requestedRanges;
-            }
-        }
-
-        public string ContentType { get; set; }
-
-        public IRequest RequestContext { get; set; }
-
-        public object Response { get; set; }
-
-        public int Status { get; set; }
-
-        public HttpStatusCode StatusCode
-        {
-            get => (HttpStatusCode)Status;
-            set => Status = (int)value;
-        }
-
-        /// <summary>
-        /// Sets the range values.
-        /// </summary>
-        private void SetRangeValues()
-        {
-            var requestedRange = RequestedRanges[0];
-
-            // If the requested range is "0-", we can optimize by just doing a stream copy
-            if (!requestedRange.Value.HasValue)
-            {
-                RangeEnd = TotalContentLength - 1;
-            }
-            else
-            {
-                RangeEnd = requestedRange.Value.Value;
-            }
-
-            RangeStart = requestedRange.Key;
-            RangeLength = 1 + RangeEnd - RangeStart;
-
-            // Content-Length is the length of what we're serving, not the original content
-            var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture);
-            Headers[HeaderNames.ContentLength] = lengthString;
-            var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
-            Headers[HeaderNames.ContentRange] = rangeString;
-
-            _logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString);
-        }
-
-        public async Task WriteToAsync(HttpResponse response, CancellationToken cancellationToken)
-        {
-            try
-            {
-                // Headers only
-                if (IsHeadRequest)
-                {
-                    return;
-                }
-
-                var path = Path;
-                var offset = RangeStart;
-                var count = RangeLength;
-
-                if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1)
-                {
-                    var extension = System.IO.Path.GetExtension(path);
-
-                    if (extension == null || !_skipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
-                    {
-                        _logger.LogDebug("Transmit file {0}", path);
-                    }
-
-                    offset = 0;
-                    count = 0;
-                }
-
-                await TransmitFile(response.Body, path, offset, count, FileShare, cancellationToken).ConfigureAwait(false);
-            }
-            finally
-            {
-                OnComplete?.Invoke();
-            }
-        }
-
-        public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShare fileShare, CancellationToken cancellationToken)
-        {
-            var fileOptions = FileOptions.SequentialScan;
-
-            // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
-            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-            {
-                fileOptions |= FileOptions.Asynchronous;
-            }
-
-            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, fileOptions))
-            {
-                if (offset > 0)
-                {
-                    fs.Position = offset;
-                }
-
-                if (count > 0)
-                {
-                    await _streamHelper.CopyToAsync(fs, stream, count, cancellationToken).ConfigureAwait(false);
-                }
-                else
-                {
-                    await fs.CopyToAsync(stream, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false);
-                }
-            }
-        }
-    }
-}

+ 0 - 766
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -1,766 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-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.Services;
-using Emby.Server.Implementations.SocketSharp;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-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
-    {
-        /// <summary>
-        /// The key for a setting that specifies the default redirect path
-        /// to use for requests where the URL base prefix is invalid or missing.
-        /// </summary>
-        public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
-
-        private readonly ILogger<HttpListenerHost> _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 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 IHostEnvironment _hostEnvironment;
-
-        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
-        private bool _disposed = false;
-
-        public HttpListenerHost(
-            IServerApplicationHost applicationHost,
-            ILogger<HttpListenerHost> logger,
-            IServerConfigurationManager config,
-            IConfiguration configuration,
-            INetworkManager networkManager,
-            IJsonSerializer jsonSerializer,
-            IXmlSerializer xmlSerializer,
-            ILocalizationManager localizationManager,
-            ServiceController serviceController,
-            IHostEnvironment hostEnvironment,
-            ILoggerFactory loggerFactory)
-        {
-            _appHost = applicationHost;
-            _logger = logger;
-            _config = config;
-            _defaultRedirectPath = configuration[DefaultRedirectKey];
-            _baseUrlPrefix = _config.Configuration.BaseUrl;
-            _networkManager = networkManager;
-            _jsonSerializer = jsonSerializer;
-            _xmlSerializer = xmlSerializer;
-            ServiceController = serviceController;
-            _hostEnvironment = hostEnvironment;
-            _loggerFactory = loggerFactory;
-
-            _funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
-
-            Instance = this;
-            ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
-            GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
-        }
-
-        public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
-
-        public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; }
-
-        public static HttpListenerHost Instance { get; protected set; }
-
-        public string[] UrlPrefixes { get; private set; }
-
-        public string GlobalResponse { get; set; }
-
-        public ServiceController ServiceController { get; }
-
-        public object CreateInstance(Type type)
-        {
-            return _appHost.CreateInstance(type);
-        }
-
-        private static string NormalizeUrlPath(string path)
-        {
-            if (path.Length > 0 && path[0] == '/')
-            {
-                // If the path begins with a leading slash, just return it as-is
-                return path;
-            }
-            else
-            {
-                // If the path does not begin with a leading slash, append one for consistency
-                return "/" + path;
-            }
-        }
-
-        /// <summary>
-        /// Applies the request filters. Returns whether or not the request has been handled
-        /// and no more processing should be done.
-        /// </summary>
-        /// <returns></returns>
-        public void ApplyRequestFilters(IRequest req, HttpResponse res, object requestDto)
-        {
-            // Exec all RequestFilter attributes with Priority < 0
-            var attributes = GetRequestFilterAttributes(requestDto.GetType());
-
-            int count = attributes.Count;
-            int i = 0;
-            for (; i < count && attributes[i].Priority < 0; i++)
-            {
-                var attribute = attributes[i];
-                attribute.RequestFilter(req, res, requestDto);
-            }
-
-            // Exec remaining RequestFilter attributes with Priority >= 0
-            for (; i < count && attributes[i].Priority >= 0; i++)
-            {
-                var attribute = attributes[i];
-                attribute.RequestFilter(req, res, requestDto);
-            }
-        }
-
-        public Type GetServiceTypeByRequest(Type requestType)
-        {
-            _serviceOperationsMap.TryGetValue(requestType, out var serviceType);
-            return serviceType;
-        }
-
-        public void AddServiceInfo(Type serviceType, Type requestType)
-        {
-            _serviceOperationsMap[requestType] = serviceType;
-        }
-
-        private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
-        {
-            var attributes = requestDtoType.GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
-
-            var serviceType = GetServiceTypeByRequest(requestDtoType);
-            if (serviceType != null)
-            {
-                attributes.AddRange(serviceType.GetCustomAttributes(true).OfType<IHasRequestFilter>());
-            }
-
-            attributes.Sort((x, y) => x.Priority - y.Priority);
-
-            return attributes;
-        }
-
-        private static Exception GetActualException(Exception ex)
-        {
-            if (ex is AggregateException agg)
-            {
-                var inner = agg.InnerException;
-                if (inner != null)
-                {
-                    return GetActualException(inner);
-                }
-                else
-                {
-                    var inners = agg.InnerExceptions;
-                    if (inners.Count > 0)
-                    {
-                        return GetActualException(inners[0]);
-                    }
-                }
-            }
-
-            return ex;
-        }
-
-        private int GetStatusCode(Exception ex)
-        {
-            switch (ex)
-            {
-                case ArgumentException _: return 400;
-                case AuthenticationException _: return 401;
-                case SecurityException _: return 403;
-                case DirectoryNotFoundException _:
-                case FileNotFoundException _:
-                case ResourceNotFoundException _: return 404;
-                case MethodNotAllowedException _: return 405;
-                default: return 500;
-            }
-        }
-
-        private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace)
-        {
-            if (ignoreStackTrace)
-            {
-                _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
-            }
-            else
-            {
-                _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
-            }
-
-            var httpRes = httpReq.Response;
-
-            if (httpRes.HasStarted)
-            {
-                return;
-            }
-
-            httpRes.StatusCode = statusCode;
-
-            var errContent = _hostEnvironment.IsDevelopment()
-                    ? (NormalizeExceptionMessage(ex) ?? string.Empty)
-                    : "Error processing request.";
-            httpRes.ContentType = "text/plain";
-            httpRes.ContentLength = errContent.Length;
-            await httpRes.WriteAsync(errContent).ConfigureAwait(false);
-        }
-
-        private string NormalizeExceptionMessage(Exception ex)
-        {
-            // Do not expose the exception message for AuthenticationException
-            if (ex is AuthenticationException)
-            {
-                return null;
-            }
-
-            // Strip any information we don't want to reveal
-            return ex.Message
-                ?.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase)
-                .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
-        }
-
-        public static string RemoveQueryStringByKey(string url, string key)
-        {
-            var uri = new Uri(url);
-
-            // this gets all the query string key value pairs as a collection
-            var newQueryString = QueryHelpers.ParseQuery(uri.Query);
-
-            var originalCount = newQueryString.Count;
-
-            if (originalCount == 0)
-            {
-                return url;
-            }
-
-            // this removes the key if exists
-            newQueryString.Remove(key);
-
-            if (originalCount == newQueryString.Count)
-            {
-                return url;
-            }
-
-            // this gets the page path from root without QueryString
-            string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0];
-
-            return newQueryString.Count > 0
-                ? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString()))
-                : pagePathWithoutQueryString;
-        }
-
-        private static string GetUrlToLog(string url)
-        {
-            url = RemoveQueryStringByKey(url, "api_key");
-
-            return url;
-        }
-
-        private static string NormalizeConfiguredLocalAddress(string address)
-        {
-            var add = address.AsSpan().Trim('/');
-            int index = add.IndexOf('/');
-            if (index != -1)
-            {
-                add = add.Slice(index + 1);
-            }
-
-            return add.TrimStart('/').ToString();
-        }
-
-        private bool ValidateHost(string host)
-        {
-            var hosts = _config
-                .Configuration
-                .LocalNetworkAddresses
-                .Select(NormalizeConfiguredLocalAddress)
-                .ToList();
-
-            if (hosts.Count == 0)
-            {
-                return true;
-            }
-
-            host ??= string.Empty;
-
-            if (_networkManager.IsInPrivateAddressSpace(host))
-            {
-                hosts.Add("localhost");
-                hosts.Add("127.0.0.1");
-
-                return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1);
-            }
-
-            return true;
-        }
-
-        private bool ValidateRequest(string remoteIp, bool isLocal)
-        {
-            if (isLocal)
-            {
-                return true;
-            }
-
-            if (_config.Configuration.EnableRemoteAccess)
-            {
-                var addressFilter = _config.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
-
-                if (addressFilter.Length > 0 && !_networkManager.IsInLocalNetwork(remoteIp))
-                {
-                    if (_config.Configuration.IsRemoteIPFilterBlacklist)
-                    {
-                        return !_networkManager.IsAddressInSubnets(remoteIp, addressFilter);
-                    }
-                    else
-                    {
-                        return _networkManager.IsAddressInSubnets(remoteIp, addressFilter);
-                    }
-                }
-            }
-            else
-            {
-                if (!_networkManager.IsInLocalNetwork(remoteIp))
-                {
-                    return false;
-                }
-            }
-
-            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.ListenWithHttps
-                && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
-            {
-                // 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;
-                }
-
-                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);
-            return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
-        }
-
-        /// <summary>
-        /// Overridable method that can be used to implement a custom handler.
-        /// </summary>
-        private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
-        {
-            var stopWatch = new Stopwatch();
-            stopWatch.Start();
-            var httpRes = httpReq.Response;
-            string urlToLog = GetUrlToLog(urlString);
-            string remoteIp = httpReq.RemoteIp;
-
-            try
-            {
-                if (_disposed)
-                {
-                    httpRes.StatusCode = 503;
-                    httpRes.ContentType = "text/plain";
-                    await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false);
-                    return;
-                }
-
-                if (!ValidateHost(host))
-                {
-                    httpRes.StatusCode = 400;
-                    httpRes.ContentType = "text/plain";
-                    await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false);
-                    return;
-                }
-
-                if (!ValidateRequest(remoteIp, httpReq.IsLocal))
-                {
-                    httpRes.StatusCode = 403;
-                    httpRes.ContentType = "text/plain";
-                    await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false);
-                    return;
-                }
-
-                if (!ValidateSsl(httpReq.RemoteIp, urlString))
-                {
-                    RedirectToSecureUrl(httpReq, httpRes, urlString);
-                    return;
-                }
-
-                if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
-                {
-                    httpRes.StatusCode = 200;
-                    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;
-                }
-
-                if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
-                    || string.IsNullOrEmpty(localPath)
-                    || !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
-                {
-                    // Always redirect back to the default path if the base prefix is invalid or missing
-                    _logger.LogDebug("Normalizing a URL at {0}", localPath);
-                    httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath);
-                    return;
-                }
-
-                if (!string.IsNullOrEmpty(GlobalResponse))
-                {
-                    // We don't want the address pings in ApplicationHost to fail
-                    if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)
-                    {
-                        httpRes.StatusCode = 503;
-                        httpRes.ContentType = "text/html";
-                        await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false);
-                        return;
-                    }
-                }
-
-                var handler = GetServiceHandler(httpReq);
-                if (handler != null)
-                {
-                    await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
-                }
-                else
-                {
-                    throw new FileNotFoundException();
-                }
-            }
-            catch (Exception requestEx)
-            {
-                try
-                {
-                    var requestInnerEx = GetActualException(requestEx);
-                    var statusCode = GetStatusCode(requestInnerEx);
-
-                    foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
-                    {
-                        if (!httpRes.Headers.ContainsKey(key))
-                        {
-                            httpRes.Headers.Add(key, value);
-                        }
-                    }
-
-                    bool ignoreStackTrace =
-                        requestInnerEx is SocketException
-                        || requestInnerEx is IOException
-                        || requestInnerEx is OperationCanceledException
-                        || requestInnerEx is SecurityException
-                        || requestInnerEx is AuthenticationException
-                        || requestInnerEx is FileNotFoundException;
-
-                    // Do not handle 500 server exceptions manually when in development mode.
-                    // Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware.
-                    // However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored,
-                    // because it will log the stack trace when it handles the exception.
-                    if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment())
-                    {
-                        throw;
-                    }
-
-                    await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
-                }
-                catch (Exception handlerException)
-                {
-                    var aggregateEx = new AggregateException("Error while handling request exception", requestEx, handlerException);
-                    _logger.LogError(aggregateEx, "Error while handling exception in response to {Url}", urlToLog);
-
-                    if (_hostEnvironment.IsDevelopment())
-                    {
-                        throw aggregateEx;
-                    }
-                }
-            }
-            finally
-            {
-                if (httpRes.StatusCode >= 500)
-                {
-                    _logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog);
-                }
-
-                stopWatch.Stop();
-                var elapsed = stopWatch.Elapsed;
-                if (elapsed.TotalMilliseconds > 500)
-                {
-                    _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog);
-                }
-            }
-        }
-
-        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);
-
-                using 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)
-        {
-            var pathInfo = httpReq.PathInfo;
-
-            pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType);
-            var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo);
-            if (restPath != null)
-            {
-                return new ServiceHandler(restPath, contentType);
-            }
-
-            _logger.LogError("Could not find handler for {PathInfo}", pathInfo);
-            return null;
-        }
-
-        private void RedirectToSecureUrl(IHttpRequest httpReq, HttpResponse httpRes, string url)
-        {
-            if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
-            {
-                var builder = new UriBuilder(uri)
-                {
-                    Port = _config.Configuration.PublicHttpsPort,
-                    Scheme = "https"
-                };
-                url = builder.Uri.ToString();
-            }
-
-            httpRes.Redirect(url);
-        }
-
-        /// <summary>
-        /// Adds the rest handlers.
-        /// </summary>
-        /// <param name="serviceTypes">The service types to register with the <see cref="ServiceController"/>.</param>
-        /// <param name="listeners">The web socket listeners.</param>
-        /// <param name="urlPrefixes">The URL prefixes. See <see cref="UrlPrefixes"/>.</param>
-        public void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listeners, IEnumerable<string> urlPrefixes)
-        {
-            _webSocketListeners = listeners.ToArray();
-            UrlPrefixes = urlPrefixes.ToArray();
-
-            ServiceController.Init(this, serviceTypes);
-
-            ResponseFilters = new Action<IRequest, HttpResponse, object>[]
-            {
-                new ResponseFilter(this, _logger).FilterResponse
-            };
-        }
-
-        public RouteAttribute[] GetRouteAttributes(Type requestType)
-        {
-            var routes = requestType.GetTypeInfo().GetCustomAttributes<RouteAttribute>(true).ToList();
-            var clone = routes.ToList();
-
-            foreach (var route in clone)
-            {
-                routes.Add(new RouteAttribute(NormalizeCustomRoutePath(route.Path), route.Verbs)
-                {
-                    Notes = route.Notes,
-                    Priority = route.Priority,
-                    Summary = route.Summary
-                });
-
-                routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs)
-                {
-                    Notes = route.Notes,
-                    Priority = route.Priority,
-                    Summary = route.Summary
-                });
-
-                routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs)
-                {
-                    Notes = route.Notes,
-                    Priority = route.Priority,
-                    Summary = route.Summary
-                });
-            }
-
-            return routes.ToArray();
-        }
-
-        public Func<string, object> GetParseFn(Type propertyType)
-        {
-            return _funcParseFn(propertyType);
-        }
-
-        public void SerializeToJson(object o, Stream stream)
-        {
-            _jsonSerializer.SerializeToStream(o, stream);
-        }
-
-        public void SerializeToXml(object o, Stream stream)
-        {
-            _xmlSerializer.SerializeToStream(o, stream);
-        }
-
-        public Task<object> DeserializeXml(Type type, Stream stream)
-        {
-            return Task.FromResult(_xmlSerializer.DeserializeFromStream(type, stream));
-        }
-
-        public Task<object> DeserializeJson(Type type, Stream stream)
-        {
-            return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
-        }
-
-        private string NormalizeEmbyRoutePath(string path)
-        {
-            _logger.LogDebug("Normalizing /emby route");
-            return _baseUrlPrefix + "/emby" + NormalizeUrlPath(path);
-        }
-
-        private string NormalizeMediaBrowserRoutePath(string path)
-        {
-            _logger.LogDebug("Normalizing /mediabrowser route");
-            return _baseUrlPrefix + "/mediabrowser" + NormalizeUrlPath(path);
-        }
-
-        private string NormalizeCustomRoutePath(string path)
-        {
-            _logger.LogDebug("Normalizing custom route {0}", path);
-            return _baseUrlPrefix + NormalizeUrlPath(path);
-        }
-
-        /// <summary>
-        /// Processes the web socket message received.
-        /// </summary>
-        /// <param name="result">The result.</param>
-        private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
-        {
-            if (_disposed)
-            {
-                return Task.CompletedTask;
-            }
-
-            IEnumerable<Task> GetTasks()
-            {
-                foreach (var x in _webSocketListeners)
-                {
-                    yield return x.ProcessMessageAsync(result);
-                }
-            }
-
-            return Task.WhenAll(GetTasks());
-        }
-    }
-}

+ 0 - 721
Emby.Server.Implementations/HttpServer/HttpResultFactory.cs

@@ -1,721 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.IO.Compression;
-using System.Net;
-using System.Runtime.Serialization;
-using System.Text;
-using System.Threading.Tasks;
-using System.Xml;
-using Emby.Server.Implementations.Services;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
-using Microsoft.Net.Http.Headers;
-using IRequest = MediaBrowser.Model.Services.IRequest;
-using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    /// <summary>
-    /// Class HttpResultFactory.
-    /// </summary>
-    public class HttpResultFactory : IHttpResultFactory
-    {
-        // Last-Modified and If-Modified-Since must follow strict date format,
-        // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
-        private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\"";
-        // We specifically use en-US culture because both day of week and month names require it
-        private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false);
-
-        /// <summary>
-        /// The logger.
-        /// </summary>
-        private readonly ILogger<HttpResultFactory> _logger;
-        private readonly IFileSystem _fileSystem;
-        private readonly IJsonSerializer _jsonSerializer;
-        private readonly IStreamHelper _streamHelper;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="HttpResultFactory" /> class.
-        /// </summary>
-        public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper)
-        {
-            _fileSystem = fileSystem;
-            _jsonSerializer = jsonSerializer;
-            _streamHelper = streamHelper;
-            _logger = loggerfactory.CreateLogger<HttpResultFactory>();
-        }
-
-        /// <summary>
-        /// Gets the result.
-        /// </summary>
-        /// <param name="requestContext">The request context.</param>
-        /// <param name="content">The content.</param>
-        /// <param name="contentType">Type of the content.</param>
-        /// <param name="responseHeaders">The response headers.</param>
-        /// <returns>System.Object.</returns>
-        public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null)
-        {
-            return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
-        }
-
-        public object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null)
-        {
-            return GetHttpResult(null, content, contentType, true, responseHeaders);
-        }
-
-        public object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null)
-        {
-            return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
-        }
-
-        public object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null)
-        {
-            return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
-        }
-
-        public object GetRedirectResult(string url)
-        {
-            var responseHeaders = new Dictionary<string, string>();
-            responseHeaders[HeaderNames.Location] = url;
-
-            var result = new HttpResult(Array.Empty<byte>(), "text/plain", HttpStatusCode.Redirect);
-
-            AddResponseHeaders(result, responseHeaders);
-
-            return result;
-        }
-
-        /// <summary>
-        /// Gets the HTTP result.
-        /// </summary>
-        private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
-        {
-            var result = new StreamWriter(content, contentType);
-
-            if (responseHeaders == null)
-            {
-                responseHeaders = new Dictionary<string, string>();
-            }
-
-            if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out _))
-            {
-                responseHeaders[HeaderNames.Expires] = "0";
-            }
-
-            AddResponseHeaders(result, responseHeaders);
-
-            return result;
-        }
-
-        /// <summary>
-        /// Gets the HTTP result.
-        /// </summary>
-        private IHasHeaders GetHttpResult(IRequest requestContext, byte[] content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
-        {
-            string compressionType = null;
-            bool isHeadRequest = false;
-
-            if (requestContext != null)
-            {
-                compressionType = GetCompressionType(requestContext, content, contentType);
-                isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
-            }
-
-            IHasHeaders result;
-            if (string.IsNullOrEmpty(compressionType))
-            {
-                var contentLength = content.Length;
-
-                if (isHeadRequest)
-                {
-                    content = Array.Empty<byte>();
-                }
-
-                result = new StreamWriter(content, contentType, contentLength);
-            }
-            else
-            {
-                result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType);
-            }
-
-            if (responseHeaders == null)
-            {
-                responseHeaders = new Dictionary<string, string>();
-            }
-
-            if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
-            {
-                responseHeaders[HeaderNames.Expires] = "0";
-            }
-
-            AddResponseHeaders(result, responseHeaders);
-
-            return result;
-        }
-
-        /// <summary>
-        /// Gets the HTTP result.
-        /// </summary>
-        private IHasHeaders GetHttpResult(IRequest requestContext, string content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
-        {
-            IHasHeaders result;
-
-            var bytes = Encoding.UTF8.GetBytes(content);
-
-            var compressionType = requestContext == null ? null : GetCompressionType(requestContext, bytes, contentType);
-
-            var isHeadRequest = requestContext == null ? false : string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
-
-            if (string.IsNullOrEmpty(compressionType))
-            {
-                var contentLength = bytes.Length;
-
-                if (isHeadRequest)
-                {
-                    bytes = Array.Empty<byte>();
-                }
-
-                result = new StreamWriter(bytes, contentType, contentLength);
-            }
-            else
-            {
-                result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType);
-            }
-
-            if (responseHeaders == null)
-            {
-                responseHeaders = new Dictionary<string, string>();
-            }
-
-            if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
-            {
-                responseHeaders[HeaderNames.Expires] = "0";
-            }
-
-            AddResponseHeaders(result, responseHeaders);
-
-            return result;
-        }
-
-        /// <summary>
-        /// Gets the optimized result.
-        /// </summary>
-        /// <typeparam name="T"></typeparam>
-        public object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null)
-            where T : class
-        {
-            if (result == null)
-            {
-                throw new ArgumentNullException(nameof(result));
-            }
-
-            if (responseHeaders == null)
-            {
-                responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-            }
-
-            responseHeaders[HeaderNames.Expires] = "0";
-
-            return ToOptimizedResultInternal(requestContext, result, responseHeaders);
-        }
-
-        private string GetCompressionType(IRequest request, byte[] content, string responseContentType)
-        {
-            if (responseContentType == null)
-            {
-                return null;
-            }
-
-            // Per apple docs, hls manifests must be compressed
-            if (!responseContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) &&
-                responseContentType.IndexOf("json", StringComparison.OrdinalIgnoreCase) == -1 &&
-                responseContentType.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) == -1 &&
-                responseContentType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) == -1 &&
-                responseContentType.IndexOf("application/x-mpegURL", StringComparison.OrdinalIgnoreCase) == -1)
-            {
-                return null;
-            }
-
-            if (content.Length < 1024)
-            {
-                return null;
-            }
-
-            return GetCompressionType(request);
-        }
-
-        private static string GetCompressionType(IRequest request)
-        {
-            var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString();
-
-            if (!string.IsNullOrEmpty(acceptEncoding))
-            {
-                // if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
-                //    return "br";
-
-                if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase))
-                {
-                    return "deflate";
-                }
-
-                if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase))
-                {
-                    return "gzip";
-                }
-            }
-
-            return null;
-        }
-
-        /// <summary>
-        /// Returns the optimized result for the IRequestContext.
-        /// Does not use or store results in any cache.
-        /// </summary>
-        /// <param name="request"></param>
-        /// <param name="dto"></param>
-        /// <returns></returns>
-        public object ToOptimizedResult<T>(IRequest request, T dto)
-        {
-            return ToOptimizedResultInternal(request, dto);
-        }
-
-        private object ToOptimizedResultInternal<T>(IRequest request, T dto, IDictionary<string, string> responseHeaders = null)
-        {
-            // TODO: @bond use Span and .Equals
-            var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant();
-
-            switch (contentType)
-            {
-                case "application/xml":
-                case "text/xml":
-                case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
-                    return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders);
-
-                case "application/json":
-                case "text/json":
-                    return GetHttpResult(request, _jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders);
-                default:
-                    break;
-            }
-
-            var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase);
-
-            var ms = new MemoryStream();
-            var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
-
-            writerFn(dto, ms);
-
-            ms.Position = 0;
-
-            if (isHeadRequest)
-            {
-                using (ms)
-                {
-                    return GetHttpResult(request, Array.Empty<byte>(), contentType, true, responseHeaders);
-                }
-            }
-
-            return GetHttpResult(request, ms, contentType, true, responseHeaders);
-        }
-
-        private IHasHeaders GetCompressedResult(
-            byte[] content,
-            string requestedCompressionType,
-            IDictionary<string, string> responseHeaders,
-            bool isHeadRequest,
-            string contentType)
-        {
-            if (responseHeaders == null)
-            {
-                responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-            }
-
-            content = Compress(content, requestedCompressionType);
-            responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType;
-
-            responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding;
-
-            var contentLength = content.Length;
-
-            if (isHeadRequest)
-            {
-                var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
-                AddResponseHeaders(result, responseHeaders);
-                return result;
-            }
-            else
-            {
-                var result = new StreamWriter(content, contentType, contentLength);
-                AddResponseHeaders(result, responseHeaders);
-                return result;
-            }
-        }
-
-        private byte[] Compress(byte[] bytes, string compressionType)
-        {
-            if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase))
-            {
-                return Deflate(bytes);
-            }
-
-            if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase))
-            {
-                return GZip(bytes);
-            }
-
-            throw new NotSupportedException(compressionType);
-        }
-
-        private static byte[] Deflate(byte[] bytes)
-        {
-            // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream
-            // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream
-            using (var ms = new MemoryStream())
-            using (var zipStream = new DeflateStream(ms, CompressionMode.Compress))
-            {
-                zipStream.Write(bytes, 0, bytes.Length);
-                zipStream.Dispose();
-
-                return ms.ToArray();
-            }
-        }
-
-        private static byte[] GZip(byte[] buffer)
-        {
-            using (var ms = new MemoryStream())
-            using (var zipStream = new GZipStream(ms, CompressionMode.Compress))
-            {
-                zipStream.Write(buffer, 0, buffer.Length);
-                zipStream.Dispose();
-
-                return ms.ToArray();
-            }
-        }
-
-        private static string SerializeToXmlString(object from)
-        {
-            using (var ms = new MemoryStream())
-            {
-                var xwSettings = new XmlWriterSettings();
-                xwSettings.Encoding = new UTF8Encoding(false);
-                xwSettings.OmitXmlDeclaration = false;
-
-                using (var xw = XmlWriter.Create(ms, xwSettings))
-                {
-                    var serializer = new DataContractSerializer(from.GetType());
-                    serializer.WriteObject(xw, from);
-                    xw.Flush();
-                    ms.Seek(0, SeekOrigin.Begin);
-                    using (var reader = new StreamReader(ms))
-                    {
-                        return reader.ReadToEnd();
-                    }
-                }
-            }
-        }
-
-        /// <summary>
-        /// Pres the process optimized result.
-        /// </summary>
-        private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options)
-        {
-            bool noCache = requestContext.Headers[HeaderNames.CacheControl].ToString().IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
-            AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
-
-            if (!noCache)
-            {
-                if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ifModifiedSinceHeader))
-                {
-                    _logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]);
-                    return null;
-                }
-
-                if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified))
-                {
-                    AddAgeHeader(responseHeaders, options.DateLastModified);
-
-                    var result = new HttpResult(Array.Empty<byte>(), options.ContentType ?? "text/html", HttpStatusCode.NotModified);
-
-                    AddResponseHeaders(result, responseHeaders);
-
-                    return result;
-                }
-            }
-
-            return null;
-        }
-
-        public Task<object> GetStaticFileResult(IRequest requestContext,
-            string path,
-            FileShare fileShare = FileShare.Read)
-        {
-            if (string.IsNullOrEmpty(path))
-            {
-                throw new ArgumentNullException(nameof(path));
-            }
-
-            return GetStaticFileResult(requestContext, new StaticFileResultOptions
-            {
-                Path = path,
-                FileShare = fileShare
-            });
-        }
-
-        public Task<object> GetStaticFileResult(IRequest requestContext, StaticFileResultOptions options)
-        {
-            var path = options.Path;
-            var fileShare = options.FileShare;
-
-            if (string.IsNullOrEmpty(path))
-            {
-                throw new ArgumentException("Path can't be empty.", nameof(options));
-            }
-
-            if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite)
-            {
-                throw new ArgumentException("FileShare must be either Read or ReadWrite");
-            }
-
-            if (string.IsNullOrEmpty(options.ContentType))
-            {
-                options.ContentType = MimeTypes.GetMimeType(path);
-            }
-
-            if (!options.DateLastModified.HasValue)
-            {
-                options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path);
-            }
-
-            options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare));
-
-            options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
-            return GetStaticResult(requestContext, options);
-        }
-
-        /// <summary>
-        /// Gets the file stream.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="fileShare">The file share.</param>
-        /// <returns>Stream.</returns>
-        private Stream GetFileStream(string path, FileShare fileShare)
-        {
-            return new FileStream(path, FileMode.Open, FileAccess.Read, fileShare);
-        }
-
-        public Task<object> GetStaticResult(IRequest requestContext,
-            Guid cacheKey,
-            DateTime? lastDateModified,
-            TimeSpan? cacheDuration,
-            string contentType,
-            Func<Task<Stream>> factoryFn,
-            IDictionary<string, string> responseHeaders = null,
-            bool isHeadRequest = false)
-        {
-            return GetStaticResult(requestContext, new StaticResultOptions
-            {
-                CacheDuration = cacheDuration,
-                ContentFactory = factoryFn,
-                ContentType = contentType,
-                DateLastModified = lastDateModified,
-                IsHeadRequest = isHeadRequest,
-                ResponseHeaders = responseHeaders
-            });
-        }
-
-        public async Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options)
-        {
-            options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
-            var contentType = options.ContentType;
-            if (!StringValues.IsNullOrEmpty(requestContext.Headers[HeaderNames.IfModifiedSince]))
-            {
-                // See if the result is already cached in the browser
-                var result = GetCachedResult(requestContext, options.ResponseHeaders, options);
-
-                if (result != null)
-                {
-                    return result;
-                }
-            }
-
-            // TODO: We don't really need the option value
-            var isHeadRequest = options.IsHeadRequest || string.Equals(requestContext.Verb, "HEAD", StringComparison.OrdinalIgnoreCase);
-            var factoryFn = options.ContentFactory;
-            var responseHeaders = options.ResponseHeaders;
-            AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified);
-            AddAgeHeader(responseHeaders, options.DateLastModified);
-
-            var rangeHeader = requestContext.Headers[HeaderNames.Range];
-
-            if (!isHeadRequest && !string.IsNullOrEmpty(options.Path))
-            {
-                var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem, _streamHelper)
-                {
-                    OnComplete = options.OnComplete,
-                    OnError = options.OnError,
-                    FileShare = options.FileShare
-                };
-
-                AddResponseHeaders(hasHeaders, options.ResponseHeaders);
-                return hasHeaders;
-            }
-
-            var stream = await factoryFn().ConfigureAwait(false);
-
-            var totalContentLength = options.ContentLength;
-            if (!totalContentLength.HasValue)
-            {
-                try
-                {
-                    totalContentLength = stream.Length;
-                }
-                catch (NotSupportedException)
-                {
-                }
-            }
-
-            if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue)
-            {
-                var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest)
-                {
-                    OnComplete = options.OnComplete
-                };
-
-                AddResponseHeaders(hasHeaders, options.ResponseHeaders);
-                return hasHeaders;
-            }
-            else
-            {
-                if (totalContentLength.HasValue)
-                {
-                    responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture);
-                }
-
-                if (isHeadRequest)
-                {
-                    using (stream)
-                    {
-                        return GetHttpResult(requestContext, Array.Empty<byte>(), contentType, true, responseHeaders);
-                    }
-                }
-
-                var hasHeaders = new StreamWriter(stream, contentType)
-                {
-                    OnComplete = options.OnComplete,
-                    OnError = options.OnError
-                };
-
-                AddResponseHeaders(hasHeaders, options.ResponseHeaders);
-                return hasHeaders;
-            }
-        }
-
-        /// <summary>
-        /// Adds the caching responseHeaders.
-        /// </summary>
-        private void AddCachingHeaders(
-            IDictionary<string, string> responseHeaders,
-            TimeSpan? cacheDuration,
-            bool noCache,
-            DateTime? lastModifiedDate)
-        {
-            if (noCache)
-            {
-                responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate";
-                responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate";
-                return;
-            }
-
-            if (cacheDuration.HasValue)
-            {
-                responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds;
-            }
-            else
-            {
-                responseHeaders[HeaderNames.CacheControl] = "public";
-            }
-
-            if (lastModifiedDate.HasValue)
-            {
-                responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture);
-            }
-        }
-
-        /// <summary>
-        /// Adds the age header.
-        /// </summary>
-        /// <param name="responseHeaders">The responseHeaders.</param>
-        /// <param name="lastDateModified">The last date modified.</param>
-        private static void AddAgeHeader(IDictionary<string, string> responseHeaders, DateTime? lastDateModified)
-        {
-            if (lastDateModified.HasValue)
-            {
-                responseHeaders[HeaderNames.Age] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture);
-            }
-        }
-
-        /// <summary>
-        /// Determines whether [is not modified] [the specified if modified since].
-        /// </summary>
-        /// <param name="ifModifiedSince">If modified since.</param>
-        /// <param name="cacheDuration">Duration of the cache.</param>
-        /// <param name="dateModified">The date modified.</param>
-        /// <returns><c>true</c> if [is not modified] [the specified if modified since]; otherwise, <c>false</c>.</returns>
-        private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified)
-        {
-            if (dateModified.HasValue)
-            {
-                var lastModified = NormalizeDateForComparison(dateModified.Value);
-                ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
-
-                return lastModified <= ifModifiedSince;
-            }
-
-            if (cacheDuration.HasValue)
-            {
-                var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value);
-
-                if (DateTime.UtcNow < cacheExpirationDate)
-                {
-                    return true;
-                }
-            }
-
-            return false;
-        }
-
-
-        /// <summary>
-        /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that.
-        /// </summary>
-        /// <param name="date">The date.</param>
-        /// <returns>DateTime.</returns>
-        private static DateTime NormalizeDateForComparison(DateTime date)
-        {
-            return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
-        }
-
-        /// <summary>
-        /// Adds the response headers.
-        /// </summary>
-        /// <param name="hasHeaders">The has options.</param>
-        /// <param name="responseHeaders">The response headers.</param>
-        private static void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable<KeyValuePair<string, string>> responseHeaders)
-        {
-            foreach (var item in responseHeaders)
-            {
-                hasHeaders.Headers[item.Key] = item.Value;
-            }
-        }
-    }
-}

+ 0 - 212
Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs

@@ -1,212 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Buffers;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult
-    {
-        private const int BufferSize = 81920;
-
-        private readonly Dictionary<string, string> _options = new Dictionary<string, string>();
-
-        private List<KeyValuePair<long, long?>> _requestedRanges;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="RangeRequestWriter" /> class.
-        /// </summary>
-        /// <param name="rangeHeader">The range header.</param>
-        /// <param name="contentLength">The content length.</param>
-        /// <param name="source">The source.</param>
-        /// <param name="contentType">Type of the content.</param>
-        /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
-        public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest)
-        {
-            if (string.IsNullOrEmpty(contentType))
-            {
-                throw new ArgumentNullException(nameof(contentType));
-            }
-
-            RangeHeader = rangeHeader;
-            SourceStream = source;
-            IsHeadRequest = isHeadRequest;
-
-            ContentType = contentType;
-            Headers[HeaderNames.ContentType] = contentType;
-            Headers[HeaderNames.AcceptRanges] = "bytes";
-            StatusCode = HttpStatusCode.PartialContent;
-
-            SetRangeValues(contentLength);
-        }
-
-        /// <summary>
-        /// Gets or sets the source stream.
-        /// </summary>
-        /// <value>The source stream.</value>
-        private Stream SourceStream { get; set; }
-        private string RangeHeader { get; set; }
-        private bool IsHeadRequest { get; set; }
-
-        private long RangeStart { get; set; }
-        private long RangeEnd { get; set; }
-        private long RangeLength { get; set; }
-        private long TotalContentLength { get; set; }
-
-        public Action OnComplete { get; set; }
-
-        /// <summary>
-        /// Additional HTTP Headers
-        /// </summary>
-        /// <value>The headers.</value>
-        public IDictionary<string, string> Headers => _options;
-
-        /// <summary>
-        /// Gets the requested ranges.
-        /// </summary>
-        /// <value>The requested ranges.</value>
-        protected List<KeyValuePair<long, long?>> RequestedRanges
-        {
-            get
-            {
-                if (_requestedRanges == null)
-                {
-                    _requestedRanges = new List<KeyValuePair<long, long?>>();
-
-                    // Example: bytes=0-,32-63
-                    var ranges = RangeHeader.Split('=')[1].Split(',');
-
-                    foreach (var range in ranges)
-                    {
-                        var vals = range.Split('-');
-
-                        long start = 0;
-                        long? end = null;
-
-                        if (!string.IsNullOrEmpty(vals[0]))
-                        {
-                            start = long.Parse(vals[0], CultureInfo.InvariantCulture);
-                        }
-
-                        if (!string.IsNullOrEmpty(vals[1]))
-                        {
-                            end = long.Parse(vals[1], CultureInfo.InvariantCulture);
-                        }
-
-                        _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
-                    }
-                }
-
-                return _requestedRanges;
-            }
-        }
-
-        public string ContentType { get; set; }
-
-        public IRequest RequestContext { get; set; }
-
-        public object Response { get; set; }
-
-        public int Status { get; set; }
-
-        public HttpStatusCode StatusCode
-        {
-            get => (HttpStatusCode)Status;
-            set => Status = (int)value;
-        }
-
-        /// <summary>
-        /// Sets the range values.
-        /// </summary>
-        private void SetRangeValues(long contentLength)
-        {
-            var requestedRange = RequestedRanges[0];
-
-            TotalContentLength = contentLength;
-
-            // If the requested range is "0-", we can optimize by just doing a stream copy
-            if (!requestedRange.Value.HasValue)
-            {
-                RangeEnd = TotalContentLength - 1;
-            }
-            else
-            {
-                RangeEnd = requestedRange.Value.Value;
-            }
-
-            RangeStart = requestedRange.Key;
-            RangeLength = 1 + RangeEnd - RangeStart;
-
-            Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
-            Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
-
-            if (RangeStart > 0 && SourceStream.CanSeek)
-            {
-                SourceStream.Position = RangeStart;
-            }
-        }
-
-        public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
-        {
-            try
-            {
-                // Headers only
-                if (IsHeadRequest)
-                {
-                    return;
-                }
-
-                using (var source = SourceStream)
-                {
-                    // If the requested range is "0-", we can optimize by just doing a stream copy
-                    if (RangeEnd >= TotalContentLength - 1)
-                    {
-                        await source.CopyToAsync(responseStream, BufferSize, cancellationToken).ConfigureAwait(false);
-                    }
-                    else
-                    {
-                        await CopyToInternalAsync(source, responseStream, RangeLength, cancellationToken).ConfigureAwait(false);
-                    }
-                }
-            }
-            finally
-            {
-                OnComplete?.Invoke();
-            }
-        }
-
-        private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
-        {
-            var array = ArrayPool<byte>.Shared.Rent(BufferSize);
-            try
-            {
-                int bytesRead;
-                while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
-                {
-                    var bytesToCopy = Math.Min(bytesRead, copyLength);
-
-                    await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy), cancellationToken).ConfigureAwait(false);
-
-                    copyLength -= bytesToCopy;
-
-                    if (copyLength <= 0)
-                    {
-                        break;
-                    }
-                }
-            }
-            finally
-            {
-                ArrayPool<byte>.Shared.Return(array);
-            }
-        }
-    }
-}

+ 0 - 113
Emby.Server.Implementations/HttpServer/ResponseFilter.cs

@@ -1,113 +0,0 @@
-using System;
-using System.Globalization;
-using System.Text;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    /// <summary>
-    /// Class ResponseFilter.
-    /// </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(IHttpServer server, ILogger logger)
-        {
-            _server = server;
-            _logger = logger;
-        }
-
-        /// <summary>
-        /// Filters the response.
-        /// </summary>
-        /// <param name="req">The req.</param>
-        /// <param name="res">The res.</param>
-        /// <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["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)
-            {
-                _logger.LogError(exception, "Error processing request for {RawUrl}", req.RawUrl);
-
-                if (!string.IsNullOrEmpty(exception.Message))
-                {
-                    var error = exception.Message.Replace(Environment.NewLine, " ", StringComparison.Ordinal);
-                    error = RemoveControlCharacters(error);
-
-                    res.Headers.Add("X-Application-Error-Code", error);
-                }
-            }
-
-            if (dto is IHasHeaders hasHeaders)
-            {
-                if (!hasHeaders.Headers.ContainsKey(HeaderNames.Server))
-                {
-                    hasHeaders.Headers[HeaderNames.Server] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50";
-                }
-
-                // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy
-                if (hasHeaders.Headers.TryGetValue(HeaderNames.ContentLength, out string contentLength)
-                    && !string.IsNullOrEmpty(contentLength))
-                {
-                    var length = long.Parse(contentLength, CultureInfo.InvariantCulture);
-
-                    if (length > 0)
-                    {
-                        res.ContentLength = length;
-                    }
-                }
-            }
-        }
-
-        /// <summary>
-        /// Removes the control characters.
-        /// </summary>
-        /// <param name="inString">The in string.</param>
-        /// <returns>System.String.</returns>
-        public static string RemoveControlCharacters(string inString)
-        {
-            if (inString == null)
-            {
-                return null;
-            }
-            else if (inString.Length == 0)
-            {
-                return inString;
-            }
-
-            var newString = new StringBuilder(inString.Length);
-
-            foreach (var ch in inString)
-            {
-                if (!char.IsControl(ch))
-                {
-                    newString.Append(ch);
-                }
-            }
-
-            return newString.ToString();
-        }
-    }
-}

+ 1 - 212
Emby.Server.Implementations/HttpServer/Security/AuthService.cs

@@ -1,17 +1,7 @@
 #pragma warning disable CS1591
 
-using System;
-using System.Linq;
-using Emby.Server.Implementations.SocketSharp;
-using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
 
 namespace Emby.Server.Implementations.HttpServer.Security
@@ -19,32 +9,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
     public class AuthService : IAuthService
     {
         private readonly IAuthorizationContext _authorizationContext;
-        private readonly ISessionManager _sessionManager;
-        private readonly IServerConfigurationManager _config;
-        private readonly INetworkManager _networkManager;
 
         public AuthService(
-            IAuthorizationContext authorizationContext,
-            IServerConfigurationManager config,
-            ISessionManager sessionManager,
-            INetworkManager networkManager)
+            IAuthorizationContext authorizationContext)
         {
             _authorizationContext = authorizationContext;
-            _config = config;
-            _sessionManager = sessionManager;
-            _networkManager = networkManager;
-        }
-
-        public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
-        {
-            ValidateUser(request, authAttributes);
-        }
-
-        public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
-        {
-            var req = new WebSocketSharpRequest(request, null, request.Path);
-            var user = ValidateUser(req, authAttributes);
-            return user;
         }
 
         public AuthorizationInfo Authenticate(HttpRequest request)
@@ -62,185 +31,5 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
             return auth;
         }
-
-        private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
-        {
-            // This code is executed before the service
-            var auth = _authorizationContext.GetAuthorizationInfo(request);
-
-            if (!IsExemptFromAuthenticationToken(authAttributes, request))
-            {
-                ValidateSecurityToken(request, auth.Token);
-            }
-
-            if (authAttributes.AllowLocalOnly && !request.IsLocal)
-            {
-                throw new SecurityException("Operation not found.");
-            }
-
-            var user = auth.User;
-
-            if (user == null && auth.UserId != Guid.Empty)
-            {
-                throw new AuthenticationException("User with Id " + auth.UserId + " not found");
-            }
-
-            if (user != null)
-            {
-                ValidateUserAccess(user, request, authAttributes);
-            }
-
-            var info = GetTokenInfo(request);
-
-            if (!IsExemptFromRoles(auth, authAttributes, request, info))
-            {
-                var roles = authAttributes.GetRoles();
-
-                ValidateRoles(roles, user);
-            }
-
-            if (!string.IsNullOrEmpty(auth.DeviceId) &&
-                !string.IsNullOrEmpty(auth.Client) &&
-                !string.IsNullOrEmpty(auth.Device))
-            {
-                _sessionManager.LogSessionActivity(
-                    auth.Client,
-                    auth.Version,
-                    auth.DeviceId,
-                    auth.Device,
-                    request.RemoteIp,
-                    user);
-            }
-
-            return user;
-        }
-
-        private void ValidateUserAccess(
-            User user,
-            IRequest request,
-            IAuthenticationAttributes authAttributes)
-        {
-            if (user.HasPermission(PermissionKind.IsDisabled))
-            {
-                throw new SecurityException("User account has been disabled.");
-            }
-
-            if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp))
-            {
-                throw new SecurityException("User account has been disabled.");
-            }
-
-            if (!user.HasPermission(PermissionKind.IsAdministrator)
-                && !authAttributes.EscapeParentalControl
-                && !user.IsParentalScheduleAllowed())
-            {
-                request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl");
-
-                throw new SecurityException("This user account is not allowed access at this time.");
-            }
-        }
-
-        private bool IsExemptFromAuthenticationToken(IAuthenticationAttributes authAttribtues, IRequest request)
-        {
-            if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
-            {
-                return true;
-            }
-
-            if (authAttribtues.AllowLocal && request.IsLocal)
-            {
-                return true;
-            }
-
-            if (authAttribtues.AllowLocalOnly && request.IsLocal)
-            {
-                return true;
-            }
-
-            if (authAttribtues.IgnoreLegacyAuth)
-            {
-                return true;
-            }
-
-            return false;
-        }
-
-        private bool IsExemptFromRoles(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request, AuthenticationInfo tokenInfo)
-        {
-            if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
-            {
-                return true;
-            }
-
-            if (authAttribtues.AllowLocal && request.IsLocal)
-            {
-                return true;
-            }
-
-            if (authAttribtues.AllowLocalOnly && request.IsLocal)
-            {
-                return true;
-            }
-
-            if (string.IsNullOrEmpty(auth.Token))
-            {
-                return true;
-            }
-
-            if (tokenInfo != null && tokenInfo.UserId.Equals(Guid.Empty))
-            {
-                return true;
-            }
-
-            return false;
-        }
-
-        private static void ValidateRoles(string[] roles, User user)
-        {
-            if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase))
-            {
-                if (user == null || !user.HasPermission(PermissionKind.IsAdministrator))
-                {
-                    throw new SecurityException("User does not have admin access.");
-                }
-            }
-
-            if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase))
-            {
-                if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion))
-                {
-                    throw new SecurityException("User does not have delete access.");
-                }
-            }
-
-            if (roles.Contains("download", StringComparer.OrdinalIgnoreCase))
-            {
-                if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading))
-                {
-                    throw new SecurityException("User does not have download access.");
-                }
-            }
-        }
-
-        private static AuthenticationInfo GetTokenInfo(IRequest request)
-        {
-            request.Items.TryGetValue("OriginalAuthenticationInfo", out var info);
-            return info as AuthenticationInfo;
-        }
-
-        private void ValidateSecurityToken(IRequest request, string token)
-        {
-            if (string.IsNullOrEmpty(token))
-            {
-                throw new AuthenticationException("Access token is required.");
-            }
-
-            var info = GetTokenInfo(request);
-
-            if (info == null)
-            {
-                throw new AuthenticationException("Access token is invalid or expired.");
-            }
-        }
     }
 }

+ 9 - 15
Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs

@@ -7,7 +7,6 @@ using System.Net;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
-using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Net.Http.Headers;
 
@@ -24,14 +23,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
             _userManager = userManager;
         }
 
-        public AuthorizationInfo GetAuthorizationInfo(object requestContext)
+        public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
         {
-            return GetAuthorizationInfo((IRequest)requestContext);
-        }
-
-        public AuthorizationInfo GetAuthorizationInfo(IRequest requestContext)
-        {
-            if (requestContext.Items.TryGetValue("AuthorizationInfo", out var cached))
+            if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
             {
                 return (AuthorizationInfo)cached;
             }
@@ -52,18 +46,18 @@ namespace Emby.Server.Implementations.HttpServer.Security
         /// </summary>
         /// <param name="httpReq">The HTTP req.</param>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        private AuthorizationInfo GetAuthorization(IRequest httpReq)
+        private AuthorizationInfo GetAuthorization(HttpContext httpReq)
         {
             var auth = GetAuthorizationDictionary(httpReq);
             var (authInfo, originalAuthInfo) =
-                GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
+                GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
 
             if (originalAuthInfo != null)
             {
-                httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
+                httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
             }
 
-            httpReq.Items["AuthorizationInfo"] = authInfo;
+            httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
             return authInfo;
         }
 
@@ -203,13 +197,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
         /// </summary>
         /// <param name="httpReq">The HTTP req.</param>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        private Dictionary<string, string> GetAuthorizationDictionary(IRequest httpReq)
+        private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
         {
-            var auth = httpReq.Headers["X-Emby-Authorization"];
+            var auth = httpReq.Request.Headers["X-Emby-Authorization"];
 
             if (string.IsNullOrEmpty(auth))
             {
-                auth = httpReq.Headers[HeaderNames.Authorization];
+                auth = httpReq.Request.Headers[HeaderNames.Authorization];
             }
 
             return GetAuthorization(auth);

+ 7 - 13
Emby.Server.Implementations/HttpServer/Security/SessionContext.cs

@@ -2,11 +2,11 @@
 
 using System;
 using Jellyfin.Data.Entities;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
 
 namespace Emby.Server.Implementations.HttpServer.Security
 {
@@ -23,26 +23,20 @@ namespace Emby.Server.Implementations.HttpServer.Security
             _sessionManager = sessionManager;
         }
 
-        public SessionInfo GetSession(IRequest requestContext)
+        public SessionInfo GetSession(HttpContext requestContext)
         {
             var authorization = _authContext.GetAuthorizationInfo(requestContext);
 
             var user = authorization.User;
-            return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user);
-        }
-
-        private AuthenticationInfo GetTokenInfo(IRequest request)
-        {
-            request.Items.TryGetValue("OriginalAuthenticationInfo", out var info);
-            return info as AuthenticationInfo;
+            return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.Request.RemoteIp(), user);
         }
 
         public SessionInfo GetSession(object requestContext)
         {
-            return GetSession((IRequest)requestContext);
+            return GetSession((HttpContext)requestContext);
         }
 
-        public User GetUser(IRequest requestContext)
+        public User GetUser(HttpContext requestContext)
         {
             var session = GetSession(requestContext);
 
@@ -51,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
         public User GetUser(object requestContext)
         {
-            return GetUser((IRequest)requestContext);
+            return GetUser((HttpContext)requestContext);
         }
     }
 }

+ 0 - 120
Emby.Server.Implementations/HttpServer/StreamWriter.cs

@@ -1,120 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    /// <summary>
-    /// Class StreamWriter.
-    /// </summary>
-    public class StreamWriter : IAsyncStreamWriter, IHasHeaders
-    {
-        /// <summary>
-        /// The options.
-        /// </summary>
-        private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="StreamWriter" /> class.
-        /// </summary>
-        /// <param name="source">The source.</param>
-        /// <param name="contentType">Type of the content.</param>
-        public StreamWriter(Stream source, string contentType)
-        {
-            if (string.IsNullOrEmpty(contentType))
-            {
-                throw new ArgumentNullException(nameof(contentType));
-            }
-
-            SourceStream = source;
-
-            Headers["Content-Type"] = contentType;
-
-            if (source.CanSeek)
-            {
-                Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture);
-            }
-
-            Headers[HeaderNames.ContentType] = contentType;
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="StreamWriter"/> class.
-        /// </summary>
-        /// <param name="source">The source.</param>
-        /// <param name="contentType">Type of the content.</param>
-        /// <param name="contentLength">The content length.</param>
-        public StreamWriter(byte[] source, string contentType, int contentLength)
-        {
-            if (string.IsNullOrEmpty(contentType))
-            {
-                throw new ArgumentNullException(nameof(contentType));
-            }
-
-            SourceBytes = source;
-
-            Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture);
-            Headers[HeaderNames.ContentType] = contentType;
-        }
-
-        /// <summary>
-        /// Gets or sets the source stream.
-        /// </summary>
-        /// <value>The source stream.</value>
-        private Stream SourceStream { get; set; }
-
-        private byte[] SourceBytes { get; set; }
-
-        /// <summary>
-        /// Gets the options.
-        /// </summary>
-        /// <value>The options.</value>
-        public IDictionary<string, string> Headers => _options;
-
-        /// <summary>
-        /// Fires when complete.
-        /// </summary>
-        public Action OnComplete { get; set; }
-
-        /// <summary>
-        /// Fires when an error occours.
-        /// </summary>
-        public Action OnError { get; set; }
-
-        /// <inheritdoc />
-        public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
-        {
-            try
-            {
-                var bytes = SourceBytes;
-
-                if (bytes != null)
-                {
-                    await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
-                }
-                else
-                {
-                    using (var src = SourceStream)
-                    {
-                        await src.CopyToAsync(responseStream, cancellationToken).ConfigureAwait(false);
-                    }
-                }
-            }
-            catch
-            {
-                OnError?.Invoke();
-
-                throw;
-            }
-            finally
-            {
-                OnComplete?.Invoke();
-            }
-        }
-    }
-}

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

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

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

@@ -0,0 +1,102 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using Jellyfin.Data.Events;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.HttpServer
+{
+    public class WebSocketManager : IWebSocketManager
+    {
+        private readonly ILogger<WebSocketManager> _logger;
+        private readonly ILoggerFactory _loggerFactory;
+
+        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
+        private bool _disposed = false;
+
+        public WebSocketManager(
+            ILogger<WebSocketManager> logger,
+            ILoggerFactory loggerFactory)
+        {
+            _logger = logger;
+            _loggerFactory = loggerFactory;
+        }
+
+        public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
+
+        /// <inheritdoc />
+        public 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);
+
+                using 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>
+        /// Adds the rest handlers.
+        /// </summary>
+        /// <param name="listeners">The web socket listeners.</param>
+        public void Init(IEnumerable<IWebSocketListener> listeners)
+        {
+            _webSocketListeners = listeners.ToArray();
+        }
+
+        /// <summary>
+        /// Processes the web socket message received.
+        /// </summary>
+        /// <param name="result">The result.</param>
+        private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
+        {
+            if (_disposed)
+            {
+                return Task.CompletedTask;
+            }
+
+            IEnumerable<Task> GetTasks()
+            {
+                foreach (var x in _webSocketListeners)
+                {
+                    yield return x.ProcessMessageAsync(result);
+                }
+            }
+
+            return Task.WhenAll(GetTasks());
+        }
+    }
+}

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

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

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

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

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

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

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

@@ -17,5 +17,8 @@
     "Genres": "Géneros",
     "Folders": "Carpetas",
     "Favorites": "Favoritos",
-    "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}"
+    "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}",
+    "HeaderFavoriteSongs": "Canciones Favoritas",
+    "HeaderFavoriteEpisodes": "Episodios Favoritos",
+    "HeaderFavoriteArtists": "Artistas Favoritos"
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/id.json

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

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

@@ -45,7 +45,7 @@
     "NameSeasonNumber": "Sesong {0}",
     "NameSeasonUnknown": "Sesong ukjent",
     "NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.",
-    "NotificationOptionApplicationUpdateAvailable": "Programvareoppdatering er tilgjengelig",
+    "NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig",
     "NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert",
     "NotificationOptionAudioPlayback": "Lydavspilling startet",
     "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet",

+ 104 - 60
Emby.Server.Implementations/Localization/Core/th.json

@@ -1,73 +1,117 @@
 {
     "ProviderValue": "ผู้ให้บริการ: {0}",
-    "PluginUpdatedWithName": "{0} ได้รับการ update แล้ว",
-    "PluginUninstalledWithName": "ถอนการติดตั้ง {0}",
-    "PluginInstalledWithName": "{0} ได้รับการติดตั้ง",
-    "Plugin": "Plugin",
-    "Playlists": "รายการ",
+    "PluginUpdatedWithName": "อัปเดต {0} แล้ว",
+    "PluginUninstalledWithName": "ถอนการติดตั้ง {0} แล้ว",
+    "PluginInstalledWithName": "ติดตั้ง {0} แล้ว",
+    "Plugin": "ปลั๊กอิน",
+    "Playlists": "เพลย์ลิสต์",
     "Photos": "รูปภาพ",
-    "NotificationOptionVideoPlaybackStopped": "หยุดการเล่น Video",
-    "NotificationOptionVideoPlayback": "เริ่มแสดง Video",
-    "NotificationOptionUserLockedOut": "ผู้ใช้ Locked Out",
-    "NotificationOptionTaskFailed": "ตารางการทำงานล้มเหลว",
-    "NotificationOptionServerRestartRequired": "ควร Restart Server",
-    "NotificationOptionPluginUpdateInstalled": "Update Plugin แล้ว",
-    "NotificationOptionPluginUninstalled": "ถอด Plugin",
-    "NotificationOptionPluginInstalled": "ติดตั้ง Plugin แล้ว",
-    "NotificationOptionPluginError": "Plugin ล้มเหลว",
-    "NotificationOptionNewLibraryContent": "เพิ่มข้อมูลใหม่แล้ว",
-    "NotificationOptionInstallationFailed": "ติดตั้งล้มเหลว",
-    "NotificationOptionCameraImageUploaded": "รูปภาพถูก upload",
-    "NotificationOptionAudioPlaybackStopped": "หยุดการเล่นเสียง",
+    "NotificationOptionVideoPlaybackStopped": "หยุดเล่นวิดีโอ",
+    "NotificationOptionVideoPlayback": "เริ่มเล่นวิดีโอ",
+    "NotificationOptionUserLockedOut": "ผู้ใช้ถูกล็อก",
+    "NotificationOptionTaskFailed": "งานตามกำหนดการล้มเหลว",
+    "NotificationOptionServerRestartRequired": "จำเป็นต้องรีสตาร์ทเซิร์ฟเวอร์",
+    "NotificationOptionPluginUpdateInstalled": "ติดตั้งการอัปเดตปลั๊กอินแล้ว",
+    "NotificationOptionPluginUninstalled": "ถอนการติดตั้งปลั๊กอินแล้ว",
+    "NotificationOptionPluginInstalled": "ติดตั้งปลั๊กอินแล้ว",
+    "NotificationOptionPluginError": "ปลั๊กอินล้มเหลว",
+    "NotificationOptionNewLibraryContent": "เพิ่มเนื้อหาใหม่แล้ว",
+    "NotificationOptionInstallationFailed": "การติดตั้งล้มเหลว",
+    "NotificationOptionCameraImageUploaded": "อัปโหลดภาพถ่ายแล้ว",
+    "NotificationOptionAudioPlaybackStopped": "หยุดเล่นเสียง",
     "NotificationOptionAudioPlayback": "เริ่มเล่นเสียง",
-    "NotificationOptionApplicationUpdateInstalled": "Update ระบบแล้ว",
-    "NotificationOptionApplicationUpdateAvailable": "ระบบ update สามารถใช้ได้แล้ว",
-    "NewVersionIsAvailable": "ตรวจพบ Jellyfin เวอร์ชั่นใหม่",
-    "NameSeasonUnknown": "ไม่ทราบปี",
-    "NameSeasonNumber": "ปี {0}",
-    "NameInstallFailed": "{0} ติดตั้งไม่สำเร็จ",
-    "MusicVideos": "MV",
-    "Music": "เพลง",
-    "Movies": "ภาพยนต์",
-    "MixedContent": "รายการแบบผสม",
-    "MessageServerConfigurationUpdated": "การตั้งค่า update แล้ว",
-    "MessageNamedServerConfigurationUpdatedWithValue": "รายการตั้งค่า {0} ได้รับการ update แล้ว",
-    "MessageApplicationUpdatedTo": "Jellyfin Server จะ update ไปที่ {0}",
-    "MessageApplicationUpdated": "Jellyfin Server update แล้ว",
+    "NotificationOptionApplicationUpdateInstalled": "ติดตั้งการอัปเดตแอพพลิเคชันแล้ว",
+    "NotificationOptionApplicationUpdateAvailable": "มีการอัปเดตแอพพลิเคชัน",
+    "NewVersionIsAvailable": "เวอร์ชันใหม่ของเซิร์ฟเวอร์ Jellyfin พร้อมให้ดาวน์โหลดแล้ว",
+    "NameSeasonUnknown": "ไม่ทราบซีซัน",
+    "NameSeasonNumber": "ซีซัน {0}",
+    "NameInstallFailed": "การติดตั้ง {0} ล้มเหลว",
+    "MusicVideos": "มิวสิควิดีโอ",
+    "Music": "ดนตรี",
+    "Movies": "ภาพยนต์",
+    "MixedContent": "เนื้อหาผสม",
+    "MessageServerConfigurationUpdated": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์แล้ว",
+    "MessageNamedServerConfigurationUpdatedWithValue": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์ในส่วน {0} แล้ว",
+    "MessageApplicationUpdatedTo": "เซิร์ฟเวอร์ Jellyfin ได้รับการอัปเดตเป็น {0}",
+    "MessageApplicationUpdated": "อัพเดตเซิร์ฟเวอร์ Jellyfin แล้ว",
     "Latest": "ล่าสุด",
-    "LabelRunningTimeValue": "เวลาที่เล่น : {0}",
-    "LabelIpAddressValue": "IP address: {0}",
-    "ItemRemovedWithName": "{0} ถูกลบจากรายการ",
-    "ItemAddedWithName": "{0} ถูกเพิ่มในรายการ",
-    "Inherit": "การสืบทอด",
-    "HomeVideos": "วีดีโอส่วนตัว",
-    "HeaderRecordingGroups": "ค่ายบันทึก",
+    "LabelRunningTimeValue": "ผ่านไปแล้ว: {0}",
+    "LabelIpAddressValue": "ที่อยู่ IP: {0}",
+    "ItemRemovedWithName": "{0} ถูกลบออกจากไลบราร",
+    "ItemAddedWithName": "{0} ถูกเพิ่มลงในไลบรารีแล้ว",
+    "Inherit": "สืบทอด",
+    "HomeVideos": "โฮมวิดีโอ",
+    "HeaderRecordingGroups": "กลุ่มการบันทึก",
     "HeaderNextUp": "ถัดไป",
-    "HeaderLiveTV": "รายการสด",
-    "HeaderFavoriteSongs": "เพลงโปรด",
-    "HeaderFavoriteShows": "รายการโชว์โปรด",
-    "HeaderFavoriteEpisodes": "ฉากโปรด",
-    "HeaderFavoriteArtists": "นักแสดงโปรด",
-    "HeaderFavoriteAlbums": "อัมบั้มโปรด",
-    "HeaderContinueWatching": "ชมต่อจากเดิม",
-    "HeaderCameraUploads": "Upload รูปภาพ",
-    "HeaderAlbumArtists": "อัลบั้มนักแสดง",
+    "HeaderLiveTV": "ทีวีสด",
+    "HeaderFavoriteSongs": "เพลงที่ชื่นชอบ",
+    "HeaderFavoriteShows": "รายการที่ชื่นชอบ",
+    "HeaderFavoriteEpisodes": "ตอนที่ชื่นชอบ",
+    "HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ",
+    "HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ",
+    "HeaderContinueWatching": "ดูต่อ",
+    "HeaderCameraUploads": "อัปโหลดรูปถ่าย",
+    "HeaderAlbumArtists": "อัลบั้มศิลปิน",
     "Genres": "ประเภท",
     "Folders": "โฟลเดอร์",
     "Favorites": "รายการโปรด",
-    "FailedLoginAttemptWithUserName": "การเชื่อมต่อล้มเหลวจาก {0}",
-    "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จ",
-    "DeviceOfflineWithName": "{0} ตัดการเชื่อมต่อ",
-    "Collections": "ชุด",
-    "ChapterNameValue": "บทที่ {0}",
-    "Channels": "ชาแนล",
-    "CameraImageUploadedFrom": "รูปภาพถูก upload จาก {0}",
+    "FailedLoginAttemptWithUserName": "ความพยายามในการเข้าสู่ระบบล้มเหลวจาก {0}",
+    "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จแล้ว",
+    "DeviceOfflineWithName": "{0} ยกเลิกการเชื่อมต่อแล้ว",
+    "Collections": "คอลเลกชัน",
+    "ChapterNameValue": "บท {0}",
+    "Channels": "ช่อง",
+    "CameraImageUploadedFrom": "ภาพถ่ายใหม่ได้ถูกอัปโหลดมาจาก {0}",
     "Books": "หนังสือ",
-    "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จ",
-    "Artists": "นักแสดง",
-    "Application": "แอปพลิเคชั่น",
-    "AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
+    "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จแล้ว",
+    "Artists": "ศิลปิน",
+    "Application": "แอพพลิเคชัน",
+    "AppDeviceValues": "แอพ: {0}, อุปกรณ์: {1}",
     "Albums": "อัลบั้ม",
     "ScheduledTaskStartedWithName": "{0} เริ่มต้น",
-    "ScheduledTaskFailedWithName": "{0} ล้มเหลว"
+    "ScheduledTaskFailedWithName": "{0} ล้มเหลว",
+    "Songs": "เพลง",
+    "Shows": "รายการ",
+    "ServerNameNeedsToBeRestarted": "{0} ต้องการการรีสตาร์ท",
+    "TaskDownloadMissingSubtitlesDescription": "ค้นหาคำบรรยายที่หายไปในอินเทอร์เน็ตตามค่ากำหนดในข้อมูลเมตา",
+    "TaskDownloadMissingSubtitles": "ดาวน์โหลดคำบรรยายที่ขาดหายไป",
+    "TaskRefreshChannelsDescription": "รีเฟรชข้อมูลช่องอินเทอร์เน็ต",
+    "TaskRefreshChannels": "รีเฟรชช่อง",
+    "TaskCleanTranscodeDescription": "ลบไฟล์ทรานส์โค้ดที่มีอายุมากกว่าหนึ่งวัน",
+    "TaskCleanTranscode": "ล้างไดเรกทอรีทรานส์โค้ด",
+    "TaskUpdatePluginsDescription": "ดาวน์โหลดและติดตั้งโปรแกรมปรับปรุงให้กับปลั๊กอินที่กำหนดค่าให้อัปเดตโดยอัตโนมัติ",
+    "TaskUpdatePlugins": "อัปเดตปลั๊กอิน",
+    "TaskRefreshPeopleDescription": "อัปเดตข้อมูลเมตานักแสดงและผู้กำกับในไลบรารีสื่อ",
+    "TaskRefreshPeople": "รีเฟรชบุคคล",
+    "TaskCleanLogsDescription": "ลบไฟล์บันทึกที่เก่ากว่า {0} วัน",
+    "TaskCleanLogs": "ล้างไดเรกทอรีบันทึก",
+    "TaskRefreshLibraryDescription": "สแกนไลบรารีสื่อของคุณเพื่อหาไฟล์ใหม่และรีเฟรชข้อมูลเมตา",
+    "TaskRefreshLibrary": "สแกนไลบรารีสื่อ",
+    "TaskRefreshChapterImagesDescription": "สร้างภาพขนาดย่อสำหรับวิดีโอที่มีบท",
+    "TaskRefreshChapterImages": "แตกรูปภาพบท",
+    "TaskCleanCacheDescription": "ลบไฟล์แคชที่ระบบไม่ต้องการ",
+    "TaskCleanCache": "ล้างไดเรกทอรีแคช",
+    "TasksChannelsCategory": "ช่องอินเทอร์เน็ต",
+    "TasksApplicationCategory": "แอพพลิเคชัน",
+    "TasksLibraryCategory": "ไลบรารี",
+    "TasksMaintenanceCategory": "ปิดซ่อมบำรุง",
+    "VersionNumber": "เวอร์ชัน {0}",
+    "ValueSpecialEpisodeName": "พิเศษ - {0}",
+    "ValueHasBeenAddedToLibrary": "เพิ่ม {0} ลงในไลบรารีสื่อของคุณแล้ว",
+    "UserStoppedPlayingItemWithValues": "{0} เล่นเสร็จแล้ว {1} บน {2}",
+    "UserStartedPlayingItemWithValues": "{0} กำลังเล่น {1} บน {2}",
+    "UserPolicyUpdatedWithName": "มีการอัปเดตนโยบายผู้ใช้ของ {0}",
+    "UserPasswordChangedWithName": "มีการเปลี่ยนรหัสผ่านของผู้ใช้ {0}",
+    "UserOnlineFromDevice": "{0} ออนไลน์จาก {1}",
+    "UserOfflineFromDevice": "{0} ได้ยกเลิกการเชื่อมต่อจาก {1}",
+    "UserLockedOutWithName": "ผู้ใช้ {0} ถูกล็อก",
+    "UserDownloadingItemWithValues": "{0} กำลังดาวน์โหลด {1}",
+    "UserDeletedWithName": "ลบผู้ใช้ {0} แล้ว",
+    "UserCreatedWithName": "สร้างผู้ใช้ {0} แล้ว",
+    "User": "ผู้ใช้งาน",
+    "TvShows": "รายการทีวี",
+    "System": "ระบบ",
+    "Sync": "ซิงค์",
+    "SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้",
+    "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่"
 }

+ 285 - 0
Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs

@@ -0,0 +1,285 @@
+using System;
+using System.Collections.Concurrent;
+using System.Globalization;
+using System.Linq;
+using System.Security.Cryptography;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.QuickConnect;
+using MediaBrowser.Controller.Security;
+using MediaBrowser.Model.QuickConnect;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.QuickConnect
+{
+    /// <summary>
+    /// Quick connect implementation.
+    /// </summary>
+    public class QuickConnectManager : IQuickConnect, IDisposable
+    {
+        private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider();
+        private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ConcurrentDictionary<string, QuickConnectResult>();
+
+        private readonly IServerConfigurationManager _config;
+        private readonly ILogger<QuickConnectManager> _logger;
+        private readonly IAuthenticationRepository _authenticationRepository;
+        private readonly IAuthorizationContext _authContext;
+        private readonly IServerApplicationHost _appHost;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="QuickConnectManager"/> class.
+        /// Should only be called at server startup when a singleton is created.
+        /// </summary>
+        /// <param name="config">Configuration.</param>
+        /// <param name="logger">Logger.</param>
+        /// <param name="appHost">Application host.</param>
+        /// <param name="authContext">Authentication context.</param>
+        /// <param name="authenticationRepository">Authentication repository.</param>
+        public QuickConnectManager(
+            IServerConfigurationManager config,
+            ILogger<QuickConnectManager> logger,
+            IServerApplicationHost appHost,
+            IAuthorizationContext authContext,
+            IAuthenticationRepository authenticationRepository)
+        {
+            _config = config;
+            _logger = logger;
+            _appHost = appHost;
+            _authContext = authContext;
+            _authenticationRepository = authenticationRepository;
+
+            ReloadConfiguration();
+        }
+
+        /// <inheritdoc/>
+        public int CodeLength { get; set; } = 6;
+
+        /// <inheritdoc/>
+        public string TokenName { get; set; } = "QuickConnect";
+
+        /// <inheritdoc/>
+        public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable;
+
+        /// <inheritdoc/>
+        public int Timeout { get; set; } = 5;
+
+        private DateTime DateActivated { get; set; }
+
+        /// <inheritdoc/>
+        public void AssertActive()
+        {
+            if (State != QuickConnectState.Active)
+            {
+                throw new ArgumentException("Quick connect is not active on this server");
+            }
+        }
+
+        /// <inheritdoc/>
+        public void Activate()
+        {
+            DateActivated = DateTime.UtcNow;
+            SetState(QuickConnectState.Active);
+        }
+
+        /// <inheritdoc/>
+        public void SetState(QuickConnectState newState)
+        {
+            _logger.LogDebug("Changed quick connect state from {State} to {newState}", State, newState);
+
+            ExpireRequests(true);
+
+            State = newState;
+            _config.Configuration.QuickConnectAvailable = newState == QuickConnectState.Available || newState == QuickConnectState.Active;
+            _config.SaveConfiguration();
+
+            _logger.LogDebug("Configuration saved");
+        }
+
+        /// <inheritdoc/>
+        public QuickConnectResult TryConnect()
+        {
+            ExpireRequests();
+
+            if (State != QuickConnectState.Active)
+            {
+                _logger.LogDebug("Refusing quick connect initiation request, current state is {State}", State);
+                throw new AuthenticationException("Quick connect is not active on this server");
+            }
+
+            var code = GenerateCode();
+            var result = new QuickConnectResult()
+            {
+                Secret = GenerateSecureRandom(),
+                DateAdded = DateTime.UtcNow,
+                Code = code
+            };
+
+            _currentRequests[code] = result;
+            return result;
+        }
+
+        /// <inheritdoc/>
+        public QuickConnectResult CheckRequestStatus(string secret)
+        {
+            ExpireRequests();
+            AssertActive();
+
+            string code = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Code).DefaultIfEmpty(string.Empty).First();
+
+            if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
+            {
+                throw new ResourceNotFoundException("Unable to find request with provided secret");
+            }
+
+            return result;
+        }
+
+        /// <inheritdoc/>
+        public string GenerateCode()
+        {
+            Span<byte> raw = stackalloc byte[4];
+
+            int min = (int)Math.Pow(10, CodeLength - 1);
+            int max = (int)Math.Pow(10, CodeLength);
+
+            uint scale = uint.MaxValue;
+            while (scale == uint.MaxValue)
+            {
+                _rng.GetBytes(raw);
+                scale = BitConverter.ToUInt32(raw);
+            }
+
+            int code = (int)(min + ((max - min) * (scale / (double)uint.MaxValue)));
+            return code.ToString(CultureInfo.InvariantCulture);
+        }
+
+        /// <inheritdoc/>
+        public bool AuthorizeRequest(Guid userId, string code)
+        {
+            ExpireRequests();
+            AssertActive();
+
+            if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
+            {
+                throw new ResourceNotFoundException("Unable to find request");
+            }
+
+            if (result.Authenticated)
+            {
+                throw new InvalidOperationException("Request is already authorized");
+            }
+
+            result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+
+            // Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
+            var added = result.DateAdded ?? DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(Timeout));
+            result.DateAdded = added.Subtract(TimeSpan.FromMinutes(Timeout - 1));
+
+            _authenticationRepository.Create(new AuthenticationInfo
+            {
+                AppName = TokenName,
+                AccessToken = result.Authentication,
+                DateCreated = DateTime.UtcNow,
+                DeviceId = _appHost.SystemId,
+                DeviceName = _appHost.FriendlyName,
+                AppVersion = _appHost.ApplicationVersionString,
+                UserId = userId
+            });
+
+            _logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId);
+
+            return true;
+        }
+
+        /// <inheritdoc/>
+        public int DeleteAllDevices(Guid user)
+        {
+            var raw = _authenticationRepository.Get(new AuthenticationInfoQuery()
+            {
+                DeviceId = _appHost.SystemId,
+                UserId = user
+            });
+
+            var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.Ordinal));
+
+            var removed = 0;
+            foreach (var token in tokens)
+            {
+                _authenticationRepository.Delete(token);
+                _logger.LogDebug("Deleted token {AccessToken}", token.AccessToken);
+                removed++;
+            }
+
+            return removed;
+        }
+
+        /// <summary>
+        /// Dispose.
+        /// </summary>
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        /// <summary>
+        /// Dispose.
+        /// </summary>
+        /// <param name="disposing">Dispose unmanaged resources.</param>
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _rng?.Dispose();
+            }
+        }
+
+        private string GenerateSecureRandom(int length = 32)
+        {
+            Span<byte> bytes = stackalloc byte[length];
+            _rng.GetBytes(bytes);
+
+            return Hex.Encode(bytes);
+        }
+
+        /// <inheritdoc/>
+        public void ExpireRequests(bool expireAll = false)
+        {
+            // Check if quick connect should be deactivated
+            if (State == QuickConnectState.Active && DateTime.UtcNow > DateActivated.AddMinutes(Timeout) && !expireAll)
+            {
+                _logger.LogDebug("Quick connect time expired, deactivating");
+                SetState(QuickConnectState.Available);
+                expireAll = true;
+            }
+
+            // Expire stale connection requests
+            var code = string.Empty;
+            var values = _currentRequests.Values.ToList();
+
+            for (int i = 0; i < values.Count; i++)
+            {
+                var added = values[i].DateAdded ?? DateTime.UnixEpoch;
+                if (DateTime.UtcNow > added.AddMinutes(Timeout) || expireAll)
+                {
+                    code = values[i].Code;
+                    _logger.LogDebug("Removing expired request {code}", code);
+
+                    if (!_currentRequests.TryRemove(code, out _))
+                    {
+                        _logger.LogWarning("Request {code} already expired", code);
+                    }
+                }
+            }
+        }
+
+        private void ReloadConfiguration()
+        {
+            State = _config.Configuration.QuickConnectAvailable ? QuickConnectState.Available : QuickConnectState.Unavailable;
+        }
+    }
+}

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

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

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

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

+ 0 - 64
Emby.Server.Implementations/Services/HttpResult.cs

@@ -1,64 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Server.Implementations.Services
-{
-    public class HttpResult
-        : IHttpResult, IAsyncStreamWriter
-    {
-        public HttpResult(object response, string contentType, HttpStatusCode statusCode)
-        {
-            this.Headers = new Dictionary<string, string>();
-
-            this.Response = response;
-            this.ContentType = contentType;
-            this.StatusCode = statusCode;
-        }
-
-        public object Response { get; set; }
-
-        public string ContentType { get; set; }
-
-        public IDictionary<string, string> Headers { get; private set; }
-
-        public int Status { get; set; }
-
-        public HttpStatusCode StatusCode
-        {
-            get => (HttpStatusCode)Status;
-            set => Status = (int)value;
-        }
-
-        public IRequest RequestContext { get; set; }
-
-        public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
-        {
-            var response = RequestContext?.Response;
-
-            if (this.Response is byte[] bytesResponse)
-            {
-                var contentLength = bytesResponse.Length;
-
-                if (response != null)
-                {
-                    response.ContentLength = contentLength;
-                }
-
-                if (contentLength > 0)
-                {
-                    await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false);
-                }
-
-                return;
-            }
-
-            await ResponseHelper.WriteObject(this.RequestContext, this.Response, response).ConfigureAwait(false);
-        }
-    }
-}

+ 0 - 51
Emby.Server.Implementations/Services/RequestHelper.cs

@@ -1,51 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.IO;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-
-namespace Emby.Server.Implementations.Services
-{
-    public class RequestHelper
-    {
-        public static Func<Type, Stream, Task<object>> GetRequestReader(HttpListenerHost host, string contentType)
-        {
-            switch (GetContentTypeWithoutEncoding(contentType))
-            {
-                case "application/xml":
-                case "text/xml":
-                case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
-                    return host.DeserializeXml;
-
-                case "application/json":
-                case "text/json":
-                    return host.DeserializeJson;
-            }
-
-            return null;
-        }
-
-        public static Action<object, Stream> GetResponseWriter(HttpListenerHost host, string contentType)
-        {
-            switch (GetContentTypeWithoutEncoding(contentType))
-            {
-                case "application/xml":
-                case "text/xml":
-                case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
-                    return host.SerializeToXml;
-
-                case "application/json":
-                case "text/json":
-                    return host.SerializeToJson;
-            }
-
-            return null;
-        }
-
-        private static string GetContentTypeWithoutEncoding(string contentType)
-        {
-            return contentType?.Split(';')[0].ToLowerInvariant().Trim();
-        }
-    }
-}

+ 0 - 141
Emby.Server.Implementations/Services/ResponseHelper.cs

@@ -1,141 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.IO;
-using System.Net;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-
-namespace Emby.Server.Implementations.Services
-{
-    public static class ResponseHelper
-    {
-        public static Task WriteToResponse(HttpResponse response, IRequest request, object result, CancellationToken cancellationToken)
-        {
-            if (result == null)
-            {
-                if (response.StatusCode == (int)HttpStatusCode.OK)
-                {
-                    response.StatusCode = (int)HttpStatusCode.NoContent;
-                }
-
-                response.ContentLength = 0;
-                return Task.CompletedTask;
-            }
-
-            var httpResult = result as IHttpResult;
-            if (httpResult != null)
-            {
-                httpResult.RequestContext = request;
-                request.ResponseContentType = httpResult.ContentType ?? request.ResponseContentType;
-            }
-
-            var defaultContentType = request.ResponseContentType;
-
-            if (httpResult != null)
-            {
-                if (httpResult.RequestContext == null)
-                {
-                    httpResult.RequestContext = request;
-                }
-
-                response.StatusCode = httpResult.Status;
-            }
-
-            if (result is IHasHeaders responseOptions)
-            {
-                foreach (var responseHeaders in responseOptions.Headers)
-                {
-                    if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
-                    {
-                        response.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture);
-                        continue;
-                    }
-
-                    response.Headers.Add(responseHeaders.Key, responseHeaders.Value);
-                }
-            }
-
-            // ContentType='text/html' is the default for a HttpResponse
-            // Do not override if another has been set
-            if (response.ContentType == null || response.ContentType == "text/html")
-            {
-                response.ContentType = defaultContentType;
-            }
-
-            if (response.ContentType == "application/json")
-            {
-                response.ContentType += "; charset=utf-8";
-            }
-
-            switch (result)
-            {
-                case IAsyncStreamWriter asyncStreamWriter:
-                    return asyncStreamWriter.WriteToAsync(response.Body, cancellationToken);
-                case IStreamWriter streamWriter:
-                    streamWriter.WriteTo(response.Body);
-                    return Task.CompletedTask;
-                case FileWriter fileWriter:
-                    return fileWriter.WriteToAsync(response, cancellationToken);
-                case Stream stream:
-                    return CopyStream(stream, response.Body);
-                case byte[] bytes:
-                    response.ContentType = "application/octet-stream";
-                    response.ContentLength = bytes.Length;
-
-                    if (bytes.Length > 0)
-                    {
-                        return response.Body.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
-                    }
-
-                    return Task.CompletedTask;
-                case string responseText:
-                    var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText);
-                    response.ContentLength = responseTextAsBytes.Length;
-
-                    if (responseTextAsBytes.Length > 0)
-                    {
-                        return response.Body.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken);
-                    }
-
-                    return Task.CompletedTask;
-            }
-
-            return WriteObject(request, result, response);
-        }
-
-        private static async Task CopyStream(Stream src, Stream dest)
-        {
-            using (src)
-            {
-                await src.CopyToAsync(dest).ConfigureAwait(false);
-            }
-        }
-
-        public static async Task WriteObject(IRequest request, object result, HttpResponse response)
-        {
-            var contentType = request.ResponseContentType;
-            var serializer = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
-
-            using (var ms = new MemoryStream())
-            {
-                serializer(result, ms);
-
-                ms.Position = 0;
-
-                var contentLength = ms.Length;
-                response.ContentLength = contentLength;
-
-                if (contentLength > 0)
-                {
-                    await ms.CopyToAsync(response.Body).ConfigureAwait(false);
-                }
-            }
-        }
-    }
-}

+ 0 - 202
Emby.Server.Implementations/Services/ServiceController.cs

@@ -1,202 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Services
-{
-    public delegate object ActionInvokerFn(object intance, object request);
-
-    public delegate void VoidActionInvokerFn(object intance, object request);
-
-    public class ServiceController
-    {
-        private readonly ILogger<ServiceController> _logger;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ServiceController"/> class.
-        /// </summary>
-        /// <param name="logger">The <see cref="ServiceController"/> logger.</param>
-        public ServiceController(ILogger<ServiceController> logger)
-        {
-            _logger = logger;
-        }
-
-        public void Init(HttpListenerHost appHost, IEnumerable<Type> serviceTypes)
-        {
-            foreach (var serviceType in serviceTypes)
-            {
-                RegisterService(appHost, serviceType);
-            }
-        }
-
-        public void RegisterService(HttpListenerHost appHost, Type serviceType)
-        {
-            // Make sure the provided type implements IService
-            if (!typeof(IService).IsAssignableFrom(serviceType))
-            {
-                _logger.LogWarning("Tried to register a service that does not implement IService: {ServiceType}", serviceType);
-                return;
-            }
-
-            var processedReqs = new HashSet<Type>();
-
-            var actions = ServiceExecGeneral.Reset(serviceType);
-
-            foreach (var mi in serviceType.GetActions())
-            {
-                var requestType = mi.GetParameters()[0].ParameterType;
-                if (processedReqs.Contains(requestType))
-                {
-                    continue;
-                }
-
-                processedReqs.Add(requestType);
-
-                ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions);
-
-                // var returnMarker = GetTypeWithGenericTypeDefinitionOf(requestType, typeof(IReturn<>));
-                // var responseType = returnMarker != null ?
-                //      GetGenericArguments(returnMarker)[0]
-                //    : mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ?
-                //      mi.ReturnType
-                //    : Type.GetType(requestType.FullName + "Response");
-
-                RegisterRestPaths(appHost, requestType, serviceType);
-
-                appHost.AddServiceInfo(serviceType, requestType);
-            }
-        }
-
-        public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap();
-
-        public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType)
-        {
-            var attrs = appHost.GetRouteAttributes(requestType);
-            foreach (var attr in attrs)
-            {
-                var restPath = new RestPath(appHost.CreateInstance, appHost.GetParseFn, requestType, serviceType, attr.Path, attr.Verbs, attr.IsHidden, attr.Summary, attr.Description);
-
-                RegisterRestPath(restPath);
-            }
-        }
-
-        private static readonly char[] InvalidRouteChars = new[] { '?', '&' };
-
-        public void RegisterRestPath(RestPath restPath)
-        {
-            if (restPath.Path[0] != '/')
-            {
-                throw new ArgumentException(
-                    string.Format(
-                        CultureInfo.InvariantCulture,
-                        "Route '{0}' on '{1}' must start with a '/'",
-                        restPath.Path,
-                        restPath.RequestType.GetMethodName()));
-            }
-
-            if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
-            {
-                throw new ArgumentException(
-                    string.Format(
-                        CultureInfo.InvariantCulture,
-                        "Route '{0}' on '{1}' contains invalid chars. ",
-                        restPath.Path,
-                        restPath.RequestType.GetMethodName()));
-            }
-
-            if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
-            {
-                pathsAtFirstMatch.Add(restPath);
-            }
-            else
-            {
-                RestPathMap[restPath.FirstMatchHashKey] = new List<RestPath>() { restPath };
-            }
-        }
-
-        public RestPath GetRestPathForRequest(string httpMethod, string pathInfo)
-        {
-            var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo);
-
-            List<RestPath> firstMatches;
-
-            var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts);
-            foreach (var potentialHashMatch in yieldedHashMatches)
-            {
-                if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
-                {
-                    continue;
-                }
-
-                var bestScore = -1;
-                RestPath bestMatch = null;
-                foreach (var restPath in firstMatches)
-                {
-                    var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
-                    if (score > bestScore)
-                    {
-                        bestScore = score;
-                        bestMatch = restPath;
-                    }
-                }
-
-                if (bestScore > 0 && bestMatch != null)
-                {
-                    return bestMatch;
-                }
-            }
-
-            var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts);
-            foreach (var potentialHashMatch in yieldedWildcardMatches)
-            {
-                if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
-                {
-                    continue;
-                }
-
-                var bestScore = -1;
-                RestPath bestMatch = null;
-                foreach (var restPath in firstMatches)
-                {
-                    var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
-                    if (score > bestScore)
-                    {
-                        bestScore = score;
-                        bestMatch = restPath;
-                    }
-                }
-
-                if (bestScore > 0 && bestMatch != null)
-                {
-                    return bestMatch;
-                }
-            }
-
-            return null;
-        }
-
-        public Task<object> Execute(HttpListenerHost httpHost, object requestDto, IRequest req)
-        {
-            var requestType = requestDto.GetType();
-            req.OperationName = requestType.Name;
-
-            var serviceType = httpHost.GetServiceTypeByRequest(requestType);
-
-            var service = httpHost.CreateInstance(serviceType);
-
-            if (service is IRequiresRequest serviceRequiresContext)
-            {
-                serviceRequiresContext.Request = req;
-            }
-
-            // Executes the service and returns the result
-            return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
-        }
-    }
-}

+ 0 - 230
Emby.Server.Implementations/Services/ServiceExec.cs

@@ -1,230 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Linq.Expressions;
-using System.Reflection;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Server.Implementations.Services
-{
-    public static class ServiceExecExtensions
-    {
-        public static string[] AllVerbs = new[] {
-            "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616
-            "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK",    // RFC 2518
-            "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT",
-            "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY",  // RFC 3253
-            "ORDERPATCH", // RFC 3648
-            "ACL",        // RFC 3744
-            "PATCH",      // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/
-            "SEARCH",     // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/
-            "BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY",
-            "POLL",  "SUBSCRIBE", "UNSUBSCRIBE"
-        };
-
-        public static List<MethodInfo> GetActions(this Type serviceType)
-        {
-            var list = new List<MethodInfo>();
-
-            foreach (var mi in serviceType.GetRuntimeMethods())
-            {
-                if (!mi.IsPublic)
-                {
-                    continue;
-                }
-
-                if (mi.IsStatic)
-                {
-                    continue;
-                }
-
-                if (mi.GetParameters().Length != 1)
-                {
-                    continue;
-                }
-
-                var actionName = mi.Name;
-                if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase))
-                {
-                    continue;
-                }
-
-                list.Add(mi);
-            }
-
-            return list;
-        }
-    }
-
-    internal static class ServiceExecGeneral
-    {
-        private static Dictionary<string, ServiceMethod> execMap = new Dictionary<string, ServiceMethod>();
-
-        public static void CreateServiceRunnersFor(Type requestType, List<ServiceMethod> actions)
-        {
-            foreach (var actionCtx in actions)
-            {
-                if (execMap.ContainsKey(actionCtx.Id))
-                {
-                    continue;
-                }
-
-                execMap[actionCtx.Id] = actionCtx;
-            }
-        }
-
-        public static Task<object> Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName)
-        {
-            var actionName = request.Verb ?? "POST";
-
-            if (execMap.TryGetValue(ServiceMethod.Key(serviceType, actionName, requestName), out ServiceMethod actionContext))
-            {
-                if (actionContext.RequestFilters != null)
-                {
-                    foreach (var requestFilter in actionContext.RequestFilters)
-                    {
-                        requestFilter.RequestFilter(request, request.Response, requestDto);
-                        if (request.Response.HasStarted)
-                        {
-                            Task.FromResult<object>(null);
-                        }
-                    }
-                }
-
-                var response = actionContext.ServiceAction(instance, requestDto);
-
-                if (response is Task taskResponse)
-                {
-                    return GetTaskResult(taskResponse);
-                }
-
-                return Task.FromResult(response);
-            }
-
-            var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant();
-            throw new NotImplementedException(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    "Could not find method named {1}({0}) or Any({0}) on Service {2}",
-                    requestDto.GetType().GetMethodName(),
-                    expectedMethodName,
-                    serviceType.GetMethodName()));
-        }
-
-        private static async Task<object> GetTaskResult(Task task)
-        {
-            try
-            {
-                if (task is Task<object> taskObject)
-                {
-                    return await taskObject.ConfigureAwait(false);
-                }
-
-                await task.ConfigureAwait(false);
-
-                var type = task.GetType().GetTypeInfo();
-                if (!type.IsGenericType)
-                {
-                    return null;
-                }
-
-                var resultProperty = type.GetDeclaredProperty("Result");
-                if (resultProperty == null)
-                {
-                    return null;
-                }
-
-                var result = resultProperty.GetValue(task);
-
-                // hack alert
-                if (result.GetType().Name.IndexOf("voidtaskresult", StringComparison.OrdinalIgnoreCase) != -1)
-                {
-                    return null;
-                }
-
-                return result;
-            }
-            catch (TypeAccessException)
-            {
-                return null; // return null for void Task's
-            }
-        }
-
-        public static List<ServiceMethod> Reset(Type serviceType)
-        {
-            var actions = new List<ServiceMethod>();
-
-            foreach (var mi in serviceType.GetActions())
-            {
-                var actionName = mi.Name;
-                var args = mi.GetParameters();
-
-                var requestType = args[0].ParameterType;
-                var actionCtx = new ServiceMethod
-                {
-                    Id = ServiceMethod.Key(serviceType, actionName, requestType.GetMethodName())
-                };
-
-                actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi);
-
-                var reqFilters = new List<IHasRequestFilter>();
-
-                foreach (var attr in mi.GetCustomAttributes(true))
-                {
-                    if (attr is IHasRequestFilter hasReqFilter)
-                    {
-                        reqFilters.Add(hasReqFilter);
-                    }
-                }
-
-                if (reqFilters.Count > 0)
-                {
-                    actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray();
-                }
-
-                actions.Add(actionCtx);
-            }
-
-            return actions;
-        }
-
-        private static ActionInvokerFn CreateExecFn(Type serviceType, Type requestType, MethodInfo mi)
-        {
-            var serviceParam = Expression.Parameter(typeof(object), "serviceObj");
-            var serviceStrong = Expression.Convert(serviceParam, serviceType);
-
-            var requestDtoParam = Expression.Parameter(typeof(object), "requestDto");
-            var requestDtoStrong = Expression.Convert(requestDtoParam, requestType);
-
-            Expression callExecute = Expression.Call(
-            serviceStrong, mi, requestDtoStrong);
-
-            if (mi.ReturnType != typeof(void))
-            {
-                var executeFunc = Expression.Lambda<ActionInvokerFn>(
-                    callExecute,
-                    serviceParam,
-                    requestDtoParam).Compile();
-
-                return executeFunc;
-            }
-            else
-            {
-                var executeFunc = Expression.Lambda<VoidActionInvokerFn>(
-                    callExecute,
-                    serviceParam,
-                    requestDtoParam).Compile();
-
-                return (service, request) =>
-                {
-                    executeFunc(service, request);
-                    return null;
-                };
-            }
-        }
-    }
-}

+ 0 - 212
Emby.Server.Implementations/Services/ServiceHandler.cs

@@ -1,212 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Net.Mime;
-using System.Reflection;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Services
-{
-    public class ServiceHandler
-    {
-        private RestPath _restPath;
-
-        private string _responseContentType;
-
-        internal ServiceHandler(RestPath restPath, string responseContentType)
-        {
-            _restPath = restPath;
-            _responseContentType = responseContentType;
-        }
-
-        protected static Task<object> CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType)
-        {
-            if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0)
-            {
-                var deserializer = RequestHelper.GetRequestReader(host, contentType);
-                if (deserializer != null)
-                {
-                    return deserializer.Invoke(requestType, httpReq.InputStream);
-                }
-            }
-
-            return Task.FromResult(host.CreateInstance(requestType));
-        }
-
-        public static string GetSanitizedPathInfo(string pathInfo, out string contentType)
-        {
-            contentType = null;
-            var pos = pathInfo.LastIndexOf('.');
-            if (pos != -1)
-            {
-                var format = pathInfo.AsSpan().Slice(pos + 1);
-                contentType = GetFormatContentType(format);
-                if (contentType != null)
-                {
-                    pathInfo = pathInfo.Substring(0, pos);
-                }
-            }
-
-            return pathInfo;
-        }
-
-        private static string GetFormatContentType(ReadOnlySpan<char> format)
-        {
-            if (format.Equals("json", StringComparison.Ordinal))
-            {
-                return MediaTypeNames.Application.Json;
-            }
-            else if (format.Equals("xml", StringComparison.Ordinal))
-            {
-                return MediaTypeNames.Application.Xml;
-            }
-
-            return null;
-        }
-
-        public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken)
-        {
-            httpReq.Items["__route"] = _restPath;
-
-            if (_responseContentType != null)
-            {
-                httpReq.ResponseContentType = _responseContentType;
-            }
-
-            var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false);
-
-            httpHost.ApplyRequestFilters(httpReq, httpRes, request);
-
-            httpRes.HttpContext.SetServiceStackRequest(httpReq);
-            var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
-
-            // Apply response filters
-            foreach (var responseFilter in httpHost.ResponseFilters)
-            {
-                responseFilter(httpReq, httpRes, response);
-            }
-
-            await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
-        }
-
-        public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath)
-        {
-            var requestType = restPath.RequestType;
-
-            if (RequireqRequestStream(requestType))
-            {
-                // Used by IRequiresRequestStream
-                var requestParams = GetRequestParams(httpReq.Response.HttpContext.Request);
-                var request = ServiceHandler.CreateRequest(httpReq, restPath, requestParams, host.CreateInstance(requestType));
-
-                var rawReq = (IRequiresRequestStream)request;
-                rawReq.RequestStream = httpReq.InputStream;
-                return rawReq;
-            }
-            else
-            {
-                var requestParams = GetFlattenedRequestParams(httpReq.Response.HttpContext.Request);
-
-                var requestDto = await CreateContentTypeRequest(host, httpReq, restPath.RequestType, httpReq.ContentType).ConfigureAwait(false);
-
-                return CreateRequest(httpReq, restPath, requestParams, requestDto);
-            }
-        }
-
-        public static bool RequireqRequestStream(Type requestType)
-        {
-            var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo();
-
-            return requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo());
-        }
-
-        public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams, object requestDto)
-        {
-            var pathInfo = !restPath.IsWildCardPath
-                ? GetSanitizedPathInfo(httpReq.PathInfo, out _)
-                : httpReq.PathInfo;
-
-            return restPath.CreateRequest(pathInfo, requestParams, requestDto);
-        }
-
-        /// <summary>
-        /// Duplicate Params are given a unique key by appending a #1 suffix
-        /// </summary>
-        private static Dictionary<string, string> GetRequestParams(HttpRequest request)
-        {
-            var map = new Dictionary<string, string>();
-
-            foreach (var pair in request.Query)
-            {
-                var values = pair.Value;
-                if (values.Count == 1)
-                {
-                    map[pair.Key] = values[0];
-                }
-                else
-                {
-                    for (var i = 0; i < values.Count; i++)
-                    {
-                        map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i];
-                    }
-                }
-            }
-
-            if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT"))
-                && request.HasFormContentType)
-            {
-                foreach (var pair in request.Form)
-                {
-                    var values = pair.Value;
-                    if (values.Count == 1)
-                    {
-                        map[pair.Key] = values[0];
-                    }
-                    else
-                    {
-                        for (var i = 0; i < values.Count; i++)
-                        {
-                            map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i];
-                        }
-                    }
-                }
-            }
-
-            return map;
-        }
-
-        private static bool IsMethod(string method, string expected)
-            => string.Equals(method, expected, StringComparison.OrdinalIgnoreCase);
-
-        /// <summary>
-        /// Duplicate params have their values joined together in a comma-delimited string.
-        /// </summary>
-        private static Dictionary<string, string> GetFlattenedRequestParams(HttpRequest request)
-        {
-            var map = new Dictionary<string, string>();
-
-            foreach (var pair in request.Query)
-            {
-                map[pair.Key] = pair.Value;
-            }
-
-            if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT"))
-                && request.HasFormContentType)
-            {
-                foreach (var pair in request.Form)
-                {
-                    map[pair.Key] = pair.Value;
-                }
-            }
-
-            return map;
-        }
-    }
-}

+ 0 - 20
Emby.Server.Implementations/Services/ServiceMethod.cs

@@ -1,20 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace Emby.Server.Implementations.Services
-{
-    public class ServiceMethod
-    {
-        public string Id { get; set; }
-
-        public ActionInvokerFn ServiceAction { get; set; }
-
-        public MediaBrowser.Model.Services.IHasRequestFilter[] RequestFilters { get; set; }
-
-        public static string Key(Type serviceType, string method, string requestDtoName)
-        {
-            return serviceType.FullName + " " + method.ToUpperInvariant() + " " + requestDtoName;
-        }
-    }
-}

+ 0 - 550
Emby.Server.Implementations/Services/ServicePath.cs

@@ -1,550 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Text;
-using System.Text.Json.Serialization;
-
-namespace Emby.Server.Implementations.Services
-{
-    public class RestPath
-    {
-        private const string WildCard = "*";
-        private const char WildCardChar = '*';
-        private const string PathSeperator = "/";
-        private const char PathSeperatorChar = '/';
-        private const char ComponentSeperator = '.';
-        private const string VariablePrefix = "{";
-
-        private readonly bool[] componentsWithSeparators;
-
-        private readonly string restPath;
-        public bool IsWildCardPath { get; private set; }
-
-        private readonly string[] literalsToMatch;
-
-        private readonly string[] variablesNames;
-
-        private readonly bool[] isWildcard;
-        private readonly int wildcardCount = 0;
-
-        internal static string[] IgnoreAttributesNamed = new[]
-        {
-            nameof(JsonIgnoreAttribute)
-        };
-
-        private static Type _excludeType = typeof(Stream);
-
-        public int VariableArgsCount { get; set; }
-
-        /// <summary>
-        /// The number of segments separated by '/' determinable by path.Split('/').Length
-        /// e.g. /path/to/here.ext == 3
-        /// </summary>
-        public int PathComponentsCount { get; set; }
-
-        /// <summary>
-        /// Gets or sets the total number of segments after subparts have been exploded ('.')
-        /// e.g. /path/to/here.ext == 4.
-        /// </summary>
-        public int TotalComponentsCount { get; set; }
-
-        public string[] Verbs { get; private set; }
-
-        public Type RequestType { get; private set; }
-
-        public Type ServiceType { get; private set; }
-
-        public string Path => this.restPath;
-
-        public string Summary { get; private set; }
-
-        public string Description { get; private set; }
-
-        public bool IsHidden { get; private set; }
-
-        public static string[] GetPathPartsForMatching(string pathInfo)
-        {
-            return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public static List<string> GetFirstMatchHashKeys(string[] pathPartsForMatching)
-        {
-            var hashPrefix = pathPartsForMatching.Length + PathSeperator;
-            return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
-        }
-
-        public static List<string> GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching)
-        {
-            const string HashPrefix = WildCard + PathSeperator;
-            return GetPotentialMatchesWithPrefix(HashPrefix, pathPartsForMatching);
-        }
-
-        private static List<string> GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching)
-        {
-            var list = new List<string>();
-
-            foreach (var part in pathPartsForMatching)
-            {
-                list.Add(hashPrefix + part);
-
-                if (part.IndexOf(ComponentSeperator, StringComparison.Ordinal) == -1)
-                {
-                    continue;
-                }
-
-                var subParts = part.Split(ComponentSeperator);
-                foreach (var subPart in subParts)
-                {
-                    list.Add(hashPrefix + subPart);
-                }
-            }
-
-            return list;
-        }
-
-        public RestPath(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type requestType, Type serviceType, string path, string verbs, bool isHidden = false, string summary = null, string description = null)
-        {
-            this.RequestType = requestType;
-            this.ServiceType = serviceType;
-            this.Summary = summary;
-            this.IsHidden = isHidden;
-            this.Description = description;
-            this.restPath = path;
-
-            this.Verbs = string.IsNullOrWhiteSpace(verbs) ? ServiceExecExtensions.AllVerbs : verbs.ToUpperInvariant().Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
-
-            var componentsList = new List<string>();
-
-            // We only split on '.' if the restPath has them. Allows for /{action}.{type}
-            var hasSeparators = new List<bool>();
-            foreach (var component in this.restPath.Split(PathSeperatorChar))
-            {
-                if (string.IsNullOrEmpty(component))
-                {
-                    continue;
-                }
-
-                if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1
-                    && component.IndexOf(ComponentSeperator, StringComparison.Ordinal) != -1)
-                {
-                    hasSeparators.Add(true);
-                    componentsList.AddRange(component.Split(ComponentSeperator));
-                }
-                else
-                {
-                    hasSeparators.Add(false);
-                    componentsList.Add(component);
-                }
-            }
-
-            var components = componentsList.ToArray();
-            this.TotalComponentsCount = components.Length;
-
-            this.literalsToMatch = new string[this.TotalComponentsCount];
-            this.variablesNames = new string[this.TotalComponentsCount];
-            this.isWildcard = new bool[this.TotalComponentsCount];
-            this.componentsWithSeparators = hasSeparators.ToArray();
-            this.PathComponentsCount = this.componentsWithSeparators.Length;
-            string firstLiteralMatch = null;
-
-            for (var i = 0; i < components.Length; i++)
-            {
-                var component = components[i];
-
-                if (component.StartsWith(VariablePrefix, StringComparison.Ordinal))
-                {
-                    var variableName = component.Substring(1, component.Length - 2);
-                    if (variableName[variableName.Length - 1] == WildCardChar)
-                    {
-                        this.isWildcard[i] = true;
-                        variableName = variableName.Substring(0, variableName.Length - 1);
-                    }
-
-                    this.variablesNames[i] = variableName;
-                    this.VariableArgsCount++;
-                }
-                else
-                {
-                    this.literalsToMatch[i] = component.ToLowerInvariant();
-
-                    if (firstLiteralMatch == null)
-                    {
-                        firstLiteralMatch = this.literalsToMatch[i];
-                    }
-                }
-            }
-
-            for (var i = 0; i < components.Length - 1; i++)
-            {
-                if (!this.isWildcard[i])
-                {
-                    continue;
-                }
-
-                if (this.literalsToMatch[i + 1] == null)
-                {
-                    throw new ArgumentException(
-                        "A wildcard path component must be at the end of the path or followed by a literal path component.");
-                }
-            }
-
-            this.wildcardCount = this.isWildcard.Length;
-            this.IsWildCardPath = this.wildcardCount > 0;
-
-            this.FirstMatchHashKey = !this.IsWildCardPath
-                ? this.PathComponentsCount + PathSeperator + firstLiteralMatch
-                : WildCardChar + PathSeperator + firstLiteralMatch;
-
-            this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType);
-
-            _propertyNamesMap = new HashSet<string>(
-                    GetSerializableProperties(RequestType).Select(x => x.Name),
-                    StringComparer.OrdinalIgnoreCase);
-        }
-
-        internal static IEnumerable<PropertyInfo> GetSerializableProperties(Type type)
-        {
-            foreach (var prop in GetPublicProperties(type))
-            {
-                if (prop.GetMethod == null
-                    || _excludeType == prop.PropertyType)
-                {
-                    continue;
-                }
-
-                var ignored = false;
-                foreach (var attr in prop.GetCustomAttributes(true))
-                {
-                    if (IgnoreAttributesNamed.Contains(attr.GetType().Name))
-                    {
-                        ignored = true;
-                        break;
-                    }
-                }
-
-                if (!ignored)
-                {
-                    yield return prop;
-                }
-            }
-        }
-
-        private static IEnumerable<PropertyInfo> GetPublicProperties(Type type)
-        {
-            if (type.IsInterface)
-            {
-                var propertyInfos = new List<PropertyInfo>();
-                var considered = new List<Type>()
-                {
-                    type
-                };
-                var queue = new Queue<Type>();
-                queue.Enqueue(type);
-
-                while (queue.Count > 0)
-                {
-                    var subType = queue.Dequeue();
-                    foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces)
-                    {
-                        if (considered.Contains(subInterface))
-                        {
-                            continue;
-                        }
-
-                        considered.Add(subInterface);
-                        queue.Enqueue(subInterface);
-                    }
-
-                    var newPropertyInfos = GetTypesPublicProperties(subType)
-                        .Where(x => !propertyInfos.Contains(x));
-
-                    propertyInfos.InsertRange(0, newPropertyInfos);
-                }
-
-                return propertyInfos;
-            }
-
-            return GetTypesPublicProperties(type)
-                .Where(x => x.GetIndexParameters().Length == 0);
-        }
-
-        private static IEnumerable<PropertyInfo> GetTypesPublicProperties(Type subType)
-        {
-            foreach (var pi in subType.GetRuntimeProperties())
-            {
-                var mi = pi.GetMethod ?? pi.SetMethod;
-                if (mi != null && mi.IsStatic)
-                {
-                    continue;
-                }
-
-                yield return pi;
-            }
-        }
-
-        /// <summary>
-        /// Provide for quick lookups based on hashes that can be determined from a request url.
-        /// </summary>
-        public string FirstMatchHashKey { get; private set; }
-
-        private readonly StringMapTypeDeserializer typeDeserializer;
-
-        private readonly HashSet<string> _propertyNamesMap;
-
-        public int MatchScore(string httpMethod, string[] withPathInfoParts)
-        {
-            var isMatch = IsMatch(httpMethod, withPathInfoParts, out var wildcardMatchCount);
-            if (!isMatch)
-            {
-                return -1;
-            }
-
-            // Routes with least wildcard matches get the highest score
-            var score = Math.Max(100 - wildcardMatchCount, 1) * 1000
-                        // Routes with less variable (and more literal) matches
-                        + Math.Max(10 - VariableArgsCount, 1) * 100;
-
-            // Exact verb match is better than ANY
-            if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
-            {
-                score += 10;
-            }
-            else
-            {
-                score += 1;
-            }
-
-            return score;
-        }
-
-        /// <summary>
-        /// For performance withPathInfoParts should already be a lower case string
-        /// to minimize redundant matching operations.
-        /// </summary>
-        public bool IsMatch(string httpMethod, string[] withPathInfoParts, out int wildcardMatchCount)
-        {
-            wildcardMatchCount = 0;
-
-            if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath)
-            {
-                return false;
-            }
-
-            if (!Verbs.Contains(httpMethod, StringComparer.OrdinalIgnoreCase))
-            {
-                return false;
-            }
-
-            if (!ExplodeComponents(ref withPathInfoParts))
-            {
-                return false;
-            }
-
-            if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath)
-            {
-                return false;
-            }
-
-            int pathIx = 0;
-            for (var i = 0; i < this.TotalComponentsCount; i++)
-            {
-                if (this.isWildcard[i])
-                {
-                    if (i < this.TotalComponentsCount - 1)
-                    {
-                        // Continue to consume up until a match with the next literal
-                        while (pathIx < withPathInfoParts.Length
-                            && !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase))
-                        {
-                            pathIx++;
-                            wildcardMatchCount++;
-                        }
-
-                        // Ensure there are still enough parts left to match the remainder
-                        if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1))
-                        {
-                            return false;
-                        }
-                    }
-                    else
-                    {
-                        // A wildcard at the end matches the remainder of path
-                        wildcardMatchCount += withPathInfoParts.Length - pathIx;
-                        pathIx = withPathInfoParts.Length;
-                    }
-                }
-                else
-                {
-                    var literalToMatch = this.literalsToMatch[i];
-                    if (literalToMatch == null)
-                    {
-                        // Matching an ordinary (non-wildcard) variable consumes a single part
-                        pathIx++;
-                        continue;
-                    }
-
-                    if (withPathInfoParts.Length <= pathIx
-                        || !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase))
-                    {
-                        return false;
-                    }
-
-                    pathIx++;
-                }
-            }
-
-            return pathIx == withPathInfoParts.Length;
-        }
-
-        private bool ExplodeComponents(ref string[] withPathInfoParts)
-        {
-            var totalComponents = new List<string>();
-            for (var i = 0; i < withPathInfoParts.Length; i++)
-            {
-                var component = withPathInfoParts[i];
-                if (string.IsNullOrEmpty(component))
-                {
-                    continue;
-                }
-
-                if (this.PathComponentsCount != this.TotalComponentsCount
-                    && this.componentsWithSeparators[i])
-                {
-                    var subComponents = component.Split(ComponentSeperator);
-                    if (subComponents.Length < 2)
-                    {
-                        return false;
-                    }
-
-                    totalComponents.AddRange(subComponents);
-                }
-                else
-                {
-                    totalComponents.Add(component);
-                }
-            }
-
-            withPathInfoParts = totalComponents.ToArray();
-            return true;
-        }
-
-        public object CreateRequest(string pathInfo, Dictionary<string, string> queryStringAndFormData, object fromInstance)
-        {
-            var requestComponents = pathInfo.Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
-
-            ExplodeComponents(ref requestComponents);
-
-            if (requestComponents.Length != this.TotalComponentsCount)
-            {
-                var isValidWildCardPath = this.IsWildCardPath
-                    && requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount;
-
-                if (!isValidWildCardPath)
-                {
-                    throw new ArgumentException(
-                        string.Format(
-                            CultureInfo.InvariantCulture,
-                            "Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'",
-                            pathInfo,
-                            this.restPath));
-                }
-            }
-
-            var requestKeyValuesMap = new Dictionary<string, string>();
-            var pathIx = 0;
-            for (var i = 0; i < this.TotalComponentsCount; i++)
-            {
-                var variableName = this.variablesNames[i];
-                if (variableName == null)
-                {
-                    pathIx++;
-                    continue;
-                }
-
-                if (!this._propertyNamesMap.Contains(variableName))
-                {
-                    if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
-                    {
-                        pathIx++;
-                        continue;
-                    }
-
-                    throw new ArgumentException("Could not find property "
-                        + variableName + " on " + RequestType.GetMethodName());
-                }
-
-                var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; // wildcard has arg mismatch
-                if (value != null && this.isWildcard[i])
-                {
-                    if (i == this.TotalComponentsCount - 1)
-                    {
-                        // Wildcard at end of path definition consumes all the rest
-                        var sb = new StringBuilder();
-                        sb.Append(value);
-                        for (var j = pathIx + 1; j < requestComponents.Length; j++)
-                        {
-                            sb.Append(PathSeperatorChar)
-                                .Append(requestComponents[j]);
-                        }
-
-                        value = sb.ToString();
-                    }
-                    else
-                    {
-                        // Wildcard in middle of path definition consumes up until it
-                        // hits a match for the next element in the definition (which must be a literal)
-                        // It may consume 0 or more path parts
-                        var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
-                        if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
-                        {
-                            var sb = new StringBuilder(value);
-                            pathIx++;
-                            while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
-                            {
-                                sb.Append(PathSeperatorChar)
-                                    .Append(requestComponents[pathIx++]);
-                            }
-
-                            value = sb.ToString();
-                        }
-                        else
-                        {
-                            value = null;
-                        }
-                    }
-                }
-                else
-                {
-                    // Variable consumes single path item
-                    pathIx++;
-                }
-
-                requestKeyValuesMap[variableName] = value;
-            }
-
-            if (queryStringAndFormData != null)
-            {
-                // Query String and form data can override variable path matches
-                // path variables < query string < form data
-                foreach (var name in queryStringAndFormData)
-                {
-                    requestKeyValuesMap[name.Key] = name.Value;
-                }
-            }
-
-            return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap);
-        }
-
-        public class RestPathMap : SortedDictionary<string, List<RestPath>>
-        {
-            public RestPathMap() : base(StringComparer.OrdinalIgnoreCase)
-            {
-            }
-        }
-    }
-}

+ 0 - 118
Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs

@@ -1,118 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Reflection;
-using MediaBrowser.Common.Extensions;
-
-namespace Emby.Server.Implementations.Services
-{
-    /// <summary>
-    /// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls)
-    /// </summary>
-    public class StringMapTypeDeserializer
-    {
-        internal class PropertySerializerEntry
-        {
-            public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn, Type propertyType)
-            {
-                PropertySetFn = propertySetFn;
-                PropertyParseStringFn = propertyParseStringFn;
-                PropertyType = propertyType;
-            }
-
-            public Action<object, object> PropertySetFn { get; private set; }
-
-            public Func<string, object> PropertyParseStringFn { get; private set; }
-
-            public Type PropertyType { get; private set; }
-        }
-
-        private readonly Type type;
-        private readonly Dictionary<string, PropertySerializerEntry> propertySetterMap
-            = new Dictionary<string, PropertySerializerEntry>(StringComparer.OrdinalIgnoreCase);
-
-        public Func<string, object> GetParseFn(Type propertyType)
-        {
-            if (propertyType == typeof(string))
-            {
-                return s => s;
-            }
-
-            return _GetParseFn(propertyType);
-        }
-
-        private readonly Func<Type, object> _CreateInstanceFn;
-        private readonly Func<Type, Func<string, object>> _GetParseFn;
-
-        public StringMapTypeDeserializer(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type type)
-        {
-            _CreateInstanceFn = createInstanceFn;
-            _GetParseFn = getParseFn;
-            this.type = type;
-
-            foreach (var propertyInfo in RestPath.GetSerializableProperties(type))
-            {
-                var propertySetFn = TypeAccessor.GetSetPropertyMethod(propertyInfo);
-                var propertyType = propertyInfo.PropertyType;
-                var propertyParseStringFn = GetParseFn(propertyType);
-                var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType);
-
-                propertySetterMap[propertyInfo.Name] = propertySerializer;
-            }
-        }
-
-        public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs)
-        {
-            PropertySerializerEntry propertySerializerEntry = null;
-
-            if (instance == null)
-            {
-                instance = _CreateInstanceFn(type);
-            }
-
-            foreach (var pair in keyValuePairs)
-            {
-                string propertyName = pair.Key;
-                string propertyTextValue = pair.Value;
-
-                if (propertyTextValue == null
-                    || !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
-                    || propertySerializerEntry.PropertySetFn == null)
-                {
-                    continue;
-                }
-
-                if (propertySerializerEntry.PropertyType == typeof(bool))
-                {
-                    // InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value
-                    propertyTextValue = StringExtensions.LeftPart(propertyTextValue, ',').ToString();
-                }
-
-                var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue);
-                if (value == null)
-                {
-                    continue;
-                }
-
-                propertySerializerEntry.PropertySetFn(instance, value);
-            }
-
-            return instance;
-        }
-    }
-
-    internal static class TypeAccessor
-    {
-        public static Action<object, object> GetSetPropertyMethod(PropertyInfo propertyInfo)
-        {
-            if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0)
-            {
-                return null;
-            }
-
-            var setMethodInfo = propertyInfo.SetMethod;
-            return (instance, value) => setMethodInfo.Invoke(instance, new[] { value });
-        }
-    }
-}

+ 0 - 27
Emby.Server.Implementations/Services/UrlExtensions.cs

@@ -1,27 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Common.Extensions;
-
-namespace Emby.Server.Implementations.Services
-{
-    /// <summary>
-    /// Donated by Ivan Korneliuk from his post:
-    /// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html
-    ///
-    /// Modified to only allow using routes matching the supplied HTTP Verb.
-    /// </summary>
-    public static class UrlExtensions
-    {
-        public static string GetMethodName(this Type type)
-        {
-            var typeName = type.FullName != null // can be null, e.g. generic types
-                ? StringExtensions.LeftPart(type.FullName, "[[", StringComparison.Ordinal).ToString() // Generic Fullname
-                    .Replace(type.Namespace + ".", string.Empty, StringComparison.Ordinal) // Trim Namespaces
-                    .Replace("+", ".", StringComparison.Ordinal) // Convert nested into normal type
-                : type.Name;
-
-            return type.IsGenericParameter ? "'" + typeName : typeName;
-        }
-    }
-}

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

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

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

@@ -4,9 +4,9 @@ using System.Linq;
 using System.Net.WebSockets;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Net;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
@@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Session
         private readonly ILogger<SessionWebSocketListener> _logger;
         private readonly ILoggerFactory _loggerFactory;
 
-        private readonly IHttpServer _httpServer;
+        private readonly IWebSocketManager _webSocketManager;
 
         /// <summary>
         /// The KeepAlive cancellation token.
@@ -72,19 +72,19 @@ namespace Emby.Server.Implementations.Session
         /// <param name="logger">The logger.</param>
         /// <param name="sessionManager">The session manager.</param>
         /// <param name="loggerFactory">The logger factory.</param>
-        /// <param name="httpServer">The HTTP server.</param>
+        /// <param name="webSocketManager">The HTTP server.</param>
         public SessionWebSocketListener(
             ILogger<SessionWebSocketListener> logger,
             ISessionManager sessionManager,
             ILoggerFactory loggerFactory,
-            IHttpServer httpServer)
+            IWebSocketManager webSocketManager)
         {
             _logger = logger;
             _sessionManager = sessionManager;
             _loggerFactory = loggerFactory;
-            _httpServer = httpServer;
+            _webSocketManager = webSocketManager;
 
-            httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
+            webSocketManager.WebSocketConnected += OnServerManagerWebSocketConnected;
         }
 
         private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Session
         /// <inheritdoc />
         public void Dispose()
         {
-            _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
+            _webSocketManager.WebSocketConnected -= OnServerManagerWebSocketConnected;
             StopKeepAlive();
         }
 

+ 0 - 248
Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs

@@ -1,248 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Net.Mime;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
-using Microsoft.Net.Http.Headers;
-using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
-    public class WebSocketSharpRequest : IHttpRequest
-    {
-        private const string FormUrlEncoded = "application/x-www-form-urlencoded";
-        private const string MultiPartFormData = "multipart/form-data";
-        private const string Soap11 = "text/xml; charset=utf-8";
-
-        private string _remoteIp;
-        private Dictionary<string, object> _items;
-        private string _responseContentType;
-
-        public WebSocketSharpRequest(HttpRequest httpRequest, HttpResponse httpResponse, string operationName)
-        {
-            this.OperationName = operationName;
-            this.Request = httpRequest;
-            this.Response = httpResponse;
-        }
-
-        public string Accept => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Accept]) ? null : Request.Headers[HeaderNames.Accept].ToString();
-
-        public string Authorization => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Authorization]) ? null : Request.Headers[HeaderNames.Authorization].ToString();
-
-        public HttpRequest Request { get; }
-
-        public HttpResponse Response { get; }
-
-        public string OperationName { get; set; }
-
-        public string RawUrl => Request.GetEncodedPathAndQuery();
-
-        public string AbsoluteUri => Request.GetDisplayUrl().TrimEnd('/');
-
-        public string RemoteIp
-        {
-            get
-            {
-                if (_remoteIp != null)
-                {
-                    return _remoteIp;
-                }
-
-                IPAddress ip;
-
-                // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip
-                // (if the server is behind a reverse proxy for example)
-                if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip))
-                {
-                    if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip))
-                    {
-                        ip = Request.HttpContext.Connection.RemoteIpAddress;
-
-                        // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests)
-                        ip ??= IPAddress.Loopback;
-                    }
-                }
-
-                return _remoteIp = NormalizeIp(ip).ToString();
-            }
-        }
-
-        public string[] AcceptTypes => Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
-
-        public Dictionary<string, object> Items => _items ?? (_items = new Dictionary<string, object>());
-
-        public string ResponseContentType
-        {
-            get =>
-                _responseContentType
-                ?? (_responseContentType = GetResponseContentType(Request));
-            set => _responseContentType = value;
-        }
-
-        public string PathInfo => Request.Path.Value;
-
-        public string UserAgent => Request.Headers[HeaderNames.UserAgent];
-
-        public IHeaderDictionary Headers => Request.Headers;
-
-        public IQueryCollection QueryString => Request.Query;
-
-        public bool IsLocal =>
-            (Request.HttpContext.Connection.LocalIpAddress == null
-            && Request.HttpContext.Connection.RemoteIpAddress == null)
-            || Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress);
-
-        public string HttpMethod => Request.Method;
-
-        public string Verb => HttpMethod;
-
-        public string ContentType => Request.ContentType;
-
-        public Uri UrlReferrer => Request.GetTypedHeaders().Referer;
-
-        public Stream InputStream => Request.Body;
-
-        public long ContentLength => Request.ContentLength ?? 0;
-
-        private string GetHeader(string name) => Request.Headers[name].ToString();
-
-        private static IPAddress NormalizeIp(IPAddress ip)
-        {
-            if (ip.IsIPv4MappedToIPv6)
-            {
-                return ip.MapToIPv4();
-            }
-
-            return ip;
-        }
-
-        public static string GetResponseContentType(HttpRequest httpReq)
-        {
-            var specifiedContentType = GetQueryStringContentType(httpReq);
-            if (!string.IsNullOrEmpty(specifiedContentType))
-            {
-                return specifiedContentType;
-            }
-
-            const string ServerDefaultContentType = MediaTypeNames.Application.Json;
-
-            var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
-            string defaultContentType = null;
-            if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData))
-            {
-                defaultContentType = ServerDefaultContentType;
-            }
-
-            var acceptsAnything = false;
-            var hasDefaultContentType = defaultContentType != null;
-            if (acceptContentTypes != null)
-            {
-                foreach (ReadOnlySpan<char> acceptsType in acceptContentTypes)
-                {
-                    ReadOnlySpan<char> contentType = acceptsType;
-                    var index = contentType.IndexOf(';');
-                    if (index != -1)
-                    {
-                        contentType = contentType.Slice(0, index);
-                    }
-
-                    contentType = contentType.Trim();
-                    acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase);
-
-                    if (acceptsAnything)
-                    {
-                        break;
-                    }
-                }
-
-                if (acceptsAnything)
-                {
-                    if (hasDefaultContentType)
-                    {
-                        return defaultContentType;
-                    }
-                    else
-                    {
-                        return ServerDefaultContentType;
-                    }
-                }
-            }
-
-            if (acceptContentTypes == null && httpReq.ContentType == Soap11)
-            {
-                return Soap11;
-            }
-
-            // We could also send a '406 Not Acceptable', but this is allowed also
-            return ServerDefaultContentType;
-        }
-
-        public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes)
-        {
-            if (contentTypes == null || request.ContentType == null)
-            {
-                return false;
-            }
-
-            foreach (var contentType in contentTypes)
-            {
-                if (IsContentType(request, contentType))
-                {
-                    return true;
-                }
-            }
-
-            return false;
-        }
-
-        public static bool IsContentType(HttpRequest request, string contentType)
-        {
-            return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase);
-        }
-
-        private static string GetQueryStringContentType(HttpRequest httpReq)
-        {
-            ReadOnlySpan<char> format = httpReq.Query["format"].ToString();
-            if (format == ReadOnlySpan<char>.Empty)
-            {
-                const int FormatMaxLength = 4;
-                ReadOnlySpan<char> pi = httpReq.Path.ToString();
-                if (pi == null || pi.Length <= FormatMaxLength)
-                {
-                    return null;
-                }
-
-                if (pi[0] == '/')
-                {
-                    pi = pi.Slice(1);
-                }
-
-                format = pi.LeftPart('/');
-                if (format.Length > FormatMaxLength)
-                {
-                    return null;
-                }
-            }
-
-            format = format.LeftPart('.');
-            if (format.Contains("json", StringComparison.OrdinalIgnoreCase))
-            {
-                return "application/json";
-            }
-            else if (format.Contains("xml", StringComparison.OrdinalIgnoreCase))
-            {
-                return "application/xml";
-            }
-
-            return null;
-        }
-    }
-}

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

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

+ 1 - 1
Jellyfin.Api/Controllers/DynamicHlsController.cs

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

+ 2 - 2
Jellyfin.Api/Controllers/ImageController.cs

@@ -1281,9 +1281,9 @@ namespace Jellyfin.Api.Controllers
                 Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false)));
 
                 // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
-                if (!(dateImageModified > ifModifiedSinceHeader))
+                if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue)
                 {
-                    if (ifModifiedSinceHeader.Add(cacheDuration!.Value) < DateTime.UtcNow)
+                    if (ifModifiedSinceHeader.Add(cacheDuration.Value) < DateTime.UtcNow)
                     {
                         Response.StatusCode = StatusCodes.Status304NotModified;
                         return new ContentResult();

+ 4 - 2
Jellyfin.Api/Controllers/ItemsController.cs

@@ -4,10 +4,10 @@ using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
-using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
@@ -266,7 +266,9 @@ namespace Jellyfin.Api.Controllers
 
             bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
                                      // Assume all folders inside an EnabledChannel are enabled
-                                     || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id);
+                                     || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id)
+                                     // Assume all items inside an EnabledChannel are enabled
+                                     || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.ChannelId);
 
             var collectionFolders = _libraryManager.GetCollectionFolders(item);
             foreach (var collectionFolder in collectionFolders)

+ 6 - 7
Jellyfin.Api/Controllers/LibraryController.cs

@@ -19,6 +19,7 @@ using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Net;
@@ -35,8 +36,6 @@ using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Logging;
 using Book = MediaBrowser.Controller.Entities.Book;
-using Movie = Jellyfin.Data.Entities.Movie;
-using MusicAlbum = Jellyfin.Data.Entities.MusicAlbum;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -619,7 +618,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.Download)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult GetDownload([FromRoute] Guid itemId)
+        public async Task<ActionResult> GetDownload([FromRoute] Guid itemId)
         {
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)
@@ -648,7 +647,7 @@ namespace Jellyfin.Api.Controllers
 
             if (user != null)
             {
-                LogDownload(item, user, auth);
+                await LogDownloadAsync(item, user, auth).ConfigureAwait(false);
             }
 
             var path = item.Path;
@@ -861,17 +860,17 @@ namespace Jellyfin.Api.Controllers
                 : item;
         }
 
-        private void LogDownload(BaseItem item, User user, AuthorizationInfo auth)
+        private async Task LogDownloadAsync(BaseItem item, User user, AuthorizationInfo auth)
         {
             try
             {
-                _activityManager.Create(new ActivityLog(
+                await _activityManager.CreateAsync(new ActivityLog(
                     string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
                     "UserDownloadingContent",
                     auth.UserId)
                 {
                     ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device),
-                });
+                }).ConfigureAwait(false);
             }
             catch
             {

+ 5 - 7
Jellyfin.Api/Controllers/LibraryStructureController.cs

@@ -76,7 +76,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? name,
             [FromQuery] string? collectionType,
             [FromQuery] string[] paths,
-            [FromBody] LibraryOptionsDto? libraryOptionsDto,
+            [FromBody] AddVirtualFolderDto? libraryOptionsDto,
             [FromQuery] bool refreshLibrary = false)
         {
             var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions();
@@ -312,19 +312,17 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Update library options.
         /// </summary>
-        /// <param name="id">The library name.</param>
-        /// <param name="libraryOptions">The library options.</param>
+        /// <param name="request">The library name and options.</param>
         /// <response code="204">Library updated.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("LibraryOptions")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult UpdateLibraryOptions(
-            [FromQuery] string? id,
-            [FromBody] LibraryOptions? libraryOptions)
+            [FromBody] UpdateLibraryOptionsDto request)
         {
-            var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id);
+            var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id);
 
-            collectionFolder.UpdateLibraryOptions(libraryOptions);
+            collectionFolder.UpdateLibraryOptions(request.LibraryOptions);
             return NoContent();
         }
     }

+ 2 - 1
Jellyfin.Api/Controllers/MoviesController.cs

@@ -10,6 +10,7 @@ using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Dto;
@@ -181,7 +182,7 @@ namespace Jellyfin.Api.Controllers
             DtoOptions dtoOptions,
             RecommendationType type)
         {
-            var itemTypes = new List<string> { nameof(MediaBrowser.Controller.Entities.Movies.Movie) };
+            var itemTypes = new List<string> { nameof(Movie) };
             if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
             {
                 itemTypes.Add(nameof(Trailer));

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

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

+ 154 - 0
Jellyfin.Api/Controllers/QuickConnectController.cs

@@ -0,0 +1,154 @@
+using System.ComponentModel.DataAnnotations;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.QuickConnect;
+using MediaBrowser.Model.QuickConnect;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Quick connect controller.
+    /// </summary>
+    public class QuickConnectController : BaseJellyfinApiController
+    {
+        private readonly IQuickConnect _quickConnect;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="QuickConnectController"/> class.
+        /// </summary>
+        /// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param>
+        public QuickConnectController(IQuickConnect quickConnect)
+        {
+            _quickConnect = quickConnect;
+        }
+
+        /// <summary>
+        /// Gets the current quick connect state.
+        /// </summary>
+        /// <response code="200">Quick connect state returned.</response>
+        /// <returns>The current <see cref="QuickConnectState"/>.</returns>
+        [HttpGet("Status")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QuickConnectState> GetStatus()
+        {
+            _quickConnect.ExpireRequests();
+            return _quickConnect.State;
+        }
+
+        /// <summary>
+        /// Initiate a new quick connect request.
+        /// </summary>
+        /// <response code="200">Quick connect request successfully created.</response>
+        /// <response code="401">Quick connect is not active on this server.</response>
+        /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
+        [HttpGet("Initiate")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QuickConnectResult> Initiate()
+        {
+            return _quickConnect.TryConnect();
+        }
+
+        /// <summary>
+        /// Attempts to retrieve authentication information.
+        /// </summary>
+        /// <param name="secret">Secret previously returned from the Initiate endpoint.</param>
+        /// <response code="200">Quick connect result returned.</response>
+        /// <response code="404">Unknown quick connect secret.</response>
+        /// <returns>An updated <see cref="QuickConnectResult"/>.</returns>
+        [HttpGet("Connect")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult<QuickConnectResult> Connect([FromQuery, Required] string secret)
+        {
+            try
+            {
+                return _quickConnect.CheckRequestStatus(secret);
+            }
+            catch (ResourceNotFoundException)
+            {
+                return NotFound("Unknown secret");
+            }
+        }
+
+        /// <summary>
+        /// Temporarily activates quick connect for five minutes.
+        /// </summary>
+        /// <response code="204">Quick connect has been temporarily activated.</response>
+        /// <response code="403">Quick connect is unavailable on this server.</response>
+        /// <returns>An <see cref="NoContentResult"/> on success.</returns>
+        [HttpPost("Activate")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status403Forbidden)]
+        public ActionResult Activate()
+        {
+            if (_quickConnect.State == QuickConnectState.Unavailable)
+            {
+                return Forbid("Quick connect is unavailable");
+            }
+
+            _quickConnect.Activate();
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Enables or disables quick connect.
+        /// </summary>
+        /// <param name="status">New <see cref="QuickConnectState"/>.</param>
+        /// <response code="204">Quick connect state set successfully.</response>
+        /// <returns>An <see cref="NoContentResult"/> on success.</returns>
+        [HttpPost("Available")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult Available([FromQuery] QuickConnectState status = QuickConnectState.Available)
+        {
+            _quickConnect.SetState(status);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Authorizes a pending quick connect request.
+        /// </summary>
+        /// <param name="code">Quick connect code to authorize.</param>
+        /// <response code="200">Quick connect result authorized successfully.</response>
+        /// <response code="403">Unknown user id.</response>
+        /// <returns>Boolean indicating if the authorization was successful.</returns>
+        [HttpPost("Authorize")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status403Forbidden)]
+        public ActionResult<bool> Authorize([FromQuery, Required] string code)
+        {
+            var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
+            if (!userId.HasValue)
+            {
+                return Forbid("Unknown user id");
+            }
+
+            return _quickConnect.AuthorizeRequest(userId.Value, code);
+        }
+
+        /// <summary>
+        /// Deauthorize all quick connect devices for the current user.
+        /// </summary>
+        /// <response code="200">All quick connect devices were deleted.</response>
+        /// <returns>The number of devices that were deleted.</returns>
+        [HttpPost("Deauthorize")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<int> Deauthorize()
+        {
+            var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
+            if (!userId.HasValue)
+            {
+                return 0;
+            }
+
+            return _quickConnect.DeleteAllDevices(userId.Value);
+        }
+    }
+}

+ 34 - 0
Jellyfin.Api/Controllers/UserController.cs

@@ -216,6 +216,40 @@ namespace Jellyfin.Api.Controllers
             }
         }
 
+        /// <summary>
+        /// Authenticates a user with quick connect.
+        /// </summary>
+        /// <param name="request">The <see cref="QuickConnectDto"/> request.</param>
+        /// <response code="200">User authenticated.</response>
+        /// <response code="400">Missing token.</response>
+        /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
+        [HttpPost("AuthenticateWithQuickConnect")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<AuthenticationResult>> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
+        {
+            var auth = _authContext.GetAuthorizationInfo(Request);
+
+            try
+            {
+                var authRequest = new AuthenticationRequest
+                {
+                    App = auth.Client,
+                    AppVersion = auth.Version,
+                    DeviceId = auth.DeviceId,
+                    DeviceName = auth.Device,
+                };
+
+                return await _sessionManager.AuthenticateQuickConnect(
+                    authRequest,
+                    request.Token).ConfigureAwait(false);
+            }
+            catch (SecurityException e)
+            {
+                // rethrow adding IP address to message
+                throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}", e);
+            }
+        }
+
         /// <summary>
         /// Updates a user's password.
         /// </summary>

+ 11 - 8
Jellyfin.Api/Controllers/VideosController.cs

@@ -233,7 +233,7 @@ namespace Jellyfin.Api.Controllers
                     .First();
             }
 
-            var list = primaryVersion.LinkedAlternateVersions.ToList();
+            var alternateVersionsOfPrimary = primaryVersion.LinkedAlternateVersions.ToList();
 
             foreach (var item in items.Where(i => i.Id != primaryVersion.Id))
             {
@@ -241,17 +241,20 @@ namespace Jellyfin.Api.Controllers
 
                 await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
 
-                list.Add(new LinkedChild
+                if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase)))
                 {
-                    Path = item.Path,
-                    ItemId = item.Id
-                });
+                    alternateVersionsOfPrimary.Add(new LinkedChild
+                    {
+                        Path = item.Path,
+                        ItemId = item.Id
+                    });
+                }
 
                 foreach (var linkedItem in item.LinkedAlternateVersions)
                 {
-                    if (!list.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
+                    if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
                     {
-                        list.Add(linkedItem);
+                        alternateVersionsOfPrimary.Add(linkedItem);
                     }
                 }
 
@@ -262,7 +265,7 @@ namespace Jellyfin.Api.Controllers
                 }
             }
 
-            primaryVersion.LinkedAlternateVersions = list.ToArray();
+            primaryVersion.LinkedAlternateVersions = alternateVersionsOfPrimary.ToArray();
             await primaryVersion.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
             return NoContent();
         }

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

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

+ 36 - 29
Jellyfin.Api/Helpers/ProgressiveFileCopier.cs

@@ -130,34 +130,10 @@ namespace Jellyfin.Api.Helpers
         private async Task<int> CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken)
         {
             var array = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
-            int bytesRead;
-            int totalBytesRead = 0;
-
-            if (readAsync)
-            {
-                bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
-            }
-            else
-            {
-                bytesRead = source.Read(array, 0, array.Length);
-            }
-
-            while (bytesRead != 0)
+            try
             {
-                var bytesToWrite = bytesRead;
-
-                if (bytesToWrite > 0)
-                {
-                    await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
-
-                    _bytesWritten += bytesRead;
-                    totalBytesRead += bytesRead;
-
-                    if (_job != null)
-                    {
-                        _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
-                    }
-                }
+                int bytesRead;
+                int totalBytesRead = 0;
 
                 if (readAsync)
                 {
@@ -167,9 +143,40 @@ namespace Jellyfin.Api.Helpers
                 {
                     bytesRead = source.Read(array, 0, array.Length);
                 }
-            }
 
-            return totalBytesRead;
+                while (bytesRead != 0)
+                {
+                    var bytesToWrite = bytesRead;
+
+                    if (bytesToWrite > 0)
+                    {
+                        await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
+
+                        _bytesWritten += bytesRead;
+                        totalBytesRead += bytesRead;
+
+                        if (_job != null)
+                        {
+                            _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
+                        }
+                    }
+
+                    if (readAsync)
+                    {
+                        bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
+                    }
+                    else
+                    {
+                        bytesRead = source.Read(array, 0, array.Length);
+                    }
+                }
+
+                return totalBytesRead;
+            }
+            finally
+            {
+                ArrayPool<byte>.Shared.Return(array);
+            }
         }
     }
 }

+ 2 - 2
Jellyfin.Api/Jellyfin.Api.csproj

@@ -14,9 +14,9 @@
 
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
-    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.6" />
+    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.7" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
-    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.5.1" />
   </ItemGroup>

+ 3 - 3
Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs → Jellyfin.Api/Models/LibraryStructureDto/AddVirtualFolderDto.cs

@@ -3,13 +3,13 @@
 namespace Jellyfin.Api.Models.LibraryStructureDto
 {
     /// <summary>
-    /// Library options dto.
+    /// Add virtual folder dto.
     /// </summary>
-    public class LibraryOptionsDto
+    public class AddVirtualFolderDto
     {
         /// <summary>
         /// Gets or sets library options.
         /// </summary>
         public LibraryOptions? LibraryOptions { get; set; }
     }
-}
+}

+ 21 - 0
Jellyfin.Api/Models/LibraryStructureDto/UpdateLibraryOptionsDto.cs

@@ -0,0 +1,21 @@
+using System;
+using MediaBrowser.Model.Configuration;
+
+namespace Jellyfin.Api.Models.LibraryStructureDto
+{
+    /// <summary>
+    /// Update library options dto.
+    /// </summary>
+    public class UpdateLibraryOptionsDto
+    {
+        /// <summary>
+        /// Gets or sets the library item id.
+        /// </summary>
+        public Guid Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets library options.
+        /// </summary>
+        public LibraryOptions? LibraryOptions { get; set; }
+    }
+}

+ 16 - 0
Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs

@@ -0,0 +1,16 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Api.Models.UserDtos
+{
+    /// <summary>
+    /// The quick connect request body.
+    /// </summary>
+    public class QuickConnectDto
+    {
+        /// <summary>
+        /// Gets or sets the quick connect token.
+        /// </summary>
+        [Required]
+        public string? Token { get; set; }
+    }
+}

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

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

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

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

+ 2 - 1
Jellyfin.Data/Entities/ActivityLog.cs

@@ -3,6 +3,7 @@
 using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Interfaces;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Data.Entities
@@ -10,7 +11,7 @@ namespace Jellyfin.Data.Entities
     /// <summary>
     /// An entity referencing an activity log entry.
     /// </summary>
-    public partial class ActivityLog : ISavingChanges
+    public partial class ActivityLog : IHasConcurrencyToken
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="ActivityLog"/> class.

+ 0 - 210
Jellyfin.Data/Entities/Artwork.cs

@@ -1,210 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.ComponentModel.DataAnnotations;
-
-namespace Jellyfin.Data.Entities
-{
-    public partial class Artwork
-    {
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected Artwork()
-        {
-            Init();
-        }
-
-        /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static Artwork CreateArtworkUnsafe()
-        {
-            return new Artwork();
-        }
-
-        /// <summary>
-        /// Public constructor with required data.
-        /// </summary>
-        /// <param name="path"></param>
-        /// <param name="kind"></param>
-        /// <param name="_metadata0"></param>
-        /// <param name="_personrole1"></param>
-        public Artwork(string path, Enums.ArtKind kind, Metadata _metadata0, PersonRole _personrole1)
-        {
-            if (string.IsNullOrEmpty(path))
-            {
-                throw new ArgumentNullException(nameof(path));
-            }
-
-            this.Path = path;
-
-            this.Kind = kind;
-
-            if (_metadata0 == null)
-            {
-                throw new ArgumentNullException(nameof(_metadata0));
-            }
-
-            _metadata0.Artwork.Add(this);
-
-            if (_personrole1 == null)
-            {
-                throw new ArgumentNullException(nameof(_personrole1));
-            }
-
-            _personrole1.Artwork = this;
-
-            Init();
-        }
-
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="path"></param>
-        /// <param name="kind"></param>
-        /// <param name="_metadata0"></param>
-        /// <param name="_personrole1"></param>
-        public static Artwork Create(string path, Enums.ArtKind kind, Metadata _metadata0, PersonRole _personrole1)
-        {
-            return new Artwork(path, kind, _metadata0, _personrole1);
-        }
-
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
-        /// <summary>
-        /// Backing field for Id.
-        /// </summary>
-        internal int _Id;
-        /// <summary>
-        /// When provided in a partial class, allows value of Id to be changed before setting.
-        /// </summary>
-        partial void SetId(int oldValue, ref int newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Id to be changed before returning.
-        /// </summary>
-        partial void GetId(ref int result);
-
-        /// <summary>
-        /// Identity, Indexed, Required.
-        /// </summary>
-        [Key]
-        [Required]
-        public int Id
-        {
-            get
-            {
-                int value = _Id;
-                GetId(ref value);
-                return _Id = value;
-            }
-
-            protected set
-            {
-                int oldValue = _Id;
-                SetId(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Id = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Backing field for Path.
-        /// </summary>
-        protected string _Path;
-        /// <summary>
-        /// When provided in a partial class, allows value of Path to be changed before setting.
-        /// </summary>
-        partial void SetPath(string oldValue, ref string newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Path to be changed before returning.
-        /// </summary>
-        partial void GetPath(ref string result);
-
-        /// <summary>
-        /// Required, Max length = 65535
-        /// </summary>
-        [Required]
-        [MaxLength(65535)]
-        [StringLength(65535)]
-        public string Path
-        {
-            get
-            {
-                string value = _Path;
-                GetPath(ref value);
-                return _Path = value;
-            }
-
-            set
-            {
-                string oldValue = _Path;
-                SetPath(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Path = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Backing field for Kind.
-        /// </summary>
-        internal Enums.ArtKind _Kind;
-        /// <summary>
-        /// When provided in a partial class, allows value of Kind to be changed before setting.
-        /// </summary>
-        partial void SetKind(Enums.ArtKind oldValue, ref Enums.ArtKind newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Kind to be changed before returning.
-        /// </summary>
-        partial void GetKind(ref Enums.ArtKind result);
-
-        /// <summary>
-        /// Indexed, Required.
-        /// </summary>
-        [Required]
-        public Enums.ArtKind Kind
-        {
-            get
-            {
-                Enums.ArtKind value = _Kind;
-                GetKind(ref value);
-                return _Kind = value;
-            }
-
-            set
-            {
-                Enums.ArtKind oldValue = _Kind;
-                SetKind(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Kind = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Required, ConcurrenyToken.
-        /// </summary>
-        [ConcurrencyCheck]
-        [Required]
-        public uint RowVersion { get; set; }
-
-        public void OnSavingChanges()
-        {
-            RowVersion++;
-        }
-
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-    }
-}
-

+ 0 - 72
Jellyfin.Data/Entities/Book.cs

@@ -1,72 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations.Schema;
-
-namespace Jellyfin.Data.Entities
-{
-    public partial class Book : LibraryItem
-    {
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected Book()
-        {
-            BookMetadata = new HashSet<BookMetadata>();
-            Releases = new HashSet<Release>();
-
-            Init();
-        }
-
-        /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static Book CreateBookUnsafe()
-        {
-            return new Book();
-        }
-
-        /// <summary>
-        /// Public constructor with required data.
-        /// </summary>
-        /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
-        /// <param name="dateadded">The date the object was added.</param>
-        public Book(Guid urlid, DateTime dateadded)
-        {
-            this.UrlId = urlid;
-
-            this.BookMetadata = new HashSet<BookMetadata>();
-            this.Releases = new HashSet<Release>();
-
-            Init();
-        }
-
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
-        /// <param name="dateadded">The date the object was added.</param>
-        public static Book Create(Guid urlid, DateTime dateadded)
-        {
-            return new Book(urlid, dateadded);
-        }
-
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-
-        [ForeignKey("BookMetadata_BookMetadata_Id")]
-        public virtual ICollection<BookMetadata> BookMetadata { get; protected set; }
-
-        [ForeignKey("Release_Releases_Id")]
-        public virtual ICollection<Release> Releases { get; protected set; }
-    }
-}
-

+ 0 - 125
Jellyfin.Data/Entities/BookMetadata.cs

@@ -1,125 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations.Schema;
-
-namespace Jellyfin.Data.Entities
-{
-    public partial class BookMetadata : Metadata
-    {
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected BookMetadata()
-        {
-            Publishers = new HashSet<Company>();
-
-            Init();
-        }
-
-        /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static BookMetadata CreateBookMetadataUnsafe()
-        {
-            return new BookMetadata();
-        }
-
-        /// <summary>
-        /// Public constructor with required data.
-        /// </summary>
-        /// <param name="title">The title or name of the object.</param>
-        /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="dateadded">The date the object was added.</param>
-        /// <param name="datemodified">The date the object was last modified.</param>
-        /// <param name="_book0"></param>
-        public BookMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0)
-        {
-            if (string.IsNullOrEmpty(title))
-            {
-                throw new ArgumentNullException(nameof(title));
-            }
-
-            this.Title = title;
-
-            if (string.IsNullOrEmpty(language))
-            {
-                throw new ArgumentNullException(nameof(language));
-            }
-
-            this.Language = language;
-
-            if (_book0 == null)
-            {
-                throw new ArgumentNullException(nameof(_book0));
-            }
-
-            _book0.BookMetadata.Add(this);
-
-            this.Publishers = new HashSet<Company>();
-
-            Init();
-        }
-
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="title">The title or name of the object.</param>
-        /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="dateadded">The date the object was added.</param>
-        /// <param name="datemodified">The date the object was last modified.</param>
-        /// <param name="_book0"></param>
-        public static BookMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0)
-        {
-            return new BookMetadata(title, language, dateadded, datemodified, _book0);
-        }
-
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
-        /// <summary>
-        /// Backing field for ISBN.
-        /// </summary>
-        protected long? _ISBN;
-        /// <summary>
-        /// When provided in a partial class, allows value of ISBN to be changed before setting.
-        /// </summary>
-        partial void SetISBN(long? oldValue, ref long? newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of ISBN to be changed before returning.
-        /// </summary>
-        partial void GetISBN(ref long? result);
-
-        public long? ISBN
-        {
-            get
-            {
-                long? value = _ISBN;
-                GetISBN(ref value);
-                return _ISBN = value;
-            }
-
-            set
-            {
-                long? oldValue = _ISBN;
-                SetISBN(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _ISBN = value;
-                }
-            }
-        }
-
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-
-        [ForeignKey("Company_Publishers_Id")]
-        public virtual ICollection<Company> Publishers { get; protected set; }
-    }
-}
-

+ 0 - 277
Jellyfin.Data/Entities/Chapter.cs

@@ -1,277 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
-
-namespace Jellyfin.Data.Entities
-{
-    public partial class Chapter
-    {
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected Chapter()
-        {
-            Init();
-        }
-
-        /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static Chapter CreateChapterUnsafe()
-        {
-            return new Chapter();
-        }
-
-        /// <summary>
-        /// Public constructor with required data.
-        /// </summary>
-        /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="timestart"></param>
-        /// <param name="_release0"></param>
-        public Chapter(string language, long timestart, Release _release0)
-        {
-            if (string.IsNullOrEmpty(language))
-            {
-                throw new ArgumentNullException(nameof(language));
-            }
-
-            this.Language = language;
-
-            this.TimeStart = timestart;
-
-            if (_release0 == null)
-            {
-                throw new ArgumentNullException(nameof(_release0));
-            }
-
-            _release0.Chapters.Add(this);
-
-
-            Init();
-        }
-
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="timestart"></param>
-        /// <param name="_release0"></param>
-        public static Chapter Create(string language, long timestart, Release _release0)
-        {
-            return new Chapter(language, timestart, _release0);
-        }
-
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
-        /// <summary>
-        /// Backing field for Id.
-        /// </summary>
-        internal int _Id;
-        /// <summary>
-        /// When provided in a partial class, allows value of Id to be changed before setting.
-        /// </summary>
-        partial void SetId(int oldValue, ref int newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Id to be changed before returning.
-        /// </summary>
-        partial void GetId(ref int result);
-
-        /// <summary>
-        /// Identity, Indexed, Required.
-        /// </summary>
-        [Key]
-        [Required]
-        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
-        public int Id
-        {
-            get
-            {
-                int value = _Id;
-                GetId(ref value);
-                return _Id = value;
-            }
-
-            protected set
-            {
-                int oldValue = _Id;
-                SetId(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Id = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Backing field for Name.
-        /// </summary>
-        protected string _Name;
-        /// <summary>
-        /// When provided in a partial class, allows value of Name to be changed before setting.
-        /// </summary>
-        partial void SetName(string oldValue, ref string newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Name to be changed before returning.
-        /// </summary>
-        partial void GetName(ref string result);
-
-        /// <summary>
-        /// Max length = 1024
-        /// </summary>
-        [MaxLength(1024)]
-        [StringLength(1024)]
-        public string Name
-        {
-            get
-            {
-                string value = _Name;
-                GetName(ref value);
-                return _Name = value;
-            }
-
-            set
-            {
-                string oldValue = _Name;
-                SetName(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Name = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Backing field for Language.
-        /// </summary>
-        protected string _Language;
-        /// <summary>
-        /// When provided in a partial class, allows value of Language to be changed before setting.
-        /// </summary>
-        partial void SetLanguage(string oldValue, ref string newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Language to be changed before returning.
-        /// </summary>
-        partial void GetLanguage(ref string result);
-
-        /// <summary>
-        /// Required, Min length = 3, Max length = 3
-        /// ISO-639-3 3-character language codes.
-        /// </summary>
-        [Required]
-        [MinLength(3)]
-        [MaxLength(3)]
-        [StringLength(3)]
-        public string Language
-        {
-            get
-            {
-                string value = _Language;
-                GetLanguage(ref value);
-                return _Language = value;
-            }
-
-            set
-            {
-                string oldValue = _Language;
-                SetLanguage(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Language = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Backing field for TimeStart.
-        /// </summary>
-        protected long _TimeStart;
-        /// <summary>
-        /// When provided in a partial class, allows value of TimeStart to be changed before setting.
-        /// </summary>
-        partial void SetTimeStart(long oldValue, ref long newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of TimeStart to be changed before returning.
-        /// </summary>
-        partial void GetTimeStart(ref long result);
-
-        /// <summary>
-        /// Required.
-        /// </summary>
-        [Required]
-        public long TimeStart
-        {
-            get
-            {
-                long value = _TimeStart;
-                GetTimeStart(ref value);
-                return _TimeStart = value;
-            }
-
-            set
-            {
-                long oldValue = _TimeStart;
-                SetTimeStart(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _TimeStart = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Backing field for TimeEnd.
-        /// </summary>
-        protected long? _TimeEnd;
-        /// <summary>
-        /// When provided in a partial class, allows value of TimeEnd to be changed before setting.
-        /// </summary>
-        partial void SetTimeEnd(long? oldValue, ref long? newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of TimeEnd to be changed before returning.
-        /// </summary>
-        partial void GetTimeEnd(ref long? result);
-
-        public long? TimeEnd
-        {
-            get
-            {
-                long? value = _TimeEnd;
-                GetTimeEnd(ref value);
-                return _TimeEnd = value;
-            }
-
-            set
-            {
-                long? oldValue = _TimeEnd;
-                SetTimeEnd(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _TimeEnd = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Required, ConcurrenyToken.
-        /// </summary>
-        [ConcurrencyCheck]
-        [Required]
-        public uint RowVersion { get; set; }
-
-        public void OnSavingChanges()
-        {
-            RowVersion++;
-        }
-
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-    }
-}
-

+ 0 - 123
Jellyfin.Data/Entities/Collection.cs

@@ -1,123 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
-
-namespace Jellyfin.Data.Entities
-{
-    public partial class Collection
-    {
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor.
-        /// </summary>
-        public Collection()
-        {
-            CollectionItem = new LinkedList<CollectionItem>();
-
-            Init();
-        }
-
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
-        /// <summary>
-        /// Backing field for Id.
-        /// </summary>
-        internal int _Id;
-        /// <summary>
-        /// When provided in a partial class, allows value of Id to be changed before setting.
-        /// </summary>
-        partial void SetId(int oldValue, ref int newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Id to be changed before returning.
-        /// </summary>
-        partial void GetId(ref int result);
-
-        /// <summary>
-        /// Identity, Indexed, Required.
-        /// </summary>
-        [Key]
-        [Required]
-        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
-        public int Id
-        {
-            get
-            {
-                int value = _Id;
-                GetId(ref value);
-                return _Id = value;
-            }
-
-            protected set
-            {
-                int oldValue = _Id;
-                SetId(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Id = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Backing field for Name.
-        /// </summary>
-        protected string _Name;
-        /// <summary>
-        /// When provided in a partial class, allows value of Name to be changed before setting.
-        /// </summary>
-        partial void SetName(string oldValue, ref string newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Name to be changed before returning.
-        /// </summary>
-        partial void GetName(ref string result);
-
-        /// <summary>
-        /// Max length = 1024
-        /// </summary>
-        [MaxLength(1024)]
-        [StringLength(1024)]
-        public string Name
-        {
-            get
-            {
-                string value = _Name;
-                GetName(ref value);
-                return _Name = value;
-            }
-
-            set
-            {
-                string oldValue = _Name;
-                SetName(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Name = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Required, ConcurrenyToken.
-        /// </summary>
-        [ConcurrencyCheck]
-        [Required]
-        public uint RowVersion { get; set; }
-
-        public void OnSavingChanges()
-        {
-            RowVersion++;
-        }
-
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-        [ForeignKey("CollectionItem_CollectionItem_Id")]
-        public virtual ICollection<CollectionItem> CollectionItem { get; protected set; }
-    }
-}
-

+ 0 - 156
Jellyfin.Data/Entities/CollectionItem.cs

@@ -1,156 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
-
-namespace Jellyfin.Data.Entities
-{
-    public partial class CollectionItem
-    {
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected CollectionItem()
-        {
-            // NOTE: This class has one-to-one associations with CollectionItem.
-            // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
-
-            Init();
-        }
-
-        /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static CollectionItem CreateCollectionItemUnsafe()
-        {
-            return new CollectionItem();
-        }
-
-        /// <summary>
-        /// Public constructor with required data.
-        /// </summary>
-        /// <param name="_collection0"></param>
-        /// <param name="_collectionitem1"></param>
-        /// <param name="_collectionitem2"></param>
-        public CollectionItem(Collection _collection0, CollectionItem _collectionitem1, CollectionItem _collectionitem2)
-        {
-            // NOTE: This class has one-to-one associations with CollectionItem.
-            // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
-
-            if (_collection0 == null)
-            {
-                throw new ArgumentNullException(nameof(_collection0));
-            }
-
-            _collection0.CollectionItem.Add(this);
-
-            if (_collectionitem1 == null)
-            {
-                throw new ArgumentNullException(nameof(_collectionitem1));
-            }
-
-            _collectionitem1.Next = this;
-
-            if (_collectionitem2 == null)
-            {
-                throw new ArgumentNullException(nameof(_collectionitem2));
-            }
-
-            _collectionitem2.Previous = this;
-
-            Init();
-        }
-
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="_collection0"></param>
-        /// <param name="_collectionitem1"></param>
-        /// <param name="_collectionitem2"></param>
-        public static CollectionItem Create(Collection _collection0, CollectionItem _collectionitem1, CollectionItem _collectionitem2)
-        {
-            return new CollectionItem(_collection0, _collectionitem1, _collectionitem2);
-        }
-
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
-        /// <summary>
-        /// Backing field for Id.
-        /// </summary>
-        internal int _Id;
-        /// <summary>
-        /// When provided in a partial class, allows value of Id to be changed before setting.
-        /// </summary>
-        partial void SetId(int oldValue, ref int newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Id to be changed before returning.
-        /// </summary>
-        partial void GetId(ref int result);
-
-        /// <summary>
-        /// Identity, Indexed, Required.
-        /// </summary>
-        [Key]
-        [Required]
-        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
-        public int Id
-        {
-            get
-            {
-                int value = _Id;
-                GetId(ref value);
-                return _Id = value;
-            }
-
-            protected set
-            {
-                int oldValue = _Id;
-                SetId(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Id = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Required, ConcurrenyToken.
-        /// </summary>
-        [ConcurrencyCheck]
-        [Required]
-        public uint RowVersion { get; set; }
-
-        public void OnSavingChanges()
-        {
-            RowVersion++;
-        }
-
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-
-        /// <summary>
-        /// Required.
-        /// </summary>
-        [ForeignKey("LibraryItem_Id")]
-        public virtual LibraryItem LibraryItem { get; set; }
-
-        /// <remarks>
-        /// TODO check if this properly updated dependant and has the proper principal relationship
-        /// </remarks>
-        [ForeignKey("CollectionItem_Next_Id")]
-        public virtual CollectionItem Next { get; set; }
-
-        /// <remarks>
-        /// TODO check if this properly updated dependant and has the proper principal relationship
-        /// </remarks>
-        [ForeignKey("CollectionItem_Previous_Id")]
-        public virtual CollectionItem Previous { get; set; }
-    }
-}
-

+ 0 - 159
Jellyfin.Data/Entities/Company.cs

@@ -1,159 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
-
-namespace Jellyfin.Data.Entities
-{
-    public partial class Company
-    {
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected Company()
-        {
-            CompanyMetadata = new HashSet<CompanyMetadata>();
-
-            Init();
-        }
-
-        /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static Company CreateCompanyUnsafe()
-        {
-            return new Company();
-        }
-
-        /// <summary>
-        /// Public constructor with required data.
-        /// </summary>
-        /// <param name="_moviemetadata0"></param>
-        /// <param name="_seriesmetadata1"></param>
-        /// <param name="_musicalbummetadata2"></param>
-        /// <param name="_bookmetadata3"></param>
-        /// <param name="_company4"></param>
-        public Company(MovieMetadata _moviemetadata0, SeriesMetadata _seriesmetadata1, MusicAlbumMetadata _musicalbummetadata2, BookMetadata _bookmetadata3, Company _company4)
-        {
-            if (_moviemetadata0 == null)
-            {
-                throw new ArgumentNullException(nameof(_moviemetadata0));
-            }
-
-            _moviemetadata0.Studios.Add(this);
-
-            if (_seriesmetadata1 == null)
-            {
-                throw new ArgumentNullException(nameof(_seriesmetadata1));
-            }
-
-            _seriesmetadata1.Networks.Add(this);
-
-            if (_musicalbummetadata2 == null)
-            {
-                throw new ArgumentNullException(nameof(_musicalbummetadata2));
-            }
-
-            _musicalbummetadata2.Labels.Add(this);
-
-            if (_bookmetadata3 == null)
-            {
-                throw new ArgumentNullException(nameof(_bookmetadata3));
-            }
-
-            _bookmetadata3.Publishers.Add(this);
-
-            if (_company4 == null)
-            {
-                throw new ArgumentNullException(nameof(_company4));
-            }
-
-            _company4.Parent = this;
-
-            this.CompanyMetadata = new HashSet<CompanyMetadata>();
-
-            Init();
-        }
-
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="_moviemetadata0"></param>
-        /// <param name="_seriesmetadata1"></param>
-        /// <param name="_musicalbummetadata2"></param>
-        /// <param name="_bookmetadata3"></param>
-        /// <param name="_company4"></param>
-        public static Company Create(MovieMetadata _moviemetadata0, SeriesMetadata _seriesmetadata1, MusicAlbumMetadata _musicalbummetadata2, BookMetadata _bookmetadata3, Company _company4)
-        {
-            return new Company(_moviemetadata0, _seriesmetadata1, _musicalbummetadata2, _bookmetadata3, _company4);
-        }
-
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
-        /// <summary>
-        /// Backing field for Id.
-        /// </summary>
-        internal int _Id;
-        /// <summary>
-        /// When provided in a partial class, allows value of Id to be changed before setting.
-        /// </summary>
-        partial void SetId(int oldValue, ref int newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Id to be changed before returning.
-        /// </summary>
-        partial void GetId(ref int result);
-
-        /// <summary>
-        /// Identity, Indexed, Required.
-        /// </summary>
-        [Key]
-        [Required]
-        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
-        public int Id
-        {
-            get
-            {
-                int value = _Id;
-                GetId(ref value);
-                return _Id = value;
-            }
-
-            protected set
-            {
-                int oldValue = _Id;
-                SetId(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Id = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Required, ConcurrenyToken.
-        /// </summary>
-        [ConcurrencyCheck]
-        [Required]
-        public uint RowVersion { get; set; }
-
-        public void OnSavingChanges()
-        {
-            RowVersion++;
-        }
-
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-        [ForeignKey("CompanyMetadata_CompanyMetadata_Id")]
-        public virtual ICollection<CompanyMetadata> CompanyMetadata { get; protected set; }
-        [ForeignKey("Company_Parent_Id")]
-        public virtual Company Parent { get; set; }
-    }
-}
-

+ 0 - 236
Jellyfin.Data/Entities/CompanyMetadata.cs

@@ -1,236 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.ComponentModel.DataAnnotations;
-
-namespace Jellyfin.Data.Entities
-{
-    public partial class CompanyMetadata : Metadata
-    {
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected CompanyMetadata()
-        {
-            Init();
-        }
-
-        /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static CompanyMetadata CreateCompanyMetadataUnsafe()
-        {
-            return new CompanyMetadata();
-        }
-
-        /// <summary>
-        /// Public constructor with required data.
-        /// </summary>
-        /// <param name="title">The title or name of the object.</param>
-        /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="dateadded">The date the object was added.</param>
-        /// <param name="datemodified">The date the object was last modified.</param>
-        /// <param name="_company0"></param>
-        public CompanyMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Company _company0)
-        {
-            if (string.IsNullOrEmpty(title))
-            {
-                throw new ArgumentNullException(nameof(title));
-            }
-
-            this.Title = title;
-
-            if (string.IsNullOrEmpty(language))
-            {
-                throw new ArgumentNullException(nameof(language));
-            }
-
-            this.Language = language;
-
-            if (_company0 == null)
-            {
-                throw new ArgumentNullException(nameof(_company0));
-            }
-
-            _company0.CompanyMetadata.Add(this);
-
-            Init();
-        }
-
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="title">The title or name of the object.</param>
-        /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="dateadded">The date the object was added.</param>
-        /// <param name="datemodified">The date the object was last modified.</param>
-        /// <param name="_company0"></param>
-        public static CompanyMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Company _company0)
-        {
-            return new CompanyMetadata(title, language, dateadded, datemodified, _company0);
-        }
-
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
-        /// <summary>
-        /// Backing field for Description.
-        /// </summary>
-        protected string _Description;
-        /// <summary>
-        /// When provided in a partial class, allows value of Description to be changed before setting.
-        /// </summary>
-        partial void SetDescription(string oldValue, ref string newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Description to be changed before returning.
-        /// </summary>
-        partial void GetDescription(ref string result);
-
-        /// <summary>
-        /// Max length = 65535
-        /// </summary>
-        [MaxLength(65535)]
-        [StringLength(65535)]
-        public string Description
-        {
-            get
-            {
-                string value = _Description;
-                GetDescription(ref value);
-                return _Description = value;
-            }
-
-            set
-            {
-                string oldValue = _Description;
-                SetDescription(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Description = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Backing field for Headquarters.
-        /// </summary>
-        protected string _Headquarters;
-        /// <summary>
-        /// When provided in a partial class, allows value of Headquarters to be changed before setting.
-        /// </summary>
-        partial void SetHeadquarters(string oldValue, ref string newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Headquarters to be changed before returning.
-        /// </summary>
-        partial void GetHeadquarters(ref string result);
-
-        /// <summary>
-        /// Max length = 255
-        /// </summary>
-        [MaxLength(255)]
-        [StringLength(255)]
-        public string Headquarters
-        {
-            get
-            {
-                string value = _Headquarters;
-                GetHeadquarters(ref value);
-                return _Headquarters = value;
-            }
-
-            set
-            {
-                string oldValue = _Headquarters;
-                SetHeadquarters(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Headquarters = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Backing field for Country.
-        /// </summary>
-        protected string _Country;
-        /// <summary>
-        /// When provided in a partial class, allows value of Country to be changed before setting.
-        /// </summary>
-        partial void SetCountry(string oldValue, ref string newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Country to be changed before returning.
-        /// </summary>
-        partial void GetCountry(ref string result);
-
-        /// <summary>
-        /// Max length = 2
-        /// </summary>
-        [MaxLength(2)]
-        [StringLength(2)]
-        public string Country
-        {
-            get
-            {
-                string value = _Country;
-                GetCountry(ref value);
-                return _Country = value;
-            }
-
-            set
-            {
-                string oldValue = _Country;
-                SetCountry(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Country = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Backing field for Homepage.
-        /// </summary>
-        protected string _Homepage;
-        /// <summary>
-        /// When provided in a partial class, allows value of Homepage to be changed before setting.
-        /// </summary>
-        partial void SetHomepage(string oldValue, ref string newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Homepage to be changed before returning.
-        /// </summary>
-        partial void GetHomepage(ref string result);
-
-        /// <summary>
-        /// Max length = 1024
-        /// </summary>
-        [MaxLength(1024)]
-        [StringLength(1024)]
-        public string Homepage
-        {
-            get
-            {
-                string value = _Homepage;
-                GetHomepage(ref value);
-                return _Homepage = value;
-            }
-
-            set
-            {
-                string oldValue = _Homepage;
-                SetHomepage(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Homepage = value;
-                }
-            }
-        }
-
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-    }
-}
-

+ 0 - 71
Jellyfin.Data/Entities/CustomItem.cs

@@ -1,71 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations.Schema;
-
-namespace Jellyfin.Data.Entities
-{
-    public partial class CustomItem : LibraryItem
-    {
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected CustomItem()
-        {
-            CustomItemMetadata = new HashSet<CustomItemMetadata>();
-            Releases = new HashSet<Release>();
-
-            Init();
-        }
-
-        /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static CustomItem CreateCustomItemUnsafe()
-        {
-            return new CustomItem();
-        }
-
-        /// <summary>
-        /// Public constructor with required data.
-        /// </summary>
-        /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
-        /// <param name="dateadded">The date the object was added.</param>
-        public CustomItem(Guid urlid, DateTime dateadded)
-        {
-            this.UrlId = urlid;
-
-            this.CustomItemMetadata = new HashSet<CustomItemMetadata>();
-            this.Releases = new HashSet<Release>();
-
-            Init();
-        }
-
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
-        /// <param name="dateadded">The date the object was added.</param>
-        public static CustomItem Create(Guid urlid, DateTime dateadded)
-        {
-            return new CustomItem(urlid, dateadded);
-        }
-
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-        [ForeignKey("CustomItemMetadata_CustomItemMetadata_Id")]
-        public virtual ICollection<CustomItemMetadata> CustomItemMetadata { get; protected set; }
-
-        [ForeignKey("Release_Releases_Id")]
-        public virtual ICollection<Release> Releases { get; protected set; }
-    }
-}
-

+ 0 - 83
Jellyfin.Data/Entities/CustomItemMetadata.cs

@@ -1,83 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace Jellyfin.Data.Entities
-{
-    public partial class CustomItemMetadata : Metadata
-    {
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected CustomItemMetadata()
-        {
-            Init();
-        }
-
-        /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static CustomItemMetadata CreateCustomItemMetadataUnsafe()
-        {
-            return new CustomItemMetadata();
-        }
-
-        /// <summary>
-        /// Public constructor with required data.
-        /// </summary>
-        /// <param name="title">The title or name of the object.</param>
-        /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="dateadded">The date the object was added.</param>
-        /// <param name="datemodified">The date the object was last modified.</param>
-        /// <param name="_customitem0"></param>
-        public CustomItemMetadata(string title, string language, DateTime dateadded, DateTime datemodified, CustomItem _customitem0)
-        {
-            if (string.IsNullOrEmpty(title))
-            {
-                throw new ArgumentNullException(nameof(title));
-            }
-
-            this.Title = title;
-
-            if (string.IsNullOrEmpty(language))
-            {
-                throw new ArgumentNullException(nameof(language));
-            }
-
-            this.Language = language;
-
-            if (_customitem0 == null)
-            {
-                throw new ArgumentNullException(nameof(_customitem0));
-            }
-
-            _customitem0.CustomItemMetadata.Add(this);
-
-            Init();
-        }
-
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="title">The title or name of the object.</param>
-        /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="dateadded">The date the object was added.</param>
-        /// <param name="datemodified">The date the object was last modified.</param>
-        /// <param name="_customitem0"></param>
-        public static CustomItemMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, CustomItem _customitem0)
-        {
-            return new CustomItemMetadata(title, language, dateadded, datemodified, _customitem0);
-        }
-
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-    }
-}
-

+ 0 - 118
Jellyfin.Data/Entities/Episode.cs

@@ -1,118 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations.Schema;
-
-namespace Jellyfin.Data.Entities
-{
-    public partial class Episode : LibraryItem
-    {
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected Episode()
-        {
-            // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem.
-            // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
-
-            Releases = new HashSet<Release>();
-            EpisodeMetadata = new HashSet<EpisodeMetadata>();
-
-            Init();
-        }
-
-        /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static Episode CreateEpisodeUnsafe()
-        {
-            return new Episode();
-        }
-
-        /// <summary>
-        /// Public constructor with required data.
-        /// </summary>
-        /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
-        /// <param name="dateadded">The date the object was added.</param>
-        /// <param name="_season0"></param>
-        public Episode(Guid urlid, DateTime dateadded, Season _season0)
-        {
-            // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem.
-            // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
-
-            this.UrlId = urlid;
-
-            if (_season0 == null)
-            {
-                throw new ArgumentNullException(nameof(_season0));
-            }
-
-            _season0.Episodes.Add(this);
-
-            this.Releases = new HashSet<Release>();
-            this.EpisodeMetadata = new HashSet<EpisodeMetadata>();
-
-            Init();
-        }
-
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
-        /// <param name="dateadded">The date the object was added.</param>
-        /// <param name="_season0"></param>
-        public static Episode Create(Guid urlid, DateTime dateadded, Season _season0)
-        {
-            return new Episode(urlid, dateadded, _season0);
-        }
-
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
-        /// <summary>
-        /// Backing field for EpisodeNumber.
-        /// </summary>
-        protected int? _EpisodeNumber;
-        /// <summary>
-        /// When provided in a partial class, allows value of EpisodeNumber to be changed before setting.
-        /// </summary>
-        partial void SetEpisodeNumber(int? oldValue, ref int? newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of EpisodeNumber to be changed before returning.
-        /// </summary>
-        partial void GetEpisodeNumber(ref int? result);
-
-        public int? EpisodeNumber
-        {
-            get
-            {
-                int? value = _EpisodeNumber;
-                GetEpisodeNumber(ref value);
-                return _EpisodeNumber = value;
-            }
-
-            set
-            {
-                int? oldValue = _EpisodeNumber;
-                SetEpisodeNumber(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _EpisodeNumber = value;
-                }
-            }
-        }
-
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-        [ForeignKey("Release_Releases_Id")]
-        public virtual ICollection<Release> Releases { get; protected set; }
-        [ForeignKey("EpisodeMetadata_EpisodeMetadata_Id")]
-        public virtual ICollection<EpisodeMetadata> EpisodeMetadata { get; protected set; }
-    }
-}
-

+ 0 - 198
Jellyfin.Data/Entities/EpisodeMetadata.cs

@@ -1,198 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.ComponentModel.DataAnnotations;
-
-namespace Jellyfin.Data.Entities
-{
-    public partial class EpisodeMetadata : Metadata
-    {
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected EpisodeMetadata()
-        {
-            Init();
-        }
-
-        /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static EpisodeMetadata CreateEpisodeMetadataUnsafe()
-        {
-            return new EpisodeMetadata();
-        }
-
-        /// <summary>
-        /// Public constructor with required data.
-        /// </summary>
-        /// <param name="title">The title or name of the object.</param>
-        /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="dateadded">The date the object was added.</param>
-        /// <param name="datemodified">The date the object was last modified.</param>
-        /// <param name="_episode0"></param>
-        public EpisodeMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Episode _episode0)
-        {
-            if (string.IsNullOrEmpty(title))
-            {
-                throw new ArgumentNullException(nameof(title));
-            }
-
-            this.Title = title;
-
-            if (string.IsNullOrEmpty(language))
-            {
-                throw new ArgumentNullException(nameof(language));
-            }
-
-            this.Language = language;
-
-            if (_episode0 == null)
-            {
-                throw new ArgumentNullException(nameof(_episode0));
-            }
-
-            _episode0.EpisodeMetadata.Add(this);
-
-            Init();
-        }
-
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="title">The title or name of the object.</param>
-        /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="dateadded">The date the object was added.</param>
-        /// <param name="datemodified">The date the object was last modified.</param>
-        /// <param name="_episode0"></param>
-        public static EpisodeMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Episode _episode0)
-        {
-            return new EpisodeMetadata(title, language, dateadded, datemodified, _episode0);
-        }
-
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
-        /// <summary>
-        /// Backing field for Outline.
-        /// </summary>
-        protected string _Outline;
-        /// <summary>
-        /// When provided in a partial class, allows value of Outline to be changed before setting.
-        /// </summary>
-        partial void SetOutline(string oldValue, ref string newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Outline to be changed before returning.
-        /// </summary>
-        partial void GetOutline(ref string result);
-
-        /// <summary>
-        /// Max length = 1024
-        /// </summary>
-        [MaxLength(1024)]
-        [StringLength(1024)]
-        public string Outline
-        {
-            get
-            {
-                string value = _Outline;
-                GetOutline(ref value);
-                return _Outline = value;
-            }
-
-            set
-            {
-                string oldValue = _Outline;
-                SetOutline(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Outline = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Backing field for Plot.
-        /// </summary>
-        protected string _Plot;
-        /// <summary>
-        /// When provided in a partial class, allows value of Plot to be changed before setting.
-        /// </summary>
-        partial void SetPlot(string oldValue, ref string newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Plot to be changed before returning.
-        /// </summary>
-        partial void GetPlot(ref string result);
-
-        /// <summary>
-        /// Max length = 65535
-        /// </summary>
-        [MaxLength(65535)]
-        [StringLength(65535)]
-        public string Plot
-        {
-            get
-            {
-                string value = _Plot;
-                GetPlot(ref value);
-                return _Plot = value;
-            }
-
-            set
-            {
-                string oldValue = _Plot;
-                SetPlot(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Plot = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Backing field for Tagline.
-        /// </summary>
-        protected string _Tagline;
-        /// <summary>
-        /// When provided in a partial class, allows value of Tagline to be changed before setting.
-        /// </summary>
-        partial void SetTagline(string oldValue, ref string newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Tagline to be changed before returning.
-        /// </summary>
-        partial void GetTagline(ref string result);
-
-        /// <summary>
-        /// Max length = 1024
-        /// </summary>
-        [MaxLength(1024)]
-        [StringLength(1024)]
-        public string Tagline
-        {
-            get
-            {
-                string value = _Tagline;
-                GetTagline(ref value);
-                return _Tagline = value;
-            }
-
-            set
-            {
-                string oldValue = _Tagline;
-                SetTagline(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Tagline = value;
-                }
-            }
-        }
-
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-    }
-}
-

+ 0 - 162
Jellyfin.Data/Entities/Genre.cs

@@ -1,162 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
-
-namespace Jellyfin.Data.Entities
-{
-    public partial class Genre
-    {
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected Genre()
-        {
-            Init();
-        }
-
-        /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static Genre CreateGenreUnsafe()
-        {
-            return new Genre();
-        }
-
-        /// <summary>
-        /// Public constructor with required data.
-        /// </summary>
-        /// <param name="name"></param>
-        /// <param name="_metadata0"></param>
-        public Genre(string name, Metadata _metadata0)
-        {
-            if (string.IsNullOrEmpty(name))
-            {
-                throw new ArgumentNullException(nameof(name));
-            }
-
-            this.Name = name;
-
-            if (_metadata0 == null)
-            {
-                throw new ArgumentNullException(nameof(_metadata0));
-            }
-
-            _metadata0.Genres.Add(this);
-
-            Init();
-        }
-
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="name"></param>
-        /// <param name="_metadata0"></param>
-        public static Genre Create(string name, Metadata _metadata0)
-        {
-            return new Genre(name, _metadata0);
-        }
-
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
-        /// <summary>
-        /// Backing field for Id.
-        /// </summary>
-        internal int _Id;
-        /// <summary>
-        /// When provided in a partial class, allows value of Id to be changed before setting.
-        /// </summary>
-        partial void SetId(int oldValue, ref int newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Id to be changed before returning.
-        /// </summary>
-        partial void GetId(ref int result);
-
-        /// <summary>
-        /// Identity, Indexed, Required.
-        /// </summary>
-        [Key]
-        [Required]
-        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
-        public int Id
-        {
-            get
-            {
-                int value = _Id;
-                GetId(ref value);
-                return _Id = value;
-            }
-
-            protected set
-            {
-                int oldValue = _Id;
-                SetId(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Id = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Backing field for Name.
-        /// </summary>
-        internal string _Name;
-        /// <summary>
-        /// When provided in a partial class, allows value of Name to be changed before setting.
-        /// </summary>
-        partial void SetName(string oldValue, ref string newValue);
-        /// <summary>
-        /// When provided in a partial class, allows value of Name to be changed before returning.
-        /// </summary>
-        partial void GetName(ref string result);
-
-        /// <summary>
-        /// Indexed, Required, Max length = 255
-        /// </summary>
-        [Required]
-        [MaxLength(255)]
-        [StringLength(255)]
-        public string Name
-        {
-            get
-            {
-                string value = _Name;
-                GetName(ref value);
-                return _Name = value;
-            }
-
-            set
-            {
-                string oldValue = _Name;
-                SetName(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Name = value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Required, ConcurrenyToken.
-        /// </summary>
-        [ConcurrencyCheck]
-        [Required]
-        public uint RowVersion { get; set; }
-
-        public void OnSavingChanges()
-        {
-            RowVersion++;
-        }
-
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-    }
-}
-

+ 2 - 1
Jellyfin.Data/Entities/Group.cs

@@ -6,13 +6,14 @@ using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 using System.Linq;
 using Jellyfin.Data.Enums;
+using Jellyfin.Data.Interfaces;
 
 namespace Jellyfin.Data.Entities
 {
     /// <summary>
     /// An entity representing a group.
     /// </summary>
-    public partial class Group : IHasPermissions, ISavingChanges
+    public partial class Group : IHasPermissions, IHasConcurrencyToken
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="Group"/> class.

+ 81 - 0
Jellyfin.Data/Entities/Libraries/Artwork.cs

@@ -0,0 +1,81 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Enums;
+using Jellyfin.Data.Interfaces;
+
+namespace Jellyfin.Data.Entities.Libraries
+{
+    /// <summary>
+    /// An entity representing artwork.
+    /// </summary>
+    public class Artwork : IHasConcurrencyToken
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Artwork"/> class.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="kind">The kind of art.</param>
+        /// <param name="owner">The owner.</param>
+        public Artwork(string path, ArtKind kind, IHasArtwork owner)
+        {
+            if (string.IsNullOrEmpty(path))
+            {
+                throw new ArgumentNullException(nameof(path));
+            }
+
+            Path = path;
+            Kind = kind;
+
+            owner?.Artwork.Add(this);
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Artwork"/> class.
+        /// </summary>
+        /// <remarks>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </remarks>
+        protected Artwork()
+        {
+        }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <remarks>
+        /// Identity, Indexed, Required.
+        /// </remarks>
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id { get; protected set; }
+
+        /// <summary>
+        /// Gets or sets the path.
+        /// </summary>
+        /// <remarks>
+        /// Required, Max length = 65535.
+        /// </remarks>
+        [Required]
+        [MaxLength(65535)]
+        [StringLength(65535)]
+        public string Path { get; set; }
+
+        /// <summary>
+        /// Gets or sets the kind of artwork.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public ArtKind Kind { get; set; }
+
+        /// <inheritdoc />
+        [ConcurrencyCheck]
+        public uint RowVersion { get; set; }
+
+        /// <inheritdoc />
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+    }
+}

+ 28 - 0
Jellyfin.Data/Entities/Libraries/Book.cs

@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using Jellyfin.Data.Interfaces;
+
+namespace Jellyfin.Data.Entities.Libraries
+{
+    /// <summary>
+    /// An entity representing a book.
+    /// </summary>
+    public class Book : LibraryItem, IHasReleases
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Book"/> class.
+        /// </summary>
+        public Book()
+        {
+            BookMetadata = new HashSet<BookMetadata>();
+            Releases = new HashSet<Release>();
+        }
+
+        /// <summary>
+        /// Gets or sets a collection containing the metadata for this book.
+        /// </summary>
+        public virtual ICollection<BookMetadata> BookMetadata { get; protected set; }
+
+        /// <inheritdoc />
+        public virtual ICollection<Release> Releases { get; protected set; }
+    }
+}

+ 55 - 0
Jellyfin.Data/Entities/Libraries/BookMetadata.cs

@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Interfaces;
+
+namespace Jellyfin.Data.Entities.Libraries
+{
+    /// <summary>
+    /// An entity containing metadata for a book.
+    /// </summary>
+    public class BookMetadata : Metadata, IHasCompanies
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="BookMetadata"/> class.
+        /// </summary>
+        /// <param name="title">The title or name of the object.</param>
+        /// <param name="language">ISO-639-3 3-character language codes.</param>
+        /// <param name="book">The book.</param>
+        public BookMetadata(string title, string language, Book book) : base(title, language)
+        {
+            if (book == null)
+            {
+                throw new ArgumentNullException(nameof(book));
+            }
+
+            book.BookMetadata.Add(this);
+
+            Publishers = new HashSet<Company>();
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="BookMetadata"/> class.
+        /// </summary>
+        /// <remarks>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </remarks>
+        protected BookMetadata()
+        {
+        }
+
+        /// <summary>
+        /// Gets or sets the ISBN.
+        /// </summary>
+        public long? Isbn { get; set; }
+
+        /// <summary>
+        /// Gets or sets a collection of the publishers for this book.
+        /// </summary>
+        public virtual ICollection<Company> Publishers { get; protected set; }
+
+        /// <inheritdoc />
+        [NotMapped]
+        public ICollection<Company> Companies => Publishers;
+    }
+}

+ 102 - 0
Jellyfin.Data/Entities/Libraries/Chapter.cs

@@ -0,0 +1,102 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Interfaces;
+
+namespace Jellyfin.Data.Entities.Libraries
+{
+    /// <summary>
+    /// An entity representing a chapter.
+    /// </summary>
+    public class Chapter : IHasConcurrencyToken
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Chapter"/> class.
+        /// </summary>
+        /// <param name="language">ISO-639-3 3-character language codes.</param>
+        /// <param name="startTime">The start time for this chapter.</param>
+        /// <param name="release">The release.</param>
+        public Chapter(string language, long startTime, Release release)
+        {
+            if (string.IsNullOrEmpty(language))
+            {
+                throw new ArgumentNullException(nameof(language));
+            }
+
+            Language = language;
+            StartTime = startTime;
+
+            if (release == null)
+            {
+                throw new ArgumentNullException(nameof(release));
+            }
+
+            release.Chapters.Add(this);
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Chapter"/> class.
+        /// </summary>
+        /// <remarks>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </remarks>
+        protected Chapter()
+        {
+        }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <remarks>
+        /// Identity, Indexed, Required.
+        /// </remarks>
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id { get; protected set; }
+
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <remarks>
+        /// Max length = 1024.
+        /// </remarks>
+        [MaxLength(1024)]
+        [StringLength(1024)]
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets the language.
+        /// </summary>
+        /// <remarks>
+        /// Required, Min length = 3, Max length = 3
+        /// ISO-639-3 3-character language codes.
+        /// </remarks>
+        [Required]
+        [MinLength(3)]
+        [MaxLength(3)]
+        [StringLength(3)]
+        public string Language { get; set; }
+
+        /// <summary>
+        /// Gets or sets the start time.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public long StartTime { get; set; }
+
+        /// <summary>
+        /// Gets or sets the end time.
+        /// </summary>
+        public long? EndTime { get; set; }
+
+        /// <inheritdoc />
+        [ConcurrencyCheck]
+        public uint RowVersion { get; protected set; }
+
+        /// <inheritdoc />
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+    }
+}

+ 55 - 0
Jellyfin.Data/Entities/Libraries/Collection.cs

@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Interfaces;
+
+namespace Jellyfin.Data.Entities.Libraries
+{
+    /// <summary>
+    /// An entity representing a collection.
+    /// </summary>
+    public class Collection : IHasConcurrencyToken
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Collection"/> class.
+        /// </summary>
+        public Collection()
+        {
+            Items = new HashSet<CollectionItem>();
+        }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <remarks>
+        /// Identity, Indexed, Required.
+        /// </remarks>
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id { get; protected set; }
+
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <remarks>
+        /// Max length = 1024.
+        /// </remarks>
+        [MaxLength(1024)]
+        [StringLength(1024)]
+        public string Name { get; set; }
+
+        /// <inheritdoc />
+        [ConcurrencyCheck]
+        public uint RowVersion { get; set; }
+
+        /// <summary>
+        /// Gets or sets a collection containing this collection's items.
+        /// </summary>
+        public virtual ICollection<CollectionItem> Items { get; protected set; }
+
+        /// <inheritdoc />
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+    }
+}

+ 94 - 0
Jellyfin.Data/Entities/Libraries/CollectionItem.cs

@@ -0,0 +1,94 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Interfaces;
+
+namespace Jellyfin.Data.Entities.Libraries
+{
+    /// <summary>
+    /// An entity representing a collection item.
+    /// </summary>
+    public class CollectionItem : IHasConcurrencyToken
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CollectionItem"/> class.
+        /// </summary>
+        /// <param name="collection">The collection.</param>
+        /// <param name="previous">The previous item.</param>
+        /// <param name="next">The next item.</param>
+        public CollectionItem(Collection collection, CollectionItem previous, CollectionItem next)
+        {
+            if (collection == null)
+            {
+                throw new ArgumentNullException(nameof(collection));
+            }
+
+            collection.Items.Add(this);
+
+            if (next != null)
+            {
+                Next = next;
+                next.Previous = this;
+            }
+
+            if (previous != null)
+            {
+                Previous = previous;
+                previous.Next = this;
+            }
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CollectionItem"/> class.
+        /// </summary>
+        /// <remarks>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </remarks>
+        protected CollectionItem()
+        {
+        }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <remarks>
+        /// Identity, Indexed, Required.
+        /// </remarks>
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id { get; set; }
+
+        /// <inheritdoc />
+        [ConcurrencyCheck]
+        public uint RowVersion { get; set; }
+
+        /// <summary>
+        /// Gets or sets the library item.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public virtual LibraryItem LibraryItem { get; set; }
+
+        /// <summary>
+        /// Gets or sets the next item in the collection.
+        /// </summary>
+        /// <remarks>
+        /// TODO check if this properly updated dependant and has the proper principal relationship
+        /// </remarks>
+        public virtual CollectionItem Next { get; set; }
+
+        /// <summary>
+        /// Gets or sets the previous item in the collection.
+        /// </summary>
+        /// <remarks>
+        /// TODO check if this properly updated dependant and has the proper principal relationship
+        /// </remarks>
+        public virtual CollectionItem Previous { get; set; }
+
+        /// <inheritdoc />
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+    }
+}

+ 67 - 0
Jellyfin.Data/Entities/Libraries/Company.cs

@@ -0,0 +1,67 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Interfaces;
+
+namespace Jellyfin.Data.Entities.Libraries
+{
+    /// <summary>
+    /// An entity representing a company.
+    /// </summary>
+    public class Company : IHasCompanies, IHasConcurrencyToken
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Company"/> class.
+        /// </summary>
+        /// <param name="owner">The owner of this company.</param>
+        public Company(IHasCompanies owner)
+        {
+            owner?.Companies.Add(this);
+
+            CompanyMetadata = new HashSet<CompanyMetadata>();
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Company"/> class.
+        /// </summary>
+        /// <remarks>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </remarks>
+        protected Company()
+        {
+        }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <remarks>
+        /// Identity, Indexed, Required.
+        /// </remarks>
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id { get; protected set; }
+
+        /// <inheritdoc />
+        [ConcurrencyCheck]
+        public uint RowVersion { get; set; }
+
+        /// <summary>
+        /// Gets or sets a collection containing the metadata.
+        /// </summary>
+        public virtual ICollection<CompanyMetadata> CompanyMetadata { get; protected set; }
+
+        /// <summary>
+        /// Gets or sets a collection containing this company's child companies.
+        /// </summary>
+        public virtual ICollection<Company> ChildCompanies { get; protected set; }
+
+        /// <inheritdoc />
+        [NotMapped]
+        public ICollection<Company> Companies => ChildCompanies;
+
+        /// <inheritdoc />
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+    }
+}

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