浏览代码

Merge remote-tracking branch 'upstream/master' into package-install-repo

crobibero 4 年之前
父节点
当前提交
b7022e8dc1
共有 100 个文件被更改,包括 1528 次插入5763 次删除
  1. 0 3
      .ci/azure-pipelines-abi.yml
  2. 4 4
      .ci/azure-pipelines-main.yml
  3. 39 14
      .ci/azure-pipelines-package.yml
  4. 7 2
      .ci/azure-pipelines-test.yml
  5. 10 4
      .ci/azure-pipelines.yml
  6. 1 1
      .vscode/tasks.json
  7. 4 0
      CONTRIBUTORS.md
  8. 1 1
      Dockerfile
  9. 1 1
      Dockerfile.arm
  10. 1 1
      Dockerfile.arm64
  11. 3 3
      Emby.Dlna/ConnectionManager/ConnectionManagerService.cs
  12. 2 2
      Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
  13. 1 1
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  14. 2 2
      Emby.Dlna/Didl/DidlBuilder.cs
  15. 19 29
      Emby.Dlna/DlnaManager.cs
  16. 1 0
      Emby.Dlna/Emby.Dlna.csproj
  17. 12 18
      Emby.Dlna/Eventing/DlnaEventManager.cs
  18. 二进制
      Emby.Dlna/Images/logo240.jpg
  19. 二进制
      Emby.Dlna/Images/people48.png
  20. 8 7
      Emby.Dlna/Main/DlnaEntryPoint.cs
  21. 3 3
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs
  22. 21 20
      Emby.Dlna/PlayTo/Device.cs
  23. 41 46
      Emby.Dlna/PlayTo/PlayToController.cs
  24. 13 16
      Emby.Dlna/PlayTo/PlayToManager.cs
  25. 45 71
      Emby.Dlna/PlayTo/SsdpHttpClient.cs
  26. 3 7
      Emby.Dlna/Service/BaseService.cs
  27. 1 1
      Emby.Drawing/ImageProcessor.cs
  28. 4 22
      Emby.Naming/AudioBook/AudioBookFilePathParser.cs
  29. 2 1
      Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
  30. 1 1
      Emby.Naming/AudioBook/AudioBookResolver.cs
  31. 2 2
      Emby.Naming/Common/NamingOptions.cs
  32. 13 0
      Emby.Naming/Emby.Naming.csproj
  33. 1 1
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  34. 161 50
      Emby.Server.Implementations/ApplicationHost.cs
  35. 1 1
      Emby.Server.Implementations/Channels/ChannelManager.cs
  36. 2 2
      Emby.Server.Implementations/ConfigurationOptions.cs
  37. 11 2
      Emby.Server.Implementations/Data/BaseSqliteRepository.cs
  38. 3 1
      Emby.Server.Implementations/Data/SqliteExtensions.cs
  39. 48 32
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  40. 2 2
      Emby.Server.Implementations/Dto/DtoService.cs
  41. 4 4
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  42. 0 335
      Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
  43. 0 250
      Emby.Server.Implementations/HttpServer/FileWriter.cs
  44. 0 766
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  45. 0 721
      Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
  46. 0 212
      Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
  47. 0 113
      Emby.Server.Implementations/HttpServer/ResponseFilter.cs
  48. 1 212
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  49. 9 15
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  50. 7 13
      Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
  51. 0 120
      Emby.Server.Implementations/HttpServer/StreamWriter.cs
  52. 102 0
      Emby.Server.Implementations/HttpServer/WebSocketManager.cs
  53. 4 3
      Emby.Server.Implementations/IO/FileRefresher.cs
  54. 1 1
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  55. 0 32
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  56. 1 31
      Emby.Server.Implementations/IO/StreamHelper.cs
  57. 14 19
      Emby.Server.Implementations/Library/LibraryManager.cs
  58. 21 30
      Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
  59. 6 25
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  60. 10 33
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  61. 148 243
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  62. 7 24
      Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
  63. 40 58
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  64. 7 6
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  65. 6 10
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  66. 17 30
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  67. 26 8
      Emby.Server.Implementations/Localization/Core/af.json
  68. 1 1
      Emby.Server.Implementations/Localization/Core/fr.json
  69. 9 1
      Emby.Server.Implementations/Localization/Core/gl.json
  70. 1 1
      Emby.Server.Implementations/Localization/Core/id.json
  71. 2 2
      Emby.Server.Implementations/Localization/Core/ko.json
  72. 4 4
      Emby.Server.Implementations/Localization/Core/nb.json
  73. 60 3
      Emby.Server.Implementations/Localization/Core/nn.json
  74. 117 0
      Emby.Server.Implementations/Localization/Core/sq.json
  75. 17 17
      Emby.Server.Implementations/Localization/Core/ta.json
  76. 102 61
      Emby.Server.Implementations/Localization/Core/th.json
  77. 117 0
      Emby.Server.Implementations/Localization/Core/vi.json
  78. 27 27
      Emby.Server.Implementations/Localization/Core/zh-TW.json
  79. 1 0
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  80. 60 0
      Emby.Server.Implementations/Plugins/PluginManifest.cs
  81. 28 31
      Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
  82. 21 21
      Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
  83. 6 6
      Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
  84. 7 7
      Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
  85. 6 9
      Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
  86. 7 10
      Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
  87. 2 3
      Emby.Server.Implementations/Security/AuthenticationRepository.cs
  88. 0 64
      Emby.Server.Implementations/Services/HttpResult.cs
  89. 0 51
      Emby.Server.Implementations/Services/RequestHelper.cs
  90. 0 141
      Emby.Server.Implementations/Services/ResponseHelper.cs
  91. 0 202
      Emby.Server.Implementations/Services/ServiceController.cs
  92. 0 230
      Emby.Server.Implementations/Services/ServiceExec.cs
  93. 0 212
      Emby.Server.Implementations/Services/ServiceHandler.cs
  94. 0 20
      Emby.Server.Implementations/Services/ServiceMethod.cs
  95. 0 550
      Emby.Server.Implementations/Services/ServicePath.cs
  96. 0 118
      Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
  97. 0 27
      Emby.Server.Implementations/Services/UrlExtensions.cs
  98. 2 2
      Emby.Server.Implementations/Session/SessionManager.cs
  99. 6 6
      Emby.Server.Implementations/Session/SessionWebSocketListener.cs
  100. 0 248
      Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.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

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

@@ -64,28 +64,28 @@ jobs:
           arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
           zipAfterPublish: false
 
-      - task: PublishPipelineArtifact@0
+      - task: PublishPipelineArtifact@1
         displayName: 'Publish Artifact Naming'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:
           targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll'
           artifactName: 'Jellyfin.Naming'
 
-      - task: PublishPipelineArtifact@0
+      - task: PublishPipelineArtifact@1
         displayName: 'Publish Artifact Controller'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:
           targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
           artifactName: 'Jellyfin.Controller'
 
-      - task: PublishPipelineArtifact@0
+      - task: PublishPipelineArtifact@1
         displayName: 'Publish Artifact Model'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:
           targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
           artifactName: 'Jellyfin.Model'
 
-      - task: PublishPipelineArtifact@0
+      - task: PublishPipelineArtifact@1
         displayName: 'Publish Artifact Common'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:

+ 39 - 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,20 +130,22 @@ 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:
@@ -155,7 +158,7 @@ jobs:
   steps:
   - task: DotNetCoreCLI@2
     displayName: 'Build Stable Nuget packages'
-    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+    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'
@@ -172,7 +175,7 @@ jobs:
         MediaBrowser.Model/MediaBrowser.Model.csproj
         Emby.Naming/Emby.Naming.csproj
       custom: 'pack'
-      arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory)'
+      arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
 
   - task: PublishBuildArtifacts@1
     displayName: 'Publish Nuget packages'
@@ -180,10 +183,32 @@ jobs:
       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 feed'
-    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+    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

+ 7 - 2
.ci/azure-pipelines-test.yml

@@ -74,7 +74,6 @@ jobs:
       - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
         displayName: 'Run ReportGenerator'
-        enabled: false
         inputs:
           reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
           targetdir: "$(Agent.TempDirectory)/merged/"
@@ -84,10 +83,16 @@ jobs:
       - task: PublishCodeCoverageResults@1
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
         displayName: 'Publish Code Coverage'
-        enabled: false
         inputs:
           codeCoverageTool: "cobertura"
           #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
           summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml"
           pathToSources: $(Build.SourcesDirectory)
           failIfCoverageEmpty: true
+
+      - task: PublishPipelineArtifact@1
+        displayName: 'Publish OpenAPI Artifact'
+        condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
+        inputs:
+          targetPath: "tests/Jellyfin.Api.Tests/bin/Release/netcoreapp3.1/openapi.json"
+          artifactName: 'OpenAPI Spec'

+ 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 - 1
.vscode/tasks.json

@@ -17,7 +17,7 @@
             "type": "process",
             "args": [
                 "test",
-                "${workspaceFolder}/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj"
+                "${workspaceFolder}/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj"
             ],
             "problemMatcher": "$msCompile"
         }

+ 4 - 0
CONTRIBUTORS.md

@@ -57,6 +57,7 @@
  - [Larvitar](https://github.com/Larvitar)
  - [LeoVerto](https://github.com/LeoVerto)
  - [Liggy](https://github.com/Liggy)
+ - [lmaonator](https://github.com/lmaonator)
  - [LogicalPhallacy](https://github.com/LogicalPhallacy)
  - [loli10K](https://github.com/loli10K)
  - [lostmypillow](https://github.com/lostmypillow)
@@ -78,6 +79,7 @@
  - [nvllsvm](https://github.com/nvllsvm)
  - [nyanmisaka](https://github.com/nyanmisaka)
  - [oddstr13](https://github.com/oddstr13)
+ - [orryverducci](https://github.com/orryverducci)
  - [petermcneil](https://github.com/petermcneil)
  - [Phlogi](https://github.com/Phlogi)
  - [pjeanjean](https://github.com/pjeanjean)
@@ -133,6 +135,7 @@
  - [YouKnowBlom](https://github.com/YouKnowBlom)
  - [KristupasSavickas](https://github.com/KristupasSavickas)
  - [Pusta](https://github.com/pusta)
+ - [nielsvanvelzen](https://github.com/nielsvanvelzen)
 
 # Emby Contributors
 
@@ -196,3 +199,4 @@
  - [tikuf](https://github.com/tikuf/)
  - [Tim Hobbs](https://github.com/timhobbs)
  - [SvenVandenbrande](https://github.com/SvenVandenbrande)
+ - [olsh](https://github.com/olsh)

+ 1 - 1
Dockerfile

@@ -14,7 +14,7 @@ COPY . .
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 # because of changes in docker and systemd we need to not build in parallel at the moment
 # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
 
 FROM debian:buster-slim
 

+ 1 - 1
Dockerfile.arm

@@ -21,7 +21,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 # Discard objs - may cause failures if exists
 RUN find . -type d -name obj | xargs -r rm -r
 # Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
 
 
 FROM multiarch/qemu-user-static:x86_64-arm as qemu

+ 1 - 1
Dockerfile.arm64

@@ -21,7 +21,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 # Discard objs - may cause failures if exists
 RUN find . -type d -name obj | xargs -r rm -r
 # Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
 
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
 FROM arm64v8/debian:buster-slim

+ 3 - 3
Emby.Dlna/ConnectionManager/ConnectionManagerService.cs

@@ -1,8 +1,8 @@
 #pragma warning disable CS1591
 
+using System.Net.Http;
 using System.Threading.Tasks;
 using Emby.Dlna.Service;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
 using Microsoft.Extensions.Logging;
@@ -18,8 +18,8 @@ namespace Emby.Dlna.ConnectionManager
             IDlnaManager dlna,
             IServerConfigurationManager config,
             ILogger<ConnectionManagerService> logger,
-            IHttpClient httpClient)
-            : base(logger, httpClient)
+            IHttpClientFactory httpClientFactory)
+            : base(logger, httpClientFactory)
         {
             _dlna = dlna;
             _config = config;

+ 2 - 2
Emby.Dlna/ContentDirectory/ContentDirectoryService.cs

@@ -2,11 +2,11 @@
 
 using System;
 using System.Linq;
+using System.Net.Http;
 using System.Threading.Tasks;
 using Emby.Dlna.Service;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
@@ -41,7 +41,7 @@ namespace Emby.Dlna.ContentDirectory
             IServerConfigurationManager config,
             IUserManager userManager,
             ILogger<ContentDirectoryService> logger,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClient,
             ILocalizationManager localization,
             IMediaSourceManager mediaSourceManager,
             IUserViewManager userViewManager,

+ 1 - 1
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -1363,7 +1363,7 @@ namespace Emby.Dlna.ContentDirectory
                 };
             }
 
-            Logger.LogError("Error parsing item Id: {id}. Returning user root folder.", id);
+            Logger.LogError("Error parsing item Id: {Id}. Returning user root folder.", id);
 
             return new ServerItem(_libraryManager.GetUserRootFolder());
         }

+ 2 - 2
Emby.Dlna/Didl/DidlBuilder.cs

@@ -948,7 +948,7 @@ namespace Emby.Dlna.Didl
             }
             catch (XmlException ex)
             {
-                _logger.LogError(ex, "Error adding xml value: {value}", name);
+                _logger.LogError(ex, "Error adding xml value: {Value}", name);
             }
         }
 
@@ -960,7 +960,7 @@ namespace Emby.Dlna.Didl
             }
             catch (XmlException ex)
             {
-                _logger.LogError(ex, "Error adding xml value: {value}", value);
+                _logger.LogError(ex, "Error adding xml value: {Value}", value);
             }
         }
 

+ 19 - 29
Emby.Dlna/DlnaManager.cs

@@ -126,32 +126,23 @@ namespace Emby.Dlna
             var builder = new StringBuilder();
 
             builder.AppendLine("No matching device profile found. The default will need to be used.");
-            builder.AppendFormat(CultureInfo.InvariantCulture, "DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty).AppendLine();
-            builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
-            builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
-            builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
-            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
-            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
-            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
-            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
-            builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
+            builder.Append("FriendlyName:").AppendLine(profile.FriendlyName);
+            builder.Append("Manufacturer:").AppendLine(profile.Manufacturer);
+            builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl);
+            builder.Append("ModelDescription:").AppendLine(profile.ModelDescription);
+            builder.Append("ModelName:").AppendLine(profile.ModelName);
+            builder.Append("ModelNumber:").AppendLine(profile.ModelNumber);
+            builder.Append("ModelUrl:").AppendLine(profile.ModelUrl);
+            builder.Append("SerialNumber:").AppendLine(profile.SerialNumber);
 
             _logger.LogInformation(builder.ToString());
         }
 
         private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
         {
-            if (!string.IsNullOrEmpty(profileInfo.DeviceDescription))
-            {
-                if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
-                {
-                    return false;
-                }
-            }
-
             if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
             {
-                if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
+                if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
                 {
                     return false;
                 }
@@ -159,7 +150,7 @@ namespace Emby.Dlna
 
             if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
             {
-                if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
+                if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
                 {
                     return false;
                 }
@@ -167,7 +158,7 @@ namespace Emby.Dlna
 
             if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
             {
-                if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
+                if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
                 {
                     return false;
                 }
@@ -175,7 +166,7 @@ namespace Emby.Dlna
 
             if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
             {
-                if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
+                if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
                 {
                     return false;
                 }
@@ -183,7 +174,7 @@ namespace Emby.Dlna
 
             if (!string.IsNullOrEmpty(profileInfo.ModelName))
             {
-                if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName))
+                if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
                 {
                     return false;
                 }
@@ -191,7 +182,7 @@ namespace Emby.Dlna
 
             if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
             {
-                if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
+                if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
                 {
                     return false;
                 }
@@ -199,7 +190,7 @@ namespace Emby.Dlna
 
             if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
             {
-                if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
+                if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
                 {
                     return false;
                 }
@@ -207,7 +198,7 @@ namespace Emby.Dlna
 
             if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
             {
-                if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
+                if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
                 {
                     return false;
                 }
@@ -216,11 +207,11 @@ namespace Emby.Dlna
             return true;
         }
 
-        private bool IsRegexMatch(string input, string pattern)
+        private bool IsRegexOrSubstringMatch(string input, string pattern)
         {
             try
             {
-                return Regex.IsMatch(input, pattern);
+                return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
             }
             catch (ArgumentException ex)
             {
@@ -511,8 +502,7 @@ namespace Emby.Dlna
 
         public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
         {
-            var profile = GetProfile(headers) ??
-                          GetDefaultProfile();
+            var profile = GetDefaultProfile();
 
             var serverId = _appHost.SystemId;
 

+ 1 - 0
Emby.Dlna/Emby.Dlna.csproj

@@ -80,6 +80,7 @@
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
     <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
   </ItemGroup>
 
 </Project>

+ 12 - 18
Emby.Dlna/Eventing/DlnaEventManager.cs

@@ -6,6 +6,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Net.Http;
+using System.Net.Mime;
 using System.Text;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
@@ -20,13 +21,13 @@ namespace Emby.Dlna.Eventing
             new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
 
         private readonly ILogger _logger;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
 
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
-        public DlnaEventManager(ILogger logger, IHttpClient httpClient)
+        public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _logger = logger;
         }
 
@@ -167,24 +168,17 @@ namespace Emby.Dlna.Eventing
 
             builder.Append("</e:propertyset>");
 
-            var options = new HttpRequestOptions
-            {
-                RequestContent = builder.ToString(),
-                RequestContentType = "text/xml",
-                Url = subscription.CallbackUrl,
-                BufferContent = false
-            };
-
-            options.RequestHeaders.Add("NT", subscription.NotificationType);
-            options.RequestHeaders.Add("NTS", "upnp:propchange");
-            options.RequestHeaders.Add("SID", subscription.Id);
-            options.RequestHeaders.Add("SEQ", subscription.TriggerCount.ToString(_usCulture));
+            using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"),  subscription.CallbackUrl);
+            options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
+            options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
+            options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
+            options.Headers.TryAddWithoutValidation("SID", subscription.Id);
+            options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture));
 
             try
             {
-                using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false))
-                {
-                }
+                using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                    .SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
             }
             catch (OperationCanceledException)
             {

二进制
Emby.Dlna/Images/logo240.jpg


二进制
Emby.Dlna/Images/people48.png


+ 8 - 7
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -2,6 +2,7 @@
 
 using System;
 using System.Globalization;
+using System.Net.Http;
 using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
@@ -36,7 +37,7 @@ namespace Emby.Dlna.Main
         private readonly ILogger<DlnaEntryPoint> _logger;
         private readonly IServerApplicationHost _appHost;
         private readonly ISessionManager _sessionManager;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILibraryManager _libraryManager;
         private readonly IUserManager _userManager;
         private readonly IDlnaManager _dlnaManager;
@@ -61,7 +62,7 @@ namespace Emby.Dlna.Main
             ILoggerFactory loggerFactory,
             IServerApplicationHost appHost,
             ISessionManager sessionManager,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILibraryManager libraryManager,
             IUserManager userManager,
             IDlnaManager dlnaManager,
@@ -79,7 +80,7 @@ namespace Emby.Dlna.Main
             _config = config;
             _appHost = appHost;
             _sessionManager = sessionManager;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _libraryManager = libraryManager;
             _userManager = userManager;
             _dlnaManager = dlnaManager;
@@ -101,7 +102,7 @@ namespace Emby.Dlna.Main
                 config,
                 userManager,
                 loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
-                httpClient,
+                httpClientFactory,
                 localizationManager,
                 mediaSourceManager,
                 userViewManager,
@@ -112,11 +113,11 @@ namespace Emby.Dlna.Main
                 dlnaManager,
                 config,
                 loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
-                httpClient);
+                httpClientFactory);
 
             MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
                 loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
-                httpClient,
+                httpClientFactory,
                 config);
             Current = this;
         }
@@ -364,7 +365,7 @@ namespace Emby.Dlna.Main
                         _appHost,
                         _imageProcessor,
                         _deviceDiscovery,
-                        _httpClient,
+                        _httpClientFactory,
                         _config,
                         _userDataManager,
                         _localization,

+ 3 - 3
Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs

@@ -1,8 +1,8 @@
 #pragma warning disable CS1591
 
+using System.Net.Http;
 using System.Threading.Tasks;
 using Emby.Dlna.Service;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using Microsoft.Extensions.Logging;
 
@@ -14,9 +14,9 @@ namespace Emby.Dlna.MediaReceiverRegistrar
 
         public MediaReceiverRegistrarService(
             ILogger<MediaReceiverRegistrarService> logger,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IServerConfigurationManager config)
-            : base(logger, httpClient)
+            : base(logger, httpClientFactory)
         {
             _config = config;
         }

+ 21 - 20
Emby.Dlna/PlayTo/Device.cs

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Net.Http;
 using System.Security;
 using System.Threading;
 using System.Threading.Tasks;
@@ -21,7 +22,7 @@ namespace Emby.Dlna.PlayTo
     {
         private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
 
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
 
         private readonly ILogger _logger;
 
@@ -34,10 +35,10 @@ namespace Emby.Dlna.PlayTo
         private int _connectFailureCount;
         private bool _disposed;
 
-        public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger)
+        public Device(DeviceInfo deviceProperties, IHttpClientFactory httpClientFactory, ILogger logger)
         {
             Properties = deviceProperties;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _logger = logger;
         }
 
@@ -236,7 +237,7 @@ namespace Emby.Dlna.PlayTo
             _logger.LogDebug("Setting mute");
             var value = mute ? 1 : 0;
 
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
                 .ConfigureAwait(false);
 
             IsMuted = mute;
@@ -271,7 +272,7 @@ namespace Emby.Dlna.PlayTo
             // Remote control will perform better
             Volume = value;
 
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
                 .ConfigureAwait(false);
         }
 
@@ -292,7 +293,7 @@ namespace Emby.Dlna.PlayTo
                 throw new InvalidOperationException("Unable to find service");
             }
 
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
                 .ConfigureAwait(false);
 
             RestartTimer(true);
@@ -326,7 +327,7 @@ namespace Emby.Dlna.PlayTo
             }
 
             var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
                 .ConfigureAwait(false);
 
             await Task.Delay(50).ConfigureAwait(false);
@@ -368,7 +369,7 @@ namespace Emby.Dlna.PlayTo
                 throw new InvalidOperationException("Unable to find service");
             }
 
-            return new SsdpHttpClient(_httpClient).SendCommandAsync(
+            return new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -397,7 +398,7 @@ namespace Emby.Dlna.PlayTo
 
             var service = GetAvTransportService();
 
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
                 .ConfigureAwait(false);
 
             RestartTimer(true);
@@ -415,7 +416,7 @@ namespace Emby.Dlna.PlayTo
 
             var service = GetAvTransportService();
 
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
                 .ConfigureAwait(false);
 
             TransportState = TransportState.Paused;
@@ -542,7 +543,7 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
+            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -592,7 +593,7 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
+            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -625,7 +626,7 @@ namespace Emby.Dlna.PlayTo
                 return null;
             }
 
-            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
+            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -667,7 +668,7 @@ namespace Emby.Dlna.PlayTo
 
             var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
 
-            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
+            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -734,7 +735,7 @@ namespace Emby.Dlna.PlayTo
 
             var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
 
-            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
+            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -912,7 +913,7 @@ namespace Emby.Dlna.PlayTo
 
             string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
 
-            var httpClient = new SsdpHttpClient(_httpClient);
+            var httpClient = new SsdpHttpClient(_httpClientFactory);
 
             var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
 
@@ -940,7 +941,7 @@ namespace Emby.Dlna.PlayTo
 
             string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
 
-            var httpClient = new SsdpHttpClient(_httpClient);
+            var httpClient = new SsdpHttpClient(_httpClientFactory);
             _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
             var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
 
@@ -969,9 +970,9 @@ namespace Emby.Dlna.PlayTo
             return baseUrl + url;
         }
 
-        public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, ILogger logger, CancellationToken cancellationToken)
+        public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
         {
-            var ssdpHttpClient = new SsdpHttpClient(httpClient);
+            var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
 
             var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
 
@@ -1079,7 +1080,7 @@ namespace Emby.Dlna.PlayTo
                 }
             }
 
-            return new Device(deviceProperties, httpClient, logger);
+            return new Device(deviceProperties, httpClientFactory, logger);
         }
 
         private static DeviceIcon CreateIcon(XElement element)

+ 41 - 46
Emby.Dlna/PlayTo/PlayToController.cs

@@ -669,62 +669,57 @@ namespace Emby.Dlna.PlayTo
 
         private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
         {
-            if (Enum.TryParse(command.Name, true, out GeneralCommandType commandType))
-            {
-                switch (commandType)
-                {
-                    case GeneralCommandType.VolumeDown:
-                        return _device.VolumeDown(cancellationToken);
-                    case GeneralCommandType.VolumeUp:
-                        return _device.VolumeUp(cancellationToken);
-                    case GeneralCommandType.Mute:
-                        return _device.Mute(cancellationToken);
-                    case GeneralCommandType.Unmute:
-                        return _device.Unmute(cancellationToken);
-                    case GeneralCommandType.ToggleMute:
-                        return _device.ToggleMute(cancellationToken);
-                    case GeneralCommandType.SetAudioStreamIndex:
-                        if (command.Arguments.TryGetValue("Index", out string index))
+            switch (command.Name)
+            {
+                case GeneralCommandType.VolumeDown:
+                    return _device.VolumeDown(cancellationToken);
+                case GeneralCommandType.VolumeUp:
+                    return _device.VolumeUp(cancellationToken);
+                case GeneralCommandType.Mute:
+                    return _device.Mute(cancellationToken);
+                case GeneralCommandType.Unmute:
+                    return _device.Unmute(cancellationToken);
+                case GeneralCommandType.ToggleMute:
+                    return _device.ToggleMute(cancellationToken);
+                case GeneralCommandType.SetAudioStreamIndex:
+                    if (command.Arguments.TryGetValue("Index", out string index))
+                    {
+                        if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
                         {
-                            if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
-                            {
-                                return SetAudioStreamIndex(val);
-                            }
-
-                            throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
+                            return SetAudioStreamIndex(val);
                         }
 
-                        throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
-                    case GeneralCommandType.SetSubtitleStreamIndex:
-                        if (command.Arguments.TryGetValue("Index", out index))
-                        {
-                            if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
-                            {
-                                return SetSubtitleStreamIndex(val);
-                            }
+                        throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
+                    }
 
-                            throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
+                    throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
+                case GeneralCommandType.SetSubtitleStreamIndex:
+                    if (command.Arguments.TryGetValue("Index", out index))
+                    {
+                        if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
+                        {
+                            return SetSubtitleStreamIndex(val);
                         }
 
-                        throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
-                    case GeneralCommandType.SetVolume:
-                        if (command.Arguments.TryGetValue("Volume", out string vol))
-                        {
-                            if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
-                            {
-                                return _device.SetVolume(volume, cancellationToken);
-                            }
+                        throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
+                    }
 
-                            throw new ArgumentException("Unsupported volume value supplied.");
+                    throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
+                case GeneralCommandType.SetVolume:
+                    if (command.Arguments.TryGetValue("Volume", out string vol))
+                    {
+                        if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
+                        {
+                            return _device.SetVolume(volume, cancellationToken);
                         }
 
-                        throw new ArgumentException("Volume argument cannot be null");
-                    default:
-                        return Task.CompletedTask;
-                }
-            }
+                        throw new ArgumentException("Unsupported volume value supplied.");
+                    }
 
-            return Task.CompletedTask;
+                    throw new ArgumentException("Volume argument cannot be null");
+                default:
+                    return Task.CompletedTask;
+            }
         }
 
         private async Task SetAudioStreamIndex(int? newIndex)

+ 13 - 16
Emby.Dlna/PlayTo/PlayToManager.cs

@@ -4,6 +4,7 @@ using System;
 using System.Globalization;
 using System.Linq;
 using System.Net;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Events;
@@ -33,7 +34,7 @@ namespace Emby.Dlna.PlayTo
         private readonly IDlnaManager _dlnaManager;
         private readonly IServerApplicationHost _appHost;
         private readonly IImageProcessor _imageProcessor;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerConfigurationManager _config;
         private readonly IUserDataManager _userDataManager;
         private readonly ILocalizationManager _localization;
@@ -46,7 +47,7 @@ namespace Emby.Dlna.PlayTo
         private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
         private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
 
-        public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
+        public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
         {
             _logger = logger;
             _sessionManager = sessionManager;
@@ -56,7 +57,7 @@ namespace Emby.Dlna.PlayTo
             _appHost = appHost;
             _imageProcessor = imageProcessor;
             _deviceDiscovery = deviceDiscovery;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _config = config;
             _userDataManager = userDataManager;
             _localization = localization;
@@ -129,25 +130,21 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        private string GetUuid(string usn)
+        private static string GetUuid(string usn)
         {
-            var found = false;
-            var index = usn.IndexOf("uuid:", StringComparison.OrdinalIgnoreCase);
-            if (index != -1)
-            {
-                usn = usn.Substring(index);
-                found = true;
-            }
+            const string UuidStr = "uuid:";
+            const string UuidColonStr = "::";
 
-            index = usn.IndexOf("::", StringComparison.OrdinalIgnoreCase);
+            var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase);
             if (index != -1)
             {
-                usn = usn.Substring(0, index);
+                return usn.Substring(index + UuidStr.Length);
             }
 
-            if (found)
+            index = usn.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase);
+            if (index != -1)
             {
-                return usn;
+                usn = usn.Substring(0, index + UuidColonStr.Length);
             }
 
             return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture);
@@ -174,7 +171,7 @@ namespace Emby.Dlna.PlayTo
 
             if (controller == null)
             {
-                var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _logger, cancellationToken).ConfigureAwait(false);
+                var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
 
                 string deviceName = device.Properties.Name;
 

+ 45 - 71
Emby.Dlna/PlayTo/SsdpHttpClient.cs

@@ -4,6 +4,8 @@ using System;
 using System.Globalization;
 using System.IO;
 using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
@@ -20,11 +22,11 @@ namespace Emby.Dlna.PlayTo
 
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
 
-        public SsdpHttpClient(IHttpClient httpClient)
+        public SsdpHttpClient(IHttpClientFactory httpClientFactory)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
         }
 
         public async Task<XDocument> SendCommandAsync(
@@ -36,20 +38,18 @@ namespace Emby.Dlna.PlayTo
             CancellationToken cancellationToken = default)
         {
             var url = NormalizeServiceUrl(baseUrl, service.ControlUrl);
-            using (var response = await PostSoapDataAsync(
-                url,
-                $"\"{service.ServiceType}#{command}\"",
-                postData,
-                header,
-                cancellationToken)
-                .ConfigureAwait(false))
-            using (var stream = response.Content)
-            using (var reader = new StreamReader(stream, Encoding.UTF8))
-            {
-                return XDocument.Parse(
-                    await reader.ReadToEndAsync().ConfigureAwait(false),
-                    LoadOptions.PreserveWhitespace);
-            }
+            using var response = await PostSoapDataAsync(
+                    url,
+                    $"\"{service.ServiceType}#{command}\"",
+                    postData,
+                    header,
+                    cancellationToken)
+                .ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            using var reader = new StreamReader(stream, Encoding.UTF8);
+            return XDocument.Parse(
+                await reader.ReadToEndAsync().ConfigureAwait(false),
+                LoadOptions.PreserveWhitespace);
         }
 
         private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
@@ -76,49 +76,32 @@ namespace Emby.Dlna.PlayTo
             int eventport,
             int timeOut = 3600)
         {
-            var options = new HttpRequestOptions
-            {
-                Url = url,
-                UserAgent = USERAGENT,
-                LogErrorResponseBody = true,
-                BufferContent = false,
-            };
-
-            options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture);
-            options.RequestHeaders["CALLBACK"] = "<" + localIp + ":" + eventport.ToString(_usCulture) + ">";
-            options.RequestHeaders["NT"] = "upnp:event";
-            options.RequestHeaders["TIMEOUT"] = "Second-" + timeOut.ToString(_usCulture);
-
-            using (await _httpClient.SendAsync(options, new HttpMethod("SUBSCRIBE")).ConfigureAwait(false))
-            {
-            }
+            using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
+            options.Headers.UserAgent.ParseAdd(USERAGENT);
+            options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(_usCulture));
+            options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">");
+            options.Headers.TryAddWithoutValidation("NT", "upnp:event");
+            options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture));
+
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                .SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
+                .ConfigureAwait(false);
         }
 
         public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken)
         {
-            var options = new HttpRequestOptions
-            {
-                Url = url,
-                UserAgent = USERAGENT,
-                LogErrorResponseBody = true,
-                BufferContent = false,
-
-                CancellationToken = cancellationToken
-            };
-
-            options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName;
-
-            using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false))
-            using (var stream = response.Content)
-            using (var reader = new StreamReader(stream, Encoding.UTF8))
-            {
-                return XDocument.Parse(
-                    await reader.ReadToEndAsync().ConfigureAwait(false),
-                    LoadOptions.PreserveWhitespace);
-            }
+            using var options = new HttpRequestMessage(HttpMethod.Get, url);
+            options.Headers.UserAgent.ParseAdd(USERAGENT);
+            options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            using var reader = new StreamReader(stream, Encoding.UTF8);
+            return XDocument.Parse(
+                await reader.ReadToEndAsync().ConfigureAwait(false),
+                LoadOptions.PreserveWhitespace);
         }
 
-        private Task<HttpResponseInfo> PostSoapDataAsync(
+        private async Task<HttpResponseMessage> PostSoapDataAsync(
             string url,
             string soapAction,
             string postData,
@@ -130,29 +113,20 @@ namespace Emby.Dlna.PlayTo
                 soapAction = $"\"{soapAction}\"";
             }
 
-            var options = new HttpRequestOptions
-            {
-                Url = url,
-                UserAgent = USERAGENT,
-                LogErrorResponseBody = true,
-                BufferContent = false,
-
-                CancellationToken = cancellationToken
-            };
-
-            options.RequestHeaders["SOAPAction"] = soapAction;
-            options.RequestHeaders["Pragma"] = "no-cache";
-            options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName;
+            using var options = new HttpRequestMessage(HttpMethod.Post, url);
+            options.Headers.UserAgent.ParseAdd(USERAGENT);
+            options.Headers.TryAddWithoutValidation("SOAPACTION", soapAction);
+            options.Headers.TryAddWithoutValidation("Pragma", "no-cache");
+            options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
 
             if (!string.IsNullOrEmpty(header))
             {
-                options.RequestHeaders["contentFeatures.dlna.org"] = header;
+                options.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header);
             }
 
-            options.RequestContentType = "text/xml";
-            options.RequestContent = postData;
+            options.Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml);
 
-            return _httpClient.Post(options);
+            return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
         }
     }
 }

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

@@ -1,25 +1,21 @@
 #pragma warning disable CS1591
 
+using System.Net.Http;
 using Emby.Dlna.Eventing;
-using MediaBrowser.Common.Net;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.Service
 {
     public class BaseService : IDlnaEventManager
     {
-        protected BaseService(ILogger<BaseService> logger, IHttpClient httpClient)
+        protected BaseService(ILogger<BaseService> logger, IHttpClientFactory httpClientFactory)
         {
             Logger = logger;
-            HttpClient = httpClient;
-
-            EventManager = new DlnaEventManager(logger, HttpClient);
+            EventManager = new DlnaEventManager(logger, httpClientFactory);
         }
 
         protected IDlnaEventManager EventManager { get; }
 
-        protected IHttpClient HttpClient { get; }
-
         protected ILogger Logger { get; }
 
         public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)

+ 1 - 1
Emby.Drawing/ImageProcessor.cs

@@ -455,7 +455,7 @@ namespace Emby.Drawing
                 throw new ArgumentException("Path can't be empty.", nameof(path));
             }
 
-            if (path.IsEmpty)
+            if (filename.IsEmpty)
             {
                 throw new ArgumentException("Filename can't be empty.", nameof(filename));
             }

+ 4 - 22
Emby.Naming/AudioBook/AudioBookFilePathParser.cs

@@ -1,6 +1,6 @@
+#nullable enable
 #pragma warning disable CS1591
 
-using System;
 using System.Globalization;
 using System.IO;
 using System.Text.RegularExpressions;
@@ -19,12 +19,7 @@ namespace Emby.Naming.AudioBook
 
         public AudioBookFilePathParserResult Parse(string path)
         {
-            if (path == null)
-            {
-                throw new ArgumentNullException(nameof(path));
-            }
-
-            var result = new AudioBookFilePathParserResult();
+            AudioBookFilePathParserResult result = default;
             var fileName = Path.GetFileNameWithoutExtension(path);
             foreach (var expression in _options.AudioBookPartsExpressions)
             {
@@ -50,27 +45,14 @@ namespace Emby.Naming.AudioBook
                         {
                             if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
                             {
-                                result.ChapterNumber = intValue;
+                                result.PartNumber = intValue;
                             }
                         }
                     }
                 }
             }
 
-            /*var matches = _iRegexProvider.GetRegex("\\d+", RegexOptions.IgnoreCase).Matches(fileName);
-            if (matches.Count > 0)
-            {
-                if (!result.ChapterNumber.HasValue)
-                {
-                    result.ChapterNumber = int.Parse(matches[0].Groups[0].Value);
-                }
-
-                if (matches.Count > 1)
-                {
-                    result.PartNumber = int.Parse(matches[matches.Count - 1].Groups[0].Value);
-                }
-            }*/
-            result.Success = result.PartNumber.HasValue || result.ChapterNumber.HasValue;
+            result.Success = result.ChapterNumber.HasValue || result.PartNumber.HasValue;
 
             return result;
         }

+ 2 - 1
Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs

@@ -1,8 +1,9 @@
+#nullable enable
 #pragma warning disable CS1591
 
 namespace Emby.Naming.AudioBook
 {
-    public class AudioBookFilePathParserResult
+    public struct AudioBookFilePathParserResult
     {
         public int? PartNumber { get; set; }
 

+ 1 - 1
Emby.Naming/AudioBook/AudioBookResolver.cs

@@ -55,8 +55,8 @@ namespace Emby.Naming.AudioBook
             {
                 Path = path,
                 Container = container,
-                PartNumber = parsingResult.PartNumber,
                 ChapterNumber = parsingResult.ChapterNumber,
+                PartNumber = parsingResult.PartNumber,
                 IsDirectory = isDirectory
             };
         }

+ 2 - 2
Emby.Naming/Common/NamingOptions.cs

@@ -136,8 +136,8 @@ namespace Emby.Naming.Common
 
             CleanDateTimes = new[]
             {
-                @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
-                @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
+                @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
+                @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
             };
 
             CleanStrings = new[]

+ 13 - 0
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>
@@ -28,6 +37,10 @@
     <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.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -308,7 +308,7 @@ namespace Emby.Server.Implementations.AppBase
             }
             catch (Exception ex)
             {
-                Logger.LogError(ex, "Error loading configuration file: {path}", path);
+                Logger.LogError(ex, "Error loading configuration file: {Path}", path);
 
                 return Activator.CreateInstance(configurationType);
             }

+ 161 - 50
Emby.Server.Implementations/ApplicationHost.cs

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Net;
@@ -37,11 +38,11 @@ using Emby.Server.Implementations.LiveTv;
 using Emby.Server.Implementations.Localization;
 using Emby.Server.Implementations.Net;
 using Emby.Server.Implementations.Playlists;
+using Emby.Server.Implementations.Plugins;
 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;
@@ -50,6 +51,7 @@ using Jellyfin.Api.Helpers;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Json;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
@@ -90,7 +92,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;
@@ -98,12 +99,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
 {
@@ -120,18 +121,23 @@ namespace Emby.Server.Implementations
         private readonly IFileSystem _fileSystemManager;
         private readonly INetworkManager _networkManager;
         private readonly IXmlSerializer _xmlSerializer;
+        private readonly IJsonSerializer _jsonSerializer;
         private readonly IStartupOptions _startupOptions;
 
         private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
-        private IHttpServer _httpServer;
-        private IHttpClient _httpClient;
+        private IHttpClientFactory _httpClientFactory;
+        private IWebSocketManager _webSocketManager;
+
+        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
@@ -235,8 +241,14 @@ namespace Emby.Server.Implementations
         public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="ApplicationHost" /> class.
+        /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
         /// </summary>
+        /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
+        /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+        /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
         public ApplicationHost(
             IServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
@@ -246,6 +258,8 @@ namespace Emby.Server.Implementations
             IServiceCollection serviceCollection)
         {
             _xmlSerializer = new MyXmlSerializer();
+            _jsonSerializer = new JsonSerializer();            
+            
             ServiceCollection = serviceCollection;
 
             _networkManager = networkManager;
@@ -277,6 +291,10 @@ namespace Emby.Server.Implementations
                 Password = ServerConfigurationManager.Configuration.CertificatePassword
             };
             Certificate = GetCertificate(CertificateInfo);
+
+            ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
+            ApplicationVersionString = ApplicationVersion.ToString(3);
+            ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
         }
 
         public string ExpandVirtualPath(string path)
@@ -306,16 +324,16 @@ namespace Emby.Server.Implementations
         }
 
         /// <inheritdoc />
-        public Version ApplicationVersion { get; } = typeof(ApplicationHost).Assembly.GetName().Version;
+        public Version ApplicationVersion { get; }
 
         /// <inheritdoc />
-        public string ApplicationVersionString { get; } = typeof(ApplicationHost).Assembly.GetName().Version.ToString(3);
+        public string ApplicationVersionString { get; }
 
         /// <summary>
         /// Gets the current application user agent.
         /// </summary>
         /// <value>The application user agent.</value>
-        public string ApplicationUserAgent => Name.Replace(' ', '-') + "/" + ApplicationVersionString;
+        public string ApplicationUserAgent { get; }
 
         /// <summary>
         /// Gets the email address for use within a comment section of a user agent field.
@@ -446,8 +464,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);
@@ -502,9 +519,6 @@ namespace Emby.Server.Implementations
             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>
@@ -524,8 +538,6 @@ namespace Emby.Server.Implementations
             ServiceCollection.AddSingleton(_fileSystemManager);
             ServiceCollection.AddSingleton<TvdbClientManager>();
 
-            ServiceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
-
             ServiceCollection.AddSingleton(_networkManager);
 
             ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
@@ -544,8 +556,6 @@ namespace Emby.Server.Implementations
 
             ServiceCollection.AddSingleton<IZipClient, ZipClient>();
 
-            ServiceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>();
-
             ServiceCollection.AddSingleton<IServerApplicationHost>(this);
             ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
 
@@ -581,8 +591,7 @@ namespace Emby.Server.Implementations
 
             ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
 
-            ServiceCollection.AddSingleton<ServiceController>();
-            ServiceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
+            ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
 
             ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
 
@@ -655,8 +664,8 @@ namespace Emby.Server.Implementations
 
             _mediaEncoder = Resolve<IMediaEncoder>();
             _sessionManager = Resolve<ISessionManager>();
-            _httpServer = Resolve<IHttpServer>();
-            _httpClient = Resolve<IHttpClient>();
+            _httpClientFactory = Resolve<IHttpClientFactory>();
+            _webSocketManager = Resolve<IWebSocketManager>();
 
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
 
@@ -757,7 +766,6 @@ namespace Emby.Server.Implementations
             CollectionFolder.XmlSerializer = _xmlSerializer;
             CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
             CollectionFolder.ApplicationHost = this;
-            AuthenticatedAttribute.AuthService = Resolve<IAuthService>();
         }
 
         /// <summary>
@@ -777,7 +785,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>(),
@@ -943,7 +952,7 @@ namespace Emby.Server.Implementations
                 }
             }
 
-            if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
+            if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
             {
                 requiresRestart = true;
             }
@@ -1017,6 +1026,119 @@ namespace Emby.Server.Implementations
 
         protected abstract void RestartInternal();
 
+        /// <summary>
+        /// Comparison function used in <see cref="GetPlugins" />.
+        /// </summary>
+        /// <param name="a">Item to compare.</param>
+        /// <param name="b">Item to compare with.</param>
+        /// <returns>Boolean result of the operation.</returns>
+        private static int VersionCompare(
+            (Version PluginVersion, string Name, string Path) a,
+            (Version PluginVersion, string Name, string Path) b)
+        {
+            int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
+
+            if (compare == 0)
+            {
+                return a.PluginVersion.CompareTo(b.PluginVersion);
+            }
+
+            return compare;
+        }
+
+        /// <summary>
+        /// Returns a list of plugins to install.
+        /// </summary>
+        /// <param name="path">Path to check.</param>
+        /// <param name="cleanup">True if an attempt should be made to delete old plugs.</param>
+        /// <returns>Enumerable list of dlls to load.</returns>
+        private IEnumerable<string> GetPlugins(string path, bool cleanup = true)
+        {
+            var dllList = new List<string>();
+            var versions = new List<(Version PluginVersion, string Name, string Path)>();
+            var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
+            string metafile;
+
+            foreach (var dir in directories)
+            {
+                try
+                {
+                    metafile = Path.Combine(dir, "meta.json");
+                    if (File.Exists(metafile))
+                    {
+                        var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
+
+                        if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
+                        {
+                            targetAbi = new Version(0, 0, 0, 1);
+                        }
+
+                        if (!Version.TryParse(manifest.Version, out var version))
+                        {
+                            version = new Version(0, 0, 0, 1);
+                        }
+
+                        if (ApplicationVersion >= targetAbi)
+                        {
+                            // Only load Plugins if the plugin is built for this version or below.
+                            versions.Add((version, manifest.Name, dir));
+                        }
+                    }
+                    else
+                    {
+                        // No metafile, so lets see if the folder is versioned.
+                        metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
+                        
+                        int versionIndex = dir.LastIndexOf('_');
+                        if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver))
+                        {
+                            // Versioned folder.
+                            versions.Add((ver, metafile, dir));
+                        }
+                        else
+                        {
+                            // Un-versioned folder - Add it under the path name and version 0.0.0.1.                        
+                            versions.Add((new Version(0, 0, 0, 1), metafile, dir));
+                        }   
+                    }
+                }
+                catch
+                {
+                    continue;
+                }
+            }
+
+            string lastName = string.Empty;
+            versions.Sort(VersionCompare);
+            // Traverse backwards through the list.
+            // The first item will be the latest version.
+            for (int x = versions.Count - 1; x >= 0; x--)
+            {
+                if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
+                {
+                    dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
+                    lastName = versions[x].Name;
+                    continue;
+                }
+
+                if (!string.IsNullOrEmpty(lastName) && cleanup)
+                {
+                    // Attempt a cleanup of old folders.
+                    try
+                    {
+                        Logger.LogDebug("Deleting {Path}", versions[x].Path);
+                        Directory.Delete(versions[x].Path, true);
+                    }
+                    catch (Exception e)
+                    {
+                        Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
+                    }
+                }
+            }
+
+            return dllList;
+        }
+
         /// <summary>
         /// Gets the composable part assemblies.
         /// </summary>
@@ -1025,7 +1147,7 @@ namespace Emby.Server.Implementations
         {
             if (Directory.Exists(ApplicationPaths.PluginsPath))
             {
-                foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories))
+                foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
                 {
                     Assembly plugAss;
                     try
@@ -1139,7 +1261,8 @@ namespace Emby.Server.Implementations
                 Id = SystemId,
                 OperatingSystem = OperatingSystem.Id.ToString(),
                 ServerName = FriendlyName,
-                LocalAddress = localAddress
+                LocalAddress = localAddress,
+                StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
             };
         }
 
@@ -1301,25 +1424,17 @@ namespace Emby.Server.Implementations
 
             try
             {
-                using (var response = await _httpClient.SendAsync(
-                    new HttpRequestOptions
-                    {
-                        Url = apiUrl,
-                        LogErrorResponseBody = false,
-                        BufferContent = false,
-                        CancellationToken = cancellationToken
-                    }, HttpMethod.Post).ConfigureAwait(false))
-                {
-                    using (var reader = new StreamReader(response.Content))
-                    {
-                        var result = await reader.ReadToEndAsync().ConfigureAwait(false);
-                        var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
+                using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl);
+                using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                    .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
 
-                        _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid);
-                        Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid);
-                        return valid;
-                    }
-                }
+                await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                var result = await System.Text.Json.JsonSerializer.DeserializeAsync<string>(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false);
+                var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
+
+                _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid);
+                Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid);
+                return valid;
             }
             catch (OperationCanceledException)
             {
@@ -1406,7 +1521,7 @@ namespace Emby.Server.Implementations
 
             foreach (var assembly in assemblies)
             {
-                Logger.LogDebug("Found API endpoints in plugin {name}", assembly.FullName);
+                Logger.LogDebug("Found API endpoints in plugin {Name}", assembly.FullName);
                 yield return assembly;
             }
         }
@@ -1441,10 +1556,6 @@ namespace Emby.Server.Implementations
             }
         }
 
-        public virtual void EnableLoopback(string appName)
-        {
-        }
-
         private bool _disposed = false;
 
         /// <summary>

+ 1 - 1
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -890,7 +890,7 @@ namespace Emby.Server.Implementations.Channels
             }
             catch (Exception ex)
             {
-                _logger.LogError(ex, "Error writing to channel cache file: {path}", path);
+                _logger.LogError(ex, "Error writing to channel cache file: {Path}", path);
             }
         }
 

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

@@ -15,10 +15,10 @@ 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 },
+            { PlaylistsAllowDuplicatesKey, bool.FalseString },
             { BindToUnixSocketKey, bool.FalseString }
         };
     }

+ 11 - 2
Emby.Server.Implementations/Data/BaseSqliteRepository.cs

@@ -143,8 +143,17 @@ namespace Emby.Server.Implementations.Data
         public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
             => connection.PrepareStatement(sql);
 
-        public IEnumerable<IStatement> PrepareAll(IDatabaseConnection connection, IEnumerable<string> sql)
-            => sql.Select(connection.PrepareStatement);
+        public IStatement[] PrepareAll(IDatabaseConnection connection, IReadOnlyList<string> sql)
+        {
+            int len = sql.Count;
+            IStatement[] statements = new IStatement[len];
+            for (int i = 0; i < len; i++)
+            {
+                statements[i] = connection.PrepareStatement(sql[i]);
+            }
+
+            return statements;
+        }
 
         protected bool TableExists(ManagedConnection connection, string name)
         {

+ 3 - 1
Emby.Server.Implementations/Data/SqliteExtensions.cs

@@ -234,7 +234,9 @@ namespace Emby.Server.Implementations.Data
         {
             if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
             {
-                bindParam.Bind(value.ToByteArray());
+                Span<byte> byteValue = stackalloc byte[16];
+                value.TryWriteBytes(byteValue);
+                bindParam.Bind(byteValue);
             }
             else
             {

+ 48 - 32
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -138,7 +138,6 @@ namespace Emby.Server.Implementations.Data
                 "pragma shrink_memory"
             };
 
-
             string[] postQueries =
             {
                 // obsolete
@@ -560,7 +559,7 @@ namespace Emby.Server.Implementations.Data
             {
                 SaveItemCommandText,
                 "delete from AncestorIds where ItemId=@ItemId"
-            }).ToList();
+            });
 
             using (var saveItemStatement = statements[0])
             using (var deleteAncestorsStatement = statements[1])
@@ -2925,7 +2924,7 @@ namespace Emby.Server.Implementations.Data
             {
                 connection.RunInTransaction(db =>
                 {
-                    var statements = PrepareAll(db, statementTexts).ToList();
+                    var statements = PrepareAll(db, statementTexts);
 
                     if (!isReturningZeroItems)
                     {
@@ -2963,7 +2962,7 @@ namespace Emby.Server.Implementations.Data
 
                     if (query.EnableTotalRecordCount)
                     {
-                        using (var statement = statements[statements.Count - 1])
+                        using (var statement = statements[statements.Length - 1])
                         {
                             if (EnableJoinUserData(query))
                             {
@@ -3329,7 +3328,7 @@ namespace Emby.Server.Implementations.Data
             {
                 connection.RunInTransaction(db =>
                 {
-                    var statements = PrepareAll(db, statementTexts).ToList();
+                    var statements = PrepareAll(db, statementTexts);
 
                     if (!isReturningZeroItems)
                     {
@@ -3355,7 +3354,7 @@ namespace Emby.Server.Implementations.Data
 
                     if (query.EnableTotalRecordCount)
                     {
-                        using (var statement = statements[statements.Count - 1])
+                        using (var statement = statements[statements.Length - 1])
                         {
                             if (EnableJoinUserData(query))
                             {
@@ -3718,26 +3717,31 @@ namespace Emby.Server.Implementations.Data
                 statement?.TryBind("@MaxPremiereDate", query.MaxPremiereDate.Value);
             }
 
+            StringBuilder clauseBuilder = new StringBuilder();
+            const string Or = " OR ";
+
             var trailerTypes = query.TrailerTypes;
             int trailerTypesLen = trailerTypes.Length;
             if (trailerTypesLen > 0)
             {
-                const string Or = " OR ";
-                StringBuilder clause = new StringBuilder("(", trailerTypesLen * 32);
+                clauseBuilder.Append('(');
+
                 for (int i = 0; i < trailerTypesLen; i++)
                 {
                     var paramName = "@TrailerTypes" + i;
-                    clause.Append("TrailerTypes like ")
+                    clauseBuilder.Append("TrailerTypes like ")
                         .Append(paramName)
                         .Append(Or);
                     statement?.TryBind(paramName, "%" + trailerTypes[i] + "%");
                 }
 
                 // Remove last " OR "
-                clause.Length -= Or.Length;
-                clause.Append(')');
+                clauseBuilder.Length -= Or.Length;
+                clauseBuilder.Append(')');
 
-                whereClauses.Add(clause.ToString());
+                whereClauses.Add(clauseBuilder.ToString());
+
+                clauseBuilder.Length = 0;
             }
 
             if (query.IsAiring.HasValue)
@@ -3757,23 +3761,35 @@ namespace Emby.Server.Implementations.Data
                 }
             }
 
-            if (query.PersonIds.Length > 0)
+            int personIdsLen = query.PersonIds.Length;
+            if (personIdsLen > 0)
             {
                 // TODO: Should this query with CleanName ?
 
-                var clauses = new List<string>();
-                var index = 0;
-                foreach (var personId in query.PersonIds)
+                clauseBuilder.Append('(');
+
+                Span<byte> idBytes = stackalloc byte[16];
+                for (int i = 0; i < personIdsLen; i++)
                 {
-                    var paramName = "@PersonId" + index;
+                    string paramName = "@PersonId" + i;
+                    clauseBuilder.Append("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=")
+                        .Append(paramName)
+                        .Append("))) OR ");
 
-                    clauses.Add("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=" + paramName + ")))");
-                    statement?.TryBind(paramName, personId.ToByteArray());
-                    index++;
+                    if (statement != null)
+                    {
+                        query.PersonIds[i].TryWriteBytes(idBytes);
+                        statement.TryBind(paramName, idBytes);
+                    }
                 }
 
-                var clause = "(" + string.Join(" OR ", clauses) + ")";
-                whereClauses.Add(clause);
+                // Remove last " OR "
+                clauseBuilder.Length -= Or.Length;
+                clauseBuilder.Append(')');
+
+                whereClauses.Add(clauseBuilder.ToString());
+
+                clauseBuilder.Length = 0;
             }
 
             if (!string.IsNullOrWhiteSpace(query.Person))
@@ -5149,7 +5165,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             CheckDisposed();
 
-            var itemIdBlob = itemId.ToByteArray();
+            Span<byte> itemIdBlob = stackalloc byte[16];
+            itemId.TryWriteBytes(itemIdBlob);
 
             // First delete
             deleteAncestorsStatement.Reset();
@@ -5165,17 +5182,15 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             for (var i = 0; i < ancestorIds.Count; i++)
             {
-                if (i > 0)
-                {
-                    insertText.Append(',');
-                }
-
                 insertText.AppendFormat(
                     CultureInfo.InvariantCulture,
-                    "(@ItemId, @AncestorId{0}, @AncestorIdText{0})",
+                    "(@ItemId, @AncestorId{0}, @AncestorIdText{0}),",
                     i.ToString(CultureInfo.InvariantCulture));
             }
 
+            // Remove last ,
+            insertText.Length--;
+
             using (var statement = PrepareStatement(db, insertText.ToString()))
             {
                 statement.TryBind("@ItemId", itemIdBlob);
@@ -5185,8 +5200,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                     var index = i.ToString(CultureInfo.InvariantCulture);
 
                     var ancestorId = ancestorIds[i];
+                    ancestorId.TryWriteBytes(itemIdBlob);
 
-                    statement.TryBind("@AncestorId" + index, ancestorId.ToByteArray());
+                    statement.TryBind("@AncestorId" + index, itemIdBlob);
                     statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture));
                 }
 
@@ -5466,7 +5482,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 connection.RunInTransaction(
                     db =>
                     {
-                        var statements = PrepareAll(db, statementTexts).ToList();
+                        var statements = PrepareAll(db, statementTexts);
 
                         if (!isReturningZeroItems)
                         {
@@ -5517,7 +5533,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                                         + GetJoinUserDataText(query)
                                         + whereText;
 
-                            using (var statement = statements[statements.Count - 1])
+                            using (var statement = statements[statements.Length - 1])
                             {
                                 statement.TryBind("@SelectType", returnType);
                                 if (EnableJoinUserData(query))

+ 2 - 2
Emby.Server.Implementations/Dto/DtoService.cs

@@ -197,7 +197,7 @@ namespace Emby.Server.Implementations.Dto
                 catch (Exception ex)
                 {
                     // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
-                    _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {itemName}", item.Name);
+                    _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {ItemName}", item.Name);
                 }
             }
 
@@ -1431,7 +1431,7 @@ namespace Emby.Server.Implementations.Dto
                 return null;
             }
 
-            return width / height;
+            return (double)width / height;
         }
     }
 }

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

@@ -32,10 +32,10 @@
     <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.8" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.8" />
     <PackageReference Include="Mono.Nat" Version="2.0.2" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />

+ 0 - 335
Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs

@@ -1,335 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpClientManager
-{
-    /// <summary>
-    /// Class HttpClientManager.
-    /// </summary>
-    public class HttpClientManager : IHttpClient
-    {
-        private readonly ILogger<HttpClientManager> _logger;
-        private readonly IApplicationPaths _appPaths;
-        private readonly IFileSystem _fileSystem;
-        private readonly IApplicationHost _appHost;
-
-        /// <summary>
-        /// Holds a dictionary of http clients by host.  Use GetHttpClient(host) to retrieve or create a client for web requests.
-        /// DON'T dispose it after use.
-        /// </summary>
-        /// <value>The HTTP clients.</value>
-        private readonly ConcurrentDictionary<string, HttpClient> _httpClients = new ConcurrentDictionary<string, HttpClient>();
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="HttpClientManager" /> class.
-        /// </summary>
-        public HttpClientManager(
-            IApplicationPaths appPaths,
-            ILogger<HttpClientManager> logger,
-            IFileSystem fileSystem,
-            IApplicationHost appHost)
-        {
-            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
-            _fileSystem = fileSystem;
-            _appPaths = appPaths ?? throw new ArgumentNullException(nameof(appPaths));
-            _appHost = appHost;
-        }
-
-        /// <summary>
-        /// Gets the correct http client for the given url.
-        /// </summary>
-        /// <param name="url">The url.</param>
-        /// <returns>HttpClient.</returns>
-        private HttpClient GetHttpClient(string url)
-        {
-            var key = GetHostFromUrl(url);
-
-            if (!_httpClients.TryGetValue(key, out var client))
-            {
-                client = new HttpClient()
-                {
-                    BaseAddress = new Uri(url)
-                };
-
-                _httpClients.TryAdd(key, client);
-            }
-
-            return client;
-        }
-
-        private HttpRequestMessage GetRequestMessage(HttpRequestOptions options, HttpMethod method)
-        {
-            string url = options.Url;
-            var uriAddress = new Uri(url);
-            string userInfo = uriAddress.UserInfo;
-            if (!string.IsNullOrWhiteSpace(userInfo))
-            {
-                _logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url);
-                url = url.Replace(userInfo + '@', string.Empty, StringComparison.Ordinal);
-            }
-
-            var request = new HttpRequestMessage(method, url);
-
-            foreach (var header in options.RequestHeaders)
-            {
-                request.Headers.TryAddWithoutValidation(header.Key, header.Value);
-            }
-
-            if (options.EnableDefaultUserAgent
-                && !request.Headers.TryGetValues(HeaderNames.UserAgent, out _))
-            {
-                request.Headers.Add(HeaderNames.UserAgent, _appHost.ApplicationUserAgent);
-            }
-
-            switch (options.DecompressionMethod)
-            {
-                case CompressionMethods.Deflate | CompressionMethods.Gzip:
-                    request.Headers.Add(HeaderNames.AcceptEncoding, new[] { "gzip", "deflate" });
-                    break;
-                case CompressionMethods.Deflate:
-                    request.Headers.Add(HeaderNames.AcceptEncoding, "deflate");
-                    break;
-                case CompressionMethods.Gzip:
-                    request.Headers.Add(HeaderNames.AcceptEncoding, "gzip");
-                    break;
-                default:
-                    break;
-            }
-
-            if (options.EnableKeepAlive)
-            {
-                request.Headers.Add(HeaderNames.Connection, "Keep-Alive");
-            }
-
-            // request.Headers.Add(HeaderNames.CacheControl, "no-cache");
-
-            /*
-            if (!string.IsNullOrWhiteSpace(userInfo))
-            {
-                var parts = userInfo.Split(':');
-                if (parts.Length == 2)
-                {
-                    request.Headers.Add(HeaderNames., GetCredential(url, parts[0], parts[1]);
-                }
-            }
-            */
-
-            return request;
-        }
-
-        /// <summary>
-        /// Gets the response internal.
-        /// </summary>
-        /// <param name="options">The options.</param>
-        /// <returns>Task{HttpResponseInfo}.</returns>
-        public Task<HttpResponseInfo> GetResponse(HttpRequestOptions options)
-            => SendAsync(options, HttpMethod.Get);
-
-        /// <summary>
-        /// Performs a GET request and returns the resulting stream.
-        /// </summary>
-        /// <param name="options">The options.</param>
-        /// <returns>Task{Stream}.</returns>
-        public async Task<Stream> Get(HttpRequestOptions options)
-        {
-            var response = await GetResponse(options).ConfigureAwait(false);
-            return response.Content;
-        }
-
-        /// <summary>
-        /// send as an asynchronous operation.
-        /// </summary>
-        /// <param name="options">The options.</param>
-        /// <param name="httpMethod">The HTTP method.</param>
-        /// <returns>Task{HttpResponseInfo}.</returns>
-        public Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod)
-            => SendAsync(options, new HttpMethod(httpMethod));
-
-        /// <summary>
-        /// send as an asynchronous operation.
-        /// </summary>
-        /// <param name="options">The options.</param>
-        /// <param name="httpMethod">The HTTP method.</param>
-        /// <returns>Task{HttpResponseInfo}.</returns>
-        public async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, HttpMethod httpMethod)
-        {
-            if (options.CacheMode == CacheMode.None)
-            {
-                return await SendAsyncInternal(options, httpMethod).ConfigureAwait(false);
-            }
-
-            var url = options.Url;
-            var urlHash = url.ToUpperInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
-
-            var responseCachePath = Path.Combine(_appPaths.CachePath, "httpclient", urlHash);
-
-            var response = GetCachedResponse(responseCachePath, options.CacheLength, url);
-            if (response != null)
-            {
-                return response;
-            }
-
-            response = await SendAsyncInternal(options, httpMethod).ConfigureAwait(false);
-
-            if (response.StatusCode == HttpStatusCode.OK)
-            {
-                await CacheResponse(response, responseCachePath).ConfigureAwait(false);
-            }
-
-            return response;
-        }
-
-        private HttpResponseInfo GetCachedResponse(string responseCachePath, TimeSpan cacheLength, string url)
-        {
-            if (File.Exists(responseCachePath)
-                && _fileSystem.GetLastWriteTimeUtc(responseCachePath).Add(cacheLength) > DateTime.UtcNow)
-            {
-                var stream = new FileStream(responseCachePath, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true);
-
-                return new HttpResponseInfo
-                {
-                    ResponseUrl = url,
-                    Content = stream,
-                    StatusCode = HttpStatusCode.OK,
-                    ContentLength = stream.Length
-                };
-            }
-
-            return null;
-        }
-
-        private async Task CacheResponse(HttpResponseInfo response, string responseCachePath)
-        {
-            Directory.CreateDirectory(Path.GetDirectoryName(responseCachePath));
-
-            using (var fileStream = new FileStream(
-                responseCachePath,
-                FileMode.Create,
-                FileAccess.Write,
-                FileShare.None,
-                IODefaults.FileStreamBufferSize,
-                true))
-            {
-                await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
-
-                response.Content.Position = 0;
-            }
-        }
-
-        private async Task<HttpResponseInfo> SendAsyncInternal(HttpRequestOptions options, HttpMethod httpMethod)
-        {
-            ValidateParams(options);
-
-            options.CancellationToken.ThrowIfCancellationRequested();
-
-            var client = GetHttpClient(options.Url);
-
-            var httpWebRequest = GetRequestMessage(options, httpMethod);
-
-            if (!string.IsNullOrEmpty(options.RequestContent)
-                || httpMethod == HttpMethod.Post)
-            {
-                if (options.RequestContent != null)
-                {
-                    httpWebRequest.Content = new StringContent(
-                        options.RequestContent,
-                        null,
-                        options.RequestContentType);
-                }
-                else
-                {
-                    httpWebRequest.Content = new ByteArrayContent(Array.Empty<byte>());
-                }
-            }
-
-            options.CancellationToken.ThrowIfCancellationRequested();
-
-            var response = await client.SendAsync(
-                httpWebRequest,
-                options.BufferContent || options.CacheMode == CacheMode.Unconditional ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead,
-                options.CancellationToken).ConfigureAwait(false);
-
-            await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
-
-            options.CancellationToken.ThrowIfCancellationRequested();
-
-            var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
-            return new HttpResponseInfo(response.Headers, response.Content.Headers)
-            {
-                Content = stream,
-                StatusCode = response.StatusCode,
-                ContentType = response.Content.Headers.ContentType?.MediaType,
-                ContentLength = response.Content.Headers.ContentLength,
-                ResponseUrl = response.Content.Headers.ContentLocation?.ToString()
-            };
-        }
-
-        /// <inheritdoc />
-        public Task<HttpResponseInfo> Post(HttpRequestOptions options)
-            => SendAsync(options, HttpMethod.Post);
-
-        private void ValidateParams(HttpRequestOptions options)
-        {
-            if (string.IsNullOrEmpty(options.Url))
-            {
-                throw new ArgumentNullException(nameof(options));
-            }
-        }
-
-        /// <summary>
-        /// Gets the host from URL.
-        /// </summary>
-        /// <param name="url">The URL.</param>
-        /// <returns>System.String.</returns>
-        private static string GetHostFromUrl(string url)
-        {
-            var index = url.IndexOf("://", StringComparison.OrdinalIgnoreCase);
-
-            if (index != -1)
-            {
-                url = url.Substring(index + 3);
-                var host = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
-
-                if (!string.IsNullOrWhiteSpace(host))
-                {
-                    return host;
-                }
-            }
-
-            return url;
-        }
-
-        private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options)
-        {
-            if (response.IsSuccessStatusCode)
-            {
-                return;
-            }
-
-            if (options.LogErrorResponseBody)
-            {
-                string msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
-                _logger.LogError("HTTP request failed with message: {Message}", msg);
-            }
-
-            throw new HttpException(response.ReasonPhrase)
-            {
-                StatusCode = response.StatusCode
-            };
-        }
-    }
-}

+ 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 Jellyfin.Data.Events;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.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.GetNormalizedRemoteIp(), 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();
-            }
-        }
-    }
-}

+ 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());
+        }
+    }
+}

+ 4 - 3
Emby.Server.Implementations/IO/FileRefresher.cs

@@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.IO
                     continue;
                 }
 
-                _logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path);
+                _logger.LogInformation("{Name} ({Path}) will be refreshed.", item.Name, item.Path);
 
                 try
                 {
@@ -160,11 +160,11 @@ namespace Emby.Server.Implementations.IO
                     // For now swallow and log.
                     // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
                     // Should we remove it from it's parent?
-                    _logger.LogError(ex, "Error refreshing {name}", item.Name);
+                    _logger.LogError(ex, "Error refreshing {Name}", item.Name);
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error refreshing {name}", item.Name);
+                    _logger.LogError(ex, "Error refreshing {Name}", item.Name);
                 }
             }
         }
@@ -214,6 +214,7 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
             _disposed = true;

+ 1 - 1
Emby.Server.Implementations/IO/LibraryMonitor.cs

@@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.IO
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error in ReportFileSystemChanged for {path}", path);
+                    _logger.LogError(ex, "Error in ReportFileSystemChanged for {Path}", path);
                 }
             }
         }

+ 0 - 32
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -398,30 +398,6 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
-        public virtual void SetReadOnly(string path, bool isReadOnly)
-        {
-            if (OperatingSystem.Id != OperatingSystemId.Windows)
-            {
-                return;
-            }
-
-            var info = GetExtendedFileSystemInfo(path);
-
-            if (info.Exists && info.IsReadOnly != isReadOnly)
-            {
-                if (isReadOnly)
-                {
-                    File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.ReadOnly);
-                }
-                else
-                {
-                    var attributes = File.GetAttributes(path);
-                    attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
-                    File.SetAttributes(path, attributes);
-                }
-            }
-        }
-
         public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly)
         {
             if (OperatingSystem.Id != OperatingSystemId.Windows)
@@ -707,14 +683,6 @@ namespace Emby.Server.Implementations.IO
             return Directory.EnumerateFileSystemEntries(path, "*", searchOption);
         }
 
-        public virtual void SetExecutable(string path)
-        {
-            if (OperatingSystem.Id == OperatingSystemId.Darwin)
-            {
-                RunProcess("chmod", "+x \"" + path + "\"", Path.GetDirectoryName(path));
-            }
-        }
-
         private static void RunProcess(string path, string args, string workingDirectory)
         {
             using (var process = Process.Start(new ProcessStartInfo

+ 1 - 31
Emby.Server.Implementations/IO/StreamHelper.cs

@@ -11,8 +11,6 @@ namespace Emby.Server.Implementations.IO
 {
     public class StreamHelper : IStreamHelper
     {
-        private const int StreamCopyToBufferSize = 81920;
-
         public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken)
         {
             byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
@@ -83,37 +81,9 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
-        public async Task<int> CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken)
-        {
-            byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
-            try
-            {
-                int totalBytesRead = 0;
-
-                int bytesRead;
-                while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
-                {
-                    var bytesToWrite = bytesRead;
-
-                    if (bytesToWrite > 0)
-                    {
-                        await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
-
-                        totalBytesRead += bytesRead;
-                    }
-                }
-
-                return totalBytesRead;
-            }
-            finally
-            {
-                ArrayPool<byte>.Shared.Return(buffer);
-            }
-        }
-
         public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
         {
-            byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
+            byte[] buffer = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
             try
             {
                 int bytesRead;

+ 14 - 19
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -513,10 +513,11 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentNullException(nameof(type));
             }
 
-            if (key.StartsWith(_configurationManager.ApplicationPaths.ProgramDataPath, StringComparison.Ordinal))
+            string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath;
+            if (key.StartsWith(programDataPath, StringComparison.Ordinal))
             {
                 // Try to normalize paths located underneath program-data in an attempt to make them more portable
-                key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
+                key = key.Substring(programDataPath.Length)
                     .TrimStart('/', '\\')
                     .Replace('/', '\\');
             }
@@ -871,17 +872,17 @@ namespace Emby.Server.Implementations.Library
 
         public Guid GetStudioId(string name)
         {
-            return GetItemByNameId<Studio>(Studio.GetPath, name);
+            return GetItemByNameId<Studio>(Studio.GetPath(name));
         }
 
         public Guid GetGenreId(string name)
         {
-            return GetItemByNameId<Genre>(Genre.GetPath, name);
+            return GetItemByNameId<Genre>(Genre.GetPath(name));
         }
 
         public Guid GetMusicGenreId(string name)
         {
-            return GetItemByNameId<MusicGenre>(MusicGenre.GetPath, name);
+            return GetItemByNameId<MusicGenre>(MusicGenre.GetPath(name));
         }
 
         /// <summary>
@@ -943,7 +944,7 @@ namespace Emby.Server.Implementations.Library
             {
                 var existing = GetItemList(new InternalItemsQuery
                 {
-                    IncludeItemTypes = new[] { typeof(T).Name },
+                    IncludeItemTypes = new[] { nameof(MusicArtist) },
                     Name = name,
                     DtoOptions = options
                 }).Cast<MusicArtist>()
@@ -957,13 +958,11 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            var id = GetItemByNameId<T>(getPathFn, name);
-
+            var path = getPathFn(name);
+            var id = GetItemByNameId<T>(path);
             var item = GetItemById(id) as T;
-
             if (item == null)
             {
-                var path = getPathFn(name);
                 item = new T
                 {
                     Name = name,
@@ -979,10 +978,9 @@ namespace Emby.Server.Implementations.Library
             return item;
         }
 
-        private Guid GetItemByNameId<T>(Func<string, string> getPathFn, string name)
+        private Guid GetItemByNameId<T>(string path)
               where T : BaseItem, new()
         {
-            var path = getPathFn(name);
             var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
             return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
         }
@@ -1805,21 +1803,18 @@ namespace Emby.Server.Implementations.Library
         /// <param name="items">The items.</param>
         /// <param name="parent">The parent item.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        public void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken)
+        public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem parent, CancellationToken cancellationToken)
         {
-            // Don't iterate multiple times
-            var itemsList = items.ToList();
-
-            _itemRepository.SaveItems(itemsList, cancellationToken);
+            _itemRepository.SaveItems(items, cancellationToken);
 
-            foreach (var item in itemsList)
+            foreach (var item in items)
             {
                 RegisterItem(item);
             }
 
             if (ItemAdded != null)
             {
-                foreach (var item in itemsList)
+                foreach (var item in items)
                 {
                     // With the live tv guide this just creates too much noise
                     if (item.SourceType != SourceType.Library)

+ 21 - 30
Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs

@@ -16,13 +16,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
     public class DirectRecorder : IRecorder
     {
         private readonly ILogger _logger;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IStreamHelper _streamHelper;
 
-        public DirectRecorder(ILogger logger, IHttpClient httpClient, IStreamHelper streamHelper)
+        public DirectRecorder(ILogger logger, IHttpClientFactory httpClientFactory, IStreamHelper streamHelper)
         {
             _logger = logger;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _streamHelper = streamHelper;
         }
 
@@ -52,10 +52,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 _logger.LogInformation("Copying recording stream to file {0}", targetFile);
 
                 // The media source is infinite so we need to handle stopping ourselves
-                var durationToken = new CancellationTokenSource(duration);
-                cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+                using var durationToken = new CancellationTokenSource(duration);
+                using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
 
-                await directStreamProvider.CopyToAsync(output, cancellationToken).ConfigureAwait(false);
+                await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false);
             }
 
             _logger.LogInformation("Recording completed to file {0}", targetFile);
@@ -63,37 +63,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
         private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
         {
-            var httpRequestOptions = new HttpRequestOptions
-            {
-                Url = mediaSource.Path,
-                BufferContent = false,
-
-                // Some remote urls will expect a user agent to be supplied
-                UserAgent = "Emby/3.0",
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                .GetAsync(mediaSource.Path, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
 
-                // Shouldn't matter but may cause issues
-                DecompressionMethod = CompressionMethods.None
-            };
+            _logger.LogInformation("Opened recording stream from tuner provider");
 
-            using (var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false))
-            {
-                _logger.LogInformation("Opened recording stream from tuner provider");
+            Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
 
-                Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
+            await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read);
 
-                using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
-                {
-                    onStarted();
+            onStarted();
 
-                    _logger.LogInformation("Copying recording stream to file {0}", targetFile);
+            _logger.LogInformation("Copying recording stream to file {0}", targetFile);
 
-                    // The media source if infinite so we need to handle stopping ourselves
-                    var durationToken = new CancellationTokenSource(duration);
-                    cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+            // The media source if infinite so we need to handle stopping ourselves
+            var durationToken = new CancellationTokenSource(duration);
+            cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
 
-                    await _streamHelper.CopyUntilCancelled(response.Content, output, 81920, cancellationToken).ConfigureAwait(false);
-                }
-            }
+            await _streamHelper.CopyUntilCancelled(
+                await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+                output,
+                IODefaults.CopyToBufferSize,
+                cancellationToken).ConfigureAwait(false);
 
             _logger.LogInformation("Recording completed to file {0}", targetFile);
         }

+ 6 - 25
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -7,6 +7,7 @@ using System.Diagnostics;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Net.Http;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
@@ -48,7 +49,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
         private readonly IServerApplicationHost _appHost;
         private readonly ILogger<EmbyTV> _logger;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerConfigurationManager _config;
         private readonly IJsonSerializer _jsonSerializer;
 
@@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             IMediaSourceManager mediaSourceManager,
             ILogger<EmbyTV> logger,
             IJsonSerializer jsonSerializer,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IServerConfigurationManager config,
             ILiveTvManager liveTvManager,
             IFileSystem fileSystem,
@@ -94,7 +95,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
             _appHost = appHost;
             _logger = logger;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _config = config;
             _fileSystem = fileSystem;
             _libraryManager = libraryManager;
@@ -604,11 +605,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             return Task.CompletedTask;
         }
 
-        public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
-        {
-            return Task.CompletedTask;
-        }
-
         public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
         {
             throw new NotImplementedException();
@@ -808,11 +804,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             return null;
         }
 
-        public IEnumerable<ActiveRecordingInfo> GetAllActiveRecordings()
-        {
-            return _activeRecordings.Values.Where(i => i.Timer.Status == RecordingStatus.InProgress && !i.CancellationTokenSource.IsCancellationRequested);
-        }
-
         public ActiveRecordingInfo GetActiveRecordingInfo(string path)
         {
             if (string.IsNullOrWhiteSpace(path))
@@ -1015,16 +1006,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             throw new Exception("Tuner not found.");
         }
 
-        private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing)
-        {
-            var json = _jsonSerializer.SerializeToString(mediaSource);
-            mediaSource = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
-
-            mediaSource.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + "_" + mediaSource.Id;
-
-            return mediaSource;
-        }
-
         public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
         {
             if (string.IsNullOrWhiteSpace(channelId))
@@ -1654,10 +1635,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
             if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
             {
-                return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config);
+                return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer);
             }
 
-            return new DirectRecorder(_logger, _httpClient, _streamHelper);
+            return new DirectRecorder(_logger, _httpClientFactory, _streamHelper);
         }
 
         private void OnSuccessfulRecording(TimerInfo timer, string path)

+ 10 - 33
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -8,12 +8,9 @@ using System.IO;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
@@ -26,26 +23,24 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         private readonly ILogger _logger;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IServerApplicationPaths _appPaths;
+        private readonly IJsonSerializer _json;
+        private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
+
         private bool _hasExited;
         private Stream _logFileStream;
         private string _targetPath;
         private Process _process;
-        private readonly IJsonSerializer _json;
-        private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
-        private readonly IServerConfigurationManager _config;
 
         public EncodedRecorder(
             ILogger logger,
             IMediaEncoder mediaEncoder,
             IServerApplicationPaths appPaths,
-            IJsonSerializer json,
-            IServerConfigurationManager config)
+            IJsonSerializer json)
         {
             _logger = logger;
             _mediaEncoder = mediaEncoder;
             _appPaths = appPaths;
             _json = json;
-            _config = config;
         }
 
         private static bool CopySubtitles => false;
@@ -58,19 +53,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         public async Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
         {
             // The media source is infinite so we need to handle stopping ourselves
-            var durationToken = new CancellationTokenSource(duration);
-            cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+            using var durationToken = new CancellationTokenSource(duration);
+            using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
 
-            await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationToken).ConfigureAwait(false);
+            await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationTokenSource.Token).ConfigureAwait(false);
 
             _logger.LogInformation("Recording completed to file {0}", targetFile);
         }
 
-        private EncodingOptions GetEncodingOptions()
-        {
-            return _config.GetConfiguration<EncodingOptions>("encoding");
-        }
-
         private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
         {
             _targetPath = targetFile;
@@ -108,7 +98,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 StartInfo = processStartInfo,
                 EnableRaisingEvents = true
             };
-            _process.Exited += (sender, args) => OnFfMpegProcessExited(_process, inputFile);
+            _process.Exited += (sender, args) => OnFfMpegProcessExited(_process);
 
             _process.Start();
 
@@ -221,20 +211,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         }
 
         protected string GetOutputSizeParam()
-        {
-            var filters = new List<string>();
-
-            filters.Add("yadif=0:-1:0");
-
-            var output = string.Empty;
-
-            if (filters.Count > 0)
-            {
-                output += string.Format(CultureInfo.InvariantCulture, " -vf \"{0}\"", string.Join(",", filters.ToArray()));
-            }
-
-            return output;
-        }
+            => "-vf \"yadif=0:-1:0\"";
 
         private void Stop()
         {
@@ -291,7 +268,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         /// <summary>
         /// Processes the exited.
         /// </summary>
-        private void OnFfMpegProcessExited(Process process, string inputFile)
+        private void OnFfMpegProcessExited(Process process)
         {
             using (process)
             {

+ 148 - 243
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -8,6 +8,8 @@ using System.IO;
 using System.Linq;
 using System.Net;
 using System.Net.Http;
+using System.Net.Mime;
+using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
@@ -24,23 +26,23 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 {
     public class SchedulesDirect : IListingsProvider
     {
+        private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
+
         private readonly ILogger<SchedulesDirect> _logger;
         private readonly IJsonSerializer _jsonSerializer;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
         private readonly IApplicationHost _appHost;
 
-        private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
-
         public SchedulesDirect(
             ILogger<SchedulesDirect> logger,
             IJsonSerializer jsonSerializer,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IApplicationHost appHost)
         {
             _logger = logger;
             _jsonSerializer = jsonSerializer;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
         }
 
@@ -61,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             while (start <= end)
             {
-                dates.Add(start.ToString("yyyy-MM-dd"));
+                dates.Add(start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
                 start = start.AddDays(1);
             }
 
@@ -102,95 +104,78 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             var requestString = _jsonSerializer.SerializeToString(requestList);
             _logger.LogDebug("Request string for schedules is: {RequestString}", requestString);
 
-            var httpOptions = new HttpRequestOptions()
-            {
-                Url = ApiUrl + "/schedules",
-                UserAgent = UserAgent,
-                CancellationToken = cancellationToken,
-                LogErrorResponseBody = true,
-                RequestContent = requestString
-            };
+            using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules");
+            options.Content = new StringContent(requestString, Encoding.UTF8, MediaTypeNames.Application.Json);
+            options.Headers.TryAddWithoutValidation("token", token);
+            using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
+            await using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(responseStream).ConfigureAwait(false);
+            _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
 
-            httpOptions.RequestHeaders["token"] = token;
+            using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
+            programRequestOptions.Headers.TryAddWithoutValidation("token", token);
 
-            using (var response = await Post(httpOptions, true, info).ConfigureAwait(false))
-            {
-                var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(response.Content).ConfigureAwait(false);
-                _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
+            var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct();
+            programRequestOptions.Content = new StringContent("[\"" + string.Join("\", \"", programsID) + "\"]", Encoding.UTF8, MediaTypeNames.Application.Json);
 
-                httpOptions = new HttpRequestOptions()
-                {
-                    Url = ApiUrl + "/programs",
-                    UserAgent = UserAgent,
-                    CancellationToken = cancellationToken,
-                    LogErrorResponseBody = true
-                };
-
-                httpOptions.RequestHeaders["token"] = token;
+            using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
+            await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream).ConfigureAwait(false);
+            var programDict = programDetails.ToDictionary(p => p.programID, y => y);
 
-                var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct();
-                httpOptions.RequestContent = "[\"" + string.Join("\", \"", programsID) + "\"]";
+            var programIdsWithImages =
+                programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
+                    .ToList();
 
-                using (var innerResponse = await Post(httpOptions, true, info).ConfigureAwait(false))
-                {
-                    var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponse.Content).ConfigureAwait(false);
-                    var programDict = programDetails.ToDictionary(p => p.programID, y => y);
-
-                    var programIdsWithImages =
-                        programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
-                        .ToList();
+            var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
 
-                    var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
+            var programsInfo = new List<ProgramInfo>();
+            foreach (ScheduleDirect.Program schedule in dailySchedules.SelectMany(d => d.programs))
+            {
+                // _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
+                //              " which corresponds to channel " + channelNumber + " and program id " +
+                //              schedule.programID + " which says it has images? " +
+                //              programDict[schedule.programID].hasImageArtwork);
 
-                    var programsInfo = new List<ProgramInfo>();
-                    foreach (ScheduleDirect.Program schedule in dailySchedules.SelectMany(d => d.programs))
+                if (images != null)
+                {
+                    var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10));
+                    if (imageIndex > -1)
                     {
-                        // _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
-                        //              " which corresponds to channel " + channelNumber + " and program id " +
-                        //              schedule.programID + " which says it has images? " +
-                        //              programDict[schedule.programID].hasImageArtwork);
-
-                        if (images != null)
-                        {
-                            var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10));
-                            if (imageIndex > -1)
-                            {
-                                var programEntry = programDict[schedule.programID];
-
-                                var allImages = images[imageIndex].data ?? new List<ScheduleDirect.ImageData>();
-                                var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase));
-                                var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase));
-
-                                const double DesiredAspect = 2.0 / 3;
+                        var programEntry = programDict[schedule.programID];
 
-                                programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
-                                    GetProgramImage(ApiUrl, allImages, true, DesiredAspect);
+                        var allImages = images[imageIndex].data ?? new List<ScheduleDirect.ImageData>();
+                        var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase));
+                        var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase));
 
-                                const double WideAspect = 16.0 / 9;
+                        const double DesiredAspect = 2.0 / 3;
 
-                                programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
+                        programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
+                                                    GetProgramImage(ApiUrl, allImages, true, DesiredAspect);
 
-                                // Don't supply the same image twice
-                                if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal))
-                                {
-                                    programEntry.thumbImage = null;
-                                }
+                        const double WideAspect = 16.0 / 9;
 
-                                programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
+                        programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
 
-                                // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
-                                //    GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
-                                //    GetProgramImage(ApiUrl, data, "Banner-LO", false) ??
-                                //    GetProgramImage(ApiUrl, data, "Banner-LOT", false);
-                            }
+                        // Don't supply the same image twice
+                        if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal))
+                        {
+                            programEntry.thumbImage = null;
                         }
 
-                        programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID]));
-                    }
+                        programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
 
-                    return programsInfo;
+                        // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
+                        //    GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
+                        //    GetProgramImage(ApiUrl, data, "Banner-LO", false) ??
+                        //    GetProgramImage(ApiUrl, data, "Banner-LOT", false);
+                    }
                 }
+
+                programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID]));
             }
+
+            return programsInfo;
         }
 
         private static int GetSizeOrder(ScheduleDirect.ImageData image)
@@ -367,13 +352,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             if (!string.IsNullOrWhiteSpace(details.originalAirDate))
             {
-                info.OriginalAirDate = DateTime.Parse(details.originalAirDate);
+                info.OriginalAirDate = DateTime.Parse(details.originalAirDate, CultureInfo.InvariantCulture);
                 info.ProductionYear = info.OriginalAirDate.Value.Year;
             }
 
             if (details.movie != null)
             {
-                if (!string.IsNullOrEmpty(details.movie.year) && int.TryParse(details.movie.year, out int year))
+                if (!string.IsNullOrEmpty(details.movie.year)
+                    && int.TryParse(details.movie.year, out int year))
                 {
                     info.ProductionYear = year;
                 }
@@ -482,22 +468,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             imageIdString = imageIdString.TrimEnd(',') + "]";
 
-            var httpOptions = new HttpRequestOptions()
+            using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs")
             {
-                Url = ApiUrl + "/metadata/programs",
-                UserAgent = UserAgent,
-                CancellationToken = cancellationToken,
-                RequestContent = imageIdString,
-                LogErrorResponseBody = true,
+                Content = new StringContent(imageIdString, Encoding.UTF8, MediaTypeNames.Application.Json)
             };
 
             try
             {
-                using (var innerResponse2 = await Post(httpOptions, true, info).ConfigureAwait(false))
-                {
-                    return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>(
-                        innerResponse2.Content).ConfigureAwait(false);
-                }
+                using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
+                await using var response = await innerResponse2.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>(
+                    response).ConfigureAwait(false);
             }
             catch (Exception ex)
             {
@@ -518,41 +499,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 return lineups;
             }
 
-            var options = new HttpRequestOptions()
-            {
-                Url = ApiUrl + "/headends?country=" + country + "&postalcode=" + location,
-                UserAgent = UserAgent,
-                CancellationToken = cancellationToken,
-                LogErrorResponseBody = true
-            };
-
-            options.RequestHeaders["token"] = token;
+            using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/headends?country=" + country + "&postalcode=" + location);
+            options.Headers.TryAddWithoutValidation("token", token);
 
             try
             {
-                using (var httpResponse = await Get(options, false, info).ConfigureAwait(false))
-                using (Stream responce = httpResponse.Content)
-                {
-                    var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(responce).ConfigureAwait(false);
+                using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
+                await using var response = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+
+                var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(response).ConfigureAwait(false);
 
-                    if (root != null)
+                if (root != null)
+                {
+                    foreach (ScheduleDirect.Headends headend in root)
                     {
-                        foreach (ScheduleDirect.Headends headend in root)
+                        foreach (ScheduleDirect.Lineup lineup in headend.lineups)
                         {
-                            foreach (ScheduleDirect.Lineup lineup in headend.lineups)
+                            lineups.Add(new NameIdPair
                             {
-                                lineups.Add(new NameIdPair
-                                {
-                                    Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name,
-                                    Id = lineup.uri.Substring(18)
-                                });
-                            }
+                                Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name,
+                                Id = lineup.uri.Substring(18)
+                            });
                         }
                     }
-                    else
-                    {
-                        _logger.LogInformation("No lineups available");
-                    }
+                }
+                else
+                {
+                    _logger.LogInformation("No lineups available");
                 }
             }
             catch (Exception ex)
@@ -587,7 +560,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 return null;
             }
 
-            NameValuePair savedToken = null;
+            NameValuePair savedToken;
             if (!_tokens.TryGetValue(username, out savedToken))
             {
                 savedToken = new NameValuePair();
@@ -633,16 +606,16 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             }
         }
 
-        private async Task<HttpResponseInfo> Post(HttpRequestOptions options,
+        private async Task<HttpResponseMessage> Send(
+            HttpRequestMessage options,
             bool enableRetry,
-            ListingsProviderInfo providerInfo)
+            ListingsProviderInfo providerInfo,
+            CancellationToken cancellationToken,
+            HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
         {
-            // Schedules direct requires that the client support compression and will return a 400 response without it
-            options.DecompressionMethod = CompressionMethods.Deflate;
-
             try
             {
-                return await _httpClient.Post(options).ConfigureAwait(false);
+                return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
             }
             catch (HttpException ex)
             {
@@ -659,65 +632,28 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 }
             }
 
-            options.RequestHeaders["token"] = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false);
-            return await Post(options, false, providerInfo).ConfigureAwait(false);
+            options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false));
+            return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false);
         }
 
-        private async Task<HttpResponseInfo> Get(HttpRequestOptions options,
-            bool enableRetry,
-            ListingsProviderInfo providerInfo)
-        {
-            // Schedules direct requires that the client support compression and will return a 400 response without it
-            options.DecompressionMethod = CompressionMethods.Deflate;
-
-            try
-            {
-                return await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
-            }
-            catch (HttpException ex)
-            {
-                _tokens.Clear();
-
-                if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500)
-                {
-                    enableRetry = false;
-                }
-
-                if (!enableRetry)
-                {
-                    throw;
-                }
-            }
-
-            options.RequestHeaders["token"] = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false);
-            return await Get(options, false, providerInfo).ConfigureAwait(false);
-        }
-
-        private async Task<string> GetTokenInternal(string username, string password,
+        private async Task<string> GetTokenInternal(
+            string username,
+            string password,
             CancellationToken cancellationToken)
         {
-            var httpOptions = new HttpRequestOptions()
-            {
-                Url = ApiUrl + "/token",
-                UserAgent = UserAgent,
-                RequestContent = "{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}",
-                CancellationToken = cancellationToken,
-                LogErrorResponseBody = true
-            };
-            // _logger.LogInformation("Obtaining token from Schedules Direct from addres: " + httpOptions.Url + " with body " +
-            // httpOptions.RequestContent);
+            using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
+            options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
 
-            using (var response = await Post(httpOptions, false, null).ConfigureAwait(false))
+            using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
+            if (root.message == "OK")
             {
-                var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(response.Content).ConfigureAwait(false);
-                if (root.message == "OK")
-                {
-                    _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token);
-                    return root.token;
-                }
-
-                throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message);
+                _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token);
+                return root.token;
             }
+
+            throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message);
         }
 
         private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken)
@@ -736,20 +672,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             _logger.LogInformation("Adding new LineUp ");
 
-            var httpOptions = new HttpRequestOptions()
-            {
-                Url = ApiUrl + "/lineups/" + info.ListingsId,
-                UserAgent = UserAgent,
-                CancellationToken = cancellationToken,
-                LogErrorResponseBody = true,
-                BufferContent = false
-            };
-
-            httpOptions.RequestHeaders["token"] = token;
-
-            using (await _httpClient.SendAsync(httpOptions, HttpMethod.Put).ConfigureAwait(false))
-            {
-            }
+            using var options = new HttpRequestMessage(HttpMethod.Put, ApiUrl + "/lineups/" + info.ListingsId);
+            options.Headers.TryAddWithoutValidation("token", token);
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
         }
 
         private async Task<bool> HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken)
@@ -768,25 +693,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             _logger.LogInformation("Headends on account ");
 
-            var options = new HttpRequestOptions()
-            {
-                Url = ApiUrl + "/lineups",
-                UserAgent = UserAgent,
-                CancellationToken = cancellationToken,
-                LogErrorResponseBody = true
-            };
-
-            options.RequestHeaders["token"] = token;
+            using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups");
+            options.Headers.TryAddWithoutValidation("token", token);
 
             try
             {
-                using (var httpResponse = await Get(options, false, null).ConfigureAwait(false))
-                using (var response = httpResponse.Content)
-                {
-                    var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(response).ConfigureAwait(false);
+                using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
+                await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                using var response = httpResponse.Content;
+                var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
 
-                    return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
-                }
+                return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
             }
             catch (HttpException ex)
             {
@@ -851,55 +768,43 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 throw new Exception("token required");
             }
 
-            var httpOptions = new HttpRequestOptions()
-            {
-                Url = ApiUrl + "/lineups/" + listingsId,
-                UserAgent = UserAgent,
-                CancellationToken = cancellationToken,
-                LogErrorResponseBody = true,
-            };
-
-            httpOptions.RequestHeaders["token"] = token;
+            using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId);
+            options.Headers.TryAddWithoutValidation("token", token);
 
             var list = new List<ChannelInfo>();
 
-            using (var httpResponse = await Get(httpOptions, true, info).ConfigureAwait(false))
-            using (var response = httpResponse.Content)
-            {
-                var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(response).ConfigureAwait(false);
-                _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
-                _logger.LogInformation("Mapping Stations to Channel");
-
-                var allStations = root.stations ?? Enumerable.Empty<ScheduleDirect.Station>();
+            using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
+            await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(stream).ConfigureAwait(false);
+            _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
+            _logger.LogInformation("Mapping Stations to Channel");
 
-                foreach (ScheduleDirect.Map map in root.map)
-                {
-                    var channelNumber = GetChannelNumber(map);
+            var allStations = root.stations ?? Enumerable.Empty<ScheduleDirect.Station>();
 
-                    var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase));
-                    if (station == null)
-                    {
-                        station = new ScheduleDirect.Station
-                        {
-                            stationID = map.stationID
-                        };
-                    }
+            foreach (ScheduleDirect.Map map in root.map)
+            {
+                var channelNumber = GetChannelNumber(map);
 
-                    var channelInfo = new ChannelInfo
-                    {
-                        Id = station.stationID,
-                        CallSign = station.callsign,
-                        Number = channelNumber,
-                        Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name
-                    };
+                var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase));
+                if (station == null)
+                {
+                    station = new ScheduleDirect.Station { stationID = map.stationID };
+                }
 
-                    if (station.logo != null)
-                    {
-                        channelInfo.ImageUrl = station.logo.URL;
-                    }
+                var channelInfo = new ChannelInfo
+                {
+                    Id = station.stationID,
+                    CallSign = station.callsign,
+                    Number = channelNumber,
+                    Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name
+                };
 
-                    list.Add(channelInfo);
+                if (station.logo != null)
+                {
+                    channelInfo.ImageUrl = station.logo.URL;
                 }
+
+                list.Add(channelInfo);
             }
 
             return list;

+ 7 - 24
Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs

@@ -25,20 +25,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
     public class XmlTvListingsProvider : IListingsProvider
     {
         private readonly IServerConfigurationManager _config;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILogger<XmlTvListingsProvider> _logger;
         private readonly IFileSystem _fileSystem;
         private readonly IZipClient _zipClient;
 
         public XmlTvListingsProvider(
             IServerConfigurationManager config,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILogger<XmlTvListingsProvider> logger,
             IFileSystem fileSystem,
             IZipClient zipClient)
         {
             _config = config;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _logger = logger;
             _fileSystem = fileSystem;
             _zipClient = zipClient;
@@ -78,28 +78,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
 
-            using (var res = await _httpClient.SendAsync(
-                new HttpRequestOptions
-                {
-                    CancellationToken = cancellationToken,
-                    Url = path,
-                    DecompressionMethod = CompressionMethods.Gzip,
-                },
-                HttpMethod.Get).ConfigureAwait(false))
-            using (var stream = res.Content)
-            using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew))
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew))
             {
-                if (res.ContentHeaders.ContentEncoding.Contains("gzip"))
-                {
-                    using (var gzStream = new GZipStream(stream, CompressionMode.Decompress))
-                    {
-                        await gzStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
-                    }
-                }
-                else
-                {
-                    await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
-                }
+                await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
             }
 
             return UnzipIfNeeded(path, cacheFile);

+ 40 - 58
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs

@@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 {
     public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerApplicationHost _appHost;
         private readonly ISocketFactory _socketFactory;
         private readonly INetworkManager _networkManager;
@@ -43,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             IServerConfigurationManager config,
             ILogger<HdHomerunHost> logger,
             IFileSystem fileSystem,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IServerApplicationHost appHost,
             ISocketFactory socketFactory,
             INetworkManager networkManager,
@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             IMemoryCache memoryCache)
             : base(config, logger, fileSystem, memoryCache)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
             _socketFactory = socketFactory;
             _networkManager = networkManager;
@@ -71,15 +71,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         {
             var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
 
-            var options = new HttpRequestOptions
-            {
-                Url = model.LineupURL,
-                CancellationToken = cancellationToken,
-                BufferContent = false
-            };
-
-            using var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
-            await using var stream = response.Content;
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
             var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken)
                 .ConfigureAwait(false) ?? new List<Channels>();
 
@@ -133,14 +126,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
             try
             {
-                using var response = await _httpClient.SendAsync(
-                    new HttpRequestOptions
-                {
-                    Url = string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)),
-                    CancellationToken = cancellationToken,
-                    BufferContent = false
-                }, HttpMethod.Get).ConfigureAwait(false);
-                await using var stream = response.Content;
+                using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                    .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
+                    .ConfigureAwait(false);
+                await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
                 var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken)
                     .ConfigureAwait(false);
 
@@ -183,48 +172,41 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         {
             var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
 
-            using (var response = await _httpClient.SendAsync(
-                new HttpRequestOptions()
-                {
-                    Url = string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)),
-                    CancellationToken = cancellationToken,
-                    BufferContent = false
-                },
-                HttpMethod.Get).ConfigureAwait(false))
-            using (var stream = response.Content)
-            using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
-            {
-                var tuners = new List<LiveTvTunerInfo>();
-                while (!sr.EndOfStream)
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
+                .ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
+            var tuners = new List<LiveTvTunerInfo>();
+            while (!sr.EndOfStream)
+            {
+                string line = StripXML(sr.ReadLine());
+                if (line.Contains("Channel", StringComparison.Ordinal))
                 {
-                    string line = StripXML(sr.ReadLine());
-                    if (line.Contains("Channel", StringComparison.Ordinal))
+                    LiveTvTunerStatus status;
+                    var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
+                    var name = line.Substring(0, index - 1);
+                    var currentChannel = line.Substring(index + 7);
+                    if (currentChannel != "none")
                     {
-                        LiveTvTunerStatus status;
-                        var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
-                        var name = line.Substring(0, index - 1);
-                        var currentChannel = line.Substring(index + 7);
-                        if (currentChannel != "none")
-                        {
-                            status = LiveTvTunerStatus.LiveTv;
-                        }
-                        else
-                        {
-                            status = LiveTvTunerStatus.Available;
-                        }
-
-                        tuners.Add(new LiveTvTunerInfo
-                        {
-                            Name = name,
-                            SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
-                            ProgramName = currentChannel,
-                            Status = status
-                        });
+                        status = LiveTvTunerStatus.LiveTv;
+                    }
+                    else
+                    {
+                        status = LiveTvTunerStatus.Available;
                     }
-                }
 
-                return tuners;
+                    tuners.Add(new LiveTvTunerInfo
+                    {
+                        Name = name,
+                        SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
+                        ProgramName = currentChannel,
+                        Status = status
+                    });
+                }
             }
+
+            return tuners;
         }
 
         private static string StripXML(string source)
@@ -634,7 +616,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                     info,
                     streamId,
                     FileSystem,
-                    _httpClient,
+                    _httpClientFactory,
                     Logger,
                     Config,
                     _appHost,

+ 7 - 6
Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -5,6 +5,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
@@ -26,7 +27,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 {
     public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerApplicationHost _appHost;
         private readonly INetworkManager _networkManager;
         private readonly IMediaSourceManager _mediaSourceManager;
@@ -37,14 +38,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             IMediaSourceManager mediaSourceManager,
             ILogger<M3UTunerHost> logger,
             IFileSystem fileSystem,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IServerApplicationHost appHost,
             INetworkManager networkManager,
             IStreamHelper streamHelper,
             IMemoryCache memoryCache)
             : base(config, logger, fileSystem, memoryCache)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
             _networkManager = networkManager;
             _mediaSourceManager = mediaSourceManager;
@@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         {
             var channelIdPrefix = GetFullChannelIdPrefix(info);
 
-            return await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
+            return await new M3uParser(Logger, _httpClientFactory, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
         }
 
         public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
@@ -116,7 +117,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
                 if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
                 {
-                    return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config, _appHost, _streamHelper);
+                    return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
                 }
             }
 
@@ -125,7 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         public async Task Validate(TunerHostInfo info)
         {
-            using (var stream = await new M3uParser(Logger, _httpClient, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
+            using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
             {
             }
         }

+ 6 - 10
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -5,6 +5,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Net.Http;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
@@ -19,13 +20,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
     public class M3uParser
     {
         private readonly ILogger _logger;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerApplicationHost _appHost;
 
-        public M3uParser(ILogger logger, IHttpClient httpClient, IServerApplicationHost appHost)
+        public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory, IServerApplicationHost appHost)
         {
             _logger = logger;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
         }
 
@@ -51,13 +52,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         {
             if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
             {
-                return _httpClient.Get(new HttpRequestOptions
-                {
-                    Url = url,
-                    CancellationToken = cancellationToken,
-                    // Some data providers will require a user agent
-                    UserAgent = _appHost.ApplicationUserAgent
-                });
+                return _httpClientFactory.CreateClient(NamedClient.Default)
+                    .GetStreamAsync(url);
             }
 
             return Task.FromResult((Stream)File.OpenRead(url));

+ 17 - 30
Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs

@@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 {
     public class SharedHttpStream : LiveStream, IDirectStreamProvider
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerApplicationHost _appHost;
 
         public SharedHttpStream(
@@ -29,14 +29,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             TunerHostInfo tunerHostInfo,
             string originalStreamId,
             IFileSystem fileSystem,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILogger logger,
             IConfigurationManager configurationManager,
             IServerApplicationHost appHost,
             IStreamHelper streamHelper)
             : base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
             OriginalStreamId = originalStreamId;
             EnableStreamSharing = true;
@@ -55,25 +55,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             var typeName = GetType().Name;
             Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
 
-            var httpRequestOptions = new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = CancellationToken.None,
-                BufferContent = false,
-                DecompressionMethod = CompressionMethods.None
-            };
-
-            foreach (var header in mediaSource.RequiredHttpHeaders)
-            {
-                httpRequestOptions.RequestHeaders[header.Key] = header.Value;
-            }
-
-            var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false);
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
+                .ConfigureAwait(false);
 
             var extension = "ts";
             var requiresRemux = false;
 
-            var contentType = response.ContentType ?? string.Empty;
+            var contentType = response.Content.Headers.ContentType.ToString();
             if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1)
             {
                 requiresRemux = true;
@@ -132,24 +121,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             }
         }
 
-        private Task StartStreaming(HttpResponseInfo response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
+        private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
         {
             return Task.Run(async () =>
             {
                 try
                 {
                     Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
-                    using (response)
-                    using (var stream = response.Content)
-                    using (var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read))
-                    {
-                        await StreamHelper.CopyToAsync(
-                            stream,
-                            fileStream,
-                            IODefaults.CopyToBufferSize,
-                            () => Resolve(openTaskCompletionSource),
-                            cancellationToken).ConfigureAwait(false);
-                    }
+                    using var message = response;
+                    await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                    await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read);
+                    await StreamHelper.CopyToAsync(
+                        stream,
+                        fileStream,
+                        IODefaults.CopyToBufferSize,
+                        () => Resolve(openTaskCompletionSource),
+                        cancellationToken).ConfigureAwait(false);
                 }
                 catch (OperationCanceledException ex)
                 {

+ 26 - 8
Emby.Server.Implementations/Localization/Core/af.json

@@ -1,19 +1,19 @@
 {
     "Artists": "Kunstenare",
     "Channels": "Kanale",
-    "Folders": "Fouers",
-    "Favorites": "Gunstelinge",
+    "Folders": "Lêergidse",
+    "Favorites": "Gunstellinge",
     "HeaderFavoriteShows": "Gunsteling Vertonings",
     "ValueSpecialEpisodeName": "Spesiale - {0}",
     "HeaderAlbumArtists": "Album Kunstenaars",
     "Books": "Boeke",
     "HeaderNextUp": "Volgende",
-    "Movies": "Rolprente",
-    "Shows": "Program",
-    "HeaderContinueWatching": "Hou Aan Kyk",
+    "Movies": "Flieks",
+    "Shows": "Televisie Reekse",
+    "HeaderContinueWatching": "Kyk Verder",
     "HeaderFavoriteEpisodes": "Gunsteling Episodes",
     "Photos": "Fotos",
-    "Playlists": "Speellysse",
+    "Playlists": "Snitlyste",
     "HeaderFavoriteArtists": "Gunsteling Kunstenaars",
     "HeaderFavoriteAlbums": "Gunsteling Albums",
     "Sync": "Sinkroniseer",
@@ -23,7 +23,7 @@
     "DeviceOfflineWithName": "{0} is ontkoppel",
     "Collections": "Versamelings",
     "Inherit": "Ontvang",
-    "HeaderLiveTV": "Live TV",
+    "HeaderLiveTV": "Lewendige TV",
     "Application": "Program",
     "AppDeviceValues": "App: {0}, Toestel: {1}",
     "VersionNumber": "Weergawe {0}",
@@ -95,5 +95,23 @@
     "TasksChannelsCategory": "Internet kanale",
     "TasksApplicationCategory": "aansoek",
     "TasksLibraryCategory": "biblioteek",
-    "TasksMaintenanceCategory": "onderhoud"
+    "TasksMaintenanceCategory": "onderhoud",
+    "TaskCleanCacheDescription": "Vee kasregister lêers uit wat nie meer deur die stelsel benodig word nie.",
+    "TaskCleanCache": "Reinig Kasgeheue Lêergids",
+    "TaskDownloadMissingSubtitlesDescription": "Soek aanlyn vir vermiste onderskrifte gebasseer op metadata verstellings.",
+    "TaskDownloadMissingSubtitles": "Laai vermiste onderskrifte af",
+    "TaskRefreshChannelsDescription": "Vervris internet kanaal inligting.",
+    "TaskRefreshChannels": "Vervris Kanale",
+    "TaskCleanTranscodeDescription": "Vee transkodering lêers uit wat ouer is as een dag.",
+    "TaskCleanTranscode": "Reinig Transkoderings Leêrbinder",
+    "TaskUpdatePluginsDescription": "Laai opgedateerde inprop-sagteware af en installeer inprop-sagteware wat verstel is om outomaties op te dateer.",
+    "TaskUpdatePlugins": "Dateer Inprop-Sagteware Op",
+    "TaskRefreshPeopleDescription": "Vervris metadata oor akteurs en regisseurs in u media versameling.",
+    "TaskRefreshPeople": "Vervris Mense",
+    "TaskCleanLogsDescription": "Vee loglêers wat ouer as {0} dae is uit.",
+    "TaskCleanLogs": "Reinig Loglêer Lêervouer",
+    "TaskRefreshLibraryDescription": "Skandeer u media versameling vir nuwe lêers en verfris metadata.",
+    "TaskRefreshLibrary": "Skandeer Media Versameling",
+    "TaskRefreshChapterImagesDescription": "Maak kleinkiekeis (fotos) vir films wat hoofstukke het.",
+    "TaskRefreshChapterImages": "Verkry Hoofstuk Beelde"
 }

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

@@ -107,7 +107,7 @@
     "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
     "TaskCleanLogs": "Nettoyer le répertoire des journaux",
     "TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
-    "TaskRefreshLibrary": "Scanner toute les Bibliothèques",
+    "TaskRefreshLibrary": "Scanner toutes les Bibliothèques",
     "TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.",
     "TaskRefreshChapterImages": "Extraire les images de chapitre",
     "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",

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

@@ -1,3 +1,11 @@
 {
-    "Albums": "Álbumes"
+    "Albums": "Álbumes",
+    "Collections": "Colecións",
+    "ChapterNameValue": "Capítulos {0}",
+    "Channels": "Canles",
+    "CameraImageUploadedFrom": "Cargouse unha nova imaxe da cámara desde {0}",
+    "Books": "Libros",
+    "AuthenticationSucceededWithUserName": "{0} autenticouse correctamente",
+    "Artists": "Artistas",
+    "Application": "Aplicativo"
 }

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

@@ -19,7 +19,7 @@
     "HeaderFavoriteEpisodes": "Episode Favorit",
     "HeaderFavoriteArtists": "Artis Favorit",
     "HeaderFavoriteAlbums": "Album Favorit",
-    "HeaderContinueWatching": "Lanjutkan Menonton",
+    "HeaderContinueWatching": "Lanjut Menonton",
     "HeaderCameraUploads": "Unggahan Kamera",
     "HeaderAlbumArtists": "Album Artis",
     "Genres": "Aliran",

+ 2 - 2
Emby.Server.Implementations/Localization/Core/ko.json

@@ -84,8 +84,8 @@
     "UserDeletedWithName": "사용자 {0} 삭제됨",
     "UserDownloadingItemWithValues": "{0}이(가) {1}을 다운로드 중입니다",
     "UserLockedOutWithName": "유저 {0} 은(는) 잠금처리 되었습니다",
-    "UserOfflineFromDevice": "{1}로부터 {0}의 연결이 끊겼습니다",
-    "UserOnlineFromDevice": "{0}은 {1}에서 온라인 상태입니다",
+    "UserOfflineFromDevice": "{1}에서 {0}의 연결이 끊킴",
+    "UserOnlineFromDevice": "{0}이 {1}으로 접속",
     "UserPasswordChangedWithName": "사용자 {0}의 비밀번호가 변경되었습니다",
     "UserPolicyUpdatedWithName": "{0}의 사용자 정책이 업데이트되었습니다",
     "UserStartedPlayingItemWithValues": "{2}에서 {0}이 {1} 재생 중",

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

@@ -45,12 +45,12 @@
     "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",
     "NotificationOptionCameraImageUploaded": "Kamerabilde lastet opp",
-    "NotificationOptionInstallationFailed": "Installasjonsfeil",
+    "NotificationOptionInstallationFailed": "Installasjonen feilet",
     "NotificationOptionNewLibraryContent": "Nytt innhold lagt til",
     "NotificationOptionPluginError": "Pluginfeil",
     "NotificationOptionPluginInstalled": "Plugin installert",
@@ -71,7 +71,7 @@
     "ScheduledTaskFailedWithName": "{0} mislykkes",
     "ScheduledTaskStartedWithName": "{0} startet",
     "ServerNameNeedsToBeRestarted": "{0} må startes på nytt",
-    "Shows": "Programmer",
+    "Shows": "Program",
     "Songs": "Sanger",
     "StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.",
     "SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for  {0}",
@@ -88,7 +88,7 @@
     "UserOnlineFromDevice": "{0} er tilkoblet fra {1}",
     "UserPasswordChangedWithName": "Passordet for {0} er oppdatert",
     "UserPolicyUpdatedWithName": "Brukerpolicyen har blitt oppdatert for {0}",
-    "UserStartedPlayingItemWithValues": "{0} har startet avspilling {1}",
+    "UserStartedPlayingItemWithValues": "{0} har startet avspilling {1} på {2}",
     "UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling  {1}",
     "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt",
     "ValueSpecialEpisodeName": "Spesialepisode - {0}",

+ 60 - 3
Emby.Server.Implementations/Localization/Core/nn.json

@@ -35,7 +35,7 @@
     "AuthenticationSucceededWithUserName": "{0} Har logga inn",
     "Artists": "Artistar",
     "Application": "Program",
-    "AppDeviceValues": "App: {0}, Einheit: {1}",
+    "AppDeviceValues": "App: {0}, Eining: {1}",
     "Albums": "Album",
     "NotificationOptionServerRestartRequired": "Tenaren krev omstart",
     "NotificationOptionPluginUpdateInstalled": "Tilleggsprogram-oppdatering vart installert",
@@ -43,7 +43,7 @@
     "NotificationOptionPluginInstalled": "Tilleggsprogram installert",
     "NotificationOptionPluginError": "Tilleggsprogram feila",
     "NotificationOptionNewLibraryContent": "Nytt innhald er lagt til",
-    "NotificationOptionInstallationFailed": "Installasjonen feila",
+    "NotificationOptionInstallationFailed": "Installasjonsfeil",
     "NotificationOptionCameraImageUploaded": "Kamerabilde vart lasta opp",
     "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppa",
     "NotificationOptionAudioPlayback": "Lydavspilling påbyrja",
@@ -56,5 +56,62 @@
     "MusicVideos": "Musikkvideoar",
     "Music": "Musikk",
     "Movies": "Filmar",
-    "MixedContent": "Blanda innhald"
+    "MixedContent": "Blanda innhald",
+    "Sync": "Synkronisera",
+    "TaskDownloadMissingSubtitlesDescription": "Søk Internettet for manglande undertekstar basert på metadatainnstillingar.",
+    "TaskDownloadMissingSubtitles": "Last ned manglande undertekstar",
+    "TaskRefreshChannelsDescription": "Oppdater internettkanalinformasjon.",
+    "TaskRefreshChannels": "Oppdater kanalar",
+    "TaskCleanTranscodeDescription": "Slett transkodefiler som er meir enn ein dag gamal.",
+    "TaskCleanTranscode": "Reins transkodemappe",
+    "TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringar for programtillegg som er sette opp til å oppdaterast automatisk.",
+    "TaskUpdatePlugins": "Oppdaterer programtillegg",
+    "TaskRefreshPeopleDescription": "Oppdaterer metadata for skodespelarar og regissørar i mediebiblioteket ditt.",
+    "TaskRefreshPeople": "Oppdater personar",
+    "TaskCleanLogsDescription": "Slett loggfiler som er meir enn {0} dagar gamle.",
+    "TaskCleanLogs": "Reins loggmappe",
+    "TaskRefreshLibraryDescription": "Skannar mediebiblioteket ditt for nye filer og oppdaterer metadata.",
+    "TaskRefreshLibrary": "Skann mediebibliotek",
+    "TaskRefreshChapterImagesDescription": "Lager miniatyrbilete for videoar som har kapittel.",
+    "TaskRefreshChapterImages": "Trekk ut kapittelbilete",
+    "TaskCleanCacheDescription": "Slettar mellomlagra filer som ikkje lengre trengst av systemet.",
+    "TaskCleanCache": "Rens mappe for hurtiglager",
+    "TasksChannelsCategory": "Internettkanalar",
+    "TasksApplicationCategory": "Applikasjon",
+    "TasksLibraryCategory": "Bibliotek",
+    "TasksMaintenanceCategory": "Vedlikehald",
+    "VersionNumber": "Versjon {0}",
+    "ValueSpecialEpisodeName": "Spesialepisode - {0}",
+    "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt",
+    "UserStoppedPlayingItemWithValues": "{0} har fullført avspeling {1} på {2}",
+    "UserStartedPlayingItemWithValues": "{0} spelar {1} på {2}",
+    "UserPolicyUpdatedWithName": "Brukarreglar har blitt oppdatert for {0}",
+    "UserPasswordChangedWithName": "Passordet for {0} er oppdatert",
+    "UserOnlineFromDevice": "{0} er direktekopla frå {1}",
+    "UserOfflineFromDevice": "{0} har kopla frå {1}",
+    "UserLockedOutWithName": "Brukar {0} har blitt utestengd",
+    "UserDownloadingItemWithValues": "{0} lastar ned {1}",
+    "UserDeletedWithName": "Brukar {0} er sletta",
+    "UserCreatedWithName": "Brukar {0} er oppretta",
+    "User": "Brukar",
+    "TvShows": "TV-seriar",
+    "System": "System",
+    "SubtitleDownloadFailureFromForItem": "Feila å laste ned undertekstar frå {0} for {1}",
+    "StartupEmbyServerIsLoading": "Jellyfintenaren laster. Prøv igjen om litt.",
+    "Songs": "Songar",
+    "Shows": "Program",
+    "ServerNameNeedsToBeRestarted": "{0} må omstartast",
+    "ScheduledTaskStartedWithName": "{0} starta",
+    "ScheduledTaskFailedWithName": "{0} feila",
+    "ProviderValue": "Leverandør: {0}",
+    "PluginUpdatedWithName": "{0} blei oppdatert",
+    "PluginUninstalledWithName": "{0} blei avinstallert",
+    "PluginInstalledWithName": "{0} blei installert",
+    "Plugin": "Programtillegg",
+    "Playlists": "Speleliste",
+    "Photos": "Foto",
+    "NotificationOptionVideoPlaybackStopped": "Videoavspeling stoppa",
+    "NotificationOptionVideoPlayback": "Videoavspeling starta",
+    "NotificationOptionUserLockedOut": "Brukar er utestengd",
+    "NotificationOptionTaskFailed": "Planlagt oppgåve feila"
 }

+ 117 - 0
Emby.Server.Implementations/Localization/Core/sq.json

@@ -0,0 +1,117 @@
+{
+    "MessageApplicationUpdatedTo": "Serveri Jellyfin u përditesua në versionin {0}",
+    "Inherit": "Trashgimi",
+    "TaskDownloadMissingSubtitlesDescription": "Kërkon në internet për titra që mungojnë bazuar tek konfigurimi i metadata-ve.",
+    "TaskDownloadMissingSubtitles": "Shkarko titra që mungojnë",
+    "TaskRefreshChannelsDescription": "Rifreskon informacionin e kanaleve të internetit.",
+    "TaskRefreshChannels": "Rifresko Kanalet",
+    "TaskCleanTranscodeDescription": "Fshin skedarët e transkodimit që janë më të vjetër se një ditë.",
+    "TaskCleanTranscode": "Fshi dosjen e transkodimit",
+    "TaskUpdatePluginsDescription": "Shkarkon dhe instalon përditësimi për plugin që janë konfiguruar të përditësohen automatikisht.",
+    "TaskUpdatePlugins": "Përditëso Plugin",
+    "TaskRefreshPeopleDescription": "Përditëson metadata të aktorëve dhe regjizorëve në librarinë tuaj.",
+    "TaskRefreshPeople": "Rifresko aktorët",
+    "TaskCleanLogsDescription": "Fshin skëdarët log që janë më të vjetër se {0} ditë.",
+    "TaskCleanLogs": "Fshi dosjen Log",
+    "TaskRefreshLibraryDescription": "Skanon librarinë media për skedarë të rinj dhe rifreskon metadata.",
+    "TaskRefreshLibrary": "Skano librarinë media",
+    "TaskRefreshChapterImagesDescription": "Krijon imazh për videot që kanë kapituj.",
+    "TaskRefreshChapterImages": "Ekstrakto Imazhet e Kapitullit",
+    "TaskCleanCacheDescription": "Fshi skedarët e cache-s që nuk i duhen më sistemit.",
+    "TaskCleanCache": "Pastro memorjen cache",
+    "TasksChannelsCategory": "Kanalet nga interneti",
+    "TasksApplicationCategory": "Aplikacioni",
+    "TasksLibraryCategory": "Libraria",
+    "TasksMaintenanceCategory": "Mirëmbajtje",
+    "VersionNumber": "Versioni {0}",
+    "ValueSpecialEpisodeName": "Speciale - {0}",
+    "ValueHasBeenAddedToLibrary": "{0} u shtua tek libraria juaj",
+    "UserStoppedPlayingItemWithValues": "{0} mbaroi së shikuari {1} tek {2}",
+    "UserStartedPlayingItemWithValues": "{0} po shikon {1} tek {2}",
+    "UserPolicyUpdatedWithName": "Politika e përdoruesit u përditësua për {0}",
+    "UserPasswordChangedWithName": "Fjalëkalimi u ndryshua për përdoruesin {0}",
+    "UserOnlineFromDevice": "{0} është në linjë nga {1}",
+    "UserOfflineFromDevice": "{0} u shkëput nga {1}",
+    "UserLockedOutWithName": "Përdoruesi {0} u përjashtua",
+    "UserDownloadingItemWithValues": "{0} po shkarkon {1}",
+    "UserDeletedWithName": "Përdoruesi {0} u fshi",
+    "UserCreatedWithName": "Përdoruesi {0} u krijua",
+    "User": "Përdoruesi",
+    "TvShows": "Seriale TV",
+    "System": "Sistemi",
+    "Sync": "Sinkronizo",
+    "SubtitleDownloadFailureFromForItem": "Titrat deshtuan të shkarkohen nga {0} për {1}",
+    "StartupEmbyServerIsLoading": "Serveri Jellyfin po ngarkohet. Ju lutemi provoni përseri pas pak.",
+    "Songs": "Këngë",
+    "Shows": "Seriale",
+    "ServerNameNeedsToBeRestarted": "{0} duhet të ristartoj",
+    "ScheduledTaskStartedWithName": "{0} filloi",
+    "ScheduledTaskFailedWithName": "{0} dështoi",
+    "ProviderValue": "Ofruesi: {0}",
+    "PluginUpdatedWithName": "{0} u përditësua",
+    "PluginUninstalledWithName": "{0} u çinstalua",
+    "PluginInstalledWithName": "{0} u instalua",
+    "Plugin": "Plugin",
+    "Playlists": "Listat për luajtje",
+    "Photos": "Fotografitë",
+    "NotificationOptionVideoPlaybackStopped": "Luajtja e videos ndaloi",
+    "NotificationOptionVideoPlayback": "Luajtja e videos filloi",
+    "NotificationOptionUserLockedOut": "Përdoruesi u përjashtua",
+    "NotificationOptionTaskFailed": "Ushtrimi i planifikuar dështoi",
+    "NotificationOptionServerRestartRequired": "Kërkohet ristartim i serverit",
+    "NotificationOptionPluginUpdateInstalled": "Përditësimi i plugin u instalua",
+    "NotificationOptionPluginUninstalled": "Plugin u çinstalua",
+    "NotificationOptionPluginInstalled": "Plugin u instalua",
+    "NotificationOptionPluginError": "Plugin dështoi",
+    "NotificationOptionNewLibraryContent": "Një përmbajtje e re u shtua",
+    "NotificationOptionInstallationFailed": "Instalimi dështoi",
+    "NotificationOptionCameraImageUploaded": "Fotoja nga kamera u ngarkua",
+    "NotificationOptionAudioPlaybackStopped": "Luajtja e audios ndaloi",
+    "NotificationOptionAudioPlayback": "Luajtja e audios filloi",
+    "NotificationOptionApplicationUpdateInstalled": "Përditësimi i aplikacionit u instalua",
+    "NotificationOptionApplicationUpdateAvailable": "Një perditësim i aplikacionit është gati",
+    "NewVersionIsAvailable": "Një version i ri i Jellyfin është gati për tu shkarkuar.",
+    "NameSeasonUnknown": "Sezon i panjohur",
+    "NameSeasonNumber": "Sezoni {0}",
+    "NameInstallFailed": "Instalimi i {0} dështoi",
+    "MusicVideos": "Video muzikore",
+    "Music": "Muzikë",
+    "Movies": "Filma",
+    "MixedContent": "Përmbajtje e përzier",
+    "MessageServerConfigurationUpdated": "Konfigurimet e serverit u përditësuan",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Seksioni i konfigurimit të serverit {0} u përditësua",
+    "MessageApplicationUpdated": "Serveri Jellyfin u përditësua",
+    "Latest": "Të fundit",
+    "LabelRunningTimeValue": "Kohëzgjatja: {0}",
+    "LabelIpAddressValue": "Adresa IP: {0}",
+    "ItemRemovedWithName": "{0} u fshi nga libraria",
+    "ItemAddedWithName": "{0} u shtua tek libraria",
+    "HomeVideos": "Video personale",
+    "HeaderRecordingGroups": "Grupet e regjistrimit",
+    "HeaderNextUp": "Në vazhdim",
+    "HeaderLiveTV": "TV Live",
+    "HeaderFavoriteSongs": "Kënget e preferuara",
+    "HeaderFavoriteShows": "Serialet e preferuar",
+    "HeaderFavoriteEpisodes": "Episodet e preferuar",
+    "HeaderFavoriteArtists": "Artistët e preferuar",
+    "HeaderFavoriteAlbums": "Albumet e preferuar",
+    "HeaderContinueWatching": "Vazhdo të shikosh",
+    "HeaderCameraUploads": "Ngarkimet nga Kamera",
+    "HeaderAlbumArtists": "Artistët e albumeve",
+    "Genres": "Zhanre",
+    "Folders": "Dosje",
+    "Favorites": "Të preferuara",
+    "FailedLoginAttemptWithUserName": "Përpjekja për hyrje dështoi nga {0}",
+    "DeviceOnlineWithName": "{0} u lidh",
+    "DeviceOfflineWithName": "{0} u shkëput",
+    "Collections": "Koleksione",
+    "ChapterNameValue": "Kapituj",
+    "Channels": "Kanale",
+    "CameraImageUploadedFrom": "Një foto e re nga kamera u ngarkua nga {0}",
+    "Books": "Libra",
+    "AuthenticationSucceededWithUserName": "{0} u identifikua me sukses",
+    "Artists": "Artistë",
+    "Application": "Aplikacioni",
+    "AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}",
+    "Albums": "Albumet"
+}

+ 17 - 17
Emby.Server.Implementations/Localization/Core/ta.json

@@ -18,7 +18,7 @@
     "MessageServerConfigurationUpdated": "சேவையக அமைப்புகள் புதுப்பிக்கப்பட்டன",
     "MessageApplicationUpdatedTo": "ஜெல்லிஃபின் சேவையகம் {0} இற்கு புதுப்பிக்கப்பட்டது",
     "MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது",
-    "Inherit": "மரபரிமையாகப் பெறு",
+    "Inherit": "மரபரிமையாகப் பெறு",
     "HeaderRecordingGroups": "பதிவு குழுக்கள்",
     "HeaderCameraUploads": "புகைப்பட பதிவேற்றங்கள்",
     "Folders": "கோப்புறைகள்",
@@ -26,12 +26,12 @@
     "DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
     "DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது",
     "Collections": "தொகுப்புகள்",
-    "CameraImageUploadedFrom": "{0} இலிருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது",
+    "CameraImageUploadedFrom": "{0} இல் இருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது",
     "AppDeviceValues": "செயலி: {0}, சாதனம்: {1}",
     "TaskDownloadMissingSubtitles": "விடுபட்டுபோன வசன வரிகளைப் பதிவிறக்கு",
     "TaskRefreshChannels": "சேனல்களை புதுப்பி",
     "TaskUpdatePlugins": "உட்செருகிகளை புதுப்பி",
-    "TaskRefreshLibrary": "மீடியா நூலகத்தை ஆராய்",
+    "TaskRefreshLibrary": "ஊடக நூலகத்தை ஆராய்",
     "TasksChannelsCategory": "இணைய சேனல்கள்",
     "TasksApplicationCategory": "செயலி",
     "TasksLibraryCategory": "நூலகம்",
@@ -46,7 +46,7 @@
     "Sync": "ஒத்திசைவு",
     "StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
     "Songs": "பாடல்கள்",
-    "Shows": "தொடர்கள்",
+    "Shows": "நிகழ்ச்சிகள்",
     "ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
     "ScheduledTaskStartedWithName": "{0} துவங்கியது",
     "ScheduledTaskFailedWithName": "{0} தோல்வியடைந்தது",
@@ -67,20 +67,20 @@
     "NotificationOptionAudioPlayback": "ஒலி இசைக்கத் துவங்கியுள்ளது",
     "NotificationOptionApplicationUpdateInstalled": "செயலி புதுப்பிக்கப்பட்டது",
     "NotificationOptionApplicationUpdateAvailable": "செயலியினை புதுப்பிக்கலாம்",
-    "NameSeasonUnknown": "பருவம் அறியப்படாதவை",
+    "NameSeasonUnknown": "அறியப்படாத பருவம்",
     "NameSeasonNumber": "பருவம் {0}",
     "NameInstallFailed": "{0} நிறுவல் தோல்வியடைந்தது",
     "MusicVideos": "இசைப்படங்கள்",
     "Music": "இசை",
     "Movies": "திரைப்படங்கள்",
-    "Latest": "புதிய",
+    "Latest": "புதியவை",
     "LabelRunningTimeValue": "ஓடும் நேரம்: {0}",
     "LabelIpAddressValue": "ஐபி முகவரி: {0}",
     "ItemRemovedWithName": "{0} நூலகத்திலிருந்து அகற்றப்பட்டது",
     "ItemAddedWithName": "{0} நூலகத்தில் சேர்க்கப்பட்டது",
-    "HeaderNextUp": "அடுத்ததாக",
+    "HeaderNextUp": "அடுத்தத",
     "HeaderLiveTV": "நேரடித் தொலைக்காட்சி",
-    "HeaderFavoriteSongs": "பிடித்த பாட்டுகள்",
+    "HeaderFavoriteSongs": "பிடித்த பாட்கள்",
     "HeaderFavoriteShows": "பிடித்த தொடர்கள்",
     "HeaderFavoriteEpisodes": "பிடித்த அத்தியாயங்கள்",
     "HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்",
@@ -93,25 +93,25 @@
     "Channels": "சேனல்கள்",
     "Books": "புத்தகங்கள்",
     "AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது",
-    "Artists": "கலைஞர்",
+    "Artists": "கலைஞர்கள்",
     "Application": "செயலி",
     "Albums": "ஆல்பங்கள்",
     "NewVersionIsAvailable": "ஜெல்லிஃபின் சேவையகத்தின் புதிய பதிப்பு பதிவிறக்கத்திற்கு கிடைக்கிறது.",
-    "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0 புதுப்பிக்கப்பட்டது",
+    "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0} புதுப்பிக்கப்பட்டது",
     "TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.",
     "UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
-    "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0 } இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
-    "TaskDownloadMissingSubtitlesDescription": "மெட்டாடேட்டா உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
+    "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
+    "TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
     "TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.",
-    "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட செருகுநிரல்களுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
-    "TaskRefreshPeopleDescription": "உங்கள் மீடியா நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மெட்டாடேட்டாவை புதுப்பிக்கும்.",
+    "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
+    "TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.",
     "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
-    "TaskCleanLogs": "பதிவு அடைவ சுத்தம் செய்யுங்கள்",
-    "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் மீடியா நூலகத்தை ஸ்கேன் செய்து மீத்தரவை புதுப்பிக்கும்.",
+    "TaskCleanLogs": "பதிவு அடைவ சுத்தம் செய்யுங்கள்",
+    "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் ஊடக நூலகத்தை ஆராய்ந்து மீத்தரவை புதுப்பிக்கும்.",
     "TaskRefreshChapterImagesDescription": "அத்தியாயங்களைக் கொண்ட வீடியோக்களுக்கான சிறு உருவங்களை உருவாக்குகிறது.",
     "ValueHasBeenAddedToLibrary": "உங்கள் மீடியா நூலகத்தில் {0} சேர்க்கப்பட்டது",
     "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
     "HomeVideos": "முகப்பு வீடியோக்கள்",
-    "UserStoppedPlayingItemWithValues": "{2} இல் {1} முடித்துவிட்டது",
+    "UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது",
     "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது"
 }

+ 102 - 61
Emby.Server.Implementations/Localization/Core/th.json

@@ -1,76 +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} ล้มเหลว",
     "Songs": "เพลง",
-    "Shows": "แสดง",
-    "ServerNameNeedsToBeRestarted": "{0} ต้องการรีสตาร์ท"
+    "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 โปรดลองอีกครั้งในอีกสักครู่"
 }

+ 117 - 0
Emby.Server.Implementations/Localization/Core/vi.json

@@ -0,0 +1,117 @@
+{
+    "Collections": "Bộ Sưu Tập",
+    "Favorites": "Yêu Thích",
+    "Folders": "Thư Mục",
+    "Genres": "Thể Loại",
+    "HeaderAlbumArtists": "Bộ Sưu Tập Nghệ sĩ",
+    "HeaderContinueWatching": "Xem Tiếp",
+    "HeaderLiveTV": "TV Trực Tiếp",
+    "Movies": "Phim",
+    "Photos": "Ảnh",
+    "Playlists": "Danh sách phát",
+    "Shows": "Chương Trình TV",
+    "Songs": "Các Bài Hát",
+    "Sync": "Đồng Bộ",
+    "ValueSpecialEpisodeName": "Đặc Biệt - {0}",
+    "Albums": "Albums",
+    "Artists": "Các Nghệ Sĩ",
+    "TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.",
+    "TaskDownloadMissingSubtitles": "Tải xuống phụ đề bị thiếu",
+    "TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.",
+    "TaskRefreshChannels": "Làm Mới Kênh",
+    "TaskCleanTranscodeDescription": "Xóa các tệp chuyển mã cũ hơn một ngày.",
+    "TaskCleanTranscode": "Làm Sạch Thư Mục Chuyển Mã",
+    "TaskUpdatePluginsDescription": "Tải xuống và cài đặt các bản cập nhật cho các plugin được định cấu hình để cập nhật tự động.",
+    "TaskUpdatePlugins": "Cập Nhật Plugins",
+    "TaskRefreshPeopleDescription": "Cập nhật thông tin chi tiết cho diễn viên và đạo diễn trong thư viện phương tiện của bạn.",
+    "TaskRefreshPeople": "Làm mới Người dùng",
+    "TaskCleanLogsDescription": "Xóa tập tin nhật ký cũ hơn {0} ngày.",
+    "TaskCleanLogs": "Làm sạch nhật ký",
+    "TaskRefreshLibraryDescription": "Quét thư viện phương tiện của bạn để tìm các tệp mới và làm mới thông tin chi tiết.",
+    "TaskRefreshLibrary": "Quét Thư viện Phương tiện",
+    "TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho video có các phân cảnh.",
+    "TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh",
+    "TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.",
+    "TaskCleanCache": "Làm Sạch Thư Mục Cache",
+    "TasksChannelsCategory": "Kênh Internet",
+    "TasksApplicationCategory": "Ứng Dụng",
+    "TasksLibraryCategory": "Thư Viện",
+    "TasksMaintenanceCategory": "Bảo Trì",
+    "VersionNumber": "Phiên Bản {0}",
+    "ValueHasBeenAddedToLibrary": "{0} đã được thêm vào thư viện của bạn",
+    "UserStoppedPlayingItemWithValues": "{0} đã phát xong {1} trên {2}",
+    "UserStartedPlayingItemWithValues": "{0} đang phát {1} trên {2}",
+    "UserPolicyUpdatedWithName": "Chính sách người dùng đã được cập nhật cho {0}",
+    "UserPasswordChangedWithName": "Mật khẩu đã được thay đổi cho người dùng {0}",
+    "UserOnlineFromDevice": "{0} trực tuyến từ {1}",
+    "UserOfflineFromDevice": "{0} đã ngắt kết nối từ {1}",
+    "UserLockedOutWithName": "User {0} đã bị khóa",
+    "UserDownloadingItemWithValues": "{0} đang tải xuống {1}",
+    "UserDeletedWithName": "Người Dùng {0} đã được xóa",
+    "UserCreatedWithName": "Người Dùng {0} đã được tạo",
+    "User": "Người Dùng",
+    "TvShows": "Chương Trình TV",
+    "System": "Hệ Thống",
+    "SubtitleDownloadFailureFromForItem": "Không thể tải xuống phụ đề từ {0} cho {1}",
+    "StartupEmbyServerIsLoading": "Jellyfin Server đang tải. Vui lòng thử lại trong thời gian ngắn.",
+    "ServerNameNeedsToBeRestarted": "{0} cần được khởi động lại",
+    "ScheduledTaskStartedWithName": "{0} đã bắt đầu",
+    "ScheduledTaskFailedWithName": "{0} đã thất bại",
+    "ProviderValue": "Provider: {0}",
+    "PluginUpdatedWithName": "{0} đã cập nhật",
+    "PluginUninstalledWithName": "{0} đã được gỡ bỏ",
+    "PluginInstalledWithName": "{0} đã được cài đặt",
+    "Plugin": "Plugin",
+    "NotificationOptionVideoPlaybackStopped": "Phát lại video đã dừng",
+    "NotificationOptionVideoPlayback": "Đã bắt đầu phát lại video",
+    "NotificationOptionUserLockedOut": "Người dùng bị khóa",
+    "NotificationOptionTaskFailed": "Lỗi tác vụ đã lên lịch",
+    "NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại Server",
+    "NotificationOptionPluginUpdateInstalled": "Cập nhật Plugin đã được cài đặt",
+    "NotificationOptionPluginUninstalled": "Đã gỡ bỏ Plugin",
+    "NotificationOptionPluginInstalled": "Đã cài đặt Plugin",
+    "NotificationOptionPluginError": "Thất bại Plugin",
+    "NotificationOptionNewLibraryContent": "Nội dung mới được thêm vào",
+    "NotificationOptionInstallationFailed": "Cài đặt thất bại",
+    "NotificationOptionCameraImageUploaded": "Đã tải lên hình ảnh máy ảnh",
+    "NotificationOptionAudioPlaybackStopped": "Phát lại âm thanh đã dừng",
+    "NotificationOptionAudioPlayback": "Phát lại âm thanh đã bắt đầu",
+    "NotificationOptionApplicationUpdateInstalled": "Bản cập nhật ứng dụng đã được cài đặt",
+    "NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có",
+    "NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.",
+    "NameSeasonUnknown": "Không Rõ Mùa",
+    "NameSeasonNumber": "Mùa {0}",
+    "NameInstallFailed": "{0} cài đặt thất bại",
+    "MusicVideos": "Video Nhạc",
+    "Music": "Nhạc",
+    "MixedContent": "Nội dung hỗn hợp",
+    "MessageServerConfigurationUpdated": "Cấu hình máy chủ đã được cập nhật",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Phần cấu hình máy chủ {0} đã được cập nhật",
+    "MessageApplicationUpdatedTo": "Jellyfin Server đã được cập nhật lên {0}",
+    "MessageApplicationUpdated": "Jellyfin Server đã được cập nhật",
+    "Latest": "Gần Nhất",
+    "LabelRunningTimeValue": "Thời Gian Chạy: {0}",
+    "LabelIpAddressValue": "Địa Chỉ IP: {0}",
+    "ItemRemovedWithName": "{0} đã xóa khỏi thư viện",
+    "ItemAddedWithName": "{0} được thêm vào thư viện",
+    "Inherit": "Thừa hưởng",
+    "HomeVideos": "Video nhà",
+    "HeaderRecordingGroups": "Nhóm Ghi Video",
+    "HeaderNextUp": "Tiếp Theo",
+    "HeaderFavoriteSongs": "Bài Hát Yêu Thích",
+    "HeaderFavoriteShows": "Chương Trình Yêu Thích",
+    "HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
+    "HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
+    "HeaderFavoriteAlbums": "Album Ưa Thích",
+    "HeaderCameraUploads": "Máy Ảnh Tải Lên",
+    "FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}",
+    "DeviceOnlineWithName": "{0} đã kết nối",
+    "DeviceOfflineWithName": "{0} đã ngắt kết nối",
+    "ChapterNameValue": "Phân Cảnh {0}",
+    "Channels": "Các Kênh",
+    "CameraImageUploadedFrom": "Một hình ảnh máy ảnh mới đã được tải lên từ {0}",
+    "Books": "Sách",
+    "AuthenticationSucceededWithUserName": "{0} xác thực thành công",
+    "Application": "Ứng Dụng",
+    "AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}"
+}

+ 27 - 27
Emby.Server.Implementations/Localization/Core/zh-TW.json

@@ -1,6 +1,6 @@
 {
     "Albums": "專輯",
-    "AppDeviceValues": "軟體: {0}, 裝置: {1}",
+    "AppDeviceValues": "軟體:{0},裝置:{1}",
     "Application": "應用程式",
     "Artists": "演出者",
     "AuthenticationSucceededWithUserName": "{0} 成功授權",
@@ -11,7 +11,7 @@
     "Collections": "合輯",
     "DeviceOfflineWithName": "{0} 已經斷線",
     "DeviceOnlineWithName": "{0} 已經連線",
-    "FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試",
+    "FailedLoginAttemptWithUserName": "來自使用者 {0} 的失敗登入",
     "Favorites": "我的最愛",
     "Folders": "資料夾",
     "Genres": "風格",
@@ -28,8 +28,8 @@
     "HomeVideos": "自製影片",
     "ItemAddedWithName": "{0} 已新增至媒體庫",
     "ItemRemovedWithName": "{0} 已從媒體庫移除",
-    "LabelIpAddressValue": "IP 位置: {0}",
-    "LabelRunningTimeValue": "運行時間: {0}",
+    "LabelIpAddressValue": "IP 位址:{0}",
+    "LabelRunningTimeValue": "運行時間{0}",
     "Latest": "最新",
     "MessageApplicationUpdated": "Jellyfin Server 已經更新",
     "MessageApplicationUpdatedTo": "Jellyfin Server 已經更新至 {0}",
@@ -42,18 +42,18 @@
     "NameInstallFailed": "{0} 安裝失敗",
     "NameSeasonNumber": "第 {0} 季",
     "NameSeasonUnknown": "未知季數",
-    "NewVersionIsAvailable": "新版本的Jellyfin Server 軟體已經推出可供下載。",
+    "NewVersionIsAvailable": "新版本的 Jellyfin Server 軟體已經可供下載。",
     "NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新",
-    "NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
+    "NotificationOptionApplicationUpdateInstalled": "軟體更新已安裝",
     "NotificationOptionAudioPlayback": "音樂開始播放",
     "NotificationOptionAudioPlaybackStopped": "音樂停止播放",
     "NotificationOptionCameraImageUploaded": "相機相片已上傳",
     "NotificationOptionInstallationFailed": "安裝失敗",
     "NotificationOptionNewLibraryContent": "已新增新內容",
-    "NotificationOptionPluginError": "插件安裝錯誤",
-    "NotificationOptionPluginInstalled": "插件已安裝",
-    "NotificationOptionPluginUninstalled": "插件已移除",
-    "NotificationOptionPluginUpdateInstalled": "插件已更新",
+    "NotificationOptionPluginError": "外掛安裝失敗",
+    "NotificationOptionPluginInstalled": "外掛已安裝",
+    "NotificationOptionPluginUninstalled": "外掛已移除",
+    "NotificationOptionPluginUpdateInstalled": "外掛已更新",
     "NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
     "NotificationOptionTaskFailed": "排程任務失敗",
     "NotificationOptionUserLockedOut": "使用者已鎖定",
@@ -61,14 +61,14 @@
     "NotificationOptionVideoPlaybackStopped": "影片停止播放",
     "Photos": "相片",
     "Playlists": "播放清單",
-    "Plugin": "插件",
+    "Plugin": "外掛",
     "PluginInstalledWithName": "{0} 已安裝",
     "PluginUninstalledWithName": "{0} 已移除",
     "PluginUpdatedWithName": "{0} 已更新",
     "ProviderValue": "提供商: {0}",
-    "ScheduledTaskFailedWithName": "{0} 已失敗",
-    "ScheduledTaskStartedWithName": "{0} 已開始",
-    "ServerNameNeedsToBeRestarted": "{0} 需要重新啟動",
+    "ScheduledTaskFailedWithName": "排程任務 {0} 已失敗",
+    "ScheduledTaskStartedWithName": "排程任務 {0} 已開始",
+    "ServerNameNeedsToBeRestarted": "伺服器 {0} 需要重新啟動",
     "Shows": "節目",
     "Songs": "歌曲",
     "StartupEmbyServerIsLoading": "Jellyfin Server正在啟動,請稍後再試一次。",
@@ -78,10 +78,10 @@
     "User": "使用者",
     "UserCreatedWithName": "使用者 {0} 已建立",
     "UserDeletedWithName": "使用者 {0} 已移除",
-    "UserDownloadingItemWithValues": "{0} 正在下載 {1}",
+    "UserDownloadingItemWithValues": "使用者 {0} 正在下載 {1}",
     "UserLockedOutWithName": "使用者 {0} 已鎖定",
-    "UserOfflineFromDevice": "{0} 已從 {1} 斷線",
-    "UserOnlineFromDevice": "{0} 已連線,來自 {1}",
+    "UserOfflineFromDevice": "使用者 {0} 已從 {1} 斷線",
+    "UserOnlineFromDevice": "使用者 {0} 已從 {1} 連線",
     "UserPasswordChangedWithName": "使用者 {0} 的密碼已變更",
     "UserPolicyUpdatedWithName": "使用者條約已更新為 {0}",
     "UserStartedPlayingItemWithValues": "{0}正在使用 {2} 播放 {1}",
@@ -95,23 +95,23 @@
     "TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
     "TaskDownloadMissingSubtitles": "下載遺失的字幕",
     "TaskRefreshChannels": "重新整理頻道",
-    "TaskUpdatePlugins": "更新插件",
-    "TaskRefreshPeople": "重新整理人員",
-    "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔。",
+    "TaskUpdatePlugins": "更新外掛",
+    "TaskRefreshPeople": "刷新用戶",
+    "TaskCleanLogsDescription": "刪除超過 {0} 天的紀錄檔。",
     "TaskCleanLogs": "清空紀錄資料夾",
-    "TaskRefreshLibraryDescription": "掃描媒體庫內新的檔案並重新整理描述資料。",
-    "TaskRefreshLibrary": "掃描媒體庫",
+    "TaskRefreshLibraryDescription": "重新掃描媒體庫的新檔案並更新描述資料。",
+    "TaskRefreshLibrary": "重新掃描媒體庫",
     "TaskRefreshChapterImages": "擷取章節圖片",
-    "TaskCleanCacheDescription": "刪除系統長時間不需要的快取。",
+    "TaskCleanCacheDescription": "刪除系統不需要的快取。",
     "TaskCleanCache": "清除快取資料夾",
     "TasksLibraryCategory": "媒體庫",
-    "TaskRefreshChannelsDescription": "重新整理網頻道資料。",
+    "TaskRefreshChannelsDescription": "重新整理網頻道資料。",
     "TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。",
     "TaskCleanTranscode": "清除轉碼資料夾",
-    "TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。",
+    "TaskUpdatePluginsDescription": "為設置自動更新的外掛下載並安裝更新。",
     "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的中繼資料。",
-    "TaskRefreshChapterImagesDescription": "為有章節的視頻創建縮圖。",
-    "TasksChannelsCategory": "網頻道",
+    "TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。",
+    "TasksChannelsCategory": "網頻道",
     "TasksApplicationCategory": "應用程式",
     "TasksMaintenanceCategory": "維修"
 }

+ 1 - 0
Emby.Server.Implementations/Localization/LocalizationManager.cs

@@ -413,6 +413,7 @@ namespace Emby.Server.Implementations.Localization
             yield return new LocalizationOption("Swedish", "sv");
             yield return new LocalizationOption("Swiss German", "gsw");
             yield return new LocalizationOption("Turkish", "tr");
+            yield return new LocalizationOption("Tiếng Việt", "vi");
         }
     }
 }

+ 60 - 0
Emby.Server.Implementations/Plugins/PluginManifest.cs

@@ -0,0 +1,60 @@
+using System;
+
+namespace Emby.Server.Implementations.Plugins
+{
+    /// <summary>
+    /// Defines a Plugin manifest file.
+    /// </summary>
+    public class PluginManifest
+    {
+        /// <summary>
+        /// Gets or sets the category of the plugin.
+        /// </summary>
+        public string Category { get; set; }
+
+        /// <summary>
+        /// Gets or sets the changelog information.
+        /// </summary>
+        public string Changelog { get; set; }
+
+        /// <summary>
+        /// Gets or sets the description of the plugin.
+        /// </summary>
+        public string Description { get; set; }
+
+        /// <summary>
+        /// Gets or sets the Global Unique Identifier for the plugin.
+        /// </summary>
+        public Guid Guid { get; set; }
+
+        /// <summary>
+        /// Gets or sets the Name of the plugin.
+        /// </summary>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets an overview of the plugin.
+        /// </summary>
+        public string Overview { get; set; }
+
+        /// <summary>
+        /// Gets or sets the owner of the plugin.
+        /// </summary>
+        public string Owner { get; set; }
+
+        /// <summary>
+        /// Gets or sets the compatibility version for the plugin.
+        /// </summary>
+        public string TargetAbi { get; set; }
+
+        /// <summary>
+        /// Gets or sets the timestamp of the plugin.
+        /// </summary>
+        public DateTime Timestamp { get; set; }
+
+        /// <summary>
+        /// Gets or sets the Version number of the plugin.
+        /// </summary>
+        public string Version { get; set; }
+    }
+}

+ 28 - 31
Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs

@@ -5,10 +5,10 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
-using MediaBrowser.Model.Globalization;
 
 namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 {
@@ -21,10 +21,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         /// Gets or sets the application paths.
         /// </summary>
         /// <value>The application paths.</value>
-        private IApplicationPaths ApplicationPaths { get; set; }
-
+        private readonly IApplicationPaths _applicationPaths;
         private readonly ILogger<DeleteCacheFileTask> _logger;
-
         private readonly IFileSystem _fileSystem;
         private readonly ILocalizationManager _localization;
 
@@ -37,20 +35,41 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             IFileSystem fileSystem,
             ILocalizationManager localization)
         {
-            ApplicationPaths = appPaths;
+            _applicationPaths = appPaths;
             _logger = logger;
             _fileSystem = fileSystem;
             _localization = localization;
         }
 
+        /// <inheritdoc />
+        public string Name => _localization.GetLocalizedString("TaskCleanCache");
+
+        /// <inheritdoc />
+        public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription");
+
+        /// <inheritdoc />
+        public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+        /// <inheritdoc />
+        public string Key => "DeleteCacheFiles";
+
+        /// <inheritdoc />
+        public bool IsHidden => false;
+
+        /// <inheritdoc />
+        public bool IsEnabled => true;
+
+        /// <inheritdoc />
+        public bool IsLogged => true;
+
         /// <summary>
         /// Creates the triggers that define when the task will run.
         /// </summary>
         /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
         public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
         {
-            return new[] {
-
+            return new[]
+            {
                 // Every so often
                 new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
             };
@@ -68,7 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 
             try
             {
-                DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.CachePath, minDateModified, progress);
+                DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.CachePath, minDateModified, progress);
             }
             catch (DirectoryNotFoundException)
             {
@@ -81,7 +100,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 
             try
             {
-                DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, minDateModified, progress);
+                DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.TempDirectory, minDateModified, progress);
             }
             catch (DirectoryNotFoundException)
             {
@@ -91,7 +110,6 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             return Task.CompletedTask;
         }
 
-
         /// <summary>
         /// Deletes the cache files from directory with a last write time less than a given date.
         /// </summary>
@@ -164,26 +182,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
                 _logger.LogError(ex, "Error deleting file {path}", path);
             }
         }
-
-        /// <inheritdoc />
-        public string Name => _localization.GetLocalizedString("TaskCleanCache");
-
-        /// <inheritdoc />
-        public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription");
-
-        /// <inheritdoc />
-        public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
-
-        /// <inheritdoc />
-        public string Key => "DeleteCacheFiles";
-
-        /// <inheritdoc />
-        public bool IsHidden => false;
-
-        /// <inheritdoc />
-        public bool IsEnabled => true;
-
-        /// <inheritdoc />
-        public bool IsLogged => true;
     }
 }

+ 21 - 21
Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs

@@ -34,6 +34,27 @@ namespace Emby.Server.Implementations.ScheduledTasks
             _localization = localization;
         }
 
+        /// <inheritdoc />
+        public string Name => _localization.GetLocalizedString("TaskUpdatePlugins");
+
+        /// <inheritdoc />
+        public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription");
+
+        /// <inheritdoc />
+        public string Category => _localization.GetLocalizedString("TasksApplicationCategory");
+
+        /// <inheritdoc />
+        public string Key => "PluginUpdates";
+
+        /// <inheritdoc />
+        public bool IsHidden => false;
+
+        /// <inheritdoc />
+        public bool IsEnabled => true;
+
+        /// <inheritdoc />
+        public bool IsLogged => true;
+
         /// <summary>
         /// Creates the triggers that define when the task will run.
         /// </summary>
@@ -98,26 +119,5 @@ namespace Emby.Server.Implementations.ScheduledTasks
 
             progress.Report(100);
         }
-
-        /// <inheritdoc />
-        public string Name => _localization.GetLocalizedString("TaskUpdatePlugins");
-
-        /// <inheritdoc />
-        public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription");
-
-        /// <inheritdoc />
-        public string Category => _localization.GetLocalizedString("TasksApplicationCategory");
-
-        /// <inheritdoc />
-        public string Key => "PluginUpdates";
-
-        /// <inheritdoc />
-        public bool IsHidden => false;
-
-        /// <inheritdoc />
-        public bool IsEnabled => true;
-
-        /// <inheritdoc />
-        public bool IsLogged => true;
     }
 }

+ 6 - 6
Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs

@@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
     public class DailyTrigger : ITaskTrigger
     {
         /// <summary>
-        /// Get the time of day to trigger the task to run.
+        /// Occurs when [triggered].
+        /// </summary>
+        public event EventHandler<EventArgs> Triggered;
+
+        /// <summary>
+        /// Gets or sets the time of day to trigger the task to run.
         /// </summary>
         /// <value>The time of day.</value>
         public TimeSpan TimeOfDay { get; set; }
@@ -69,11 +74,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
             }
         }
 
-        /// <summary>
-        /// Occurs when [triggered].
-        /// </summary>
-        public event EventHandler<EventArgs> Triggered;
-
         /// <summary>
         /// Called when [triggered].
         /// </summary>

+ 7 - 7
Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs

@@ -11,6 +11,13 @@ namespace Emby.Server.Implementations.ScheduledTasks
     /// </summary>
     public class IntervalTrigger : ITaskTrigger
     {
+        private DateTime _lastStartDate;
+
+        /// <summary>
+        /// Occurs when [triggered].
+        /// </summary>
+        public event EventHandler<EventArgs> Triggered;
+
         /// <summary>
         /// Gets or sets the interval.
         /// </summary>
@@ -28,8 +35,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// <value>The timer.</value>
         private Timer Timer { get; set; }
 
-        private DateTime _lastStartDate;
-
         /// <summary>
         /// Stars waiting for the trigger action.
         /// </summary>
@@ -88,11 +93,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
             }
         }
 
-        /// <summary>
-        /// Occurs when [triggered].
-        /// </summary>
-        public event EventHandler<EventArgs> Triggered;
-
         /// <summary>
         /// Called when [triggered].
         /// </summary>

+ 6 - 9
Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs

@@ -12,6 +12,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
     /// </summary>
     public class StartupTrigger : ITaskTrigger
     {
+        /// <summary>
+        /// Occurs when [triggered].
+        /// </summary>
+        public event EventHandler<EventArgs> Triggered;
+
         public int DelayMs { get; set; }
 
         /// <summary>
@@ -48,20 +53,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
         {
         }
 
-        /// <summary>
-        /// Occurs when [triggered].
-        /// </summary>
-        public event EventHandler<EventArgs> Triggered;
-
         /// <summary>
         /// Called when [triggered].
         /// </summary>
         private void OnTriggered()
         {
-            if (Triggered != null)
-            {
-                Triggered(this, EventArgs.Empty);
-            }
+            Triggered?.Invoke(this, EventArgs.Empty);
         }
     }
 }

+ 7 - 10
Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs

@@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
     public class WeeklyTrigger : ITaskTrigger
     {
         /// <summary>
-        /// Get the time of day to trigger the task to run.
+        /// Occurs when [triggered].
+        /// </summary>
+        public event EventHandler<EventArgs> Triggered;
+
+        /// <summary>
+        /// Gets or sets the time of day to trigger the task to run.
         /// </summary>
         /// <value>The time of day.</value>
         public TimeSpan TimeOfDay { get; set; }
@@ -95,20 +100,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
             }
         }
 
-        /// <summary>
-        /// Occurs when [triggered].
-        /// </summary>
-        public event EventHandler<EventArgs> Triggered;
-
         /// <summary>
         /// Called when [triggered].
         /// </summary>
         private void OnTriggered()
         {
-            if (Triggered != null)
-            {
-                Triggered(this, EventArgs.Empty);
-            }
+            Triggered?.Invoke(this, EventArgs.Empty);
         }
     }
 }

+ 2 - 3
Emby.Server.Implementations/Security/AuthenticationRepository.cs

@@ -257,8 +257,7 @@ namespace Emby.Server.Implementations.Security
                 connection.RunInTransaction(
                     db =>
                     {
-                        var statements = PrepareAll(db, statementTexts)
-                            .ToList();
+                        var statements = PrepareAll(db, statementTexts);
 
                         using (var statement = statements[0])
                         {
@@ -282,7 +281,7 @@ namespace Emby.Server.Implementations.Security
                     ReadTransactionMode);
             }
 
-            result.Items = list.ToArray();
+            result.Items = list;
             return result;
         }
 

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

+ 2 - 2
Emby.Server.Implementations/Session/SessionManager.cs

@@ -1037,7 +1037,7 @@ namespace Emby.Server.Implementations.Session
 
             var generalCommand = new GeneralCommand
             {
-                Name = GeneralCommandType.DisplayMessage.ToString()
+                Name = GeneralCommandType.DisplayMessage
             };
 
             generalCommand.Arguments["Header"] = command.Header;
@@ -1268,7 +1268,7 @@ namespace Emby.Server.Implementations.Session
         {
             var generalCommand = new GeneralCommand
             {
-                Name = GeneralCommandType.DisplayContent.ToString(),
+                Name = GeneralCommandType.DisplayContent,
                 Arguments =
                 {
                     ["ItemId"] = command.ItemId,

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

@@ -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;
-        }
-    }
-}

部分文件因为文件数量过多而无法显示