Bläddra i källkod

Merge pull request #1 from jellyfin/master

merge with upstream master
Luke Foust 5 år sedan
förälder
incheckning
dcd0d93f44
100 ändrade filer med 1161 tillägg och 834 borttagningar
  1. 96 0
      .ci/azure-pipelines-compat.yml
  2. 101 0
      .ci/azure-pipelines-main.yml
  3. 65 0
      .ci/azure-pipelines-test.yml
  4. 82 0
      .ci/azure-pipelines-windows.yml
  5. 25 299
      .ci/azure-pipelines.yml
  6. 0 46
      .ci/publish-nightly.yml
  7. 0 48
      .ci/publish-release.yml
  8. 13 7
      .github/ISSUE_TEMPLATE/bug_report.md
  9. 4 2
      .github/ISSUE_TEMPLATE/media_playback.md
  10. 121 25
      CONTRIBUTORS.md
  11. 21 5
      Dockerfile
  12. 32 4
      Dockerfile.arm
  13. 22 5
      Dockerfile.arm64
  14. 1 1
      DvdLib/DvdLib.csproj
  15. 2 2
      DvdLib/Ifo/Dvd.cs
  16. 10 8
      Emby.Dlna/Api/DlnaServerService.cs
  17. 2 0
      Emby.Dlna/Api/DlnaService.cs
  18. 1 0
      Emby.Dlna/Common/Argument.cs
  19. 9 1
      Emby.Dlna/Common/DeviceIcon.cs
  20. 3 3
      Emby.Dlna/Common/DeviceService.cs
  21. 8 5
      Emby.Dlna/Common/ServiceAction.cs
  22. 9 8
      Emby.Dlna/Common/StateVariable.cs
  23. 17 9
      Emby.Dlna/Configuration/DlnaOptions.cs
  24. 2 0
      Emby.Dlna/ConfigurationExtension.cs
  25. 12 3
      Emby.Dlna/ConnectionManager/ConnectionManager.cs
  26. 2 0
      Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
  27. 16 14
      Emby.Dlna/ConnectionManager/ControlHandler.cs
  28. 2 0
      Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs
  29. 8 3
      Emby.Dlna/ContentDirectory/ContentDirectory.cs
  30. 2 0
      Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
  31. 185 187
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  32. 2 0
      Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs
  33. 2 0
      Emby.Dlna/ControlRequest.cs
  34. 7 5
      Emby.Dlna/ControlResponse.cs
  35. 3 2
      Emby.Dlna/Didl/DidlBuilder.cs
  36. 3 1
      Emby.Dlna/Didl/Filter.cs
  37. 2 0
      Emby.Dlna/Didl/StringWriterWithEncoding.cs
  38. 3 1
      Emby.Dlna/DlnaManager.cs
  39. 13 0
      Emby.Dlna/Emby.Dlna.csproj
  40. 8 5
      Emby.Dlna/EventSubscriptionResponse.cs
  41. 4 1
      Emby.Dlna/Eventing/EventManager.cs
  42. 4 2
      Emby.Dlna/Eventing/EventSubscription.cs
  43. 1 0
      Emby.Dlna/IConnectionManager.cs
  44. 1 0
      Emby.Dlna/IContentDirectory.cs
  45. 1 0
      Emby.Dlna/IEventManager.cs
  46. 1 0
      Emby.Dlna/IMediaReceiverRegistrar.cs
  47. 5 1
      Emby.Dlna/IUpnpService.cs
  48. 16 5
      Emby.Dlna/Main/DlnaEntryPoint.cs
  49. 22 21
      Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
  50. 11 3
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs
  51. 2 0
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
  52. 2 0
      Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs
  53. 17 15
      Emby.Dlna/PlayTo/Device.cs
  54. 2 0
      Emby.Dlna/PlayTo/DeviceInfo.cs
  55. 2 1
      Emby.Dlna/PlayTo/PlayToController.cs
  56. 6 4
      Emby.Dlna/PlayTo/PlayToManager.cs
  57. 2 0
      Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
  58. 2 0
      Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
  59. 2 0
      Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
  60. 2 0
      Emby.Dlna/PlayTo/PlaylistItem.cs
  61. 2 1
      Emby.Dlna/PlayTo/PlaylistItemFactory.cs
  62. 8 6
      Emby.Dlna/PlayTo/SsdpHttpClient.cs
  63. 2 0
      Emby.Dlna/PlayTo/TRANSPORTSTATE.cs
  64. 2 0
      Emby.Dlna/PlayTo/TransportCommands.cs
  65. 2 0
      Emby.Dlna/PlayTo/UpnpContainer.cs
  66. 2 0
      Emby.Dlna/PlayTo/uBaseObject.cs
  67. 2 0
      Emby.Dlna/PlayTo/uPnpNamespaces.cs
  68. 2 0
      Emby.Dlna/Profiles/DefaultProfile.cs
  69. 2 0
      Emby.Dlna/Profiles/DenonAvrProfile.cs
  70. 2 0
      Emby.Dlna/Profiles/DirectTvProfile.cs
  71. 2 0
      Emby.Dlna/Profiles/DishHopperJoeyProfile.cs
  72. 2 0
      Emby.Dlna/Profiles/Foobar2000Profile.cs
  73. 2 0
      Emby.Dlna/Profiles/LgTvProfile.cs
  74. 2 0
      Emby.Dlna/Profiles/LinksysDMA2100Profile.cs
  75. 2 0
      Emby.Dlna/Profiles/MarantzProfile.cs
  76. 2 0
      Emby.Dlna/Profiles/MediaMonkeyProfile.cs
  77. 2 0
      Emby.Dlna/Profiles/PanasonicVieraProfile.cs
  78. 2 0
      Emby.Dlna/Profiles/PopcornHourProfile.cs
  79. 2 0
      Emby.Dlna/Profiles/SamsungSmartTvProfile.cs
  80. 2 0
      Emby.Dlna/Profiles/SharpSmartTvProfile.cs
  81. 2 0
      Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs
  82. 2 0
      Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs
  83. 2 0
      Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs
  84. 2 0
      Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs
  85. 2 0
      Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs
  86. 2 0
      Emby.Dlna/Profiles/SonyBravia2010Profile.cs
  87. 2 0
      Emby.Dlna/Profiles/SonyBravia2011Profile.cs
  88. 2 0
      Emby.Dlna/Profiles/SonyBravia2012Profile.cs
  89. 2 0
      Emby.Dlna/Profiles/SonyBravia2013Profile.cs
  90. 2 0
      Emby.Dlna/Profiles/SonyBravia2014Profile.cs
  91. 2 0
      Emby.Dlna/Profiles/SonyPs3Profile.cs
  92. 2 0
      Emby.Dlna/Profiles/SonyPs4Profile.cs
  93. 2 0
      Emby.Dlna/Profiles/WdtvLiveProfile.cs
  94. 2 0
      Emby.Dlna/Profiles/XboxOneProfile.cs
  95. 2 1
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  96. 46 66
      Emby.Dlna/Service/BaseControlHandler.cs
  97. 3 1
      Emby.Dlna/Service/BaseService.cs
  98. 4 2
      Emby.Dlna/Service/ControlErrorHandler.cs
  99. 2 0
      Emby.Dlna/Service/ServiceXmlBuilder.cs
  100. 9 5
      Emby.Dlna/Ssdp/DeviceDiscovery.cs

+ 96 - 0
.ci/azure-pipelines-compat.yml

@@ -0,0 +1,96 @@
+parameters:
+  - name: Packages
+    type: object
+    default: {}
+  - name: LinuxImage
+    type: string
+    default: "ubuntu-latest"
+  - name: DotNetSdkVersion
+    type: string
+    default: 3.1.100
+
+jobs:
+  - job: CompatibilityCheck
+    displayName: Compatibility Check
+    pool:
+      vmImage: "${{ parameters.LinuxImage }}"
+    # only execute for pull requests
+    condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
+    strategy:
+      matrix:
+        ${{ each Package in parameters.Packages }}:
+          ${{ Package.key }}:
+            NugetPackageName: ${{ Package.value.NugetPackageName }}
+            AssemblyFileName: ${{ Package.value.AssemblyFileName }}
+      maxParallel: 2
+    dependsOn: MainBuild
+    steps:
+      - checkout: none
+
+      - task: UseDotNet@2
+        displayName: "Update DotNet"
+        inputs:
+          packageType: sdk
+          version: ${{ parameters.DotNetSdkVersion }}
+
+      - task: DownloadPipelineArtifact@2
+        displayName: "Download New Assembly Build Artifact"
+        inputs:
+          source: "current"
+          artifact: "$(NugetPackageName)"
+          path: "$(System.ArtifactsDirectory)/new-artifacts"
+          runVersion: "latest"
+
+      - task: CopyFiles@2
+        displayName: "Copy New Assembly Build Artifact"
+        inputs:
+          sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
+          contents: "**/*.dll"
+          targetFolder: $(System.ArtifactsDirectory)/new-release
+          cleanTargetFolder: true
+          overWrite: true
+          flattenFolders: true
+
+      - task: DownloadPipelineArtifact@2
+        displayName: "Download Reference Assembly Build Artifact"
+        inputs:
+          source: "specific"
+          artifact: "$(NugetPackageName)"
+          path: "$(System.ArtifactsDirectory)/current-artifacts"
+          project: "$(System.TeamProjectId)"
+          pipeline: "$(System.DefinitionId)"
+          runVersion: "latestFromBranch"
+          runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
+
+      - task: CopyFiles@2
+        displayName: "Copy Reference Assembly Build Artifact"
+        inputs:
+          sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
+          contents: "**/*.dll"
+          targetFolder: $(System.ArtifactsDirectory)/current-release
+          cleanTargetFolder: true
+          overWrite: true
+          flattenFolders: true
+
+      - task: DownloadGitHubRelease@0
+        displayName: "Download ABI Compatibility Check Tool"
+        inputs:
+          connection: Jellyfin Release Download
+          userRepository: EraYaN/dotnet-compatibility
+          defaultVersionType: "latest"
+          itemPattern: "**-ci.zip"
+          downloadPath: "$(System.ArtifactsDirectory)"
+
+      - task: ExtractFiles@1
+        displayName: "Extract ABI Compatibility Check Tool"
+        inputs:
+          archiveFilePatterns: "$(System.ArtifactsDirectory)/*-ci.zip"
+          destinationFolder: $(System.ArtifactsDirectory)/tools
+          cleanDestinationFolder: true
+
+      # The `--warnings-only` switch will swallow the return code and not emit any errors.
+      - task: CmdLine@2
+        displayName: "Execute ABI Compatibility Check Tool"
+        inputs:
+          script: "dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only"
+          workingDirectory: $(System.ArtifactsDirectory)

+ 101 - 0
.ci/azure-pipelines-main.yml

@@ -0,0 +1,101 @@
+parameters:
+  LinuxImage: "ubuntu-latest"
+  RestoreBuildProjects: "Jellyfin.Server/Jellyfin.Server.csproj"
+  DotNetSdkVersion: 3.1.100
+
+jobs:
+  - job: MainBuild
+    displayName: Main Build
+    strategy:
+      matrix:
+        Release:
+          BuildConfiguration: Release
+        Debug:
+          BuildConfiguration: Debug
+      maxParallel: 2
+    pool:
+      vmImage: "${{ parameters.LinuxImage }}"
+    steps:
+      - checkout: self
+        clean: true
+        submodules: true
+        persistCredentials: true
+
+      - task: CmdLine@2
+        displayName: "Clone Web Client (Master, Release, or Tag)"
+        condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+        inputs:
+          script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
+
+      - task: CmdLine@2
+        displayName: "Clone Web Client (PR)"
+        condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
+        inputs:
+          script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
+
+      - task: NodeTool@0
+        displayName: "Install Node"
+        condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+        inputs:
+          versionSpec: "10.x"
+
+      - task: CmdLine@2
+        displayName: "Build Web Client"
+        condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+        inputs:
+          script: yarn install && yarn build
+          workingDirectory: $(Agent.TempDirectory)/jellyfin-web
+
+      - task: CopyFiles@2
+        displayName: "Copy Web Client"
+        condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+        inputs:
+          sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist
+          contents: "**"
+          targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
+          cleanTargetFolder: true
+          overWrite: true
+          flattenFolders: false
+
+      - task: UseDotNet@2
+        displayName: "Update DotNet"
+        inputs:
+          packageType: sdk
+          version: ${{ parameters.DotNetSdkVersion }}
+
+      - task: DotNetCoreCLI@2
+        displayName: "Publish Server"
+        inputs:
+          command: publish
+          publishWebProjects: false
+          projects: "${{ parameters.RestoreBuildProjects }}"
+          arguments: "--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)"
+          zipAfterPublish: false
+
+      - task: PublishPipelineArtifact@0
+        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
+        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
+        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
+        displayName: "Publish Artifact Common"
+        condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
+        inputs:
+          targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll"
+          artifactName: "Jellyfin.Common"

+ 65 - 0
.ci/azure-pipelines-test.yml

@@ -0,0 +1,65 @@
+parameters:
+  - name: ImageNames
+    type: object
+    default:
+      Linux: "ubuntu-latest"
+      Windows: "windows-latest"
+      macOS: "macos-latest"
+  - name: TestProjects
+    type: string
+    default: "tests/**/*Tests.csproj"
+  - name: DotNetSdkVersion
+    type: string
+    default: 3.1.100
+
+jobs:
+  - job: MainTest
+    displayName: Main Test
+    strategy:
+      matrix:
+        ${{ each imageName in parameters.ImageNames }}:
+          ${{ imageName.key }}:
+            ImageName: ${{ imageName.value }}
+      maxParallel: 3
+    pool:
+      vmImage: "$(ImageName)"
+    steps:
+      - checkout: self
+        clean: true
+        submodules: true
+        persistCredentials: false
+
+      - task: UseDotNet@2
+        displayName: "Update DotNet"
+        inputs:
+          packageType: sdk
+          version: ${{ parameters.DotNetSdkVersion }}
+
+      - task: DotNetCoreCLI@2
+        displayName: Run .NET Core CLI tests
+        inputs:
+          command: "test"
+          projects: ${{ parameters.TestProjects }}
+          arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"'
+          publishTestResults: true
+          testRunTitle: $(Agent.JobName)
+          workingDirectory: "$(Build.SourcesDirectory)"
+
+      - 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: ReportGenerator (merge)
+        inputs:
+          reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
+          targetdir: "$(Agent.TempDirectory)/merged/"
+          reporttypes: "Cobertura"
+
+      ## V2 is already in the repository but it does not work "wrong number of segments" YAML error.
+      - 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
+        inputs:
+          codeCoverageTool: "cobertura"
+          #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
+          summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml"
+          pathToSources: $(Build.SourcesDirectory)
+          failIfCoverageEmpty: true

+ 82 - 0
.ci/azure-pipelines-windows.yml

@@ -0,0 +1,82 @@
+parameters:
+  WindowsImage: "windows-latest"
+  TestProjects: "tests/**/*Tests.csproj"
+  DotNetSdkVersion: 3.1.100
+
+jobs:
+  - job: PublishWindows
+    displayName: Publish Windows
+    pool:
+      vmImage: ${{ parameters.WindowsImage }}
+    steps:
+      - checkout: self
+        clean: true
+        submodules: true
+        persistCredentials: true
+
+      - task: CmdLine@2
+        displayName: "Clone Web Client (Master, Release, or Tag)"
+        condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master'), contains(variables['Build.SourceBranch'], 'tag')), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+        inputs:
+          script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
+
+      - task: CmdLine@2
+        displayName: "Clone Web Client (PR)"
+        condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest'))
+        inputs:
+          script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
+
+      - task: NodeTool@0
+        displayName: "Install Node"
+        condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+        inputs:
+          versionSpec: "10.x"
+
+      - task: CmdLine@2
+        displayName: "Build Web Client"
+        condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+        inputs:
+          script: yarn install && yarn build
+          workingDirectory: $(Agent.TempDirectory)/jellyfin-web
+
+      - task: CopyFiles@2
+        displayName: "Copy Web Client"
+        condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
+        inputs:
+          sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist
+          contents: "**"
+          targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
+          cleanTargetFolder: true
+          overWrite: true
+          flattenFolders: false
+
+      - task: CmdLine@2
+        displayName: "Clone UX Repository"
+        inputs:
+          script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux
+
+      - task: PowerShell@2
+        displayName: "Build NSIS Installer"
+        inputs:
+          targetType: "filePath"
+          filePath: ./deployment/windows/build-jellyfin.ps1
+          arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory)
+          errorActionPreference: "stop"
+          workingDirectory: $(Build.SourcesDirectory)
+
+      - task: CopyFiles@2
+        displayName: "Copy NSIS Installer"
+        inputs:
+          sourceFolder: $(Build.SourcesDirectory)/deployment/windows/
+          contents: "jellyfin*.exe"
+          targetFolder: $(System.ArtifactsDirectory)/setup
+          cleanTargetFolder: true
+          overWrite: true
+          flattenFolders: true
+
+      - task: PublishPipelineArtifact@0
+        displayName: "Publish Artifact Setup"
+        condition: succeeded()
+        inputs:
+          targetPath: "$(build.artifactstagingdirectory)/setup"
+          artifactName: "Jellyfin Server Setup"

+ 25 - 299
.ci/azure-pipelines.yml

@@ -2,9 +2,11 @@ name: $(Date:yyyyMMdd)$(Rev:.r)
 
 variables:
   - name: TestProjects
-    value: 'tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj'
+    value: "tests/**/*Tests.csproj"
   - name: RestoreBuildProjects
-    value: 'Jellyfin.Server/Jellyfin.Server.csproj'
+    value: "Jellyfin.Server/Jellyfin.Server.csproj"
+  - name: DotNetSdkVersion
+    value: 3.1.100
 
 pr:
   autoCancel: true
@@ -13,232 +15,26 @@ trigger:
   batch: true
 
 jobs:
-  - job: main_build
-    displayName: Main Build
-    pool:
-      vmImage: ubuntu-latest
-    strategy:
-      matrix:
-        Release:
-          BuildConfiguration: Release
-        Debug:
-          BuildConfiguration: Debug
-      maxParallel: 2
-    steps:
-    - checkout: self
-      clean: true
-      submodules: true
-      persistCredentials: true
-
-    - task: CmdLine@2
-      displayName: "Clone Web Client (Master, Release, or Tag)"
-      condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
-      inputs:
-        script: 'git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
-
-    - task: CmdLine@2
-      displayName: "Clone Web Client (PR)"
-      condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
-      inputs:
-        script: 'git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
-
-    - task: NodeTool@0
-      displayName: 'Install Node'
-      condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
-      inputs:
-        versionSpec: '10.x'
-
-    - task: CmdLine@2
-      displayName: "Build Web Client"
-      condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
-      inputs:
-        script: yarn install
-        workingDirectory: $(Agent.TempDirectory)/jellyfin-web
-
-    - task: CopyFiles@2
-      displayName: 'Copy Web Client'
-      condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
-      inputs:
-        sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist # Optional
-        contents: '**'
-        targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
-        cleanTargetFolder: true # Optional
-        overWrite: true # Optional
-        flattenFolders: false # Optional
-
-    - task: UseDotNet@2
-      displayName: 'Update DotNet'
-      inputs:
-        packageType: sdk
-        version: 3.1.100
-
-    - task: DotNetCoreCLI@2
-      displayName: 'Publish Server'
-      inputs:
-        command: publish
-        publishWebProjects: false
-        projects: '$(RestoreBuildProjects)'
-        arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)'
-        zipAfterPublish: false
-
-    - task: PublishPipelineArtifact@0
-      displayName: 'Publish Artifact Naming'
-      condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
-      inputs:
-        targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll'
-        artifactName: 'Jellyfin.Naming'
-
-    - task: PublishPipelineArtifact@0
-      displayName: 'Publish Artifact Controller'
-      condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
-      inputs:
-        targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
-        artifactName: 'Jellyfin.Controller'
-
-    - task: PublishPipelineArtifact@0
-      displayName: 'Publish Artifact Model'
-      condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
-      inputs:
-        targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
-        artifactName: 'Jellyfin.Model'
-
-    - task: PublishPipelineArtifact@0
-      displayName: 'Publish Artifact Common'
-      condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
-      inputs:
-        targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
-        artifactName: 'Jellyfin.Common'
-
-  - job: main_test
-    displayName: Main Test
-    pool:
-      vmImage: windows-latest
-    steps:
-    - checkout: self
-      clean: true
-      submodules: true
-      persistCredentials: false
-
-    - task: DotNetCoreCLI@2
-      displayName: Build
-      inputs:
-        command: build
-        publishWebProjects: false
-        projects: '$(TestProjects)'
-        arguments: '--configuration $(BuildConfiguration)'
-        zipAfterPublish: false
-
-    - task: VisualStudioTestPlatformInstaller@1
-      inputs:
-        packageFeedSelector: 'nugetOrg' # Options: nugetOrg, customFeed, netShare
-        versionSelector: 'latestPreRelease' # Required when packageFeedSelector == NugetOrg || PackageFeedSelector == CustomFeed# Options: latestPreRelease, latestStable, specificVersion
-
-    - task: VSTest@2
-      inputs:
-        testSelector: 'testAssemblies' # Options: testAssemblies, testPlan, testRun
-        testAssemblyVer2: | # Required when testSelector == TestAssemblies
-          **\bin\$(BuildConfiguration)\**\*test*.dll
-          !**\obj\**
-          !**\xunit.runner.visualstudio.testadapter.dll
-          !**\xunit.runner.visualstudio.dotnetcore.testadapter.dll
-        searchFolder: '$(System.DefaultWorkingDirectory)'
-        runInParallel: True # Optional
-        runTestsInIsolation: True # Optional
-        codeCoverageEnabled: True # Optional
-        configuration: 'Debug' # Optional
-        publishRunAttachments: true # Optional
-
-  - job: main_build_win
-    displayName: Publish Windows
-    pool:
-      vmImage: windows-latest
-    strategy:
-      matrix:
-        Release:
-          BuildConfiguration: Release
-      maxParallel: 2
-    steps:
-    - checkout: self
-      clean: true
-      submodules: true
-      persistCredentials: true
-
-    - task: CmdLine@2
-      displayName: "Clone Web Client (Master, Release, or Tag)"
-      condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master'), contains(variables['Build.SourceBranch'], 'tag')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
-      inputs:
-        script: 'git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
-
-    - task: CmdLine@2
-      displayName: "Clone Web Client (PR)"
-      condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
-      inputs:
-        script: 'git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
-
-    - task: NodeTool@0
-      displayName: 'Install Node'
-      condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
-      inputs:
-        versionSpec: '10.x'
-
-    - task: CmdLine@2
-      displayName: "Build Web Client"
-      condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
-      inputs:
-        script: yarn install
-        workingDirectory: $(Agent.TempDirectory)/jellyfin-web
-
-    - task: CopyFiles@2
-      displayName: 'Copy Web Client'
-      condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
-      inputs:
-        sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist # Optional
-        contents: '**'
-        targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
-        cleanTargetFolder: true # Optional
-        overWrite: true # Optional
-        flattenFolders: false # Optional
-
-    - task: CmdLine@2
-      displayName: 'Clone UX Repository'
-      inputs:
-        script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux
-
-    - task: PowerShell@2
-      displayName: 'Build NSIS Installer'
-      inputs:
-        targetType: 'filePath' # Optional. Options: filePath, inline
-        filePath: ./deployment/windows/build-jellyfin.ps1 # Required when targetType == FilePath
-        arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory)
-        errorActionPreference: 'stop' # Optional. Options: stop, continue, silentlyContinue
-        workingDirectory: $(Build.SourcesDirectory) # Optional
-
-    - task: CopyFiles@2
-      displayName: 'Copy NSIS Installer'
-      inputs:
-        sourceFolder: $(Build.SourcesDirectory)/deployment/windows/ # Optional
-        contents: 'jellyfin*.exe'
-        targetFolder: $(System.ArtifactsDirectory)/setup
-        cleanTargetFolder: true # Optional
-        overWrite: true # Optional
-        flattenFolders: true # Optional
-
-    - task: PublishPipelineArtifact@0
-      displayName: 'Publish Artifact Setup'
-      condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
-      inputs:
-        targetPath: '$(build.artifactstagingdirectory)/setup'
-        artifactName: 'Jellyfin Server Setup'
-
-  - job: dotnet_compat
-    displayName: Compatibility Check
-    pool:
-      vmImage: ubuntu-latest
-    dependsOn: main_build
-    # only execute for pull requests
-    condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
-    strategy:
-      matrix:
+  - template: azure-pipelines-main.yml
+    parameters:
+      LinuxImage: "ubuntu-latest"
+      RestoreBuildProjects: $(RestoreBuildProjects)
+
+  - template: azure-pipelines-test.yml
+    parameters:
+      ImageNames:
+        Linux: "ubuntu-latest"
+        Windows: "windows-latest"
+        macOS: "macos-latest"
+
+  - template: azure-pipelines-windows.yml
+    parameters:
+      WindowsImage: "windows-latest"
+      TestProjects: $(TestProjects)
+
+  - template: azure-pipelines-compat.yml
+    parameters:
+      Packages:
         Naming:
           NugetPackageName: Jellyfin.Naming
           AssemblyFileName: Emby.Naming.dll
@@ -251,74 +47,4 @@ jobs:
         Common:
           NugetPackageName: Jellyfin.Common
           AssemblyFileName: MediaBrowser.Common.dll
-      maxParallel: 2
-    steps:
-    - checkout: none
-    
-    - task: UseDotNet@2
-      displayName: 'Update DotNet'
-      inputs:
-        packageType: sdk
-        version: 3.1.100
-
-    - task: DownloadPipelineArtifact@2
-      displayName: 'Download New Assembly Build Artifact'
-      inputs:
-        source: 'current' # Options: current, specific
-        artifact: '$(NugetPackageName)' # Optional
-        path: '$(System.ArtifactsDirectory)/new-artifacts'
-        runVersion: 'latest' # Required when source == Specific. Options: latest, latestFromBranch, specific
-
-    - task: CopyFiles@2
-      displayName: 'Copy New Assembly Build Artifact'
-      inputs:
-        sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional
-        contents: '**/*.dll'
-        targetFolder: $(System.ArtifactsDirectory)/new-release
-        cleanTargetFolder: true # Optional
-        overWrite: true # Optional
-        flattenFolders: true # Optional
-
-    - task: DownloadPipelineArtifact@2
-      displayName: 'Download Reference Assembly Build Artifact'
-      inputs:
-        source: 'specific' # Options: current, specific
-        artifact: '$(NugetPackageName)' # Optional
-        path: '$(System.ArtifactsDirectory)/current-artifacts'
-        project: '$(System.TeamProjectId)' # Required when source == Specific
-        pipeline: '$(System.DefinitionId)' # Required when source == Specific
-        runVersion: 'latestFromBranch' # Required when source == Specific. Options: latest, latestFromBranch, specific
-        runBranch: 'refs/heads/$(System.PullRequest.TargetBranch)' # Required when source == Specific && runVersion == LatestFromBranch
-
-    - task: CopyFiles@2
-      displayName: 'Copy Reference Assembly Build Artifact'
-      inputs:
-        sourceFolder: $(System.ArtifactsDirectory)/current-artifacts # Optional
-        contents: '**/*.dll'
-        targetFolder: $(System.ArtifactsDirectory)/current-release
-        cleanTargetFolder: true # Optional
-        overWrite: true # Optional
-        flattenFolders: true # Optional
-
-    - task: DownloadGitHubRelease@0
-      displayName: 'Download ABI Compatibility Check Tool'
-      inputs:
-        connection: Jellyfin Release Download
-        userRepository: EraYaN/dotnet-compatibility
-        defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag
-        itemPattern: '**-ci.zip' # Optional
-        downloadPath: '$(System.ArtifactsDirectory)'
-
-    - task: ExtractFiles@1
-      displayName: 'Extract ABI Compatibility Check Tool'
-      inputs:
-        archiveFilePatterns: '$(System.ArtifactsDirectory)/*-ci.zip'
-        destinationFolder: $(System.ArtifactsDirectory)/tools
-        cleanDestinationFolder: true
-
-    # The `--warnings-only` switch will swallow the return code and not emit any errors.
-    - task: CmdLine@2
-      displayName: 'Execute ABI Compatibility Check Tool'
-      inputs:
-        script: 'dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only'
-        workingDirectory: $(System.ArtifactsDirectory) # Optional
+      LinuxImage: "ubuntu-latest"

+ 0 - 46
.ci/publish-nightly.yml

@@ -1,46 +0,0 @@
-name: Nightly-$(date:yyyyMMdd).$(rev:r)
-
-variables:
-  - name: Version
-    value: '1.0.0'
-
-trigger: none
-pr: none
-
-jobs:
-  - job: publish_artifacts_nightly
-    displayName: Publish Artifacts Nightly
-    pool:
-      vmImage: ubuntu-latest
-    steps:
-    - checkout: none
-    - task: DownloadPipelineArtifact@2
-      displayName: Download the Windows Setup Artifact
-      inputs:
-        source: 'specific' # Options: current, specific
-        artifact: 'Jellyfin Server Setup' # Optional
-        path: '$(System.ArtifactsDirectory)/win-installer'
-        project: '$(System.TeamProjectId)' # Required when source == Specific
-        pipelineId: 1 # Required when source == Specific
-        runVersion: 'latestFromBranch' # Required when source == Specific. Options: latest, latestFromBranch, specific
-        runBranch: 'refs/heads/master' # Required when source == Specific && runVersion == LatestFromBranch
-
-    - task: SSH@0
-      displayName: 'Create Drop directory'
-      inputs:
-        sshEndpoint: 'Jellyfin Build Server'
-        commands: 'mkdir -p /srv/incoming/jellyfin_$(Version)/win-installer && ln -s /srv/incoming/jellyfin_$(Version) /srv/incoming/jellyfin_nightly_azure_upload'
-
-    - task: CopyFilesOverSSH@0
-      displayName: 'Copy the Windows Setup to the Repo'
-      inputs:
-        sshEndpoint: 'Jellyfin Build Server'
-        sourceFolder: '$(System.ArtifactsDirectory)/win-installer'
-        contents: 'jellyfin_*.exe'
-        targetFolder: '/srv/incoming/jellyfin_nightly_azure_upload/win-installer'
-
-    - task: SSH@0
-      displayName: 'Clean up SCP symlink'
-      inputs:
-        sshEndpoint: 'Jellyfin Build Server'
-        commands: 'rm -f /srv/incoming/jellyfin_nightly_azure_upload'

+ 0 - 48
.ci/publish-release.yml

@@ -1,48 +0,0 @@
-name: Release-$(Version)-$(date:yyyyMMdd).$(rev:r)
-
-variables:
-  - name: Version
-    value: '1.0.0'
-  - name: UsedRunId
-    value: 0
-
-trigger: none
-pr: none
-
-jobs:
-  - job: publish_artifacts_release
-    displayName: Publish Artifacts Release
-    pool:
-      vmImage: ubuntu-latest
-    steps:
-    - checkout: none
-    - task: DownloadPipelineArtifact@2
-      displayName: Download the Windows Setup Artifact
-      inputs:
-        source: 'specific' # Options: current, specific
-        artifact: 'Jellyfin Server Setup' # Optional
-        path: '$(System.ArtifactsDirectory)/win-installer'
-        project: '$(System.TeamProjectId)' # Required when source == Specific
-        pipelineId: 1 # Required when source == Specific
-        runVersion: 'specific' # Required when source == Specific. Options: latest, latestFromBranch, specific
-        runId: $(UsedRunId)
-
-    - task: SSH@0
-      displayName: 'Create Drop directory'
-      inputs:
-        sshEndpoint: 'Jellyfin Build Server'
-        commands: 'mkdir -p /srv/incoming/jellyfin_$(Version)/win-installer && ln -s /srv/incoming/jellyfin_$(Version) /srv/incoming/jellyfin_release_azure_upload'
-
-    - task: CopyFilesOverSSH@0
-      displayName: 'Copy the Windows Setup to the Repo'
-      inputs:
-        sshEndpoint: 'Jellyfin Build Server'
-        sourceFolder: '$(System.ArtifactsDirectory)/win-installer'
-        contents: 'jellyfin_*.exe'
-        targetFolder: '/srv/incoming/jellyfin_release_azure_upload/win-installer'
-
-    - task: SSH@0
-      displayName: 'Clean up SCP symlink'
-      inputs:
-        sshEndpoint: 'Jellyfin Build Server'
-        commands: 'rm -f /srv/incoming/jellyfin_release_azure_upload'

+ 13 - 7
.github/ISSUE_TEMPLATE/bug_report.md

@@ -10,6 +10,19 @@ assignees: ''
 **Describe the bug**
 <!-- A clear and concise description of what the bug is. -->
 
+**System (please complete the following information):**
+ - OS: [e.g. Debian, Windows]
+ - Virtualization: [e.g. Docker, KVM, LXC]
+ - Clients: [Browser, Android, Fire Stick, etc.]
+ - Browser: [e.g. Firefox 72, Chrome 80, Safari 13]
+ - Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
+ - Playback: [Direct Play, Remux, Direct Stream, Transcode] 
+ - Installed Plugins: [e.g. none, Fanart, Anime, etc.]
+ - Reverse Proxy: [e.g. none, nginx, apache, etc.]
+ - Base URL: [e.g. none, yes: /example]
+ - Networking: [e.g. Host, Bridge/NAT]
+ - Storage: [e.g. local, NFS, cloud]
+
 **To Reproduce**
 <!-- Steps to reproduce the behavior: -->
 1. Go to '...'
@@ -26,12 +39,5 @@ assignees: ''
 **Screenshots**
 <!-- If applicable, add screenshots to help explain your problem. -->
 
-**System (please complete the following information):**
- - OS: [e.g. Docker, Debian, Windows]
- - Browser: [e.g. Firefox, Chrome, Safari]
- - Jellyfin Version: [e.g. 10.0.1]
- - Installed Plugins: [e.g. none, Fanart, Anime, etc.]
- - Reverse proxy: [e.g. no, nginx, apache, etc.]
-
 **Additional context**
 <!-- Add any other context about the problem here. -->

+ 4 - 2
.github/ISSUE_TEMPLATE/media_playback.md

@@ -11,7 +11,10 @@ assignees: ''
 <!-- Use the Media Info tool (set to text format, download here: https://mediaarea.net/en/MediaInfo) or copy the info from the web ui for the file with the playback issue. -->
 
 **Logs**
-<!-- Please paste any log message from during the playback issue, for example the ffmpeg command line can be very useful. -->
+<!-- Please paste any log messages from during the playback issue. -->
+
+**FFmpeg Logs**
+<!-- Please paste any FFmpeg logs if remuxing or transcoding appears to be part of the issue. -->
 
 **Stats for Nerds Screenshots**
 <!-- If available, add screenshots of the stats for nerds screen to help show the issue problem. -->
@@ -29,4 +32,3 @@ assignees: ''
  - Client: [e.g. Web/Browser, webOS, Android, Android TV, Electron]
  - Browser (if Web client): [e.g. Firefox, Chrome, Safari]
  - Client and Browser Version: [e.g. 10.3.4 and 68.0]
-

+ 121 - 25
CONTRIBUTORS.md

@@ -1,37 +1,133 @@
 # Jellyfin Contributors
 
- - [JoshuaBoniface](https://github.com/joshuaboniface)
- - [nvllsvm](https://github.com/nvllsvm)
- - [JustAMan](https://github.com/JustAMan)
+ - [97carmine](https://github.com/97carmine)
+ - [Abbe98](https://github.com/Abbe98)
+ - [agrenott](https://github.com/agrenott)
+ - [AndreCarvalho](https://github.com/AndreCarvalho)
+ - [anthonylavado](https://github.com/anthonylavado)
+ - [Artiume](https://github.com/Artiume)
+ - [AThomsen](https://github.com/AThomsen)
+ - [bilde2910](https://github.com/bilde2910)
+ - [bfayers](https://github.com/bfayers)
+ - [BnMcG](https://github.com/BnMcG)
+ - [Bond-009](https://github.com/Bond-009)
+ - [brianjmurrell](https://github.com/brianjmurrell)
+ - [bugfixin](https://github.com/bugfixin)
+ - [chaosinnovator](https://github.com/chaosinnovator)
+ - [ckcr4lyf](https://github.com/ckcr4lyf)
+ - [crankdoofus](https://github.com/crankdoofus)
+ - [crobibero](https://github.com/crobibero)
+ - [cromefire](https://github.com/cromefire)
+ - [cryptobank](https://github.com/cryptobank)
+ - [cvium](https://github.com/cvium)
+ - [dannymichel](https://github.com/dannymichel)
+ - [DaveChild](https://github.com/DaveChild)
  - [dcrdev](https://github.com/dcrdev)
+ - [dhartung](https://github.com/dhartung)
+ - [dinki](https://github.com/dinki)
+ - [dkanada](https://github.com/dkanada)
+ - [dlahoti](https://github.com/dlahoti)
+ - [dmitrylyzo](https://github.com/dmitrylyzo)
+ - [DMouse10462](https://github.com/DMouse10462)
+ - [DrPandemic](https://github.com/DrPandemic)
  - [EraYaN](https://github.com/EraYaN)
+ - [escabe](https://github.com/escabe)
+ - [excelite](https://github.com/excelite)
+ - [fasheng](https://github.com/fasheng)
+ - [ferferga](https://github.com/ferferga)
+ - [fhriley](https://github.com/fhriley)
  - [flemse](https://github.com/flemse)
- - [bfayers](https://github.com/bfayers)
- - [Bond_009](https://github.com/Bond-009)
- - [AnthonyLavado](https://github.com/anthonylavado)
- - [sparky8251](https://github.com/sparky8251)
- - [LeoVerto](https://github.com/LeoVerto)
+ - [Froghut](https://github.com/Froghut)
+ - [fruhnow](https://github.com/fruhnow)
+ - [geilername](https://github.com/geilername)
+ - [gnattu](https://github.com/gnattu)
  - [grafixeyehero](https://github.com/grafixeyehero)
- - [cvium](https://github.com/cvium)
- - [wtayl0r](https://github.com/wtayl0r)
- - [TtheCreator](https://github.com/Tthecreator)
- - [dkanada](https://github.com/dkanada)
- - [LogicalPhallacy](https://github.com/LogicalPhallacy/)
- - [RazeLighter777](https://github.com/RazeLighter777)
- - [WillWill56](https://github.com/WillWill56)
+ - [h1nk](https://github.com/h1nk)
+ - [hawken93](https://github.com/hawken93)
+ - [HelloWorld017](https://github.com/HelloWorld017)
+ - [jftuga](https://github.com/jftuga)
+ - [joern-h](https://github.com/joern-h)
+ - [joshuaboniface](https://github.com/joshuaboniface)
+ - [JustAMan](https://github.com/JustAMan)
+ - [justinfenn](https://github.com/justinfenn)
+ - [KerryRJ](https://github.com/KerryRJ)
+ - [Larvitar](https://github.com/Larvitar)
+ - [LeoVerto](https://github.com/LeoVerto)
  - [Liggy](https://github.com/Liggy)
- - [fruhnow](https://github.com/fruhnow)
+ - [LogicalPhallacy](https://github.com/LogicalPhallacy)
+ - [loli10K](https://github.com/loli10K)
+ - [lostmypillow](https://github.com/lostmypillow)
  - [Lynxy](https://github.com/Lynxy)
- - [fasheng](https://github.com/fasheng)
- - [ploughpuff](https://github.com/ploughpuff)
- - [pjeanjean](https://github.com/pjeanjean)
- - [DrPandemic](https://github.com/drpandemic)
- - [joern-h](https://github.com/joern-h)
- - [Khinenw](https://github.com/HelloWorld017)
- - [fhriley](https://github.com/fhriley)
- - [nevado](https://github.com/nevado)
+ - [ManfredRichthofen](https://github.com/ManfredRichthofen)
+ - [Marenz](https://github.com/Marenz)
+ - [marius-luca-87](https://github.com/marius-luca-87)
  - [mark-monteiro](https://github.com/mark-monteiro)
- - [ullmie02](https://github.com/ullmie02)
+ - [Matt07211](https://github.com/Matt07211)
+ - [mcarlton00](https://github.com/mcarlton00)
+ - [mitchfizz05](https://github.com/mitchfizz05)
+ - [MrTimscampi](https://github.com/MrTimscampi)
+ - [n8225](https://github.com/n8225)
+ - [Narfinger](https://github.com/Narfinger)
+ - [NathanPickard](https://github.com/NathanPickard)
+ - [neilsb](https://github.com/neilsb)
+ - [nevado](https://github.com/nevado)
+ - [Nickbert7](https://github.com/Nickbert7)
+ - [nvllsvm](https://github.com/nvllsvm)
+ - [nyanmisaka](https://github.com/nyanmisaka)
+ - [oddstr13](https://github.com/oddstr13)
+ - [petermcneil](https://github.com/petermcneil)
+ - [Phlogi](https://github.com/Phlogi)
+ - [pjeanjean](https://github.com/pjeanjean)
+ - [ploughpuff](https://github.com/ploughpuff)
+ - [pR0Ps](https://github.com/pR0Ps)
+ - [PrplHaz4](https://github.com/PrplHaz4)
+ - [RazeLighter777](https://github.com/RazeLighter777)
+ - [redSpoutnik](https://github.com/redSpoutnik)
+ - [ringmatter](https://github.com/ringmatter)
+ - [ryan-hartzell](https://github.com/ryan-hartzell)
+ - [s0urcelab](https://github.com/s0urcelab)
+ - [sachk](https://github.com/sachk)
+ - [sammyrc34](https://github.com/sammyrc34)
+ - [samuel9554](https://github.com/samuel9554)
+ - [scheidleon](https://github.com/scheidleon)
+ - [sebPomme](https://github.com/sebPomme)
+ - [SegiH](https://github.com/SegiH)
+ - [SenorSmartyPants](https://github.com/SenorSmartyPants)
+ - [shemanaev](https://github.com/shemanaev)
+ - [skaro13](https://github.com/skaro13)
+ - [sl1288](https://github.com/sl1288)
+ - [sorinyo2004](https://github.com/sorinyo2004)
+ - [sparky8251](https://github.com/sparky8251)
+ - [stanionascu](https://github.com/stanionascu)
+ - [stevehayles](https://github.com/stevehayles)
+ - [SuperSandro2000](https://github.com/SuperSandro2000)
+ - [tbraeutigam](https://github.com/tbraeutigam)
+ - [teacupx](https://github.com/teacupx)
+ - [Terror-Gene](https://github.com/Terror-Gene)
+ - [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu)
+ - [ThibaultNocchi](https://github.com/ThibaultNocchi)
+ - [thornbill](https://github.com/thornbill)
+ - [ThreeFive-O](https://github.com/ThreeFive-O)
+ - [TrisMcC](https://github.com/TrisMcC)
+ - [trumblejoe](https://github.com/trumblejoe)
+ - [TtheCreator](https://github.com/TtheCreator)
+ - [twinkybot](https://github.com/twinkybot)
+ - [Ullmie02](https://github.com/Ullmie02)
+ - [Unhelpful](https://github.com/Unhelpful)
+ - [viaregio](https://github.com/viaregio)
+ - [vitorsemeano](https://github.com/vitorsemeano)
+ - [voodoos](https://github.com/voodoos)
+ - [whooo](https://github.com/whooo)
+ - [WiiPlayer2](https://github.com/WiiPlayer2)
+ - [WillWill56](https://github.com/WillWill56)
+ - [wtayl0r](https://github.com/wtayl0r)
+ - [Wuerfelbecher](https://github.com/Wuerfelbecher)
+ - [Wunax](https://github.com/Wunax)
+ - [WWWesten](https://github.com/WWWesten)
+ - [WX9yMOXWId](https://github.com/WX9yMOXWId)
+ - [xosdy](https://github.com/xosdy)
+ - [XVicarious](https://github.com/XVicarious)
+ - [YouKnowBlom](https://github.com/YouKnowBlom)
 
 # Emby Contributors
 

+ 21 - 5
Dockerfile

@@ -3,7 +3,7 @@ ARG FFMPEG_VERSION=latest
 
 FROM node:alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl \
+RUN apk add curl git \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
  && yarn install \
@@ -14,11 +14,20 @@ FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder
 WORKDIR /repo
 COPY . .
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
+# 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"
 
 FROM jellyfin/ffmpeg:${FFMPEG_VERSION} as ffmpeg
 FROM debian:buster-slim
 
+# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
+ARG DEBIAN_FRONTEND="noninteractive"
+# http://stackoverflow.com/questions/48162574/ddg#49462622
+ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
+# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
+ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
+
 COPY --from=ffmpeg /opt/ffmpeg /opt/ffmpeg
 COPY --from=builder /jellyfin /jellyfin
 COPY --from=web-builder /dist /jellyfin/jellyfin-web
@@ -29,9 +38,16 @@ COPY --from=web-builder /dist /jellyfin/jellyfin-web
 #   mesa-va-drivers: needed for VAAPI
 RUN apt-get update \
  && apt-get install --no-install-recommends --no-install-suggests -y \
-   libfontconfig1 libgomp1 libva-drm2 mesa-va-drivers openssl \
- && apt-get clean autoclean \
- && apt-get autoremove \
+   libfontconfig1 \
+   libgomp1 \
+   libva-drm2 \
+   mesa-va-drivers \
+   openssl \
+   ca-certificates \
+   vainfo \
+   i965-va-driver \
+ && apt-get clean autoclean -y\
+ && apt-get autoremove -y\
  && rm -rf /var/lib/apt/lists/* \
  && mkdir -p /cache /config /media \
  && chmod 777 /cache /config /media \

+ 32 - 4
Dockerfile.arm

@@ -1,3 +1,5 @@
+# DESIGNED FOR BUILDING ON AMD64 ONLY
+#####################################
 # Requires binfm_misc registration
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 ARG DOTNET_VERSION=3.1
@@ -5,7 +7,7 @@ ARG DOTNET_VERSION=3.1
 
 FROM node:alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl \
+RUN apk add curl git \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
  && yarn install \
@@ -24,10 +26,36 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin"
 
 
 FROM multiarch/qemu-user-static:x86_64-arm as qemu
-FROM debian:stretch-slim-arm32v7
+FROM arm32v7/debian:buster-slim
+
+# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
+ARG DEBIAN_FRONTEND="noninteractive"
+# http://stackoverflow.com/questions/48162574/ddg#49462622
+ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
+# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
+ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
+
 COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
 RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
+ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
+ curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
+ curl -s https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \
+ echo 'deb [arch=armhf] https://repo.jellyfin.org/debian buster main' > /etc/apt/sources.list.d/jellyfin.list && \
+ echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \
+ apt-get update && \
+ apt-get install --no-install-recommends --no-install-suggests -y \
+ jellyfin-ffmpeg \
+ libssl-dev \
+ libfontconfig1 \
+ libfreetype6 \
+ libomxil-bellagio0 \
+ libomxil-bellagio-bin \
+ libraspberrypi0 \
+ vainfo \
+ libva2 \
+ && apt-get remove curl gnupg -y \
+ && apt-get clean autoclean -y \
+ && apt-get autoremove -y \
  && rm -rf /var/lib/apt/lists/* \
  && mkdir -p /cache /config /media \
  && chmod 777 /cache /config /media
@@ -41,4 +69,4 @@ VOLUME /cache /config /media
 ENTRYPOINT ["./jellyfin/jellyfin", \
     "--datadir", "/config", \
     "--cachedir", "/cache", \
-    "--ffmpeg", "/usr/bin/ffmpeg"]
+    "--ffmpeg", "/usr/lib/jellyfin-ffmpeg"]

+ 22 - 5
Dockerfile.arm64

@@ -1,3 +1,5 @@
+# DESIGNED FOR BUILDING ON AMD64 ONLY
+#####################################
 # Requires binfm_misc registration
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 ARG DOTNET_VERSION=3.1
@@ -5,7 +7,7 @@ ARG DOTNET_VERSION=3.1
 
 FROM node:alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl \
+RUN apk add curl git \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
  && yarn install \
@@ -22,12 +24,27 @@ 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"
 
-
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
-FROM debian:stretch-slim-arm64v8
+FROM arm64v8/debian:buster-slim
+
+# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
+ARG DEBIAN_FRONTEND="noninteractive"
+# http://stackoverflow.com/questions/48162574/ddg#49462622
+ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
+# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
+ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
+
 COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
-RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
+RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \ 
+ ffmpeg \
+ libssl-dev \
+ ca-certificates \
+ libfontconfig1 \
+ libfreetype6 \
+ libomxil-bellagio0 \
+ libomxil-bellagio-bin \
+ && apt-get clean autoclean -y \
+ && apt-get autoremove -y \
  && rm -rf /var/lib/apt/lists/* \
  && mkdir -p /cache /config /media \
  && chmod 777 /cache /config /media

+ 1 - 1
DvdLib/DvdLib.csproj

@@ -9,7 +9,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 2 - 2
DvdLib/Ifo/Dvd.cs

@@ -42,7 +42,7 @@ namespace DvdLib.Ifo
             }
             else
             {
-                using (var vmgFs = _fileSystem.GetFileStream(vmgPath.FullName, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read))
+                using (var vmgFs = new FileStream(vmgPath.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))
                 {
                     using (var vmgRead = new BigEndianBinaryReader(vmgFs))
                     {
@@ -95,7 +95,7 @@ namespace DvdLib.Ifo
         {
             VTSPaths[vtsNum] = vtsPath;
 
-            using (var vtsFs = _fileSystem.GetFileStream(vtsPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read))
+            using (var vtsFs = new FileStream(vtsPath, FileMode.Open, FileAccess.Read, FileShare.Read))
             {
                 using (var vtsRead = new BigEndianBinaryReader(vtsFs))
                 {

+ 10 - 8
Emby.Dlna/Api/DlnaServerService.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.IO;
 using System.Text;
@@ -170,32 +172,32 @@ namespace Emby.Dlna.Api
             return _resultFactory.GetResult(Request, xml, XMLContentType);
         }
 
-        public object Post(ProcessMediaReceiverRegistrarControlRequest request)
+        public async Task<object> Post(ProcessMediaReceiverRegistrarControlRequest request)
         {
-            var response = PostAsync(request.RequestStream, MediaReceiverRegistrar);
+            var response = await PostAsync(request.RequestStream, MediaReceiverRegistrar).ConfigureAwait(false);
 
             return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
         }
 
-        public object Post(ProcessContentDirectoryControlRequest request)
+        public async Task<object> Post(ProcessContentDirectoryControlRequest request)
         {
-            var response = PostAsync(request.RequestStream, ContentDirectory);
+            var response = await PostAsync(request.RequestStream, ContentDirectory).ConfigureAwait(false);
 
             return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
         }
 
-        public object Post(ProcessConnectionManagerControlRequest request)
+        public async Task<object> Post(ProcessConnectionManagerControlRequest request)
         {
-            var response = PostAsync(request.RequestStream, ConnectionManager);
+            var response = await PostAsync(request.RequestStream, ConnectionManager).ConfigureAwait(false);
 
             return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
         }
 
-        private ControlResponse PostAsync(Stream requestStream, IUpnpService service)
+        private Task<ControlResponse> PostAsync(Stream requestStream, IUpnpService service)
         {
             var id = GetPathValue(2).ToString();
 
-            return service.ProcessControlRequest(new ControlRequest
+            return service.ProcessControlRequestAsync(new ControlRequest
             {
                 Headers = Request.Headers,
                 InputXml = requestStream,

+ 2 - 0
Emby.Dlna/Api/DlnaService.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Linq;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Net;

+ 1 - 0
Emby.Dlna/Common/Argument.cs

@@ -1,3 +1,4 @@
+#pragma warning disable CS1591
 
 namespace Emby.Dlna.Common
 {

+ 9 - 1
Emby.Dlna/Common/DeviceIcon.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+
+using System.Globalization;
 
 namespace Emby.Dlna.Common
 {
@@ -13,9 +16,14 @@ namespace Emby.Dlna.Common
 
         public string Depth { get; set; }
 
+        /// <inheritdoc />
         public override string ToString()
         {
-            return string.Format("{0}x{1}", Height, Width);
+            return string.Format(
+                CultureInfo.InvariantCulture,
+                "{0}x{1}",
+                Height,
+                Width);
         }
     }
 }

+ 3 - 3
Emby.Dlna/Common/DeviceService.cs

@@ -1,3 +1,4 @@
+#pragma warning disable CS1591
 
 namespace Emby.Dlna.Common
 {
@@ -13,9 +14,8 @@ namespace Emby.Dlna.Common
 
         public string EventSubUrl { get; set; }
 
+        /// <inheritdoc />
         public override string ToString()
-        {
-            return string.Format("{0}", ServiceId);
-        }
+            => ServiceId;
     }
 }

+ 8 - 5
Emby.Dlna/Common/ServiceAction.cs

@@ -1,21 +1,24 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 
 namespace Emby.Dlna.Common
 {
     public class ServiceAction
     {
+        public ServiceAction()
+        {
+            ArgumentList = new List<Argument>();
+        }
+
         public string Name { get; set; }
 
         public List<Argument> ArgumentList { get; set; }
 
+        /// <inheritdoc />
         public override string ToString()
         {
             return Name;
         }
-
-        public ServiceAction()
-        {
-            ArgumentList = new List<Argument>();
-        }
     }
 }

+ 9 - 8
Emby.Dlna/Common/StateVariable.cs

@@ -1,9 +1,16 @@
+#pragma warning disable CS1591
+
 using System;
 
 namespace Emby.Dlna.Common
 {
     public class StateVariable
     {
+        public StateVariable()
+        {
+            AllowedValues = Array.Empty<string>();
+        }
+
         public string Name { get; set; }
 
         public string DataType { get; set; }
@@ -12,14 +19,8 @@ namespace Emby.Dlna.Common
 
         public string[] AllowedValues { get; set; }
 
+        /// <inheritdoc />
         public override string ToString()
-        {
-            return Name;
-        }
-
-        public StateVariable()
-        {
-            AllowedValues = Array.Empty<string>();
-        }
+            => Name;
     }
 }

+ 17 - 9
Emby.Dlna/Configuration/DlnaOptions.cs

@@ -1,17 +1,9 @@
+#pragma warning disable CS1591
 
 namespace Emby.Dlna.Configuration
 {
     public class DlnaOptions
     {
-        public bool EnablePlayTo { get; set; }
-        public bool EnableServer { get; set; }
-        public bool EnableDebugLog { get; set; }
-        public bool BlastAliveMessages { get; set; }
-        public bool SendOnlyMatchedHost { get; set; }
-        public int ClientDiscoveryIntervalSeconds { get; set; }
-        public int BlastAliveMessageIntervalSeconds { get; set; }
-        public string DefaultUserId { get; set; }
-
         public DlnaOptions()
         {
             EnablePlayTo = true;
@@ -21,5 +13,21 @@ namespace Emby.Dlna.Configuration
             ClientDiscoveryIntervalSeconds = 60;
             BlastAliveMessageIntervalSeconds = 1800;
         }
+
+        public bool EnablePlayTo { get; set; }
+
+        public bool EnableServer { get; set; }
+
+        public bool EnableDebugLog { get; set; }
+
+        public bool BlastAliveMessages { get; set; }
+
+        public bool SendOnlyMatchedHost { get; set; }
+
+        public int ClientDiscoveryIntervalSeconds { get; set; }
+
+        public int BlastAliveMessageIntervalSeconds { get; set; }
+
+        public string DefaultUserId { get; set; }
     }
 }

+ 2 - 0
Emby.Dlna/ConfigurationExtension.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using Emby.Dlna.Configuration;
 using MediaBrowser.Common.Configuration;

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

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+
+using System.Threading.Tasks;
 using Emby.Dlna.Service;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
@@ -12,7 +15,11 @@ namespace Emby.Dlna.ConnectionManager
         private readonly ILogger _logger;
         private readonly IServerConfigurationManager _config;
 
-        public ConnectionManager(IDlnaManager dlna, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient)
+        public ConnectionManager(
+            IDlnaManager dlna,
+            IServerConfigurationManager config,
+            ILogger<ConnectionManager> logger,
+            IHttpClient httpClient)
             : base(logger, httpClient)
         {
             _dlna = dlna;
@@ -20,17 +27,19 @@ namespace Emby.Dlna.ConnectionManager
             _logger = logger;
         }
 
+        /// <inheritdoc />
         public string GetServiceXml()
         {
             return new ConnectionManagerXmlBuilder().GetXml();
         }
 
-        public ControlResponse ProcessControlRequest(ControlRequest request)
+        /// <inheritdoc />
+        public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
         {
             var profile = _dlna.GetProfile(request.Headers) ??
                          _dlna.GetDefaultProfile();
 
-            return new ControlHandler(_config, _logger, profile).ProcessControlRequest(request);
+            return new ControlHandler(_config, _logger, profile).ProcessControlRequestAsync(request);
         }
     }
 }

+ 2 - 0
Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using Emby.Dlna.Common;
 using Emby.Dlna.Service;

+ 16 - 14
Emby.Dlna/ConnectionManager/ControlHandler.cs

@@ -1,5 +1,8 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
+using System.Xml;
 using Emby.Dlna.Service;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
@@ -12,29 +15,28 @@ namespace Emby.Dlna.ConnectionManager
     {
         private readonly DeviceProfile _profile;
 
-        protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams)
+        public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile)
+            : base(config, logger)
+        {
+            _profile = profile;
+        }
+
+        /// <inheritdoc />
+        protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
         {
             if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
             {
-                return HandleGetProtocolInfo();
+                HandleGetProtocolInfo(xmlWriter);
+                return;
             }
 
             throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
         }
 
-        private IEnumerable<KeyValuePair<string, string>> HandleGetProtocolInfo()
+        private void HandleGetProtocolInfo(XmlWriter xmlWriter)
         {
-            return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
-            {
-                { "Source", _profile.ProtocolInfo },
-                { "Sink", "" }
-            };
-        }
-
-        public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile)
-            : base(config, logger)
-        {
-            _profile = profile;
+            xmlWriter.WriteElementString("Source", _profile.ProtocolInfo);
+            xmlWriter.WriteElementString("Sink", string.Empty);
         }
     }
 }

+ 2 - 0
Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using Emby.Dlna.Common;
 

+ 8 - 3
Emby.Dlna/ContentDirectory/ContentDirectory.cs

@@ -1,4 +1,7 @@
+#pragma warning disable CS1591
+
 using System;
+using System.Threading.Tasks;
 using Emby.Dlna.Service;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
@@ -34,7 +37,7 @@ namespace Emby.Dlna.ContentDirectory
             ILibraryManager libraryManager,
             IServerConfigurationManager config,
             IUserManager userManager,
-            ILogger logger,
+            ILogger<ContentDirectory> logger,
             IHttpClient httpClient,
             ILocalizationManager localization,
             IMediaSourceManager mediaSourceManager,
@@ -66,12 +69,14 @@ namespace Emby.Dlna.ContentDirectory
             }
         }
 
+        /// <inheritdoc />
         public string GetServiceXml()
         {
             return new ContentDirectoryXmlBuilder().GetXml();
         }
 
-        public ControlResponse ProcessControlRequest(ControlRequest request)
+        /// <inheritdoc />
+        public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
         {
             var profile = _dlna.GetProfile(request.Headers) ??
                           _dlna.GetDefaultProfile();
@@ -96,7 +101,7 @@ namespace Emby.Dlna.ContentDirectory
                 _userViewManager,
                 _mediaEncoder,
                 _tvSeriesManager)
-                .ProcessControlRequest(request);
+                .ProcessControlRequestAsync(request);
         }
 
         private User GetUser(DeviceProfile profile)

+ 2 - 0
Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using Emby.Dlna.Common;
 using Emby.Dlna.Service;

+ 185 - 187
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -44,7 +46,6 @@ namespace Emby.Dlna.ContentDirectory
         private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
 
         private readonly int _systemUpdateId;
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
         private readonly DidlBuilder _didlBuilder;
 
@@ -58,7 +59,8 @@ namespace Emby.Dlna.ContentDirectory
             string accessToken,
             IImageProcessor imageProcessor,
             IUserDataManager userDataManager,
-            User user, int systemUpdateId,
+            User user,
+            int systemUpdateId,
             IServerConfigurationManager config,
             ILocalizationManager localization,
             IMediaSourceManager mediaSourceManager,
@@ -76,117 +78,132 @@ namespace Emby.Dlna.ContentDirectory
             _profile = profile;
             _config = config;
 
-            _didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, _logger, mediaEncoder);
+            _didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, Logger, mediaEncoder);
         }
 
-        protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams)
+        /// <inheritdoc />
+        protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
         {
-            var deviceId = "test";
-
-            var user = _user;
+            const string DeviceId = "test";
 
             if (string.Equals(methodName, "GetSearchCapabilities", StringComparison.OrdinalIgnoreCase))
-                return HandleGetSearchCapabilities();
+            {
+                HandleGetSearchCapabilities(xmlWriter);
+                return;
+            }
 
             if (string.Equals(methodName, "GetSortCapabilities", StringComparison.OrdinalIgnoreCase))
-                return HandleGetSortCapabilities();
+            {
+                HandleGetSortCapabilities(xmlWriter);
+                return;
+            }
 
             if (string.Equals(methodName, "GetSortExtensionCapabilities", StringComparison.OrdinalIgnoreCase))
-                return HandleGetSortExtensionCapabilities();
+            {
+                HandleGetSortExtensionCapabilities(xmlWriter);
+                return;
+            }
 
             if (string.Equals(methodName, "GetSystemUpdateID", StringComparison.OrdinalIgnoreCase))
-                return HandleGetSystemUpdateID();
+            {
+                HandleGetSystemUpdateID(xmlWriter);
+                return;
+            }
 
             if (string.Equals(methodName, "Browse", StringComparison.OrdinalIgnoreCase))
-                return HandleBrowse(methodParams, user, deviceId);
+            {
+                HandleBrowse(xmlWriter, methodParams, DeviceId);
+                return;
+            }
 
             if (string.Equals(methodName, "X_GetFeatureList", StringComparison.OrdinalIgnoreCase))
-                return HandleXGetFeatureList();
+            {
+                HandleXGetFeatureList(xmlWriter);
+                return;
+            }
 
             if (string.Equals(methodName, "GetFeatureList", StringComparison.OrdinalIgnoreCase))
-                return HandleGetFeatureList();
+            {
+                HandleGetFeatureList(xmlWriter);
+                return;
+            }
 
             if (string.Equals(methodName, "X_SetBookmark", StringComparison.OrdinalIgnoreCase))
-                return HandleXSetBookmark(methodParams, user);
+            {
+                HandleXSetBookmark(methodParams);
+                return;
+            }
 
             if (string.Equals(methodName, "Search", StringComparison.OrdinalIgnoreCase))
-                return HandleSearch(methodParams, user, deviceId);
+            {
+                HandleSearch(xmlWriter, methodParams, DeviceId);
+                return;
+            }
 
             if (string.Equals(methodName, "X_BrowseByLetter", StringComparison.OrdinalIgnoreCase))
-                return HandleX_BrowseByLetter(methodParams, user, deviceId);
+            {
+                HandleXBrowseByLetter(xmlWriter, methodParams, DeviceId);
+                return;
+            }
 
             throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
         }
 
-        private IEnumerable<KeyValuePair<string, string>> HandleXSetBookmark(IDictionary<string, string> sparams, User user)
+        private void HandleXSetBookmark(IDictionary<string, string> sparams)
         {
             var id = sparams["ObjectID"];
 
-            var serverItem = GetItemFromObjectId(id, user);
+            var serverItem = GetItemFromObjectId(id, _user);
 
             var item = serverItem.Item;
 
-            var newbookmark = int.Parse(sparams["PosSecond"], _usCulture);
+            var newbookmark = int.Parse(sparams["PosSecond"], CultureInfo.InvariantCulture);
 
-            var userdata = _userDataManager.GetUserData(user, item);
+            var userdata = _userDataManager.GetUserData(_user, item);
 
             userdata.PlaybackPositionTicks = TimeSpan.FromSeconds(newbookmark).Ticks;
 
-            _userDataManager.SaveUserData(user, item, userdata, UserDataSaveReason.TogglePlayed,
+            _userDataManager.SaveUserData(_user, item, userdata, UserDataSaveReason.TogglePlayed,
                 CancellationToken.None);
-
-            return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
         }
 
-        private IEnumerable<KeyValuePair<string, string>> HandleGetSearchCapabilities()
+        private void HandleGetSearchCapabilities(XmlWriter xmlWriter)
         {
-            return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
-            {
-                { "SearchCaps", "res@resolution,res@size,res@duration,dc:title,dc:creator,upnp:actor,upnp:artist,upnp:genre,upnp:album,dc:date,upnp:class,@id,@refID,@protocolInfo,upnp:author,dc:description,pv:avKeywords" }
-            };
+            xmlWriter.WriteElementString(
+                "SearchCaps",
+                "res@resolution,res@size,res@duration,dc:title,dc:creator,upnp:actor,upnp:artist,upnp:genre,upnp:album,dc:date,upnp:class,@id,@refID,@protocolInfo,upnp:author,dc:description,pv:avKeywords");
         }
 
-        private IEnumerable<KeyValuePair<string, string>> HandleGetSortCapabilities()
+        private void HandleGetSortCapabilities(XmlWriter xmlWriter)
         {
-            return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
-            {
-                { "SortCaps", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating" }
-            };
+            xmlWriter.WriteElementString(
+                "SortCaps",
+                "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating");
         }
 
-        private IEnumerable<KeyValuePair<string, string>> HandleGetSortExtensionCapabilities()
+        private void HandleGetSortExtensionCapabilities(XmlWriter xmlWriter)
         {
-            return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
-            {
-                { "SortExtensionCaps", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating" }
-            };
+            xmlWriter.WriteElementString(
+                "SortExtensionCaps",
+                "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating");
         }
 
-        private IEnumerable<KeyValuePair<string, string>> HandleGetSystemUpdateID()
+        private void HandleGetSystemUpdateID(XmlWriter xmlWriter)
         {
-            var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-            headers.Add("Id", _systemUpdateId.ToString(_usCulture));
-            return headers;
+            xmlWriter.WriteElementString("Id", _systemUpdateId.ToString(CultureInfo.InvariantCulture));
         }
 
-        private IEnumerable<KeyValuePair<string, string>> HandleGetFeatureList()
+        private void HandleGetFeatureList(XmlWriter xmlWriter)
         {
-            return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
-            {
-                { "FeatureList", GetFeatureListXml() }
-            };
+            xmlWriter.WriteElementString("FeatureList", WriteFeatureListXml());
         }
 
-        private IEnumerable<KeyValuePair<string, string>> HandleXGetFeatureList()
-        {
-            return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
-            {
-                { "FeatureList", GetFeatureListXml() }
-            };
-        }
+        private void HandleXGetFeatureList(XmlWriter xmlWriter)
+            => HandleGetFeatureList(xmlWriter);
 
-        private string GetFeatureListXml()
+        private string WriteFeatureListXml()
         {
+            // TODO: clean this up
             var builder = new StringBuilder();
 
             builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
@@ -213,7 +230,7 @@ namespace Emby.Dlna.ContentDirectory
             return defaultValue;
         }
 
-        private IEnumerable<KeyValuePair<string, string>> HandleBrowse(IDictionary<string, string> sparams, User user, string deviceId)
+        private void HandleBrowse(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
         {
             var id = sparams["ObjectID"];
             var flag = sparams["BrowseFlag"];
@@ -237,101 +254,95 @@ namespace Emby.Dlna.ContentDirectory
                 start = startVal;
             }
 
-            var settings = new XmlWriterSettings
-            {
-                Encoding = Encoding.UTF8,
-                CloseOutput = false,
-                OmitXmlDeclaration = true,
-                ConformanceLevel = ConformanceLevel.Fragment
-            };
-
-            StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8);
-
             int totalCount;
 
-            var dlnaOptions = _config.GetDlnaConfiguration();
-
-            using (var writer = XmlWriter.Create(builder, settings))
+            using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
             {
-                //writer.WriteStartDocument();
-
-                writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
-
-                writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
-                writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
-                writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
-                //didl.SetAttribute("xmlns:sec", NS_SEC);
-
-                DidlBuilder.WriteXmlRootAttributes(_profile, writer);
-
-                var serverItem = GetItemFromObjectId(id, user);
-                var item = serverItem.Item;
+                var settings = new XmlWriterSettings()
+                {
+                    Encoding = Encoding.UTF8,
+                    CloseOutput = false,
+                    OmitXmlDeclaration = true,
+                    ConformanceLevel = ConformanceLevel.Fragment
+                };
 
-                if (string.Equals(flag, "BrowseMetadata"))
+                using (var writer = XmlWriter.Create(builder, settings))
                 {
-                    totalCount = 1;
+                    writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
 
-                    if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
-                    {
-                        var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount);
+                    writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
+                    writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
+                    writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
 
-                        _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
-                    }
-                    else
-                    {
-                        _didlBuilder.WriteItemElement(dlnaOptions, writer, item, user, null, null, deviceId, filter);
-                    }
+                    DidlBuilder.WriteXmlRootAttributes(_profile, writer);
 
-                    provided++;
-                }
-                else
-                {
-                    var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount);
-                    totalCount = childrenResult.TotalRecordCount;
+                    var serverItem = GetItemFromObjectId(id, _user);
+                    var item = serverItem.Item;
 
-                    provided = childrenResult.Items.Count;
 
-                    foreach (var i in childrenResult.Items)
+                    if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal))
                     {
-                        var childItem = i.Item;
-                        var displayStubType = i.StubType;
+                        totalCount = 1;
 
-                        if (childItem.IsDisplayedAsFolder || displayStubType.HasValue)
+                        if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
                         {
-                            var childCount = (GetUserItems(childItem, displayStubType, user, sortCriteria, null, 0))
-                                .TotalRecordCount;
+                            var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
 
-                            _didlBuilder.WriteFolderElement(writer, childItem, displayStubType, item, childCount, filter);
+                            _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
                         }
                         else
                         {
-                            _didlBuilder.WriteItemElement(dlnaOptions, writer, childItem, user, item, serverItem.StubType, deviceId, filter);
+                            var dlnaOptions = _config.GetDlnaConfiguration();
+                            _didlBuilder.WriteItemElement(dlnaOptions, writer, item, _user, null, null, deviceId, filter);
+                        }
+
+                        provided++;
+                    }
+                    else
+                    {
+                        var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
+                        totalCount = childrenResult.TotalRecordCount;
+
+                        provided = childrenResult.Items.Count;
+
+                        var dlnaOptions = _config.GetDlnaConfiguration();
+                        foreach (var i in childrenResult.Items)
+                        {
+                            var childItem = i.Item;
+                            var displayStubType = i.StubType;
+
+                            if (childItem.IsDisplayedAsFolder || displayStubType.HasValue)
+                            {
+                                var childCount = GetUserItems(childItem, displayStubType, _user, sortCriteria, null, 0)
+                                    .TotalRecordCount;
+
+                                _didlBuilder.WriteFolderElement(writer, childItem, displayStubType, item, childCount, filter);
+                            }
+                            else
+                            {
+                                _didlBuilder.WriteItemElement(dlnaOptions, writer, childItem, _user, item, serverItem.StubType, deviceId, filter);
+                            }
                         }
                     }
+
+                    writer.WriteFullEndElement();
                 }
 
-                writer.WriteFullEndElement();
-                //writer.WriteEndDocument();
+                xmlWriter.WriteElementString("Result", builder.ToString());
             }
 
-            var resXML = builder.ToString();
-
-            return new[]
-                {
-                    new KeyValuePair<string,string>("Result", resXML),
-                    new KeyValuePair<string,string>("NumberReturned", provided.ToString(_usCulture)),
-                    new KeyValuePair<string,string>("TotalMatches", totalCount.ToString(_usCulture)),
-                    new KeyValuePair<string,string>("UpdateID", _systemUpdateId.ToString(_usCulture))
-                };
+            xmlWriter.WriteElementString("NumberReturned", provided.ToString(CultureInfo.InvariantCulture));
+            xmlWriter.WriteElementString("TotalMatches", totalCount.ToString(CultureInfo.InvariantCulture));
+            xmlWriter.WriteElementString("UpdateID", _systemUpdateId.ToString(CultureInfo.InvariantCulture));
         }
 
-        private IEnumerable<KeyValuePair<string, string>> HandleX_BrowseByLetter(IDictionary<string, string> sparams, User user, string deviceId)
+        private void HandleXBrowseByLetter(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
         {
             // TODO: Implement this method
-            return HandleSearch(sparams, user, deviceId);
+            HandleSearch(xmlWriter, sparams, deviceId);
         }
 
-        private IEnumerable<KeyValuePair<string, string>> HandleSearch(IDictionary<string, string> sparams, User user, string deviceId)
+        private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
         {
             var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", ""));
             var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", ""));
@@ -354,99 +365,86 @@ namespace Emby.Dlna.ContentDirectory
                 start = startVal;
             }
 
-            var settings = new XmlWriterSettings
-            {
-                Encoding = Encoding.UTF8,
-                CloseOutput = false,
-                OmitXmlDeclaration = true,
-                ConformanceLevel = ConformanceLevel.Fragment
-            };
-
-            StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8);
-            int totalCount = 0;
-            int provided = 0;
+            QueryResult<BaseItem> childrenResult;
 
-            using (var writer = XmlWriter.Create(builder, settings))
+            using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
             {
-                //writer.WriteStartDocument();
-
-                writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
-
-                writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
-                writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
-                writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
-                //didl.SetAttribute("xmlns:sec", NS_SEC);
+                var settings = new XmlWriterSettings()
+                {
+                    Encoding = Encoding.UTF8,
+                    CloseOutput = false,
+                    OmitXmlDeclaration = true,
+                    ConformanceLevel = ConformanceLevel.Fragment
+                };
 
-                DidlBuilder.WriteXmlRootAttributes(_profile, writer);
+                using (var writer = XmlWriter.Create(builder, settings))
+                {
+                    writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
 
-                var serverItem = GetItemFromObjectId(sparams["ContainerID"], user);
+                    writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
+                    writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
+                    writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
 
-                var item = serverItem.Item;
+                    DidlBuilder.WriteXmlRootAttributes(_profile, writer);
 
-                var childrenResult = (GetChildrenSorted(item, user, searchCriteria, sortCriteria, start, requestedCount));
+                    var serverItem = GetItemFromObjectId(sparams["ContainerID"], _user);
 
-                totalCount = childrenResult.TotalRecordCount;
+                    var item = serverItem.Item;
 
-                provided = childrenResult.Items.Count;
+                    childrenResult = GetChildrenSorted(item, _user, searchCriteria, sortCriteria, start, requestedCount);
 
-                var dlnaOptions = _config.GetDlnaConfiguration();
+                    var dlnaOptions = _config.GetDlnaConfiguration();
 
-                foreach (var i in childrenResult.Items)
-                {
-                    if (i.IsDisplayedAsFolder)
+                    foreach (var i in childrenResult.Items)
                     {
-                        var childCount = (GetChildrenSorted(i, user, searchCriteria, sortCriteria, null, 0))
-                            .TotalRecordCount;
+                        if (i.IsDisplayedAsFolder)
+                        {
+                            var childCount = GetChildrenSorted(i, _user, searchCriteria, sortCriteria, null, 0)
+                                .TotalRecordCount;
 
-                        _didlBuilder.WriteFolderElement(writer, i, null, item, childCount, filter);
-                    }
-                    else
-                    {
-                        _didlBuilder.WriteItemElement(dlnaOptions, writer, i, user, item, serverItem.StubType, deviceId, filter);
+                            _didlBuilder.WriteFolderElement(writer, i, null, item, childCount, filter);
+                        }
+                        else
+                        {
+                            _didlBuilder.WriteItemElement(dlnaOptions, writer, i, _user, item, serverItem.StubType, deviceId, filter);
+                        }
                     }
+
+                    writer.WriteFullEndElement();
                 }
 
-                writer.WriteFullEndElement();
-                //writer.WriteEndDocument();
+                xmlWriter.WriteElementString("Result", builder.ToString());
             }
 
-            var resXML = builder.ToString();
-
-            return new List<KeyValuePair<string, string>>
-                {
-                    new KeyValuePair<string,string>("Result", resXML),
-                    new KeyValuePair<string,string>("NumberReturned", provided.ToString(_usCulture)),
-                    new KeyValuePair<string,string>("TotalMatches", totalCount.ToString(_usCulture)),
-                    new KeyValuePair<string,string>("UpdateID", _systemUpdateId.ToString(_usCulture))
-                };
+            xmlWriter.WriteElementString("NumberReturned", childrenResult.Items.Count.ToString(CultureInfo.InvariantCulture));
+            xmlWriter.WriteElementString("TotalMatches", childrenResult.TotalRecordCount.ToString(CultureInfo.InvariantCulture));
+            xmlWriter.WriteElementString("UpdateID", _systemUpdateId.ToString(CultureInfo.InvariantCulture));
         }
 
         private QueryResult<BaseItem> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit)
         {
             var folder = (Folder)item;
 
-            var sortOrders = new List<(string, SortOrder)>();
-            if (!folder.IsPreSorted)
-            {
-                sortOrders.Add((ItemSortBy.SortName, sort.SortOrder));
-            }
+            var sortOrders = folder.IsPreSorted
+                ? Array.Empty<(string, SortOrder)>()
+                : new[] { (ItemSortBy.SortName, sort.SortOrder) };
 
-            var mediaTypes = new List<string>();
+            string[] mediaTypes = Array.Empty<string>();
             bool? isFolder = null;
 
             if (search.SearchType == SearchType.Audio)
             {
-                mediaTypes.Add(MediaType.Audio);
+                mediaTypes = new[] { MediaType.Audio };
                 isFolder = false;
             }
             else if (search.SearchType == SearchType.Video)
             {
-                mediaTypes.Add(MediaType.Video);
+                mediaTypes = new[] { MediaType.Video };
                 isFolder = false;
             }
             else if (search.SearchType == SearchType.Image)
             {
-                mediaTypes.Add(MediaType.Photo);
+                mediaTypes = new[] { MediaType.Photo };
                 isFolder = false;
             }
             else if (search.SearchType == SearchType.Playlist)
@@ -470,7 +468,7 @@ namespace Emby.Dlna.ContentDirectory
                 IsMissing = false,
                 ExcludeItemTypes = new[] { typeof(Book).Name },
                 IsFolder = isFolder,
-                MediaTypes = mediaTypes.ToArray(),
+                MediaTypes = mediaTypes,
                 DtoOptions = GetDtoOptions()
             });
         }
@@ -771,11 +769,11 @@ namespace Emby.Dlna.ContentDirectory
                 })
                 .ToArray();
 
-            return new QueryResult<ServerItem>
+            return ApplyPaging(new QueryResult<ServerItem>
             {
                 Items = folders,
                 TotalRecordCount = folders.Length
-            };
+            }, startIndex, limit);
         }
 
         private QueryResult<ServerItem> GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit)
@@ -1304,11 +1302,11 @@ namespace Emby.Dlna.ContentDirectory
             StubType? stubType = null;
 
             // After using PlayTo, MediaMonkey sends a request to the server trying to get item info
-            const string paramsSrch = "Params=";
-            var paramsIndex = id.IndexOf(paramsSrch, StringComparison.OrdinalIgnoreCase);
+            const string ParamsSrch = "Params=";
+            var paramsIndex = id.IndexOf(ParamsSrch, StringComparison.OrdinalIgnoreCase);
             if (paramsIndex != -1)
             {
-                id = id.Substring(paramsIndex + paramsSrch.Length);
+                id = id.Substring(paramsIndex + ParamsSrch.Length);
 
                 var parts = id.Split(';');
                 id = parts[23];
@@ -1336,7 +1334,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 - 0
Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using Emby.Dlna.Common;
 

+ 2 - 0
Emby.Dlna/ControlRequest.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.IO;
 using Microsoft.AspNetCore.Http;
 

+ 7 - 5
Emby.Dlna/ControlResponse.cs

@@ -1,18 +1,20 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 
 namespace Emby.Dlna
 {
     public class ControlResponse
     {
+        public ControlResponse()
+        {
+            Headers = new Dictionary<string, string>();
+        }
+
         public IDictionary<string, string> Headers { get; set; }
 
         public string Xml { get; set; }
 
         public bool IsSuccessful { get; set; }
-
-        public ControlResponse()
-        {
-            Headers = new Dictionary<string, string>();
-        }
     }
 }

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

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Globalization;
 using System.IO;
@@ -18,7 +20,6 @@ using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Net;
 using Microsoft.Extensions.Logging;
@@ -632,7 +633,7 @@ namespace Emby.Dlna.Didl
             {
                 if (item.PremiereDate.HasValue)
                 {
-                    AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o"), NS_DC);
+                    AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NS_DC);
                 }
             }
 

+ 3 - 1
Emby.Dlna/Didl/Filter.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using MediaBrowser.Model.Extensions;
 
@@ -16,7 +18,7 @@ namespace Emby.Dlna.Didl
 
         public Filter(string filter)
         {
-            _all = StringHelper.EqualsIgnoreCase(filter, "*");
+            _all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
 
             _fields = (filter ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
         }

+ 2 - 0
Emby.Dlna/Didl/StringWriterWithEncoding.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.IO;
 using System.Text;

+ 3 - 1
Emby.Dlna/DlnaManager.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -385,7 +387,7 @@ namespace Emby.Dlna
                     {
                         Directory.CreateDirectory(systemProfilesPath);
 
-                        using (var fileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+                        using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
                         {
                             await stream.CopyToAsync(fileStream);
                         }

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

@@ -15,6 +15,19 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
+  </PropertyGroup>
+
+  <!-- Code Analyzers-->
+  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+    <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+  </ItemGroup>
+
+  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
 
   <ItemGroup>

+ 8 - 5
Emby.Dlna/EventSubscriptionResponse.cs

@@ -1,17 +1,20 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 
 namespace Emby.Dlna
 {
     public class EventSubscriptionResponse
     {
-        public string Content { get; set; }
-        public string ContentType { get; set; }
-
-        public Dictionary<string, string> Headers { get; set; }
-
         public EventSubscriptionResponse()
         {
             Headers = new Dictionary<string, string>();
         }
+
+        public string Content { get; set; }
+
+        public string ContentType { get; set; }
+
+        public Dictionary<string, string> Headers { get; set; }
     }
 }

+ 4 - 1
Emby.Dlna/Eventing/EventManager.cs

@@ -1,8 +1,11 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Net.Http;
 using System.Text;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
@@ -164,7 +167,7 @@ namespace Emby.Dlna.Eventing
 
             try
             {
-                using (await _httpClient.SendAsync(options, "NOTIFY").ConfigureAwait(false))
+                using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false))
                 {
 
                 }

+ 4 - 2
Emby.Dlna/Eventing/EventSubscription.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 
 namespace Emby.Dlna.Eventing
@@ -13,6 +15,8 @@ namespace Emby.Dlna.Eventing
 
         public long TriggerCount { get; set; }
 
+        public bool IsExpired => SubscriptionTime.AddSeconds(TimeoutSeconds) >= DateTime.UtcNow;
+
         public void IncrementTriggerCount()
         {
             if (TriggerCount == long.MaxValue)
@@ -22,7 +26,5 @@ namespace Emby.Dlna.Eventing
 
             TriggerCount++;
         }
-
-        public bool IsExpired => SubscriptionTime.AddSeconds(TimeoutSeconds) >= DateTime.UtcNow;
     }
 }

+ 1 - 0
Emby.Dlna/IConnectionManager.cs

@@ -1,3 +1,4 @@
+#pragma warning disable CS1591
 
 namespace Emby.Dlna
 {

+ 1 - 0
Emby.Dlna/IContentDirectory.cs

@@ -1,3 +1,4 @@
+#pragma warning disable CS1591
 
 namespace Emby.Dlna
 {

+ 1 - 0
Emby.Dlna/IEventManager.cs

@@ -1,3 +1,4 @@
+#pragma warning disable CS1591
 
 namespace Emby.Dlna
 {

+ 1 - 0
Emby.Dlna/IMediaReceiverRegistrar.cs

@@ -1,3 +1,4 @@
+#pragma warning disable CS1591
 
 namespace Emby.Dlna
 {

+ 5 - 1
Emby.Dlna/IUpnpService.cs

@@ -1,3 +1,7 @@
+#pragma warning disable CS1591
+
+using System.Threading.Tasks;
+
 namespace Emby.Dlna
 {
     public interface IUpnpService
@@ -13,6 +17,6 @@ namespace Emby.Dlna
         /// </summary>
         /// <param name="request">The request.</param>
         /// <returns>ControlResponse.</returns>
-        ControlResponse ProcessControlRequest(ControlRequest request);
+        Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request);
     }
 }

+ 16 - 5
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -1,6 +1,8 @@
+#pragma warning disable CS1591
+
 using System;
-using System.Net.Sockets;
 using System.Globalization;
+using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
 using Emby.Dlna.PlayTo;
@@ -24,7 +26,7 @@ using MediaBrowser.Model.System;
 using Microsoft.Extensions.Logging;
 using Rssdp;
 using Rssdp.Infrastructure;
-using OperatingSystem =  MediaBrowser.Common.System.OperatingSystem;
+using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
 
 namespace Emby.Dlna.Main
 {
@@ -56,7 +58,9 @@ namespace Emby.Dlna.Main
         private ISsdpCommunicationsServer _communicationsServer;
 
         internal IContentDirectory ContentDirectory { get; private set; }
+
         internal IConnectionManager ConnectionManager { get; private set; }
+
         internal IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
 
         public static DlnaEntryPoint Current;
@@ -104,7 +108,7 @@ namespace Emby.Dlna.Main
                 libraryManager,
                 config,
                 userManager,
-                _logger,
+                loggerFactory.CreateLogger<ContentDirectory.ContentDirectory>(),
                 httpClient,
                 localizationManager,
                 mediaSourceManager,
@@ -112,9 +116,16 @@ namespace Emby.Dlna.Main
                 mediaEncoder,
                 tvSeriesManager);
 
-            ConnectionManager = new ConnectionManager.ConnectionManager(dlnaManager, config, _logger, httpClient);
+            ConnectionManager = new ConnectionManager.ConnectionManager(
+                dlnaManager,
+                config,
+                loggerFactory.CreateLogger<ConnectionManager.ConnectionManager>(),
+                httpClient);
 
-            MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(_logger, httpClient, config);
+            MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(
+                loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrar>(),
+                httpClient,
+                config);
             Current = this;
         }
 

+ 22 - 21
Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs

@@ -1,5 +1,8 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
+using System.Xml;
 using Emby.Dlna.Service;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
@@ -9,35 +12,33 @@ namespace Emby.Dlna.MediaReceiverRegistrar
 {
     public class ControlHandler : BaseControlHandler
     {
-        protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams)
+        public ControlHandler(IServerConfigurationManager config, ILogger logger)
+            : base(config, logger)
         {
-            if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
-                return HandleIsAuthorized();
-            if (string.Equals(methodName, "IsValidated", StringComparison.OrdinalIgnoreCase))
-                return HandleIsValidated();
-
-            throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
         }
 
-        private static IEnumerable<KeyValuePair<string, string>> HandleIsAuthorized()
+        /// <inheritdoc />
+        protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
         {
-            return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+            if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
             {
-                { "Result", "1" }
-            };
-        }
+                HandleIsAuthorized(xmlWriter);
+                return;
+            }
 
-        private static IEnumerable<KeyValuePair<string, string>> HandleIsValidated()
-        {
-            return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+            if (string.Equals(methodName, "IsValidated", StringComparison.OrdinalIgnoreCase))
             {
-                { "Result", "1" }
-            };
-        }
+                HandleIsValidated(xmlWriter);
+                return;
+            }
 
-        public ControlHandler(IServerConfigurationManager config, ILogger logger)
-            : base(config, logger)
-        {
+            throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
         }
+
+        private static void HandleIsAuthorized(XmlWriter xmlWriter)
+            => xmlWriter.WriteElementString("Result", "1");
+
+        private static void HandleIsValidated(XmlWriter xmlWriter)
+            => xmlWriter.WriteElementString("Result", "1");
     }
 }

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

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+
+using System.Threading.Tasks;
 using Emby.Dlna.Service;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
@@ -9,23 +12,28 @@ namespace Emby.Dlna.MediaReceiverRegistrar
     {
         private readonly IServerConfigurationManager _config;
 
-        public MediaReceiverRegistrar(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config)
+        public MediaReceiverRegistrar(
+            ILogger<MediaReceiverRegistrar> logger,
+            IHttpClient httpClient,
+            IServerConfigurationManager config)
             : base(logger, httpClient)
         {
             _config = config;
         }
 
+        /// <inheritdoc />
         public string GetServiceXml()
         {
             return new MediaReceiverRegistrarXmlBuilder().GetXml();
         }
 
-        public ControlResponse ProcessControlRequest(ControlRequest request)
+        /// <inheritdoc />
+        public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
         {
             return new ControlHandler(
                 _config,
                 Logger)
-                .ProcessControlRequest(request);
+                .ProcessControlRequestAsync(request);
         }
     }
 }

+ 2 - 0
Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using Emby.Dlna.Common;
 using Emby.Dlna.Service;

+ 2 - 0
Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using Emby.Dlna.Common;
 

+ 17 - 15
Emby.Dlna/PlayTo/Device.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -221,7 +223,7 @@ namespace Emby.Dlna.PlayTo
             _logger.LogDebug("Setting mute");
             var value = mute ? 1 : 0;
 
-            await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
                 .ConfigureAwait(false);
 
             IsMuted = mute;
@@ -251,7 +253,7 @@ namespace Emby.Dlna.PlayTo
             // Remote control will perform better
             Volume = value;
 
-            await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
                 .ConfigureAwait(false);
         }
 
@@ -270,7 +272,7 @@ namespace Emby.Dlna.PlayTo
                 throw new InvalidOperationException("Unable to find service");
             }
 
-            await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
+            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
                 .ConfigureAwait(false);
 
             RestartTimer(true);
@@ -302,7 +304,7 @@ namespace Emby.Dlna.PlayTo
             }
 
             var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
-            await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
+            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
                 .ConfigureAwait(false);
 
             await Task.Delay(50).ConfigureAwait(false);
@@ -344,7 +346,7 @@ namespace Emby.Dlna.PlayTo
                 throw new InvalidOperationException("Unable to find service");
             }
 
-            return new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1));
+            return new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1));
         }
 
         public async Task SetPlay(CancellationToken cancellationToken)
@@ -368,7 +370,7 @@ namespace Emby.Dlna.PlayTo
 
             var service = GetAvTransportService();
 
-            await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
                 .ConfigureAwait(false);
 
             RestartTimer(true);
@@ -386,7 +388,7 @@ namespace Emby.Dlna.PlayTo
 
             var service = GetAvTransportService();
 
-            await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
                 .ConfigureAwait(false);
 
             TransportState = TRANSPORTSTATE.PAUSED;
@@ -513,7 +515,7 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true)
+            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true)
                 .ConfigureAwait(false);
 
             if (result == null || result.Document == null)
@@ -559,7 +561,7 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true)
+            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true)
                 .ConfigureAwait(false);
 
             if (result == null || result.Document == null)
@@ -586,7 +588,7 @@ namespace Emby.Dlna.PlayTo
                 return null;
             }
 
-            var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType), false)
+            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType), false)
                 .ConfigureAwait(false);
 
             if (result == null || result.Document == null)
@@ -624,7 +626,7 @@ namespace Emby.Dlna.PlayTo
 
             var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
 
-            var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false)
+            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false)
                 .ConfigureAwait(false);
 
             if (result == null || result.Document == null)
@@ -687,7 +689,7 @@ namespace Emby.Dlna.PlayTo
 
             var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
 
-            var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false)
+            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false)
                 .ConfigureAwait(false);
 
             if (result == null || result.Document == null)
@@ -868,7 +870,7 @@ namespace Emby.Dlna.PlayTo
 
             string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
 
-            var httpClient = new SsdpHttpClient(_httpClient, _config);
+            var httpClient = new SsdpHttpClient(_httpClient);
 
             var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
 
@@ -896,7 +898,7 @@ namespace Emby.Dlna.PlayTo
 
             string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
 
-            var httpClient = new SsdpHttpClient(_httpClient, _config);
+            var httpClient = new SsdpHttpClient(_httpClient);
             _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
             var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
 
@@ -931,7 +933,7 @@ namespace Emby.Dlna.PlayTo
 
         public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, CancellationToken cancellationToken)
         {
-            var ssdpHttpClient = new SsdpHttpClient(httpClient, config);
+            var ssdpHttpClient = new SsdpHttpClient(httpClient);
 
             var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
 

+ 2 - 0
Emby.Dlna/PlayTo/DeviceInfo.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using Emby.Dlna.Common;
 using MediaBrowser.Model.Dlna;

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

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -6,7 +8,6 @@ using System.Threading;
 using System.Threading.Tasks;
 using Emby.Dlna.Didl;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;

+ 6 - 4
Emby.Dlna/PlayTo/PlayToManager.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Globalization;
 using System.Linq;
@@ -21,7 +23,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.PlayTo
 {
-    class PlayToManager : IDisposable
+    public class PlayToManager : IDisposable
     {
         private readonly ILogger _logger;
         private readonly ISessionManager _sessionManager;
@@ -64,10 +66,10 @@ namespace Emby.Dlna.PlayTo
 
         public void Start()
         {
-            _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered;
+            _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
         }
 
-        async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
+        private async void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
         {
             if (_disposed)
             {
@@ -231,7 +233,7 @@ namespace Emby.Dlna.PlayTo
 
         public void Dispose()
         {
-            _deviceDiscovery.DeviceDiscovered -= _deviceDiscovery_DeviceDiscovered;
+            _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
 
             try
             {

+ 2 - 0
Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 
 namespace Emby.Dlna.PlayTo

+ 2 - 0
Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 
 namespace Emby.Dlna.PlayTo

+ 2 - 0
Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 
 namespace Emby.Dlna.PlayTo

+ 2 - 0
Emby.Dlna/PlayTo/PlaylistItem.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.PlayTo

+ 2 - 1
Emby.Dlna/PlayTo/PlaylistItemFactory.cs

@@ -1,4 +1,5 @@
-using System.Globalization;
+#pragma warning disable CS1591
+
 using System.IO;
 using System.Linq;
 using MediaBrowser.Controller.Entities;

+ 8 - 6
Emby.Dlna/PlayTo/SsdpHttpClient.cs

@@ -1,13 +1,15 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Globalization;
 using System.IO;
+using System.Net.Http;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Xml.Linq;
 using Emby.Dlna.Common;
 using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
 
 namespace Emby.Dlna.PlayTo
 {
@@ -19,12 +21,10 @@ namespace Emby.Dlna.PlayTo
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
         private readonly IHttpClient _httpClient;
-        private readonly IServerConfigurationManager _config;
 
-        public SsdpHttpClient(IHttpClient httpClient, IServerConfigurationManager config)
+        public SsdpHttpClient(IHttpClient httpClient)
         {
             _httpClient = httpClient;
-            _config = config;
         }
 
         public async Task<XDocument> SendCommandAsync(
@@ -64,7 +64,9 @@ namespace Emby.Dlna.PlayTo
             }
 
             if (!serviceUrl.StartsWith("/"))
+            {
                 serviceUrl = "/" + serviceUrl;
+            }
 
             return baseUrl + serviceUrl;
         }
@@ -90,7 +92,7 @@ namespace Emby.Dlna.PlayTo
             options.RequestHeaders["NT"] = "upnp:event";
             options.RequestHeaders["TIMEOUT"] = "Second-" + timeOut.ToString(_usCulture);
 
-            using (await _httpClient.SendAsync(options, "SUBSCRIBE").ConfigureAwait(false))
+            using (await _httpClient.SendAsync(options, new HttpMethod("SUBSCRIBE")).ConfigureAwait(false))
             {
 
             }
@@ -110,7 +112,7 @@ namespace Emby.Dlna.PlayTo
 
             options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName;
 
-            using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false))
+            using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false))
             using (var stream = response.Content)
             using (var reader = new StreamReader(stream, Encoding.UTF8))
             {

+ 2 - 0
Emby.Dlna/PlayTo/TRANSPORTSTATE.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 namespace Emby.Dlna.PlayTo
 {
     public enum TRANSPORTSTATE

+ 2 - 0
Emby.Dlna/PlayTo/TransportCommands.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.Linq;

+ 2 - 0
Emby.Dlna/PlayTo/UpnpContainer.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Xml.Linq;
 using Emby.Dlna.Ssdp;

+ 2 - 0
Emby.Dlna/PlayTo/uBaseObject.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 
 namespace Emby.Dlna.PlayTo

+ 2 - 0
Emby.Dlna/PlayTo/uPnpNamespaces.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Xml.Linq;
 
 namespace Emby.Dlna.PlayTo

+ 2 - 0
Emby.Dlna/Profiles/DefaultProfile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Linq;
 using MediaBrowser.Model.Dlna;
 

+ 2 - 0
Emby.Dlna/Profiles/DenonAvrProfile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/DirectTvProfile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/DishHopperJoeyProfile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/Foobar2000Profile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/LgTvProfile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/LinksysDMA2100Profile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/MarantzProfile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/MediaMonkeyProfile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/PanasonicVieraProfile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/PopcornHourProfile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/SamsungSmartTvProfile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/SharpSmartTvProfile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/SonyBravia2010Profile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/SonyBravia2011Profile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/SonyBravia2012Profile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/SonyBravia2013Profile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/SonyBravia2014Profile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/SonyPs3Profile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/SonyPs4Profile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/WdtvLiveProfile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 0
Emby.Dlna/Profiles/XboxOneProfile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles

+ 2 - 1
Emby.Dlna/Server/DescriptionXmlBuilder.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -5,7 +7,6 @@ using System.Linq;
 using System.Text;
 using Emby.Dlna.Common;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Extensions;
 
 namespace Emby.Dlna.Server
 {

+ 46 - 66
Emby.Dlna/Service/BaseControlHandler.cs

@@ -1,8 +1,10 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.IO;
-using System.Linq;
 using System.Text;
+using System.Threading.Tasks;
 using System.Xml;
 using Emby.Dlna.Didl;
 using MediaBrowser.Controller.Configuration;
@@ -15,44 +17,34 @@ namespace Emby.Dlna.Service
     {
         private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
 
-        protected readonly IServerConfigurationManager Config;
-        protected readonly ILogger _logger;
+        protected IServerConfigurationManager Config { get; }
+        protected ILogger Logger { get; }
 
         protected BaseControlHandler(IServerConfigurationManager config, ILogger logger)
         {
             Config = config;
-            _logger = logger;
+            Logger = logger;
         }
 
-        public ControlResponse ProcessControlRequest(ControlRequest request)
+        public async Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
         {
             try
             {
-                var enableDebugLogging = Config.GetDlnaConfiguration().EnableDebugLog;
-
-                if (enableDebugLogging)
-                {
-                    LogRequest(request);
-                }
-
-                var response = ProcessControlRequestInternal(request);
-
-                if (enableDebugLogging)
-                {
-                    LogResponse(response);
-                }
+                LogRequest(request);
 
+                var response = await ProcessControlRequestInternalAsync(request).ConfigureAwait(false);
+                LogResponse(response);
                 return response;
             }
             catch (Exception ex)
             {
-                _logger.LogError(ex, "Error processing control request");
+                Logger.LogError(ex, "Error processing control request");
 
-                return new ControlErrorHandler().GetResponse(ex);
+                return ControlErrorHandler.GetResponse(ex);
             }
         }
 
-        private ControlResponse ProcessControlRequestInternal(ControlRequest request)
+        private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
         {
             ControlRequestInfo requestInfo = null;
 
@@ -63,18 +55,17 @@ namespace Emby.Dlna.Service
                     ValidationType = ValidationType.None,
                     CheckCharacters = false,
                     IgnoreProcessingInstructions = true,
-                    IgnoreComments = true
+                    IgnoreComments = true,
+                    Async = true
                 };
 
                 using (var reader = XmlReader.Create(streamReader, readerSettings))
                 {
-                    requestInfo = ParseRequest(reader);
+                    requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
                 }
             }
 
-            _logger.LogDebug("Received control request {0}", requestInfo.LocalName);
-
-            var result = GetResult(requestInfo.LocalName, requestInfo.Headers);
+            Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
 
             var settings = new XmlWriterSettings
             {
@@ -93,12 +84,9 @@ namespace Emby.Dlna.Service
 
                 writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV);
                 writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI);
-                foreach (var i in result)
-                {
-                    writer.WriteStartElement(i.Key);
-                    writer.WriteString(i.Value);
-                    writer.WriteFullEndElement();
-                }
+
+                WriteResult(requestInfo.LocalName, requestInfo.Headers, writer);
+
                 writer.WriteFullEndElement();
                 writer.WriteFullEndElement();
 
@@ -106,7 +94,7 @@ namespace Emby.Dlna.Service
                 writer.WriteEndDocument();
             }
 
-            var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=");
+            var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal);
 
             var controlResponse = new ControlResponse
             {
@@ -114,17 +102,15 @@ namespace Emby.Dlna.Service
                 IsSuccessful = true
             };
 
-            //logger.LogDebug(xml);
-
             controlResponse.Headers.Add("EXT", string.Empty);
 
             return controlResponse;
         }
 
-        private ControlRequestInfo ParseRequest(XmlReader reader)
+        private async Task<ControlRequestInfo> ParseRequestAsync(XmlReader reader)
         {
-            reader.MoveToContent();
-            reader.Read();
+            await reader.MoveToContentAsync().ConfigureAwait(false);
+            await reader.ReadAsync().ConfigureAwait(false);
 
             // Loop through each element
             while (!reader.EOF && reader.ReadState == ReadState.Interactive)
@@ -139,37 +125,38 @@ namespace Emby.Dlna.Service
                                 {
                                     using (var subReader = reader.ReadSubtree())
                                     {
-                                        return ParseBodyTag(subReader);
+                                        return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
                                     }
                                 }
                                 else
                                 {
-                                    reader.Read();
+                                    await reader.ReadAsync().ConfigureAwait(false);
                                 }
+
                                 break;
                             }
                         default:
                             {
-                                reader.Skip();
+                                await reader.SkipAsync().ConfigureAwait(false);
                                 break;
                             }
                     }
                 }
                 else
                 {
-                    reader.Read();
+                    await reader.ReadAsync().ConfigureAwait(false);
                 }
             }
 
             return new ControlRequestInfo();
         }
 
-        private ControlRequestInfo ParseBodyTag(XmlReader reader)
+        private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
         {
             var result = new ControlRequestInfo();
 
-            reader.MoveToContent();
-            reader.Read();
+            await reader.MoveToContentAsync().ConfigureAwait(false);
+            await reader.ReadAsync().ConfigureAwait(false);
 
             // Loop through each element
             while (!reader.EOF && reader.ReadState == ReadState.Interactive)
@@ -183,28 +170,28 @@ namespace Emby.Dlna.Service
                     {
                         using (var subReader = reader.ReadSubtree())
                         {
-                            ParseFirstBodyChild(subReader, result.Headers);
+                            await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
                             return result;
                         }
                     }
                     else
                     {
-                        reader.Read();
+                        await reader.ReadAsync().ConfigureAwait(false);
                     }
                 }
                 else
                 {
-                    reader.Read();
+                    await reader.ReadAsync().ConfigureAwait(false);
                 }
             }
 
             return result;
         }
 
-        private void ParseFirstBodyChild(XmlReader reader, IDictionary<string, string> headers)
+        private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary<string, string> headers)
         {
-            reader.MoveToContent();
-            reader.Read();
+            await reader.MoveToContentAsync().ConfigureAwait(false);
+            await reader.ReadAsync().ConfigureAwait(false);
 
             // Loop through each element
             while (!reader.EOF && reader.ReadState == ReadState.Interactive)
@@ -212,23 +199,23 @@ namespace Emby.Dlna.Service
                 if (reader.NodeType == XmlNodeType.Element)
                 {
                     // TODO: Should we be doing this here, or should it be handled earlier when decoding the request?
-                    headers[reader.LocalName.RemoveDiacritics()] = reader.ReadElementContentAsString();
+                    headers[reader.LocalName.RemoveDiacritics()] = await reader.ReadElementContentAsStringAsync().ConfigureAwait(false);
                 }
                 else
                 {
-                    reader.Read();
+                    await reader.ReadAsync().ConfigureAwait(false);
                 }
             }
         }
 
         private class ControlRequestInfo
         {
-            public string LocalName;
-            public string NamespaceURI;
-            public IDictionary<string, string> Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            public string LocalName { get; set; }
+            public string NamespaceURI { get; set; }
+            public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
         }
 
-        protected abstract IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams);
+        protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter);
 
         private void LogRequest(ControlRequest request)
         {
@@ -237,10 +224,7 @@ namespace Emby.Dlna.Service
                 return;
             }
 
-            var originalHeaders = request.Headers;
-            var headers = string.Join(", ", originalHeaders.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray());
-
-            _logger.LogDebug("Control request. Headers: {0}", headers);
+            Logger.LogDebug("Control request. Headers: {@Headers}", request.Headers);
         }
 
         private void LogResponse(ControlResponse response)
@@ -250,11 +234,7 @@ namespace Emby.Dlna.Service
                 return;
             }
 
-            var originalHeaders = response.Headers;
-            var headers = string.Join(", ", originalHeaders.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray());
-            //builder.Append(response.Xml);
-
-            _logger.LogDebug("Control response. Headers: {0}", headers);
+            Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml);
         }
     }
 }

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

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using Emby.Dlna.Eventing;
 using MediaBrowser.Common.Net;
 using Microsoft.Extensions.Logging;
@@ -10,7 +12,7 @@ namespace Emby.Dlna.Service
         protected IHttpClient HttpClient;
         protected ILogger Logger;
 
-        protected BaseService(ILogger logger, IHttpClient httpClient)
+        protected BaseService(ILogger<BaseService> logger, IHttpClient httpClient)
         {
             Logger = logger;
             HttpClient = httpClient;

+ 4 - 2
Emby.Dlna/Service/ControlErrorHandler.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.IO;
 using System.Text;
@@ -6,11 +8,11 @@ using Emby.Dlna.Didl;
 
 namespace Emby.Dlna.Service
 {
-    public class ControlErrorHandler
+    public static class ControlErrorHandler
     {
         private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
 
-        public ControlResponse GetResponse(Exception ex)
+        public static ControlResponse GetResponse(Exception ex)
         {
             var settings = new XmlWriterSettings
             {

+ 2 - 0
Emby.Dlna/Service/ServiceXmlBuilder.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using System.Text;
 using Emby.Dlna.Common;

+ 9 - 5
Emby.Dlna/Ssdp/DeviceDiscovery.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -9,16 +11,16 @@ using Rssdp.Infrastructure;
 
 namespace Emby.Dlna.Ssdp
 {
-    public class DeviceDiscovery : IDeviceDiscovery
+    public sealed class DeviceDiscovery : IDeviceDiscovery, IDisposable
     {
-        private bool _disposed;
+        private readonly object _syncLock = new object();
 
         private readonly IServerConfigurationManager _config;
 
-        private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal;
-
         private int _listenerCount;
-        private object _syncLock = new object();
+        private bool _disposed;
+
+        private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal;
 
         /// <inheritdoc />
         public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered
@@ -33,6 +35,7 @@ namespace Emby.Dlna.Ssdp
 
                 StartInternal();
             }
+
             remove
             {
                 lock (_syncLock)
@@ -130,6 +133,7 @@ namespace Emby.Dlna.Ssdp
             DeviceLeft?.Invoke(this, args);
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
             if (!_disposed)

Vissa filer visades inte eftersom för många filer har ändrats