Просмотр исходного кода

Merge remote-tracking branch 'upstream/master' into api-upload-subtitle

crobibero 4 лет назад
Родитель
Сommit
f13b87afa3
100 измененных файлов с 1827 добавлено и 1855 удалено
  1. 24 30
      .ci/azure-pipelines-abi.yml
  2. 26 26
      .ci/azure-pipelines-main.yml
  3. 214 0
      .ci/azure-pipelines-package.yml
  4. 9 0
      .ci/azure-pipelines-test.yml
  5. 20 8
      .ci/azure-pipelines.yml
  6. 9 0
      .github/dependabot.yml
  7. 1 2
      .gitignore
  8. 18 8
      .vscode/launch.json
  9. 17 2
      .vscode/tasks.json
  10. 5 0
      CONTRIBUTORS.md
  11. 2 2
      Dockerfile
  12. 2 2
      Dockerfile.arm
  13. 1 1
      Dockerfile.arm64
  14. 1 0
      DvdLib/Ifo/Cell.cs
  15. 2 0
      DvdLib/Ifo/Chapter.cs
  16. 13 3
      DvdLib/Ifo/Dvd.cs
  17. 8 2
      DvdLib/Ifo/DvdTime.cs
  18. 1 1
      DvdLib/Ifo/Program.cs
  19. 13 2
      DvdLib/Ifo/ProgramChain.cs
  20. 8 1
      DvdLib/Ifo/Title.cs
  21. 0 383
      Emby.Dlna/Api/DlnaServerService.cs
  22. 0 88
      Emby.Dlna/Api/DlnaService.cs
  23. 1 1
      Emby.Dlna/Common/ServiceAction.cs
  24. 2 1
      Emby.Dlna/Common/StateVariable.cs
  25. 0 16
      Emby.Dlna/ConfigurationExtension.cs
  26. 7 9
      Emby.Dlna/ConnectionManager/ConnectionManagerService.cs
  27. 2 2
      Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
  28. 11 13
      Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
  29. 3 2
      Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
  30. 87 104
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  31. 23 0
      Emby.Dlna/ContentDirectory/ServerItem.cs
  32. 28 0
      Emby.Dlna/ContentDirectory/StubType.cs
  33. 6 6
      Emby.Dlna/ControlRequest.cs
  34. 7 1
      Emby.Dlna/ControlResponse.cs
  35. 91 103
      Emby.Dlna/Didl/DidlBuilder.cs
  36. 1 4
      Emby.Dlna/Didl/Filter.cs
  37. 1 1
      Emby.Dlna/Didl/StringWriterWithEncoding.cs
  38. 24 0
      Emby.Dlna/DlnaConfigurationFactory.cs
  39. 62 51
      Emby.Dlna/DlnaManager.cs
  40. 2 1
      Emby.Dlna/Emby.Dlna.csproj
  41. 1 1
      Emby.Dlna/EventSubscriptionResponse.cs
  42. 45 37
      Emby.Dlna/Eventing/DlnaEventManager.cs
  43. 3 0
      Emby.Dlna/Eventing/EventSubscription.cs
  44. 1 1
      Emby.Dlna/IConnectionManager.cs
  45. 1 1
      Emby.Dlna/IContentDirectory.cs
  46. 11 1
      Emby.Dlna/IDlnaEventManager.cs
  47. 1 1
      Emby.Dlna/IMediaReceiverRegistrar.cs
  48. 75 56
      Emby.Dlna/Main/DlnaEntryPoint.cs
  49. 6 6
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs
  50. 2 1
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
  51. 160 156
      Emby.Dlna/PlayTo/Device.cs
  52. 3 2
      Emby.Dlna/PlayTo/DeviceInfo.cs
  53. 13 0
      Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
  54. 152 128
      Emby.Dlna/PlayTo/PlayToController.cs
  55. 53 49
      Emby.Dlna/PlayTo/PlayToManager.cs
  56. 1 1
      Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
  57. 1 1
      Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
  58. 1 7
      Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
  59. 45 72
      Emby.Dlna/PlayTo/SsdpHttpClient.cs
  60. 0 13
      Emby.Dlna/PlayTo/TRANSPORTSTATE.cs
  61. 31 39
      Emby.Dlna/PlayTo/TransportCommands.cs
  62. 14 0
      Emby.Dlna/PlayTo/TransportState.cs
  63. 8 8
      Emby.Dlna/PlayTo/UpnpContainer.cs
  64. 15 12
      Emby.Dlna/PlayTo/uBaseObject.cs
  65. 58 32
      Emby.Dlna/PlayTo/uPnpNamespaces.cs
  66. 3 3
      Emby.Dlna/Profiles/DefaultProfile.cs
  67. 1 1
      Emby.Dlna/Profiles/DenonAvrProfile.cs
  68. 1 1
      Emby.Dlna/Profiles/DirectTvProfile.cs
  69. 6 6
      Emby.Dlna/Profiles/DishHopperJoeyProfile.cs
  70. 1 1
      Emby.Dlna/Profiles/Foobar2000Profile.cs
  71. 2 2
      Emby.Dlna/Profiles/LgTvProfile.cs
  72. 1 1
      Emby.Dlna/Profiles/LinksysDMA2100Profile.cs
  73. 1 1
      Emby.Dlna/Profiles/MarantzProfile.cs
  74. 2 1
      Emby.Dlna/Profiles/MediaMonkeyProfile.cs
  75. 1 1
      Emby.Dlna/Profiles/PanasonicVieraProfile.cs
  76. 7 7
      Emby.Dlna/Profiles/PopcornHourProfile.cs
  77. 1 1
      Emby.Dlna/Profiles/SamsungSmartTvProfile.cs
  78. 5 4
      Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs
  79. 5 4
      Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs
  80. 5 4
      Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs
  81. 5 4
      Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs
  82. 5 5
      Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs
  83. 22 22
      Emby.Dlna/Profiles/SonyBravia2010Profile.cs
  84. 21 21
      Emby.Dlna/Profiles/SonyBravia2011Profile.cs
  85. 17 17
      Emby.Dlna/Profiles/SonyBravia2012Profile.cs
  86. 18 19
      Emby.Dlna/Profiles/SonyBravia2013Profile.cs
  87. 18 19
      Emby.Dlna/Profiles/SonyBravia2014Profile.cs
  88. 7 7
      Emby.Dlna/Profiles/SonyPs3Profile.cs
  89. 7 7
      Emby.Dlna/Profiles/SonyPs4Profile.cs
  90. 4 4
      Emby.Dlna/Profiles/WdtvLiveProfile.cs
  91. 7 7
      Emby.Dlna/Profiles/XboxOneProfile.cs
  92. 75 102
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  93. 18 14
      Emby.Dlna/Service/BaseControlHandler.cs
  94. 8 10
      Emby.Dlna/Service/BaseService.cs
  95. 5 5
      Emby.Dlna/Service/ControlErrorHandler.cs
  96. 27 10
      Emby.Dlna/Service/ServiceXmlBuilder.cs
  97. 16 21
      Emby.Dlna/Ssdp/DeviceDiscovery.cs
  98. 5 11
      Emby.Dlna/Ssdp/SsdpExtensions.cs
  99. 41 11
      Emby.Drawing/ImageProcessor.cs
  100. 6 0
      Emby.Drawing/NullImageEncoder.cs

+ 24 - 30
.ci/azure-pipelines-compat.yml → .ci/azure-pipelines-abi.yml

@@ -12,10 +12,12 @@ parameters:
 jobs:
   - job: CompatibilityCheck
     displayName: Compatibility Check
+    dependsOn: Build
+    condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
+
     pool:
       vmImage: "${{ parameters.LinuxImage }}"
-    # only execute for pull requests
-    condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
+
     strategy:
       matrix:
         ${{ each Package in parameters.Packages }}:
@@ -23,7 +25,7 @@ jobs:
             NugetPackageName: ${{ Package.value.NugetPackageName }}
             AssemblyFileName: ${{ Package.value.AssemblyFileName }}
       maxParallel: 2
-    dependsOn: Build
+
     steps:
       - checkout: none
 
@@ -33,26 +35,33 @@ jobs:
           packageType: sdk
           version: ${{ parameters.DotNetSdkVersion }}
 
+      - task: DotNetCoreCLI@2
+        displayName: 'Install ABI CompatibilityChecker Tool'
+        inputs:
+          command: custom
+          custom: tool
+          arguments: 'update compatibilitychecker -g'
+
       - task: DownloadPipelineArtifact@2
-        displayName: "Download New Assembly Build Artifact"
+        displayName: 'Download New Assembly Build Artifact'
         inputs:
-          source: "current"
+          source: 'current'
           artifact: "$(NugetPackageName)"
           path: "$(System.ArtifactsDirectory)/new-artifacts"
           runVersion: "latest"
 
       - task: CopyFiles@2
-        displayName: "Copy New Assembly Build Artifact"
+        displayName: 'Copy New Assembly Build Artifact'
         inputs:
           sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
-          contents: "**/*.dll"
+          contents: '**/*.dll'
           targetFolder: $(System.ArtifactsDirectory)/new-release
           cleanTargetFolder: true
           overWrite: true
           flattenFolders: true
 
       - task: DownloadPipelineArtifact@2
-        displayName: "Download Reference Assembly Build Artifact"
+        displayName: 'Download Reference Assembly Build Artifact'
         inputs:
           source: "specific"
           artifact: "$(NugetPackageName)"
@@ -63,34 +72,19 @@ jobs:
           runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
 
       - task: CopyFiles@2
-        displayName: "Copy Reference Assembly Build Artifact"
+        displayName: 'Copy Reference Assembly Build Artifact'
         inputs:
           sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
-          contents: "**/*.dll"
+          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"
+      - task: DotNetCoreCLI@2
+        displayName: 'Execute ABI Compatibility Check Tool'
         inputs:
-          script: "dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only"
+          command: custom
+          custom: compat
+          arguments: 'current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only'
           workingDirectory: $(System.ArtifactsDirectory)

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

@@ -1,6 +1,6 @@
 parameters:
-  LinuxImage: "ubuntu-latest"
-  RestoreBuildProjects: "Jellyfin.Server/Jellyfin.Server.csproj"
+  LinuxImage: 'ubuntu-latest'
+  RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
   DotNetSdkVersion: 3.1.100
 
 jobs:
@@ -13,7 +13,7 @@ jobs:
         Debug:
           BuildConfiguration: Debug
     pool:
-      vmImage: "${{ parameters.LinuxImage }}"
+      vmImage: '${{ parameters.LinuxImage }}'
     steps:
       - checkout: self
         clean: true
@@ -21,7 +21,7 @@ jobs:
         persistCredentials: true
 
       - task: DownloadPipelineArtifact@2
-        displayName: "Download Web Branch"
+        displayName: 'Download Web Branch'
         condition: in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion')
         inputs:
           path: '$(Agent.TempDirectory)'
@@ -32,7 +32,7 @@ jobs:
           runBranch: variables['Build.SourceBranch']
 
       - task: DownloadPipelineArtifact@2
-        displayName: "Download Web Target"
+        displayName: 'Download Web Target'
         condition: eq(variables['Build.Reason'], 'PullRequest')
         inputs:
           path: '$(Agent.TempDirectory)'
@@ -43,51 +43,51 @@ jobs:
           runBranch: variables['System.PullRequest.TargetBranch']
 
       - task: ExtractFiles@1
-        displayName: "Extract Web Client"
+        displayName: 'Extract Web Client'
         inputs:
           archiveFilePatterns: '$(Agent.TempDirectory)/*.zip'
           destinationFolder: '$(Build.SourcesDirectory)/MediaBrowser.WebDashboard'
           cleanDestinationFolder: false
 
       - task: UseDotNet@2
-        displayName: "Update DotNet"
+        displayName: 'Update DotNet'
         inputs:
           packageType: sdk
           version: ${{ parameters.DotNetSdkVersion }}
 
       - task: DotNetCoreCLI@2
-        displayName: "Publish Server"
+        displayName: 'Publish Server'
         inputs:
           command: publish
           publishWebProjects: false
-          projects: "${{ parameters.RestoreBuildProjects }}"
-          arguments: "--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)"
+          projects: '${{ parameters.RestoreBuildProjects }}'
+          arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
           zipAfterPublish: false
 
-      - task: PublishPipelineArtifact@0
-        displayName: "Publish Artifact Naming"
+      - task: PublishPipelineArtifact@1
+        displayName: 'Publish Artifact Naming'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:
-          targetPath: "$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll"
-          artifactName: "Jellyfin.Naming"
+          targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll'
+          artifactName: 'Jellyfin.Naming'
 
-      - task: PublishPipelineArtifact@0
-        displayName: "Publish Artifact Controller"
+      - task: PublishPipelineArtifact@1
+        displayName: 'Publish Artifact Controller'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:
-          targetPath: "$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll"
-          artifactName: "Jellyfin.Controller"
+          targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
+          artifactName: 'Jellyfin.Controller'
 
-      - task: PublishPipelineArtifact@0
-        displayName: "Publish Artifact Model"
+      - task: PublishPipelineArtifact@1
+        displayName: 'Publish Artifact Model'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:
-          targetPath: "$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll"
-          artifactName: "Jellyfin.Model"
+          targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
+          artifactName: 'Jellyfin.Model'
 
-      - task: PublishPipelineArtifact@0
-        displayName: "Publish Artifact Common"
+      - task: PublishPipelineArtifact@1
+        displayName: 'Publish Artifact Common'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:
-          targetPath: "$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll"
-          artifactName: "Jellyfin.Common"
+          targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
+          artifactName: 'Jellyfin.Common'

+ 214 - 0
.ci/azure-pipelines-package.yml

@@ -0,0 +1,214 @@
+jobs:
+- job: BuildPackage
+  displayName: 'Build Packages'
+
+  strategy:
+    matrix:
+      CentOS.amd64:
+        BuildConfiguration: centos.amd64
+      Fedora.amd64:
+        BuildConfiguration: fedora.amd64
+      Debian.amd64:
+        BuildConfiguration: debian.amd64
+      Debian.arm64:
+        BuildConfiguration: debian.arm64
+      Debian.armhf:
+        BuildConfiguration: debian.armhf
+      Ubuntu.amd64:
+        BuildConfiguration: ubuntu.amd64
+      Ubuntu.arm64:
+        BuildConfiguration: ubuntu.arm64
+      Ubuntu.armhf:
+        BuildConfiguration: ubuntu.armhf
+      Linux.amd64:
+        BuildConfiguration: linux.amd64
+      Windows.amd64:
+        BuildConfiguration: windows.amd64
+      MacOS:
+        BuildConfiguration: macos
+      Portable:
+        BuildConfiguration: portable
+
+  pool:
+    vmImage: 'ubuntu-latest'
+
+  steps:
+  - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
+    displayName: 'Build Dockerfile'
+
+  - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
+    displayName: 'Run Dockerfile (unstable)'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+
+  - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
+    displayName: 'Run Dockerfile (stable)'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
+  - task: PublishPipelineArtifact@1
+    displayName: 'Publish Release'
+    inputs:
+      targetPath: '$(Build.SourcesDirectory)/deployment/dist'
+      artifactName: 'jellyfin-server-$(BuildConfiguration)'
+
+  - task: SSH@0
+    displayName: 'Create target directory on repository server'
+    inputs:
+      sshEndpoint: repository
+      runOptions: 'inline'
+      inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
+
+  - task: CopyFilesOverSSH@0
+    displayName: 'Upload artifacts to repository server'
+    inputs:
+      sshEndpoint: repository
+      sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
+      contents: '**'
+      targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
+
+- job: BuildDocker
+  displayName: 'Build Docker'
+
+  strategy:
+    matrix:
+      amd64:
+        BuildConfiguration: amd64
+      arm64:
+        BuildConfiguration: arm64
+      armhf:
+        BuildConfiguration: armhf
+
+  pool:
+    vmImage: 'ubuntu-latest'
+
+  variables:
+  - name: JellyfinVersion
+    value: 0.0.0
+
+  steps:
+  - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
+    displayName: Set release version (stable)
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
+  - task: Docker@2
+    displayName: 'Push Unstable Image'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+    inputs:
+      repository: 'jellyfin/jellyfin-server'
+      command: buildAndPush
+      buildContext: '.'
+      Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
+      containerRegistry: Docker Hub
+      tags: |
+        unstable-$(Build.BuildNumber)-$(BuildConfiguration)
+        unstable-$(BuildConfiguration)
+
+  - task: Docker@2
+    displayName: 'Push Stable Image'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+    inputs:
+      repository: 'jellyfin/jellyfin-server'
+      command: buildAndPush
+      buildContext: '.'
+      Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
+      containerRegistry: Docker Hub
+      tags: |
+        stable-$(Build.BuildNumber)-$(BuildConfiguration)
+        $(JellyfinVersion)-$(BuildConfiguration)
+
+- job: CollectArtifacts
+  timeoutInMinutes: 20
+  displayName: 'Collect Artifacts'
+  continueOnError: true
+  dependsOn:
+  - BuildPackage
+  - BuildDocker
+  condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
+
+  pool:
+    vmImage: 'ubuntu-latest'
+
+  steps:
+  - task: SSH@0
+    displayName: 'Update Unstable Repository'
+    continueOnError: true
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+    inputs:
+      sshEndpoint: repository
+      runOptions: 'commands'
+      commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
+
+  - task: SSH@0
+    displayName: 'Update Stable Repository'
+    continueOnError: true
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+    inputs:
+      sshEndpoint: repository
+      runOptions: 'commands'
+      commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
+
+- job: PublishNuget
+  displayName: 'Publish NuGet packages'
+  dependsOn:
+  - BuildPackage
+  condition: succeeded('BuildPackage')
+
+  pool:
+    vmImage: 'ubuntu-latest'
+
+  steps:
+  - task: DotNetCoreCLI@2
+    displayName: 'Build Stable Nuget packages'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+    inputs:
+      command: 'pack'
+      packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj'
+      versioningScheme: 'off'
+
+  - task: DotNetCoreCLI@2
+    displayName: 'Build Unstable Nuget packages'
+    inputs:
+      command: 'custom'
+      projects: |
+        Jellyfin.Data/Jellyfin.Data.csproj
+        MediaBrowser.Common/MediaBrowser.Common.csproj
+        MediaBrowser.Controller/MediaBrowser.Controller.csproj
+        MediaBrowser.Model/MediaBrowser.Model.csproj
+        Emby.Naming/Emby.Naming.csproj
+      custom: 'pack'
+      arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
+
+  - task: PublishBuildArtifacts@1
+    displayName: 'Publish Nuget packages'
+    inputs:
+      pathToPublish: $(Build.ArtifactStagingDirectory)
+      artifactName: Jellyfin Nuget Packages
+
+  - task: NuGetAuthenticate@0
+    displayName: 'Authenticate to stable Nuget feed'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+    inputs:
+      nuGetServiceConnections: 'NugetOrg'
+
+  - task: NuGetCommand@2
+    displayName: 'Push Nuget packages to stable feed'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+    inputs:
+      command: 'push'
+      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg'
+      nuGetFeedType: 'external'
+      publishFeedCredentials: 'NugetOrg'
+      allowPackageConflicts: true # This ignores an error if the version already exists
+
+  - task: NuGetAuthenticate@0
+    displayName: 'Authenticate to unstable Nuget feed'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+
+  - task: NuGetCommand@2
+    displayName: 'Push Nuget packages to unstable feed'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+    inputs:
+      command: 'push'
+      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' # No symbols since Azure Artifact does not support it
+      nuGetFeedType: 'internal'
+      publishVstsFeed: '7cce6c46-d610-45e3-9fb7-65a6bfd1b671/a5746b79-f369-42db-93ff-59cd066f9327'
+      allowPackageConflicts: true # This ignores an error if the version already exists

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

@@ -45,6 +45,7 @@ jobs:
       - task: SonarCloudPrepare@1
         displayName: 'Prepare analysis on SonarCloud'
         condition: eq(variables['ImageName'], 'ubuntu-latest')
+        enabled: false
         inputs:
           SonarCloud: 'Sonarcloud for Jellyfin'
           organization: 'jellyfin'
@@ -63,10 +64,12 @@ jobs:
       - task: SonarCloudAnalyze@1
         displayName: 'Run Code Analysis'
         condition: eq(variables['ImageName'], 'ubuntu-latest')
+        enabled: false
 
       - task: SonarCloudPublish@1
         displayName: 'Publish Quality Gate Result'
         condition: eq(variables['ImageName'], 'ubuntu-latest')
+        enabled: false
 
       - 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
@@ -87,3 +90,9 @@ jobs:
           pathToSources: $(Build.SourcesDirectory)
           failIfCoverageEmpty: true
 
+      - task: PublishPipelineArtifact@1
+        displayName: 'Publish OpenAPI Artifact'
+        condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
+        inputs:
+          targetPath: "tests/Jellyfin.Api.Tests/bin/Release/netcoreapp3.1/openapi.json"
+          artifactName: 'OpenAPI Spec'

+ 20 - 8
.ci/azure-pipelines.yml

@@ -2,9 +2,9 @@ name: $(Date:yyyyMMdd)$(Rev:.r)
 
 variables:
 - name: TestProjects
-  value: "tests/**/*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
 
@@ -13,21 +13,30 @@ pr:
 
 trigger:
   batch: true
+  branches:
+    include:
+      - '*'
+  tags:
+    include:
+      - 'v*'
 
 jobs:
+- ${{ if not(startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) }}:
   - template: azure-pipelines-main.yml
     parameters:
-      LinuxImage: "ubuntu-latest"
+      LinuxImage: 'ubuntu-latest'
       RestoreBuildProjects: $(RestoreBuildProjects)
 
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
   - template: azure-pipelines-test.yml
     parameters:
       ImageNames:
-        Linux: "ubuntu-latest"
-        Windows: "windows-latest"
-        macOS: "macos-latest"
+        Linux: 'ubuntu-latest'
+        Windows: 'windows-latest'
+        macOS: 'macos-latest'
 
-  - template: azure-pipelines-compat.yml
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
+  - template: azure-pipelines-abi.yml
     parameters:
       Packages:
         Naming:
@@ -42,4 +51,7 @@ jobs:
         Common:
           NugetPackageName: Jellyfin.Common
           AssemblyFileName: MediaBrowser.Common.dll
-      LinuxImage: "ubuntu-latest"
+      LinuxImage: 'ubuntu-latest'
+
+- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
+  - template: azure-pipelines-package.yml

+ 9 - 0
.github/dependabot.yml

@@ -0,0 +1,9 @@
+version: 2
+updates:
+- package-ecosystem: nuget
+  directory: "/"
+  schedule:
+    interval: weekly
+    time: '12:00'
+  open-pull-requests-limit: 10
+  

+ 1 - 2
.gitignore

@@ -39,7 +39,6 @@ ProgramData*/
 CorePlugins*/
 ProgramData-Server*/
 ProgramData-UI*/
-MediaBrowser.WebDashboard/jellyfin-web/**
 
 #################
 ## Visual Studio
@@ -276,4 +275,4 @@ BenchmarkDotNet.Artifacts
 # Ignore web artifacts from native builds
 web/
 web-src.*
-MediaBrowser.WebDashboard/jellyfin-web/
+MediaBrowser.WebDashboard/jellyfin-web

+ 18 - 8
.vscode/launch.json

@@ -1,19 +1,26 @@
 {
-   // Use IntelliSense to find out which attributes exist for C# debugging
-   // Use hover for the description of the existing attributes
-   // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
-   "version": "0.2.0",
-   "configurations": [
+    "version": "0.2.0",
+    "configurations": [
         {
             "name": ".NET Core Launch (console)",
             "type": "coreclr",
             "request": "launch",
             "preLaunchTask": "build",
-            // If you have changed target frameworks, make sure to update the program path.
             "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
             "args": [],
             "cwd": "${workspaceFolder}/Jellyfin.Server",
-            // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
+            "console": "internalConsole",
+            "stopAtEntry": false,
+            "internalConsoleOptions": "openOnSessionStart"
+        },
+        {
+            "name": ".NET Core Launch (nowebclient)",
+            "type": "coreclr",
+            "request": "launch",
+            "preLaunchTask": "build",
+            "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
+            "args": ["--nowebclient"],
+            "cwd": "${workspaceFolder}/Jellyfin.Server",
             "console": "internalConsole",
             "stopAtEntry": false,
             "internalConsoleOptions": "openOnSessionStart"
@@ -24,5 +31,8 @@
             "request": "attach",
             "processId": "${command:pickProcess}"
         }
-    ,]
+    ],
+    "env": {
+        "DOTNET_CLI_TELEMETRY_OPTOUT": "1"
+    }
 }

+ 17 - 2
.vscode/tasks.json

@@ -10,6 +10,21 @@
                 "${workspaceFolder}/Jellyfin.Server/Jellyfin.Server.csproj"
             ],
             "problemMatcher": "$msCompile"
+        },
+        {
+            "label": "api tests",
+            "command": "dotnet",
+            "type": "process",
+            "args": [
+                "test",
+                "${workspaceFolder}/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj"
+            ],
+            "problemMatcher": "$msCompile"
+        }
+    ],
+    "options": {
+        "env": {
+            "DOTNET_CLI_TELEMETRY_OPTOUT": "1"
         }
-    ]
-}
+    }
+}

+ 5 - 0
CONTRIBUTORS.md

@@ -7,6 +7,7 @@
  - [anthonylavado](https://github.com/anthonylavado)
  - [Artiume](https://github.com/Artiume)
  - [AThomsen](https://github.com/AThomsen)
+ - [barronpm](https://github.com/barronpm)
  - [bilde2910](https://github.com/bilde2910)
  - [bfayers](https://github.com/bfayers)
  - [BnMcG](https://github.com/BnMcG)
@@ -15,6 +16,7 @@
  - [bugfixin](https://github.com/bugfixin)
  - [chaosinnovator](https://github.com/chaosinnovator)
  - [ckcr4lyf](https://github.com/ckcr4lyf)
+ - [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
  - [crankdoofus](https://github.com/crankdoofus)
  - [crobibero](https://github.com/crobibero)
  - [cromefire](https://github.com/cromefire)
@@ -55,6 +57,7 @@
  - [Larvitar](https://github.com/Larvitar)
  - [LeoVerto](https://github.com/LeoVerto)
  - [Liggy](https://github.com/Liggy)
+ - [lmaonator](https://github.com/lmaonator)
  - [LogicalPhallacy](https://github.com/LogicalPhallacy)
  - [loli10K](https://github.com/loli10K)
  - [lostmypillow](https://github.com/lostmypillow)
@@ -76,6 +79,7 @@
  - [nvllsvm](https://github.com/nvllsvm)
  - [nyanmisaka](https://github.com/nyanmisaka)
  - [oddstr13](https://github.com/oddstr13)
+ - [orryverducci](https://github.com/orryverducci)
  - [petermcneil](https://github.com/petermcneil)
  - [Phlogi](https://github.com/Phlogi)
  - [pjeanjean](https://github.com/pjeanjean)
@@ -130,6 +134,7 @@
  - [XVicarious](https://github.com/XVicarious)
  - [YouKnowBlom](https://github.com/YouKnowBlom)
  - [KristupasSavickas](https://github.com/KristupasSavickas)
+ - [Pusta](https://github.com/pusta)
 
 # Emby Contributors
 

+ 2 - 2
Dockerfile

@@ -2,7 +2,7 @@ ARG DOTNET_VERSION=3.1
 
 FROM node:alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm \
+RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
  && yarn install \
@@ -14,7 +14,7 @@ COPY . .
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 # because of changes in docker and systemd we need to not build in parallel at the moment
 # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
 
 FROM debian:buster-slim
 

+ 2 - 2
Dockerfile.arm

@@ -21,7 +21,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 # Discard objs - may cause failures if exists
 RUN find . -type d -name obj | xargs -r rm -r
 # Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
 
 
 FROM multiarch/qemu-user-static:x86_64-arm as qemu
@@ -38,7 +38,7 @@ COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
 RUN apt-get update \
  && 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 - && \
+ curl -ks 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 && \

+ 1 - 1
Dockerfile.arm64

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

+ 1 - 0
DvdLib/Ifo/Cell.cs

@@ -7,6 +7,7 @@ namespace DvdLib.Ifo
     public class Cell
     {
         public CellPlaybackInfo PlaybackInfo { get; private set; }
+
         public CellPositionInfo PositionInfo { get; private set; }
 
         internal void ParsePlayback(BinaryReader br)

+ 2 - 0
DvdLib/Ifo/Chapter.cs

@@ -5,7 +5,9 @@ namespace DvdLib.Ifo
     public class Chapter
     {
         public ushort ProgramChainNumber { get; private set; }
+
         public ushort ProgramNumber { get; private set; }
+
         public uint ChapterNumber { get; private set; }
 
         public Chapter(ushort pgcNum, ushort programNum, uint chapterNum)

+ 13 - 3
DvdLib/Ifo/Dvd.cs

@@ -117,12 +117,19 @@ namespace DvdLib.Ifo
                         uint chapNum = 1;
                         vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin);
                         var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1));
-                        if (t == null) continue;
+                        if (t == null)
+                        {
+                            continue;
+                        }
 
                         do
                         {
                             t.Chapters.Add(new Chapter(vtsRead.ReadUInt16(), vtsRead.ReadUInt16(), chapNum));
-                            if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1])) break;
+                            if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1]))
+                            {
+                                break;
+                            }
+
                             chapNum++;
                         }
                         while (vtsFs.Position < (baseAddr + endaddr));
@@ -147,7 +154,10 @@ namespace DvdLib.Ifo
                         uint vtsPgcOffset = vtsRead.ReadUInt32();
 
                         var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum));
-                        if (t != null) t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum);
+                        if (t != null)
+                        {
+                            t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum);
+                        }
                     }
                 }
             }

+ 8 - 2
DvdLib/Ifo/DvdTime.cs

@@ -15,8 +15,14 @@ namespace DvdLib.Ifo
             Second = GetBCDValue(data[2]);
             Frames = GetBCDValue((byte)(data[3] & 0x3F));
 
-            if ((data[3] & 0x80) != 0) FrameRate = 30;
-            else if ((data[3] & 0x40) != 0) FrameRate = 25;
+            if ((data[3] & 0x80) != 0)
+            {
+                FrameRate = 30;
+            }
+            else if ((data[3] & 0x40) != 0)
+            {
+                FrameRate = 25;
+            }
         }
 
         private static byte GetBCDValue(byte data)

+ 1 - 1
DvdLib/Ifo/Program.cs

@@ -6,7 +6,7 @@ namespace DvdLib.Ifo
 {
     public class Program
     {
-        public readonly List<Cell> Cells;
+        public IReadOnlyList<Cell> Cells { get; }
 
         public Program(List<Cell> cells)
         {

+ 13 - 2
DvdLib/Ifo/ProgramChain.cs

@@ -22,7 +22,9 @@ namespace DvdLib.Ifo
         public readonly List<Cell> Cells;
 
         public DvdTime PlaybackTime { get; private set; }
+
         public UserOperation ProhibitedUserOperations { get; private set; }
+
         public byte[] AudioStreamControl { get; private set; } // 8*2 entries
         public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries
 
@@ -33,9 +35,11 @@ namespace DvdLib.Ifo
         private ushort _goupProgramNumber;
 
         public ProgramPlaybackMode PlaybackMode { get; private set; }
+
         public uint ProgramCount { get; private set; }
 
         public byte StillTime { get; private set; }
+
         public byte[] Palette { get; private set; } // 16*4 entries
 
         private ushort _commandTableOffset;
@@ -71,8 +75,15 @@ namespace DvdLib.Ifo
 
             StillTime = br.ReadByte();
             byte pbMode = br.ReadByte();
-            if (pbMode == 0) PlaybackMode = ProgramPlaybackMode.Sequential;
-            else PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle;
+            if (pbMode == 0)
+            {
+                PlaybackMode = ProgramPlaybackMode.Sequential;
+            }
+            else
+            {
+                PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle;
+            }
+
             ProgramCount = (uint)(pbMode & 0x7F);
 
             Palette = br.ReadBytes(64);

+ 8 - 1
DvdLib/Ifo/Title.cs

@@ -8,8 +8,11 @@ namespace DvdLib.Ifo
     public class Title
     {
         public uint TitleNumber { get; private set; }
+
         public uint AngleCount { get; private set; }
+
         public ushort ChapterCount { get; private set; }
+
         public byte VideoTitleSetNumber { get; private set; }
 
         private ushort _parentalManagementMask;
@@ -17,6 +20,7 @@ namespace DvdLib.Ifo
         private uint _vtsStartSector; // relative to start of entire disk
 
         public ProgramChain EntryProgramChain { get; private set; }
+
         public readonly List<ProgramChain> ProgramChains;
 
         public readonly List<Chapter> Chapters;
@@ -55,7 +59,10 @@ namespace DvdLib.Ifo
             var pgc = new ProgramChain(pgcNum);
             pgc.ParseHeader(br);
             ProgramChains.Add(pgc);
-            if (entryPgc) EntryProgramChain = pgc;
+            if (entryPgc)
+            {
+                EntryProgramChain = pgc;
+            }
 
             br.BaseStream.Seek(curPos, SeekOrigin.Begin);
         }

+ 0 - 383
Emby.Dlna/Api/DlnaServerService.cs

@@ -1,383 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
-using System.Text;
-using System.Threading.Tasks;
-using Emby.Dlna.Main;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Dlna.Api
-{
-    [Route("/Dlna/{UuId}/description.xml", "GET", Summary = "Gets dlna server info")]
-    [Route("/Dlna/{UuId}/description", "GET", Summary = "Gets dlna server info")]
-    public class GetDescriptionXml
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UuId { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/contentdirectory/contentdirectory.xml", "GET", Summary = "Gets dlna content directory xml")]
-    [Route("/Dlna/{UuId}/contentdirectory/contentdirectory", "GET", Summary = "Gets dlna content directory xml")]
-    public class GetContentDirectory
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UuId { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/connectionmanager/connectionmanager.xml", "GET", Summary = "Gets dlna connection manager xml")]
-    [Route("/Dlna/{UuId}/connectionmanager/connectionmanager", "GET", Summary = "Gets dlna connection manager xml")]
-    public class GetConnnectionManager
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UuId { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar.xml", "GET", Summary = "Gets dlna mediareceiverregistrar xml")]
-    [Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar", "GET", Summary = "Gets dlna mediareceiverregistrar xml")]
-    public class GetMediaReceiverRegistrar
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UuId { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/contentdirectory/control", "POST", Summary = "Processes a control request")]
-    public class ProcessContentDirectoryControlRequest : IRequiresRequestStream
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UuId { get; set; }
-
-        public Stream RequestStream { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/connectionmanager/control", "POST", Summary = "Processes a control request")]
-    public class ProcessConnectionManagerControlRequest : IRequiresRequestStream
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UuId { get; set; }
-
-        public Stream RequestStream { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/mediareceiverregistrar/control", "POST", Summary = "Processes a control request")]
-    public class ProcessMediaReceiverRegistrarControlRequest : IRequiresRequestStream
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UuId { get; set; }
-
-        public Stream RequestStream { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/mediareceiverregistrar/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
-    [Route("/Dlna/{UuId}/mediareceiverregistrar/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
-    public class ProcessMediaReceiverRegistrarEventRequest
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
-        public string UuId { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/contentdirectory/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
-    [Route("/Dlna/{UuId}/contentdirectory/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
-    public class ProcessContentDirectoryEventRequest
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
-        public string UuId { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/connectionmanager/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
-    [Route("/Dlna/{UuId}/connectionmanager/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
-    public class ProcessConnectionManagerEventRequest
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
-        public string UuId { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/icons/{Filename}", "GET", Summary = "Gets a server icon")]
-    [Route("/Dlna/icons/{Filename}", "GET", Summary = "Gets a server icon")]
-    public class GetIcon
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string UuId { get; set; }
-
-        [ApiMember(Name = "Filename", Description = "The icon filename", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Filename { get; set; }
-    }
-
-    public class DlnaServerService : IService, IRequiresRequest
-    {
-        private const string XMLContentType = "text/xml; charset=UTF-8";
-
-        private readonly IDlnaManager _dlnaManager;
-        private readonly IHttpResultFactory _resultFactory;
-        private readonly IServerConfigurationManager _configurationManager;
-
-        public IRequest Request { get; set; }
-
-        private IContentDirectory ContentDirectory => DlnaEntryPoint.Current.ContentDirectory;
-
-        private IConnectionManager ConnectionManager => DlnaEntryPoint.Current.ConnectionManager;
-
-        private IMediaReceiverRegistrar MediaReceiverRegistrar => DlnaEntryPoint.Current.MediaReceiverRegistrar;
-
-        public DlnaServerService(
-            IDlnaManager dlnaManager,
-            IHttpResultFactory httpResultFactory,
-            IServerConfigurationManager configurationManager)
-        {
-            _dlnaManager = dlnaManager;
-            _resultFactory = httpResultFactory;
-            _configurationManager = configurationManager;
-        }
-
-        private string GetHeader(string name)
-        {
-            return Request.Headers[name];
-        }
-
-        public object Get(GetDescriptionXml request)
-        {
-            var url = Request.AbsoluteUri;
-            var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
-            var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, request.UuId, serverAddress);
-
-            var cacheLength = TimeSpan.FromDays(1);
-            var cacheKey = Request.RawUrl.GetMD5();
-            var bytes = Encoding.UTF8.GetBytes(xml);
-
-            return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, XMLContentType, () => Task.FromResult<Stream>(new MemoryStream(bytes)));
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetContentDirectory request)
-        {
-            var xml = ContentDirectory.GetServiceXml();
-
-            return _resultFactory.GetResult(Request, xml, XMLContentType);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetMediaReceiverRegistrar request)
-        {
-            var xml = MediaReceiverRegistrar.GetServiceXml();
-
-            return _resultFactory.GetResult(Request, xml, XMLContentType);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetConnnectionManager request)
-        {
-            var xml = ConnectionManager.GetServiceXml();
-
-            return _resultFactory.GetResult(Request, xml, XMLContentType);
-        }
-
-        public async Task<object> Post(ProcessMediaReceiverRegistrarControlRequest request)
-        {
-            var response = await PostAsync(request.RequestStream, MediaReceiverRegistrar).ConfigureAwait(false);
-
-            return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
-        }
-
-        public async Task<object> Post(ProcessContentDirectoryControlRequest request)
-        {
-            var response = await PostAsync(request.RequestStream, ContentDirectory).ConfigureAwait(false);
-
-            return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
-        }
-
-        public async Task<object> Post(ProcessConnectionManagerControlRequest request)
-        {
-            var response = await PostAsync(request.RequestStream, ConnectionManager).ConfigureAwait(false);
-
-            return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
-        }
-
-        private Task<ControlResponse> PostAsync(Stream requestStream, IUpnpService service)
-        {
-            var id = GetPathValue(2).ToString();
-
-            return service.ProcessControlRequestAsync(new ControlRequest
-            {
-                Headers = Request.Headers,
-                InputXml = requestStream,
-                TargetServerUuId = id,
-                RequestedUrl = Request.AbsoluteUri
-            });
-        }
-
-        // Copied from MediaBrowser.Api/BaseApiService.cs
-        // TODO: Remove code duplication
-        /// <summary>
-        /// Gets the path segment at the specified index.
-        /// </summary>
-        /// <param name="index">The index of the path segment.</param>
-        /// <returns>The path segment at the specified index.</returns>
-        /// <exception cref="IndexOutOfRangeException" >Path doesn't contain enough segments.</exception>
-        /// <exception cref="InvalidDataException" >Path doesn't start with the base url.</exception>
-        protected internal ReadOnlySpan<char> GetPathValue(int index)
-        {
-            static void ThrowIndexOutOfRangeException()
-                => throw new IndexOutOfRangeException("Path doesn't contain enough segments.");
-
-            static void ThrowInvalidDataException()
-                => throw new InvalidDataException("Path doesn't start with the base url.");
-
-            ReadOnlySpan<char> path = Request.PathInfo;
-
-            // Remove the protocol part from the url
-            int pos = path.LastIndexOf("://");
-            if (pos != -1)
-            {
-                path = path.Slice(pos + 3);
-            }
-
-            // Remove the query string
-            pos = path.LastIndexOf('?');
-            if (pos != -1)
-            {
-                path = path.Slice(0, pos);
-            }
-
-            // Remove the domain
-            pos = path.IndexOf('/');
-            if (pos != -1)
-            {
-                path = path.Slice(pos);
-            }
-
-            // Remove base url
-            string baseUrl = _configurationManager.Configuration.BaseUrl;
-            int baseUrlLen = baseUrl.Length;
-            if (baseUrlLen != 0)
-            {
-                if (path.StartsWith(baseUrl, StringComparison.OrdinalIgnoreCase))
-                {
-                    path = path.Slice(baseUrlLen);
-                }
-                else
-                {
-                    // The path doesn't start with the base url,
-                    // how did we get here?
-                    ThrowInvalidDataException();
-                }
-            }
-
-            // Remove leading /
-            path = path.Slice(1);
-
-            // Backwards compatibility
-            const string Emby = "emby/";
-            if (path.StartsWith(Emby, StringComparison.OrdinalIgnoreCase))
-            {
-                path = path.Slice(Emby.Length);
-            }
-
-            const string MediaBrowser = "mediabrowser/";
-            if (path.StartsWith(MediaBrowser, StringComparison.OrdinalIgnoreCase))
-            {
-                path = path.Slice(MediaBrowser.Length);
-            }
-
-            // Skip segments until we are at the right index
-            for (int i = 0; i < index; i++)
-            {
-                pos = path.IndexOf('/');
-                if (pos == -1)
-                {
-                    ThrowIndexOutOfRangeException();
-                }
-
-                path = path.Slice(pos + 1);
-            }
-
-            // Remove the rest
-            pos = path.IndexOf('/');
-            if (pos != -1)
-            {
-                path = path.Slice(0, pos);
-            }
-
-            return path;
-        }
-
-        public object Get(GetIcon request)
-        {
-            var contentType = "image/" + Path.GetExtension(request.Filename)
-                                            .TrimStart('.')
-                                            .ToLowerInvariant();
-
-            var cacheLength = TimeSpan.FromDays(365);
-            var cacheKey = Request.RawUrl.GetMD5();
-
-            return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, contentType, () => Task.FromResult(_dlnaManager.GetIcon(request.Filename).Stream));
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Subscribe(ProcessContentDirectoryEventRequest request)
-        {
-            return ProcessEventRequest(ContentDirectory);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Subscribe(ProcessConnectionManagerEventRequest request)
-        {
-            return ProcessEventRequest(ConnectionManager);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Subscribe(ProcessMediaReceiverRegistrarEventRequest request)
-        {
-            return ProcessEventRequest(MediaReceiverRegistrar);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Unsubscribe(ProcessContentDirectoryEventRequest request)
-        {
-            return ProcessEventRequest(ContentDirectory);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Unsubscribe(ProcessConnectionManagerEventRequest request)
-        {
-            return ProcessEventRequest(ConnectionManager);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Unsubscribe(ProcessMediaReceiverRegistrarEventRequest request)
-        {
-            return ProcessEventRequest(MediaReceiverRegistrar);
-        }
-
-        private object ProcessEventRequest(IEventManager eventManager)
-        {
-            var subscriptionId = GetHeader("SID");
-
-            if (string.Equals(Request.Verb, "SUBSCRIBE", StringComparison.OrdinalIgnoreCase))
-            {
-                var notificationType = GetHeader("NT");
-
-                var callback = GetHeader("CALLBACK");
-                var timeoutString = GetHeader("TIMEOUT");
-
-                if (string.IsNullOrEmpty(notificationType))
-                {
-                    return GetSubscriptionResponse(eventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callback));
-                }
-
-                return GetSubscriptionResponse(eventManager.CreateEventSubscription(notificationType, timeoutString, callback));
-            }
-
-            return GetSubscriptionResponse(eventManager.CancelEventSubscription(subscriptionId));
-        }
-
-        private object GetSubscriptionResponse(EventSubscriptionResponse response)
-        {
-            return _resultFactory.GetResult(Request, response.Content, response.ContentType, response.Headers);
-        }
-    }
-}

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

@@ -1,88 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Dlna.Api
-{
-    [Route("/Dlna/ProfileInfos", "GET", Summary = "Gets a list of profiles")]
-    public class GetProfileInfos : IReturn<DeviceProfileInfo[]>
-    {
-    }
-
-    [Route("/Dlna/Profiles/{Id}", "DELETE", Summary = "Deletes a profile")]
-    public class DeleteProfile : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Dlna/Profiles/Default", "GET", Summary = "Gets the default profile")]
-    public class GetDefaultProfile : IReturn<DeviceProfile>
-    {
-    }
-
-    [Route("/Dlna/Profiles/{Id}", "GET", Summary = "Gets a single profile")]
-    public class GetProfile : IReturn<DeviceProfile>
-    {
-        [ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Dlna/Profiles/{Id}", "POST", Summary = "Updates a profile")]
-    public class UpdateProfile : DeviceProfile, IReturnVoid
-    {
-    }
-
-    [Route("/Dlna/Profiles", "POST", Summary = "Creates a profile")]
-    public class CreateProfile : DeviceProfile, IReturnVoid
-    {
-    }
-
-    [Authenticated(Roles = "Admin")]
-    public class DlnaService : IService
-    {
-        private readonly IDlnaManager _dlnaManager;
-
-        public DlnaService(IDlnaManager dlnaManager)
-        {
-            _dlnaManager = dlnaManager;
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetProfileInfos request)
-        {
-            return _dlnaManager.GetProfileInfos().ToArray();
-        }
-
-        public object Get(GetProfile request)
-        {
-            return _dlnaManager.GetProfile(request.Id);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetDefaultProfile request)
-        {
-            return _dlnaManager.GetDefaultProfile();
-        }
-
-        public void Delete(DeleteProfile request)
-        {
-            _dlnaManager.DeleteProfile(request.Id);
-        }
-
-        public void Post(UpdateProfile request)
-        {
-            _dlnaManager.UpdateProfile(request);
-        }
-
-        public void Post(CreateProfile request)
-        {
-            _dlnaManager.CreateProfile(request);
-        }
-    }
-}

+ 1 - 1
Emby.Dlna/Common/ServiceAction.cs

@@ -13,7 +13,7 @@ namespace Emby.Dlna.Common
 
         public string Name { get; set; }
 
-        public List<Argument> ArgumentList { get; set; }
+        public List<Argument> ArgumentList { get; }
 
         /// <inheritdoc />
         public override string ToString()

+ 2 - 1
Emby.Dlna/Common/StateVariable.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Collections.Generic;
 
 namespace Emby.Dlna.Common
 {
@@ -17,7 +18,7 @@ namespace Emby.Dlna.Common
 
         public bool SendsEvents { get; set; }
 
-        public string[] AllowedValues { get; set; }
+        public IReadOnlyList<string> AllowedValues { get; set; }
 
         /// <inheritdoc />
         public override string ToString()

+ 0 - 16
Emby.Dlna/ConfigurationExtension.cs

@@ -1,7 +1,6 @@
 #nullable enable
 #pragma warning disable CS1591
 
-using System.Collections.Generic;
 using Emby.Dlna.Configuration;
 using MediaBrowser.Common.Configuration;
 
@@ -14,19 +13,4 @@ namespace Emby.Dlna
             return manager.GetConfiguration<DlnaOptions>("dlna");
         }
     }
-
-    public class DlnaConfigurationFactory : IConfigurationFactory
-    {
-        public IEnumerable<ConfigurationStore> GetConfigurations()
-        {
-            return new ConfigurationStore[]
-            {
-                new ConfigurationStore
-                {
-                    Key = "dlna",
-                    ConfigurationType = typeof (DlnaOptions)
-                }
-            };
-        }
-    }
 }

+ 7 - 9
Emby.Dlna/ConnectionManager/ConnectionManager.cs → Emby.Dlna/ConnectionManager/ConnectionManagerService.cs

@@ -1,30 +1,28 @@
 #pragma warning disable CS1591
 
+using System.Net.Http;
 using System.Threading.Tasks;
 using Emby.Dlna.Service;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.ConnectionManager
 {
-    public class ConnectionManager : BaseService, IConnectionManager
+    public class ConnectionManagerService : BaseService, IConnectionManager
     {
         private readonly IDlnaManager _dlna;
-        private readonly ILogger _logger;
         private readonly IServerConfigurationManager _config;
 
-        public ConnectionManager(
+        public ConnectionManagerService(
             IDlnaManager dlna,
             IServerConfigurationManager config,
-            ILogger<ConnectionManager> logger,
-            IHttpClient httpClient)
-            : base(logger, httpClient)
+            ILogger<ConnectionManagerService> logger,
+            IHttpClientFactory httpClientFactory)
+            : base(logger, httpClientFactory)
         {
             _dlna = dlna;
             _config = config;
-            _logger = logger;
         }
 
         /// <inheritdoc />
@@ -39,7 +37,7 @@ namespace Emby.Dlna.ConnectionManager
             var profile = _dlna.GetProfile(request.Headers) ??
                          _dlna.GetDefaultProfile();
 
-            return new ControlHandler(_config, _logger, profile).ProcessControlRequestAsync(request);
+            return new ControlHandler(_config, Logger, profile).ProcessControlRequestAsync(request);
         }
     }
 }

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

@@ -44,7 +44,7 @@ namespace Emby.Dlna.ConnectionManager
                 DataType = "string",
                 SendsEvents = false,
 
-                AllowedValues = new string[]
+                AllowedValues = new[]
                 {
                     "OK",
                     "ContentFormatMismatch",
@@ -67,7 +67,7 @@ namespace Emby.Dlna.ConnectionManager
                 DataType = "string",
                 SendsEvents = false,
 
-                AllowedValues = new string[]
+                AllowedValues = new[]
                 {
                     "Output",
                     "Input"

+ 11 - 13
Emby.Dlna/ContentDirectory/ContentDirectory.cs → Emby.Dlna/ContentDirectory/ContentDirectoryService.cs

@@ -1,13 +1,15 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Linq;
+using System.Net.Http;
 using System.Threading.Tasks;
 using Emby.Dlna.Service;
-using MediaBrowser.Common.Net;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.TV;
@@ -17,7 +19,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.ContentDirectory
 {
-    public class ContentDirectory : BaseService, IContentDirectory
+    public class ContentDirectoryService : BaseService, IContentDirectory
     {
         private readonly ILibraryManager _libraryManager;
         private readonly IImageProcessor _imageProcessor;
@@ -31,14 +33,15 @@ namespace Emby.Dlna.ContentDirectory
         private readonly IMediaEncoder _mediaEncoder;
         private readonly ITVSeriesManager _tvSeriesManager;
 
-        public ContentDirectory(IDlnaManager dlna,
+        public ContentDirectoryService(
+            IDlnaManager dlna,
             IUserDataManager userDataManager,
             IImageProcessor imageProcessor,
             ILibraryManager libraryManager,
             IServerConfigurationManager config,
             IUserManager userManager,
-            ILogger<ContentDirectory> logger,
-            IHttpClient httpClient,
+            ILogger<ContentDirectoryService> logger,
+            IHttpClientFactory httpClient,
             ILocalizationManager localization,
             IMediaSourceManager mediaSourceManager,
             IUserViewManager userViewManager,
@@ -130,18 +133,13 @@ namespace Emby.Dlna.ContentDirectory
 
             foreach (var user in _userManager.Users)
             {
-                if (user.Policy.IsAdministrator)
+                if (user.HasPermission(PermissionKind.IsAdministrator))
                 {
                     return user;
                 }
             }
 
-            foreach (var user in _userManager.Users)
-            {
-                return user;
-            }
-
-            return null;
+            return _userManager.Users.FirstOrDefault();
         }
     }
 }

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

@@ -10,7 +10,8 @@ namespace Emby.Dlna.ContentDirectory
     {
         public string GetXml()
         {
-            return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
+            return new ServiceXmlBuilder().GetXml(
+                new ServiceActionListBuilder().GetActions(),
                 GetStateVariables());
         }
 
@@ -101,7 +102,7 @@ namespace Emby.Dlna.ContentDirectory
                 DataType = "string",
                 SendsEvents = false,
 
-                AllowedValues = new string[]
+                AllowedValues = new[]
                 {
                     "BrowseMetadata",
                     "BrowseDirectChildren"

+ 87 - 104
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -10,6 +10,8 @@ using System.Threading;
 using System.Xml;
 using Emby.Dlna.Didl;
 using Emby.Dlna.Service;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
@@ -17,7 +19,6 @@ using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
@@ -28,11 +29,22 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Querying;
 using Microsoft.Extensions.Logging;
+using Book = MediaBrowser.Controller.Entities.Book;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
 
 namespace Emby.Dlna.ContentDirectory
 {
     public class ControlHandler : BaseControlHandler
     {
+        private const string NsDc = "http://purl.org/dc/elements/1.1/";
+        private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
+        private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
+        private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
+
         private readonly ILibraryManager _libraryManager;
         private readonly IUserDataManager _userDataManager;
         private readonly IServerConfigurationManager _config;
@@ -40,11 +52,6 @@ namespace Emby.Dlna.ContentDirectory
         private readonly IUserViewManager _userViewManager;
         private readonly ITVSeriesManager _tvSeriesManager;
 
-        private const string NS_DC = "http://purl.org/dc/elements/1.1/";
-        private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
-        private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
-        private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
-
         private readonly int _systemUpdateId;
 
         private readonly DidlBuilder _didlBuilder;
@@ -174,7 +181,11 @@ namespace Emby.Dlna.ContentDirectory
 
             userdata.PlaybackPositionTicks = TimeSpan.FromSeconds(newbookmark).Ticks;
 
-            _userDataManager.SaveUserData(_user, item, userdata, UserDataSaveReason.TogglePlayed,
+            _userDataManager.SaveUserData(
+                _user,
+                item,
+                userdata,
+                UserDataSaveReason.TogglePlayed,
                 CancellationToken.None);
         }
 
@@ -246,7 +257,7 @@ namespace Emby.Dlna.ContentDirectory
             var id = sparams["ObjectID"];
             var flag = sparams["BrowseFlag"];
             var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
-            var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", ""));
+            var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
 
             var provided = 0;
 
@@ -279,18 +290,17 @@ namespace Emby.Dlna.ContentDirectory
 
                 using (var writer = XmlWriter.Create(builder, settings))
                 {
-                    writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
+                    writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
 
-                    writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
-                    writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
-                    writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
+                    writer.WriteAttributeString("xmlns", "dc", null, NsDc);
+                    writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
+                    writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
 
                     DidlBuilder.WriteXmlRootAttributes(_profile, writer);
 
                     var serverItem = GetItemFromObjectId(id);
                     var item = serverItem.Item;
 
-
                     if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal))
                     {
                         totalCount = 1;
@@ -355,8 +365,8 @@ namespace Emby.Dlna.ContentDirectory
 
         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", ""));
+            var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty));
+            var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
             var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
 
             // sort example: dc:title, dc:date
@@ -390,11 +400,11 @@ namespace Emby.Dlna.ContentDirectory
 
                 using (var writer = XmlWriter.Create(builder, settings))
                 {
-                    writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
+                    writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
 
-                    writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
-                    writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
-                    writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
+                    writer.WriteAttributeString("xmlns", "dc", null, NsDc);
+                    writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
+                    writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
 
                     DidlBuilder.WriteXmlRootAttributes(_profile, writer);
 
@@ -460,12 +470,12 @@ namespace Emby.Dlna.ContentDirectory
             }
             else if (search.SearchType == SearchType.Playlist)
             {
-                //items = items.OfType<Playlist>();
+                // items = items.OfType<Playlist>();
                 isFolder = true;
             }
             else if (search.SearchType == SearchType.MusicAlbum)
             {
-                //items = items.OfType<MusicAlbum>();
+                // items = items.OfType<MusicAlbum>();
                 isFolder = true;
             }
 
@@ -731,7 +741,7 @@ namespace Emby.Dlna.ContentDirectory
                 return GetGenres(item, user, query);
             }
 
-            var array = new ServerItem[]
+            var array = new[]
             {
                 new ServerItem(item)
                 {
@@ -776,11 +786,14 @@ namespace Emby.Dlna.ContentDirectory
                 })
                 .ToArray();
 
-            return ApplyPaging(new QueryResult<ServerItem>
-            {
-                Items = folders,
-                TotalRecordCount = folders.Length
-            }, startIndex, limit);
+            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)
@@ -920,7 +933,7 @@ namespace Emby.Dlna.ContentDirectory
         private QueryResult<ServerItem> GetMovieCollections(User user, InternalItemsQuery query)
         {
             query.Recursive = true;
-            //query.Parent = parent;
+            // query.Parent = parent;
             query.SetUser(user);
 
             query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
@@ -1115,7 +1128,7 @@ namespace Emby.Dlna.ContentDirectory
         private QueryResult<ServerItem> GetMusicPlaylists(User user, InternalItemsQuery query)
         {
             query.Parent = null;
-            query.IncludeItemTypes = new[] { typeof(Playlist).Name };
+            query.IncludeItemTypes = new[] { nameof(Playlist) };
             query.SetUser(user);
             query.Recursive = true;
 
@@ -1128,15 +1141,16 @@ namespace Emby.Dlna.ContentDirectory
         {
             query.OrderBy = Array.Empty<(string, SortOrder)>();
 
-            var items = _userViewManager.GetLatestItems(new LatestItemsQuery
-            {
-                UserId = user.Id,
-                Limit = 50,
-                IncludeItemTypes = new[] { typeof(Audio).Name },
-                ParentId = parent == null ? Guid.Empty : parent.Id,
-                GroupItems = true
-
-            }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
+            var items = _userViewManager.GetLatestItems(
+                new LatestItemsQuery
+                {
+                    UserId = user.Id,
+                    Limit = 50,
+                    IncludeItemTypes = new[] { nameof(Audio) },
+                    ParentId = parent?.Id ?? Guid.Empty,
+                    GroupItems = true
+                },
+                query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
 
             return ToResult(items);
         }
@@ -1145,13 +1159,15 @@ namespace Emby.Dlna.ContentDirectory
         {
             query.OrderBy = Array.Empty<(string, SortOrder)>();
 
-            var result = _tvSeriesManager.GetNextUp(new NextUpQuery
-            {
-                Limit = query.Limit,
-                StartIndex = query.StartIndex,
-                UserId = query.User.Id
-
-            }, new[] { parent }, query.DtoOptions);
+            var result = _tvSeriesManager.GetNextUp(
+                new NextUpQuery
+                {
+                    Limit = query.Limit,
+                    StartIndex = query.StartIndex,
+                    UserId = query.User.Id
+                },
+                new[] { parent },
+                query.DtoOptions);
 
             return ToResult(result);
         }
@@ -1160,15 +1176,16 @@ namespace Emby.Dlna.ContentDirectory
         {
             query.OrderBy = Array.Empty<(string, SortOrder)>();
 
-            var items = _userViewManager.GetLatestItems(new LatestItemsQuery
-            {
-                UserId = user.Id,
-                Limit = 50,
-                IncludeItemTypes = new[] { typeof(Episode).Name },
-                ParentId = parent == null ? Guid.Empty : parent.Id,
-                GroupItems = false
-
-            }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
+            var items = _userViewManager.GetLatestItems(
+                new LatestItemsQuery
+                {
+                    UserId = user.Id,
+                    Limit = 50,
+                    IncludeItemTypes = new[] { typeof(Episode).Name },
+                    ParentId = parent == null ? Guid.Empty : parent.Id,
+                    GroupItems = false
+                },
+                query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
 
             return ToResult(items);
         }
@@ -1177,15 +1194,16 @@ namespace Emby.Dlna.ContentDirectory
         {
             query.OrderBy = Array.Empty<(string, SortOrder)>();
 
-            var items = _userViewManager.GetLatestItems(new LatestItemsQuery
-            {
-                UserId = user.Id,
-                Limit = 50,
-                IncludeItemTypes = new[] { typeof(Movie).Name },
-                ParentId = parent == null ? Guid.Empty : parent.Id,
-                GroupItems = true
-
-            }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
+            var items = _userViewManager.GetLatestItems(
+                new LatestItemsQuery
+                {
+                    UserId = user.Id,
+                    Limit = 50,
+                    IncludeItemTypes = new[] { nameof(Movie) },
+                    ParentId = parent?.Id ?? Guid.Empty,
+                    GroupItems = true
+                },
+                query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
 
             return ToResult(items);
         }
@@ -1217,7 +1235,11 @@ namespace Emby.Dlna.ContentDirectory
                 Recursive = true,
                 ParentId = parentId,
                 GenreIds = new[] { item.Id },
-                IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Series).Name },
+                IncludeItemTypes = new[]
+                {
+                    nameof(Movie),
+                    nameof(Series)
+                },
                 Limit = limit,
                 StartIndex = startIndex,
                 DtoOptions = GetDtoOptions()
@@ -1341,48 +1363,9 @@ 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());
         }
     }
-
-    internal class ServerItem
-    {
-        public BaseItem Item { get; set; }
-        public StubType? StubType { get; set; }
-
-        public ServerItem(BaseItem item)
-        {
-            Item = item;
-
-            if (item is IItemByName && !(item is Folder))
-            {
-                StubType = Dlna.ContentDirectory.StubType.Folder;
-            }
-        }
-    }
-
-    public enum StubType
-    {
-        Folder = 0,
-        Latest = 2,
-        Playlists = 3,
-        Albums = 4,
-        AlbumArtists = 5,
-        Artists = 6,
-        Songs = 7,
-        Genres = 8,
-        FavoriteSongs = 9,
-        FavoriteArtists = 10,
-        FavoriteAlbums = 11,
-        ContinueWatching = 12,
-        Movies = 13,
-        Collections = 14,
-        Favorites = 15,
-        NextUp = 16,
-        Series = 17,
-        FavoriteSeries = 18,
-        FavoriteEpisodes = 19
-    }
 }

+ 23 - 0
Emby.Dlna/ContentDirectory/ServerItem.cs

@@ -0,0 +1,23 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities;
+
+namespace Emby.Dlna.ContentDirectory
+{
+    internal class ServerItem
+    {
+        public ServerItem(BaseItem item)
+        {
+            Item = item;
+
+            if (item is IItemByName && !(item is Folder))
+            {
+                StubType = Dlna.ContentDirectory.StubType.Folder;
+            }
+        }
+
+        public BaseItem Item { get; set; }
+
+        public StubType? StubType { get; set; }
+    }
+}

+ 28 - 0
Emby.Dlna/ContentDirectory/StubType.cs

@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1602
+
+namespace Emby.Dlna.ContentDirectory
+{
+    public enum StubType
+    {
+        Folder = 0,
+        Latest = 2,
+        Playlists = 3,
+        Albums = 4,
+        AlbumArtists = 5,
+        Artists = 6,
+        Songs = 7,
+        Genres = 8,
+        FavoriteSongs = 9,
+        FavoriteArtists = 10,
+        FavoriteAlbums = 11,
+        ContinueWatching = 12,
+        Movies = 13,
+        Collections = 14,
+        Favorites = 15,
+        NextUp = 16,
+        Series = 17,
+        FavoriteSeries = 18,
+        FavoriteEpisodes = 19
+    }
+}

+ 6 - 6
Emby.Dlna/ControlRequest.cs

@@ -7,17 +7,17 @@ namespace Emby.Dlna
 {
     public class ControlRequest
     {
-        public IHeaderDictionary Headers { get; set; }
+        public ControlRequest(IHeaderDictionary headers)
+        {
+            Headers = headers;
+        }
+
+        public IHeaderDictionary Headers { get; }
 
         public Stream InputXml { get; set; }
 
         public string TargetServerUuId { get; set; }
 
         public string RequestedUrl { get; set; }
-
-        public ControlRequest()
-        {
-            Headers = new HeaderDictionary();
-        }
     }
 }

+ 7 - 1
Emby.Dlna/ControlResponse.cs

@@ -11,10 +11,16 @@ namespace Emby.Dlna
             Headers = new Dictionary<string, string>();
         }
 
-        public IDictionary<string, string> Headers { get; set; }
+        public IDictionary<string, string> Headers { get; }
 
         public string Xml { get; set; }
 
         public bool IsSuccessful { get; set; }
+
+        /// <inheritdoc />
+        public override string ToString()
+        {
+            return Xml;
+        }
     }
 }

+ 91 - 103
Emby.Dlna/Didl/DidlBuilder.cs

@@ -6,14 +6,13 @@ using System.IO;
 using System.Linq;
 using System.Text;
 using System.Xml;
-using Emby.Dlna.Configuration;
 using Emby.Dlna.ContentDirectory;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Playlists;
@@ -23,17 +22,24 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Net;
 using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
+using XmlAttribute = MediaBrowser.Model.Dlna.XmlAttribute;
 
 namespace Emby.Dlna.Didl
 {
     public class DidlBuilder
     {
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
+        private const string NsDc = "http://purl.org/dc/elements/1.1/";
+        private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
+        private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
 
-        private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
-        private const string NS_DC = "http://purl.org/dc/elements/1.1/";
-        private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
-        private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
         private readonly DeviceProfile _profile;
         private readonly IImageProcessor _imageProcessor;
@@ -92,21 +98,21 @@ namespace Emby.Dlna.Didl
             {
                 using (var writer = XmlWriter.Create(builder, settings))
                 {
-                    //writer.WriteStartDocument();
+                    // writer.WriteStartDocument();
 
-                    writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
+                    writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
 
-                    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);
+                    writer.WriteAttributeString("xmlns", "dc", null, NsDc);
+                    writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
+                    writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
+                    // didl.SetAttribute("xmlns:sec", NS_SEC);
 
                     WriteXmlRootAttributes(_profile, writer);
 
                     WriteItemElement(writer, item, user, context, null, deviceId, filter, streamInfo);
 
                     writer.WriteFullEndElement();
-                    //writer.WriteEndDocument();
+                    // writer.WriteEndDocument();
                 }
 
                 return builder.ToString();
@@ -141,7 +147,7 @@ namespace Emby.Dlna.Didl
         {
             var clientId = GetClientId(item, null);
 
-            writer.WriteStartElement(string.Empty, "item", NS_DIDL);
+            writer.WriteStartElement(string.Empty, "item", NsDidl);
 
             writer.WriteAttributeString("restricted", "1");
             writer.WriteAttributeString("id", clientId);
@@ -201,7 +207,8 @@ namespace Emby.Dlna.Didl
             var targetWidth = streamInfo.TargetWidth;
             var targetHeight = streamInfo.TargetHeight;
 
-            var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(streamInfo.Container,
+            var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
+                streamInfo.Container,
                 streamInfo.TargetVideoCodec.FirstOrDefault(),
                 streamInfo.TargetAudioCodec.FirstOrDefault(),
                 targetWidth,
@@ -273,7 +280,7 @@ namespace Emby.Dlna.Didl
             }
             else if (string.Equals(subtitleMode, "smi", StringComparison.OrdinalIgnoreCase))
             {
-                writer.WriteStartElement(string.Empty, "res", NS_DIDL);
+                writer.WriteStartElement(string.Empty, "res", NsDidl);
 
                 writer.WriteAttributeString("protocolInfo", "http-get:*:smi/caption:*");
 
@@ -282,7 +289,7 @@ namespace Emby.Dlna.Didl
             }
             else
             {
-                writer.WriteStartElement(string.Empty, "res", NS_DIDL);
+                writer.WriteStartElement(string.Empty, "res", NsDidl);
                 var protocolInfo = string.Format(
                     CultureInfo.InvariantCulture,
                     "http-get:*:text/{0}:*",
@@ -298,7 +305,7 @@ namespace Emby.Dlna.Didl
 
         private void AddVideoResource(XmlWriter writer, Filter filter, string contentFeatures, StreamInfo streamInfo)
         {
-            writer.WriteStartElement(string.Empty, "res", NS_DIDL);
+            writer.WriteStartElement(string.Empty, "res", NsDidl);
 
             var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
 
@@ -358,7 +365,8 @@ namespace Emby.Dlna.Didl
                 writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
             }
 
-            var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container,
+            var mediaProfile = _profile.GetVideoMediaProfile(
+                streamInfo.Container,
                 streamInfo.TargetAudioCodec.FirstOrDefault(),
                 streamInfo.TargetVideoCodec.FirstOrDefault(),
                 streamInfo.TargetAudioBitrate,
@@ -421,7 +429,6 @@ namespace Emby.Dlna.Didl
                     case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows");
                     case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes");
                     case StubType.Series: return _localization.GetLocalizedString("Shows");
-                    default: break;
                 }
             }
 
@@ -520,7 +527,7 @@ namespace Emby.Dlna.Didl
 
         private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
         {
-            writer.WriteStartElement(string.Empty, "res", NS_DIDL);
+            writer.WriteStartElement(string.Empty, "res", NsDidl);
 
             if (streamInfo == null)
             {
@@ -577,7 +584,8 @@ namespace Emby.Dlna.Didl
                 writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
             }
 
-            var mediaProfile = _profile.GetAudioMediaProfile(streamInfo.Container,
+            var mediaProfile = _profile.GetAudioMediaProfile(
+                streamInfo.Container,
                 streamInfo.TargetAudioCodec.FirstOrDefault(),
                 targetChannels,
                 targetAudioBitrate,
@@ -590,7 +598,8 @@ namespace Emby.Dlna.Didl
                 ? MimeTypes.GetMimeType(filename)
                 : mediaProfile.MimeType;
 
-            var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(streamInfo.Container,
+            var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
+                streamInfo.Container,
                 streamInfo.TargetAudioCodec.FirstOrDefault(),
                 targetAudioBitrate,
                 targetSampleRate,
@@ -621,7 +630,7 @@ namespace Emby.Dlna.Didl
 
         public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null)
         {
-            writer.WriteStartElement(string.Empty, "container", NS_DIDL);
+            writer.WriteStartElement(string.Empty, "container", NsDidl);
 
             writer.WriteAttributeString("restricted", "1");
             writer.WriteAttributeString("searchable", "1");
@@ -670,7 +679,7 @@ namespace Emby.Dlna.Didl
                 return;
             }
 
-            MediaBrowser.Model.Dlna.XmlAttribute secAttribute = null;
+            XmlAttribute secAttribute = null;
             foreach (var attribute in _profile.XmlRootAttributes)
             {
                 if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
@@ -700,15 +709,15 @@ namespace Emby.Dlna.Didl
         }
 
         /// <summary>
-        /// Adds fields used by both items and folders
+        /// Adds fields used by both items and folders.
         /// </summary>
         private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
         {
             // Don't filter on dc:title because not all devices will include it in the filter
             // MediaMonkey for example won't display content without a title
-            //if (filter.Contains("dc:title"))
+            // if (filter.Contains("dc:title"))
             {
-                AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC);
+                AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NsDc);
             }
 
             WriteObjectClass(writer, item, itemStubType);
@@ -717,7 +726,7 @@ namespace Emby.Dlna.Didl
             {
                 if (item.PremiereDate.HasValue)
                 {
-                    AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NS_DC);
+                    AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc);
                 }
             }
 
@@ -725,13 +734,13 @@ namespace Emby.Dlna.Didl
             {
                 foreach (var genre in item.Genres)
                 {
-                    AddValue(writer, "upnp", "genre", genre, NS_UPNP);
+                    AddValue(writer, "upnp", "genre", genre, NsUpnp);
                 }
             }
 
             foreach (var studio in item.Studios)
             {
-                AddValue(writer, "upnp", "publisher", studio, NS_UPNP);
+                AddValue(writer, "upnp", "publisher", studio, NsUpnp);
             }
 
             if (!(item is Folder))
@@ -742,27 +751,29 @@ namespace Emby.Dlna.Didl
 
                     if (!string.IsNullOrWhiteSpace(desc))
                     {
-                        AddValue(writer, "dc", "description", desc, NS_DC);
+                        AddValue(writer, "dc", "description", desc, NsDc);
                     }
                 }
-                //if (filter.Contains("upnp:longDescription"))
-                //{
+
+                // if (filter.Contains("upnp:longDescription"))
+                // {
                 //    if (!string.IsNullOrWhiteSpace(item.Overview))
                 //    {
-                //        AddValue(writer, "upnp", "longDescription", item.Overview, NS_UPNP);
+                //        AddValue(writer, "upnp", "longDescription", item.Overview, NsUpnp);
                 //    }
-                //}
+                // }
             }
 
             if (!string.IsNullOrEmpty(item.OfficialRating))
             {
                 if (filter.Contains("dc:rating"))
                 {
-                    AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC);
+                    AddValue(writer, "dc", "rating", item.OfficialRating, NsDc);
                 }
+
                 if (filter.Contains("upnp:rating"))
                 {
-                    AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP);
+                    AddValue(writer, "upnp", "rating", item.OfficialRating, NsUpnp);
                 }
             }
 
@@ -774,7 +785,7 @@ namespace Emby.Dlna.Didl
             // More types here
             // http://oss.linn.co.uk/repos/Public/LibUpnpCil/DidlLite/UpnpAv/Test/TestDidlLite.cs
 
-            writer.WriteStartElement("upnp", "class", NS_UPNP);
+            writer.WriteStartElement("upnp", "class", NsUpnp);
 
             if (item.IsDisplayedAsFolder || stubType.HasValue)
             {
@@ -875,7 +886,7 @@ namespace Emby.Dlna.Didl
                 var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
                     ?? PersonType.Actor;
 
-                AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NS_UPNP);
+                AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp);
             }
         }
 
@@ -889,8 +900,8 @@ namespace Emby.Dlna.Didl
             {
                 foreach (var artist in hasArtists.Artists)
                 {
-                    AddValue(writer, "upnp", "artist", artist, NS_UPNP);
-                    AddValue(writer, "dc", "creator", artist, NS_DC);
+                    AddValue(writer, "upnp", "artist", artist, NsUpnp);
+                    AddValue(writer, "dc", "creator", artist, NsDc);
 
                     // If it doesn't support album artists (musicvideo), then tag as both
                     if (hasAlbumArtists == null)
@@ -910,16 +921,16 @@ namespace Emby.Dlna.Didl
 
             if (!string.IsNullOrWhiteSpace(item.Album))
             {
-                AddValue(writer, "upnp", "album", item.Album, NS_UPNP);
+                AddValue(writer, "upnp", "album", item.Album, NsUpnp);
             }
 
             if (item.IndexNumber.HasValue)
             {
-                AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP);
+                AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
 
                 if (item is Episode)
                 {
-                    AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP);
+                    AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
                 }
             }
         }
@@ -928,7 +939,7 @@ namespace Emby.Dlna.Didl
         {
             try
             {
-                writer.WriteStartElement("upnp", "artist", NS_UPNP);
+                writer.WriteStartElement("upnp", "artist", NsUpnp);
                 writer.WriteAttributeString("role", "AlbumArtist");
 
                 writer.WriteString(name);
@@ -937,7 +948,7 @@ namespace Emby.Dlna.Didl
             }
             catch (XmlException ex)
             {
-                _logger.LogError(ex, "Error adding xml value: {value}", name);
+                _logger.LogError(ex, "Error adding xml value: {Value}", name);
             }
         }
 
@@ -949,7 +960,7 @@ namespace Emby.Dlna.Didl
             }
             catch (XmlException ex)
             {
-                _logger.LogError(ex, "Error adding xml value: {value}", value);
+                _logger.LogError(ex, "Error adding xml value: {Value}", value);
             }
         }
 
@@ -964,14 +975,14 @@ namespace Emby.Dlna.Didl
 
             var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg");
 
-            writer.WriteStartElement("upnp", "albumArtURI", NS_UPNP);
-            writer.WriteAttributeString("dlna", "profileID", NS_DLNA, _profile.AlbumArtPn);
-            writer.WriteString(albumartUrlInfo.Url);
+            writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
+            writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
+            writer.WriteString(albumartUrlInfo.url);
             writer.WriteFullEndElement();
 
             // TOOD: Remove these default values
             var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg");
-            writer.WriteElementString("upnp", "icon", NS_UPNP, iconUrlInfo.Url);
+            writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
 
             if (!_profile.EnableAlbumArtInDidl)
             {
@@ -995,7 +1006,6 @@ namespace Emby.Dlna.Didl
             }
 
             AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
-
         }
 
         private void AddImageResElement(
@@ -1015,12 +1025,12 @@ namespace Emby.Dlna.Didl
 
             var albumartUrlInfo = GetImageUrl(imageInfo, maxWidth, maxHeight, format);
 
-            writer.WriteStartElement(string.Empty, "res", NS_DIDL);
+            writer.WriteStartElement(string.Empty, "res", NsDidl);
 
             // Images must have a reported size or many clients (Bubble upnp), will only use the first thumbnail
             // rather than using a larger one when available
-            var width = albumartUrlInfo.Width ?? maxWidth;
-            var height = albumartUrlInfo.Height ?? maxHeight;
+            var width = albumartUrlInfo.width ?? maxWidth;
+            var height = albumartUrlInfo.height ?? maxHeight;
 
             var contentFeatures = new ContentFeatureBuilder(_profile)
                 .BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
@@ -1037,7 +1047,7 @@ namespace Emby.Dlna.Didl
                 "resolution",
                 string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height));
 
-            writer.WriteString(albumartUrlInfo.Url);
+            writer.WriteString(albumartUrlInfo.url);
 
             writer.WriteFullEndElement();
         }
@@ -1048,10 +1058,12 @@ namespace Emby.Dlna.Didl
             {
                 return GetImageInfo(item, ImageType.Primary);
             }
+
             if (item.HasImage(ImageType.Thumb))
             {
                 return GetImageInfo(item, ImageType.Thumb);
             }
+
             if (item.HasImage(ImageType.Backdrop))
             {
                 if (item is Channel)
@@ -1131,29 +1143,15 @@ namespace Emby.Dlna.Didl
 
             if (width == 0 || height == 0)
             {
-                //_imageProcessor.GetImageSize(item, imageInfo);
                 width = null;
                 height = null;
             }
-
             else if (width == -1 || height == -1)
             {
                 width = null;
                 height = null;
             }
 
-            //try
-            //{
-            //    var size = _imageProcessor.GetImageSize(imageInfo);
-
-            //    width = size.Width;
-            //    height = size.Height;
-            //}
-            //catch
-            //{
-
-            //}
-
             var inputFormat = (Path.GetExtension(imageInfo.Path) ?? string.Empty)
                 .TrimStart('.')
                 .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
@@ -1170,30 +1168,6 @@ namespace Emby.Dlna.Didl
             };
         }
 
-        private class ImageDownloadInfo
-        {
-            internal Guid ItemId;
-            internal string ImageTag;
-            internal ImageType Type;
-
-            internal int? Width;
-            internal int? Height;
-
-            internal bool IsDirectStream;
-
-            internal string Format;
-
-            internal ItemImageInfo ItemImageInfo;
-        }
-
-        private class ImageUrlInfo
-        {
-            internal string Url;
-
-            internal int? Width;
-            internal int? Height;
-        }
-
         public static string GetClientId(BaseItem item, StubType? stubType)
         {
             return GetClientId(item.Id, stubType);
@@ -1211,7 +1185,7 @@ namespace Emby.Dlna.Didl
             return id;
         }
 
-        private ImageUrlInfo GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
+        private (string url, int? width, int? height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
         {
             var url = string.Format(
                 CultureInfo.InvariantCulture,
@@ -1249,12 +1223,26 @@ namespace Emby.Dlna.Didl
             // just lie
             info.IsDirectStream = true;
 
-            return new ImageUrlInfo
-            {
-                Url = url,
-                Width = width,
-                Height = height
-            };
+            return (url, width, height);
+        }
+
+        private class ImageDownloadInfo
+        {
+            internal Guid ItemId { get; set; }
+
+            internal string ImageTag { get; set; }
+
+            internal ImageType Type { get; set; }
+
+            internal int? Width { get; set; }
+
+            internal int? Height { get; set; }
+
+            internal bool IsDirectStream { get; set; }
+
+            internal string Format { get; set; }
+
+            internal ItemImageInfo ItemImageInfo { get; set; }
         }
     }
 }

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

@@ -12,7 +12,6 @@ namespace Emby.Dlna.Didl
         public Filter()
             : this("*")
         {
-
         }
 
         public Filter(string filter)
@@ -24,9 +23,7 @@ namespace Emby.Dlna.Didl
 
         public bool Contains(string field)
         {
-            // Don't bother with this. Some clients (media monkey) use the filter and then don't display very well when very little data comes back.
-            return true;
-            //return _all || ListHelper.ContainsIgnoreCase(_fields, field);
+            return _all || Array.Exists(_fields, x => x.Equals(field, StringComparison.OrdinalIgnoreCase));
         }
     }
 }

+ 1 - 1
Emby.Dlna/Didl/StringWriterWithEncoding.cs

@@ -1,4 +1,5 @@
 #pragma warning disable CS1591
+#pragma warning disable CA1305
 
 using System;
 using System.IO;
@@ -29,7 +30,6 @@ namespace Emby.Dlna.Didl
         {
         }
 
-
         public StringWriterWithEncoding(Encoding encoding)
         {
             _encoding = encoding;

+ 24 - 0
Emby.Dlna/DlnaConfigurationFactory.cs

@@ -0,0 +1,24 @@
+#nullable enable
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using Emby.Dlna.Configuration;
+using MediaBrowser.Common.Configuration;
+
+namespace Emby.Dlna
+{
+    public class DlnaConfigurationFactory : IConfigurationFactory
+    {
+        public IEnumerable<ConfigurationStore> GetConfigurations()
+        {
+            return new[]
+            {
+                new ConfigurationStore
+                {
+                    Key = "dlna",
+                    ConfigurationType = typeof(DlnaOptions)
+                }
+            };
+        }
+    }
+}

+ 62 - 51
Emby.Dlna/DlnaManager.cs

@@ -31,7 +31,7 @@ namespace Emby.Dlna
         private readonly IApplicationPaths _appPaths;
         private readonly IXmlSerializer _xmlSerializer;
         private readonly IFileSystem _fileSystem;
-        private readonly ILogger _logger;
+        private readonly ILogger<DlnaManager> _logger;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IServerApplicationHost _appHost;
         private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
@@ -49,16 +49,20 @@ namespace Emby.Dlna
             _xmlSerializer = xmlSerializer;
             _fileSystem = fileSystem;
             _appPaths = appPaths;
-            _logger = loggerFactory.CreateLogger("Dlna");
+            _logger = loggerFactory.CreateLogger<DlnaManager>();
             _jsonSerializer = jsonSerializer;
             _appHost = appHost;
         }
 
+        private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
+
+        private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
+
         public async Task InitProfilesAsync()
         {
             try
             {
-                await ExtractSystemProfilesAsync();
+                await ExtractSystemProfilesAsync().ConfigureAwait(false);
                 LoadProfiles();
             }
             catch (Exception ex)
@@ -88,7 +92,6 @@ namespace Emby.Dlna
                     .Select(i => i.Item2)
                     .ToList();
             }
-
         }
 
         public DeviceProfile GetDefaultProfile()
@@ -123,83 +126,92 @@ namespace Emby.Dlna
             var builder = new StringBuilder();
 
             builder.AppendLine("No matching device profile found. The default will need to be used.");
-            builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty));
-            builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
-            builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
-            builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
-            builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty));
-            builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty));
-            builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty));
-            builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty));
-            builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty));
+            builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
 
             _logger.LogInformation(builder.ToString());
         }
 
         private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
         {
-            if (!string.IsNullOrEmpty(profileInfo.DeviceDescription))
-            {
-                if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
-                    return false;
-            }
-
             if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
             {
-                if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
+                if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
+                {
                     return false;
+                }
             }
 
             if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
             {
-                if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
+                if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
+                {
                     return false;
+                }
             }
 
             if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
             {
-                if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
+                if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
+                {
                     return false;
+                }
             }
 
             if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
             {
-                if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
+                if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
+                {
                     return false;
+                }
             }
 
             if (!string.IsNullOrEmpty(profileInfo.ModelName))
             {
-                if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName))
+                if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
+                {
                     return false;
+                }
             }
 
             if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
             {
-                if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
+                if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
+                {
                     return false;
+                }
             }
 
             if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
             {
-                if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
+                if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
+                {
                     return false;
+                }
             }
 
             if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
             {
-                if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
+                if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
+                {
                     return false;
+                }
             }
 
             return true;
         }
 
-        private bool IsRegexMatch(string input, string pattern)
+        private bool IsRegexOrSubstringMatch(string input, string pattern)
         {
             try
             {
-                return Regex.IsMatch(input, pattern);
+                return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
             }
             catch (ArgumentException ex)
             {
@@ -223,7 +235,7 @@ namespace Emby.Dlna
             }
             else
             {
-                var headerString = string.Join(", ", headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray());
+                var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
                 _logger.LogDebug("No matching device profile found. {0}", headerString);
             }
 
@@ -251,7 +263,7 @@ namespace Emby.Dlna
                         return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
                     case HeaderMatchType.Substring:
                         var isMatch = value.ToString().IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1;
-                        //_logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
+                        // _logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
                         return isMatch;
                     case HeaderMatchType.Regex:
                         return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase);
@@ -263,10 +275,6 @@ namespace Emby.Dlna
             return false;
         }
 
-        private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
-
-        private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
-
         private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
         {
             try
@@ -370,7 +378,7 @@ namespace Emby.Dlna
 
             foreach (var name in _assembly.GetManifestResourceNames())
             {
-                if (!name.StartsWith(namespaceName))
+                if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
                 {
                     continue;
                 }
@@ -389,7 +397,7 @@ namespace Emby.Dlna
 
                         using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
                         {
-                            await stream.CopyToAsync(fileStream);
+                            await stream.CopyToAsync(fileStream).ConfigureAwait(false);
                         }
                     }
                 }
@@ -439,6 +447,7 @@ namespace Emby.Dlna
             {
                 throw new ArgumentException("Profile is missing Id");
             }
+
             if (string.IsNullOrEmpty(profile.Name))
             {
                 throw new ArgumentException("Profile is missing Name");
@@ -464,6 +473,7 @@ namespace Emby.Dlna
             {
                 _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
             }
+
             SerializeToXml(profile, path);
         }
 
@@ -474,10 +484,10 @@ namespace Emby.Dlna
 
         /// <summary>
         /// Recreates the object using serialization, to ensure it's not a subclass.
-        /// If it's a subclass it may not serlialize properly to xml (different root element tag name)
+        /// If it's a subclass it may not serlialize properly to xml (different root element tag name).
         /// </summary>
-        /// <param name="profile"></param>
-        /// <returns></returns>
+        /// <param name="profile">The device profile.</param>
+        /// <returns>The reserialized device profile.</returns>
         private DeviceProfile ReserializeProfile(DeviceProfile profile)
         {
             if (profile.GetType() == typeof(DeviceProfile))
@@ -490,16 +500,9 @@ namespace Emby.Dlna
             return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
         }
 
-        class InternalProfileInfo
-        {
-            internal DeviceProfileInfo Info { get; set; }
-            internal string Path { get; set; }
-        }
-
         public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
         {
-            var profile = GetProfile(headers) ??
-                          GetDefaultProfile();
+            var profile = GetDefaultProfile();
 
             var serverId = _appHost.SystemId;
 
@@ -520,7 +523,15 @@ namespace Emby.Dlna
                 Stream = _assembly.GetManifestResourceStream(resource)
             };
         }
+
+        private class InternalProfileInfo
+        {
+            internal DeviceProfileInfo Info { get; set; }
+
+            internal string Path { get; set; }
+        }
     }
+
     /*
     class DlnaProfileEntryPoint : IServerEntryPoint
     {
@@ -566,9 +577,9 @@ namespace Emby.Dlna
                 new Foobar2000Profile(),
                 new SharpSmartTvProfile(),
                 new MediaMonkeyProfile(),
-                //new Windows81Profile(),
-                //new WindowsMediaCenterProfile(),
-                //new WindowsPhoneProfile(),
+                // new Windows81Profile(),
+                // new WindowsMediaCenterProfile(),
+                // new WindowsPhoneProfile(),
                 new DirectTvProfile(),
                 new DishHopperJoeyProfile(),
                 new DefaultProfile(),

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

@@ -20,7 +20,7 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
   </PropertyGroup>
 
   <!-- Code Analyzers-->
@@ -80,6 +80,7 @@
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
     <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
   </ItemGroup>
 
 </Project>

+ 1 - 1
Emby.Dlna/EventSubscriptionResponse.cs

@@ -15,6 +15,6 @@ namespace Emby.Dlna
 
         public string ContentType { get; set; }
 
-        public Dictionary<string, string> Headers { get; set; }
+        public Dictionary<string, string> Headers { get; }
     }
 }

+ 45 - 37
Emby.Dlna/Eventing/EventManager.cs → Emby.Dlna/Eventing/DlnaEventManager.cs

@@ -6,6 +6,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Net.Http;
+using System.Net.Mime;
 using System.Text;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
@@ -14,35 +15,45 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.Eventing
 {
-    public class EventManager : IEventManager
+    public class DlnaEventManager : IDlnaEventManager
     {
         private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions =
             new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
 
         private readonly ILogger _logger;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
 
-        public EventManager(ILogger logger, IHttpClient httpClient)
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+        public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _logger = logger;
         }
 
         public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
         {
             var subscription = GetSubscription(subscriptionId, false);
+            if (subscription != null)
+            {
+                subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300;
+                int timeoutSeconds = subscription.TimeoutSeconds;
+                subscription.SubscriptionTime = DateTime.UtcNow;
 
-            subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300;
-            int timeoutSeconds = subscription.TimeoutSeconds;
-            subscription.SubscriptionTime = DateTime.UtcNow;
+                _logger.LogDebug(
+                    "Renewing event subscription for {0} with timeout of {1} to {2}",
+                    subscription.NotificationType,
+                    timeoutSeconds,
+                    subscription.CallbackUrl);
 
-            _logger.LogDebug(
-                "Renewing event subscription for {0} with timeout of {1} to {2}",
-                subscription.NotificationType,
-                timeoutSeconds,
-                subscription.CallbackUrl);
+                return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
+            }
 
-            return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
+            return new EventSubscriptionResponse
+            {
+                Content = string.Empty,
+                ContentType = "text/plain"
+            };
         }
 
         public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
@@ -50,7 +61,8 @@ namespace Emby.Dlna.Eventing
             var timeout = ParseTimeout(requestedTimeoutString) ?? 300;
             var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
 
-            _logger.LogDebug("Creating event subscription for {0} with timeout of {1} to {2}",
+            _logger.LogDebug(
+                "Creating event subscription for {0} with timeout of {1} to {2}",
                 notificationType,
                 timeout,
                 callbackUrl);
@@ -86,7 +98,7 @@ namespace Emby.Dlna.Eventing
         {
             _logger.LogDebug("Cancelling event subscription {0}", subscriptionId);
 
-            _subscriptions.TryRemove(subscriptionId, out EventSubscription sub);
+            _subscriptions.TryRemove(subscriptionId, out _);
 
             return new EventSubscriptionResponse
             {
@@ -95,7 +107,6 @@ namespace Emby.Dlna.Eventing
             };
         }
 
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
         {
             var response = new EventSubscriptionResponse
@@ -144,33 +155,30 @@ namespace Emby.Dlna.Eventing
             builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
             foreach (var key in stateVariables.Keys)
             {
-                builder.Append("<e:property>");
-                builder.Append("<" + key + ">");
-                builder.Append(stateVariables[key]);
-                builder.Append("</" + key + ">");
-                builder.Append("</e:property>");
+                builder.Append("<e:property>")
+                    .Append('<')
+                    .Append(key)
+                    .Append('>')
+                    .Append(stateVariables[key])
+                    .Append("</")
+                    .Append(key)
+                    .Append('>')
+                    .Append("</e:property>");
             }
-            builder.Append("</e:propertyset>");
 
-            var options = new HttpRequestOptions
-            {
-                RequestContent = builder.ToString(),
-                RequestContentType = "text/xml",
-                Url = subscription.CallbackUrl,
-                BufferContent = false
-            };
+            builder.Append("</e:propertyset>");
 
-            options.RequestHeaders.Add("NT", subscription.NotificationType);
-            options.RequestHeaders.Add("NTS", "upnp:propchange");
-            options.RequestHeaders.Add("SID", subscription.Id);
-            options.RequestHeaders.Add("SEQ", subscription.TriggerCount.ToString(_usCulture));
+            using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"),  subscription.CallbackUrl);
+            options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
+            options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
+            options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
+            options.Headers.TryAddWithoutValidation("SID", subscription.Id);
+            options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture));
 
             try
             {
-                using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false))
-                {
-
-                }
+                using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                    .SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
             }
             catch (OperationCanceledException)
             {

+ 3 - 0
Emby.Dlna/Eventing/EventSubscription.cs

@@ -7,10 +7,13 @@ namespace Emby.Dlna.Eventing
     public class EventSubscription
     {
         public string Id { get; set; }
+
         public string CallbackUrl { get; set; }
+
         public string NotificationType { get; set; }
 
         public DateTime SubscriptionTime { get; set; }
+
         public int TimeoutSeconds { get; set; }
 
         public long TriggerCount { get; set; }

+ 1 - 1
Emby.Dlna/IConnectionManager.cs

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

+ 1 - 1
Emby.Dlna/IContentDirectory.cs

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

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

@@ -2,22 +2,32 @@
 
 namespace Emby.Dlna
 {
-    public interface IEventManager
+    public interface IDlnaEventManager
     {
         /// <summary>
         /// Cancels the event subscription.
         /// </summary>
         /// <param name="subscriptionId">The subscription identifier.</param>
+        /// <returns>The response.</returns>
         EventSubscriptionResponse CancelEventSubscription(string subscriptionId);
 
         /// <summary>
         /// Renews the event subscription.
         /// </summary>
+        /// <param name="subscriptionId">The subscription identifier.</param>
+        /// <param name="notificationType">The notification type.</param>
+        /// <param name="requestedTimeoutString">The requested timeout as a sting.</param>
+        /// <param name="callbackUrl">The callback url.</param>
+        /// <returns>The response.</returns>
         EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl);
 
         /// <summary>
         /// Creates the event subscription.
         /// </summary>
+        /// <param name="notificationType">The notification type.</param>
+        /// <param name="requestedTimeoutString">The requested timeout as a sting.</param>
+        /// <param name="callbackUrl">The callback url.</param>
+        /// <returns>The response.</returns>
         EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl);
     }
 }

+ 1 - 1
Emby.Dlna/IMediaReceiverRegistrar.cs

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

+ 75 - 56
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -2,6 +2,7 @@
 
 using System;
 using System.Globalization;
+using System.Net.Http;
 using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
@@ -30,15 +31,13 @@ using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
 
 namespace Emby.Dlna.Main
 {
-    public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
+    public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
     {
         private readonly IServerConfigurationManager _config;
-        private readonly ILogger _logger;
+        private readonly ILogger<DlnaEntryPoint> _logger;
         private readonly IServerApplicationHost _appHost;
-
-        private PlayToManager _manager;
         private readonly ISessionManager _sessionManager;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILibraryManager _libraryManager;
         private readonly IUserManager _userManager;
         private readonly IDlnaManager _dlnaManager;
@@ -47,29 +46,23 @@ namespace Emby.Dlna.Main
         private readonly ILocalizationManager _localization;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaEncoder _mediaEncoder;
-
         private readonly IDeviceDiscovery _deviceDiscovery;
-
-        private SsdpDevicePublisher _Publisher;
-
         private readonly ISocketFactory _socketFactory;
         private readonly INetworkManager _networkManager;
+        private readonly object _syncLock = new object();
 
+        private PlayToManager _manager;
+        private SsdpDevicePublisher _publisher;
         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;
+        private bool _disposed;
 
-        public DlnaEntryPoint(IServerConfigurationManager config,
+        public DlnaEntryPoint(
+            IServerConfigurationManager config,
             ILoggerFactory loggerFactory,
             IServerApplicationHost appHost,
             ISessionManager sessionManager,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILibraryManager libraryManager,
             IUserManager userManager,
             IDlnaManager dlnaManager,
@@ -87,7 +80,7 @@ namespace Emby.Dlna.Main
             _config = config;
             _appHost = appHost;
             _sessionManager = sessionManager;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _libraryManager = libraryManager;
             _userManager = userManager;
             _dlnaManager = dlnaManager;
@@ -99,54 +92,62 @@ namespace Emby.Dlna.Main
             _mediaEncoder = mediaEncoder;
             _socketFactory = socketFactory;
             _networkManager = networkManager;
-            _logger = loggerFactory.CreateLogger("Dlna");
+            _logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
 
-            ContentDirectory = new ContentDirectory.ContentDirectory(
+            ContentDirectory = new ContentDirectory.ContentDirectoryService(
                 dlnaManager,
                 userDataManager,
                 imageProcessor,
                 libraryManager,
                 config,
                 userManager,
-                loggerFactory.CreateLogger<ContentDirectory.ContentDirectory>(),
-                httpClient,
+                loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
+                httpClientFactory,
                 localizationManager,
                 mediaSourceManager,
                 userViewManager,
                 mediaEncoder,
                 tvSeriesManager);
 
-            ConnectionManager = new ConnectionManager.ConnectionManager(
+            ConnectionManager = new ConnectionManager.ConnectionManagerService(
                 dlnaManager,
                 config,
-                loggerFactory.CreateLogger<ConnectionManager.ConnectionManager>(),
-                httpClient);
+                loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
+                httpClientFactory);
 
-            MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(
-                loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrar>(),
-                httpClient,
+            MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
+                loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
+                httpClientFactory,
                 config);
             Current = this;
         }
 
+        public static DlnaEntryPoint Current { get; private set; }
+
+        public IContentDirectory ContentDirectory { get; private set; }
+
+        public IConnectionManager ConnectionManager { get; private set; }
+
+        public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
+
         public async Task RunAsync()
         {
             await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
 
-            ReloadComponents();
+            await ReloadComponents().ConfigureAwait(false);
 
-            _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
+            _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
         }
 
-        void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
+        private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
         {
             if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
             {
-                ReloadComponents();
+                await ReloadComponents().ConfigureAwait(false);
             }
         }
 
-        private async void ReloadComponents()
+        private async Task ReloadComponents()
         {
             var options = _config.GetDlnaConfiguration();
 
@@ -180,7 +181,7 @@ namespace Emby.Dlna.Main
                     var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
                                                    OperatingSystem.Id == OperatingSystemId.Linux;
 
-                    _communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding)
+                    _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
                     {
                         IsShared = true
                     };
@@ -231,20 +232,22 @@ namespace Emby.Dlna.Main
                 return;
             }
 
-            if (_Publisher != null)
+            if (_publisher != null)
             {
                 return;
             }
 
             try
             {
-                _Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost);
-                _Publisher.LogFunction = LogMessage;
-                _Publisher.SupportPnpRootDevice = false;
+                _publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
+                {
+                    LogFunction = LogMessage,
+                    SupportPnpRootDevice = false
+                };
 
                 await RegisterServerEndpoints().ConfigureAwait(false);
 
-                _Publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
+                _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
             }
             catch (Exception ex)
             {
@@ -266,6 +269,12 @@ namespace Emby.Dlna.Main
                     continue;
                 }
 
+                // Limit to LAN addresses only
+                if (!_networkManager.IsAddressInSubnets(address, true, true))
+                {
+                    continue;
+                }
+
                 var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
 
                 _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
@@ -275,7 +284,7 @@ namespace Emby.Dlna.Main
 
                 var device = new SsdpRootDevice
                 {
-                    CacheLifetime = TimeSpan.FromSeconds(1800), //How long SSDP clients can cache this info.
+                    CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
                     Location = uri, // Must point to the URL that serves your devices UPnP description document.
                     Address = address,
                     SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
@@ -287,13 +296,13 @@ namespace Emby.Dlna.Main
                 };
 
                 SetProperies(device, fullService);
-                _Publisher.AddDevice(device);
+                _publisher.AddDevice(device);
 
                 var embeddedDevices = new[]
                 {
                     "urn:schemas-upnp-org:service:ContentDirectory:1",
                     "urn:schemas-upnp-org:service:ConnectionManager:1",
-                    //"urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
+                    // "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
                 };
 
                 foreach (var subDevice in embeddedDevices)
@@ -319,12 +328,13 @@ namespace Emby.Dlna.Main
             {
                 guid = text.GetMD5();
             }
+
             return guid.ToString("N", CultureInfo.InvariantCulture);
         }
 
         private void SetProperies(SsdpDevice device, string fullDeviceType)
         {
-            var service = fullDeviceType.Replace("urn:", string.Empty).Replace(":1", string.Empty);
+            var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase);
 
             var serviceParts = service.Split(':');
 
@@ -335,7 +345,6 @@ namespace Emby.Dlna.Main
             device.DeviceType = serviceParts[2];
         }
 
-        private readonly object _syncLock = new object();
         private void StartPlayToManager()
         {
             lock (_syncLock)
@@ -347,7 +356,8 @@ namespace Emby.Dlna.Main
 
                 try
                 {
-                    _manager = new PlayToManager(_logger,
+                    _manager = new PlayToManager(
+                        _logger,
                         _sessionManager,
                         _libraryManager,
                         _userManager,
@@ -355,7 +365,7 @@ namespace Emby.Dlna.Main
                         _appHost,
                         _imageProcessor,
                         _deviceDiscovery,
-                        _httpClient,
+                        _httpClientFactory,
                         _config,
                         _userDataManager,
                         _localization,
@@ -386,13 +396,30 @@ namespace Emby.Dlna.Main
                     {
                         _logger.LogError(ex, "Error disposing PlayTo manager");
                     }
+
                     _manager = null;
                 }
             }
         }
 
+        public void DisposeDevicePublisher()
+        {
+            if (_publisher != null)
+            {
+                _logger.LogInformation("Disposing SsdpDevicePublisher");
+                _publisher.Dispose();
+                _publisher = null;
+            }
+        }
+
+        /// <inheritdoc />
         public void Dispose()
         {
+            if (_disposed)
+            {
+                return;
+            }
+
             DisposeDevicePublisher();
             DisposePlayToManager();
             DisposeDeviceDiscovery();
@@ -408,16 +435,8 @@ namespace Emby.Dlna.Main
             ConnectionManager = null;
             MediaReceiverRegistrar = null;
             Current = null;
-        }
 
-        public void DisposeDevicePublisher()
-        {
-            if (_Publisher != null)
-            {
-                _logger.LogInformation("Disposing SsdpDevicePublisher");
-                _Publisher.Dispose();
-                _Publisher = null;
-            }
+            _disposed = true;
         }
     }
 }

+ 6 - 6
Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs → Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs

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

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

@@ -10,7 +10,8 @@ namespace Emby.Dlna.MediaReceiverRegistrar
     {
         public string GetXml()
         {
-            return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
+            return new ServiceXmlBuilder().GetXml(
+                new ServiceActionListBuilder().GetActions(),
                 GetStateVariables());
         }
 

+ 160 - 156
Emby.Dlna/PlayTo/Device.cs

@@ -4,12 +4,13 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Net.Http;
+using System.Security;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Xml;
 using System.Xml.Linq;
 using Emby.Dlna.Common;
-using Emby.Dlna.Server;
 using Emby.Dlna.Ssdp;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
@@ -19,24 +20,48 @@ namespace Emby.Dlna.PlayTo
 {
     public class Device : IDisposable
     {
-        #region Fields & Properties
+        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+        private readonly IHttpClientFactory _httpClientFactory;
+
+        private readonly ILogger _logger;
 
+        private readonly object _timerLock = new object();
         private Timer _timer;
+        private int _muteVol;
+        private int _volume;
+        private DateTime _lastVolumeRefresh;
+        private bool _volumeRefreshActive;
+        private int _connectFailureCount;
+        private bool _disposed;
+
+        public Device(DeviceInfo deviceProperties, IHttpClientFactory httpClientFactory, ILogger logger)
+        {
+            Properties = deviceProperties;
+            _httpClientFactory = httpClientFactory;
+            _logger = logger;
+        }
+
+        public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
+
+        public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
+
+        public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
+
+        public event EventHandler<MediaChangedEventArgs> MediaChanged;
 
         public DeviceInfo Properties { get; set; }
 
-        private int _muteVol;
         public bool IsMuted { get; set; }
 
-        private int _volume;
-
         public int Volume
         {
             get
             {
-                RefreshVolumeIfNeeded();
+                RefreshVolumeIfNeeded().GetAwaiter().GetResult();
                 return _volume;
             }
+
             set => _volume = value;
         }
 
@@ -44,29 +69,21 @@ namespace Emby.Dlna.PlayTo
 
         public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0);
 
-        public TRANSPORTSTATE TransportState { get; private set; }
+        public TransportState TransportState { get; private set; }
 
-        public bool IsPlaying => TransportState == TRANSPORTSTATE.PLAYING;
+        public bool IsPlaying => TransportState == TransportState.Playing;
 
-        public bool IsPaused => TransportState == TRANSPORTSTATE.PAUSED || TransportState == TRANSPORTSTATE.PAUSED_PLAYBACK;
+        public bool IsPaused => TransportState == TransportState.Paused || TransportState == TransportState.PausedPlayback;
 
-        public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED;
+        public bool IsStopped => TransportState == TransportState.Stopped;
 
-        #endregion
+        public Action OnDeviceUnavailable { get; set; }
 
-        private readonly IHttpClient _httpClient;
-        private readonly ILogger _logger;
-        private readonly IServerConfigurationManager _config;
+        private TransportCommands AvCommands { get; set; }
 
-        public Action OnDeviceUnavailable { get; set; }
+        private TransportCommands RendererCommands { get; set; }
 
-        public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config)
-        {
-            Properties = deviceProperties;
-            _httpClient = httpClient;
-            _logger = logger;
-            _config = config;
-        }
+        public UBaseObject CurrentMediaInfo { get; private set; }
 
         public void Start()
         {
@@ -74,26 +91,24 @@ namespace Emby.Dlna.PlayTo
             _timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite);
         }
 
-        private DateTime _lastVolumeRefresh;
-        private bool _volumeRefreshActive;
-        private void RefreshVolumeIfNeeded()
+        private Task RefreshVolumeIfNeeded()
         {
-            if (!_volumeRefreshActive)
-            {
-                return;
-            }
-
-            if (DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5))
+            if (_volumeRefreshActive
+                && DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5))
             {
                 _lastVolumeRefresh = DateTime.UtcNow;
-                RefreshVolume(CancellationToken.None);
+                return RefreshVolume();
             }
+
+            return Task.CompletedTask;
         }
 
-        private async void RefreshVolume(CancellationToken cancellationToken)
+        private async Task RefreshVolume(CancellationToken cancellationToken = default)
         {
             if (_disposed)
+            {
                 return;
+            }
 
             try
             {
@@ -106,7 +121,6 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        private readonly object _timerLock = new object();
         private void RestartTimer(bool immediate = false)
         {
             lock (_timerLock)
@@ -141,8 +155,6 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        #region Commanding
-
         public Task VolumeDown(CancellationToken cancellationToken)
         {
             var sendVolume = Math.Max(Volume - 5, 0);
@@ -211,7 +223,9 @@ namespace Emby.Dlna.PlayTo
 
             var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
             if (command == null)
+            {
                 return false;
+            }
 
             var service = GetServiceRenderingControl();
 
@@ -223,7 +237,7 @@ namespace Emby.Dlna.PlayTo
             _logger.LogDebug("Setting mute");
             var value = mute ? 1 : 0;
 
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
                 .ConfigureAwait(false);
 
             IsMuted = mute;
@@ -232,15 +246,20 @@ namespace Emby.Dlna.PlayTo
         }
 
         /// <summary>
-        /// Sets volume on a scale of 0-100
+        /// Sets volume on a scale of 0-100.
         /// </summary>
+        /// <param name="value">The volume on a scale of 0-100.</param>
+        /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
         public async Task SetVolume(int value, CancellationToken cancellationToken)
         {
             var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
 
             var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
             if (command == null)
+            {
                 return;
+            }
 
             var service = GetServiceRenderingControl();
 
@@ -253,7 +272,7 @@ namespace Emby.Dlna.PlayTo
             // Remote control will perform better
             Volume = value;
 
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
                 .ConfigureAwait(false);
         }
 
@@ -263,7 +282,9 @@ namespace Emby.Dlna.PlayTo
 
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
             if (command == null)
+            {
                 return;
+            }
 
             var service = GetAvTransportService();
 
@@ -272,7 +293,7 @@ namespace Emby.Dlna.PlayTo
                 throw new InvalidOperationException("Unable to find service");
             }
 
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
                 .ConfigureAwait(false);
 
             RestartTimer(true);
@@ -282,18 +303,20 @@ namespace Emby.Dlna.PlayTo
         {
             var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
 
-            url = url.Replace("&", "&amp;");
+            url = url.Replace("&", "&amp;", StringComparison.Ordinal);
 
             _logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
 
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
             if (command == null)
+            {
                 return;
+            }
 
             var dictionary = new Dictionary<string, string>
             {
-                {"CurrentURI", url},
-                {"CurrentURIMetaData", CreateDidlMeta(metaData)}
+                { "CurrentURI", url },
+                { "CurrentURIMetaData", CreateDidlMeta(metaData) }
             };
 
             var service = GetAvTransportService();
@@ -304,7 +327,7 @@ namespace Emby.Dlna.PlayTo
             }
 
             var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
                 .ConfigureAwait(false);
 
             await Task.Delay(50).ConfigureAwait(false);
@@ -329,7 +352,7 @@ namespace Emby.Dlna.PlayTo
                 return string.Empty;
             }
 
-            return DescriptionXmlBuilder.Escape(value);
+            return SecurityElement.Escape(value);
         }
 
         private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
@@ -346,7 +369,7 @@ namespace Emby.Dlna.PlayTo
                 throw new InvalidOperationException("Unable to find service");
             }
 
-            return new SsdpHttpClient(_httpClient).SendCommandAsync(
+            return new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -375,7 +398,7 @@ namespace Emby.Dlna.PlayTo
 
             var service = GetAvTransportService();
 
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
                 .ConfigureAwait(false);
 
             RestartTimer(true);
@@ -393,19 +416,14 @@ namespace Emby.Dlna.PlayTo
 
             var service = GetAvTransportService();
 
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
                 .ConfigureAwait(false);
 
-            TransportState = TRANSPORTSTATE.PAUSED;
+            TransportState = TransportState.Paused;
 
             RestartTimer(true);
         }
 
-        #endregion
-
-        #region Get data
-
-        private int _connectFailureCount;
         private async void TimerCallback(object sender)
         {
             if (_disposed)
@@ -434,7 +452,7 @@ namespace Emby.Dlna.PlayTo
                 if (transportState.HasValue)
                 {
                     // If we're not playing anything no need to get additional data
-                    if (transportState.Value == TRANSPORTSTATE.STOPPED)
+                    if (transportState.Value == TransportState.Stopped)
                     {
                         UpdateMediaInfo(null, transportState.Value);
                     }
@@ -458,10 +476,12 @@ namespace Emby.Dlna.PlayTo
                     _connectFailureCount = 0;
 
                     if (_disposed)
+                    {
                         return;
+                    }
 
                     // If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive
-                    if (transportState.Value == TRANSPORTSTATE.STOPPED)
+                    if (transportState.Value == TransportState.Stopped)
                     {
                         RestartTimerInactive();
                     }
@@ -478,7 +498,9 @@ namespace Emby.Dlna.PlayTo
             catch (Exception ex)
             {
                 if (_disposed)
+                {
                     return;
+                }
 
                 _logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name);
 
@@ -494,6 +516,7 @@ namespace Emby.Dlna.PlayTo
                         return;
                     }
                 }
+
                 RestartTimerInactive();
             }
         }
@@ -520,7 +543,7 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
+            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -532,7 +555,7 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var volume = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
+            var volume = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
             var volumeValue = volume?.Value;
 
             if (string.IsNullOrWhiteSpace(volumeValue))
@@ -570,7 +593,7 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
+            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -578,16 +601,18 @@ namespace Emby.Dlna.PlayTo
                 cancellationToken: cancellationToken).ConfigureAwait(false);
 
             if (result == null || result.Document == null)
+            {
                 return;
+            }
 
-            var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse")
+            var valueNode = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetMuteResponse")
                                             .Select(i => i.Element("CurrentMute"))
                                             .FirstOrDefault(i => i != null);
 
             IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase);
         }
 
-        private async Task<TRANSPORTSTATE?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
+        private async Task<TransportState?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
         {
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo");
             if (command == null)
@@ -601,7 +626,7 @@ namespace Emby.Dlna.PlayTo
                 return null;
             }
 
-            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
+            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -614,12 +639,12 @@ namespace Emby.Dlna.PlayTo
             }
 
             var transportState =
-                result.Document.Descendants(uPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
+                result.Document.Descendants(UPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
 
             var transportStateValue = transportState?.Value;
 
             if (transportStateValue != null
-                && Enum.TryParse(transportStateValue, true, out TRANSPORTSTATE state))
+                && Enum.TryParse(transportStateValue, true, out TransportState state))
             {
                 return state;
             }
@@ -627,7 +652,7 @@ namespace Emby.Dlna.PlayTo
             return null;
         }
 
-        private async Task<uBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
+        private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
         {
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
             if (command == null)
@@ -643,7 +668,7 @@ namespace Emby.Dlna.PlayTo
 
             var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
 
-            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
+            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -662,7 +687,7 @@ namespace Emby.Dlna.PlayTo
                 return null;
             }
 
-            var e = track.Element(uPnpNamespaces.items) ?? track;
+            var e = track.Element(UPnpNamespaces.Items) ?? track;
 
             var elementString = (string)e;
 
@@ -678,13 +703,13 @@ namespace Emby.Dlna.PlayTo
                 return null;
             }
 
-            e = track.Element(uPnpNamespaces.items) ?? track;
+            e = track.Element(UPnpNamespaces.Items) ?? track;
 
             elementString = (string)e;
 
             if (!string.IsNullOrWhiteSpace(elementString))
             {
-                return new uBaseObject
+                return new UBaseObject
                 {
                     Url = elementString
                 };
@@ -693,7 +718,7 @@ namespace Emby.Dlna.PlayTo
             return null;
         }
 
-        private async Task<(bool, uBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
+        private async Task<(bool, UBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
         {
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
             if (command == null)
@@ -710,7 +735,7 @@ namespace Emby.Dlna.PlayTo
 
             var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
 
-            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
+            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -722,11 +747,11 @@ namespace Emby.Dlna.PlayTo
                 return (false, null);
             }
 
-            var trackUriElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
-            var trackUri = trackUriElem == null ? null : trackUriElem.Value;
+            var trackUriElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
+            var trackUri = trackUriElem?.Value;
 
-            var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
-            var duration = durationElem == null ? null : durationElem.Value;
+            var durationElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
+            var duration = durationElem?.Value;
 
             if (!string.IsNullOrWhiteSpace(duration)
                 && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
@@ -738,8 +763,8 @@ namespace Emby.Dlna.PlayTo
                 Duration = null;
             }
 
-            var positionElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
-            var position = positionElem == null ? null : positionElem.Value;
+            var positionElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
+            var position = positionElem?.Value;
 
             if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
             {
@@ -750,7 +775,7 @@ namespace Emby.Dlna.PlayTo
 
             if (track == null)
             {
-                //If track is null, some vendors do this, use GetMediaInfo instead
+                // If track is null, some vendors do this, use GetMediaInfo instead
                 return (true, null);
             }
 
@@ -778,7 +803,7 @@ namespace Emby.Dlna.PlayTo
                 return (true, null);
             }
 
-            var e = uPnpResponse.Element(uPnpNamespaces.items);
+            var e = uPnpResponse.Element(UPnpNamespaces.Items);
 
             var uTrack = CreateUBaseObject(e, trackUri);
 
@@ -794,7 +819,6 @@ namespace Emby.Dlna.PlayTo
             }
             catch (XmlException)
             {
-
             }
 
             // first try to add a root node with a dlna namesapce
@@ -806,43 +830,41 @@ namespace Emby.Dlna.PlayTo
             }
             catch (XmlException)
             {
-
             }
 
             // some devices send back invalid xml
             try
             {
-                return XElement.Parse(xml.Replace("&", "&amp;"));
+                return XElement.Parse(xml.Replace("&", "&amp;", StringComparison.Ordinal));
             }
             catch (XmlException)
             {
-
             }
 
             return null;
         }
 
-        private static uBaseObject CreateUBaseObject(XElement container, string trackUri)
+        private static UBaseObject CreateUBaseObject(XElement container, string trackUri)
         {
             if (container == null)
             {
                 throw new ArgumentNullException(nameof(container));
             }
 
-            var url = container.GetValue(uPnpNamespaces.Res);
+            var url = container.GetValue(UPnpNamespaces.Res);
 
             if (string.IsNullOrWhiteSpace(url))
             {
                 url = trackUri;
             }
 
-            return new uBaseObject
+            return new UBaseObject
             {
-                Id = container.GetAttributeValue(uPnpNamespaces.Id),
-                ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
-                Title = container.GetValue(uPnpNamespaces.title),
-                IconUrl = container.GetValue(uPnpNamespaces.Artwork),
-                SecondText = "",
+                Id = container.GetAttributeValue(UPnpNamespaces.Id),
+                ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId),
+                Title = container.GetValue(UPnpNamespaces.Title),
+                IconUrl = container.GetValue(UPnpNamespaces.Artwork),
+                SecondText = string.Empty,
                 Url = url,
                 ProtocolInfo = GetProtocolInfo(container),
                 MetaData = container.ToString()
@@ -856,11 +878,11 @@ namespace Emby.Dlna.PlayTo
                 throw new ArgumentNullException(nameof(container));
             }
 
-            var resElement = container.Element(uPnpNamespaces.Res);
+            var resElement = container.Element(UPnpNamespaces.Res);
 
             if (resElement != null)
             {
-                var info = resElement.Attribute(uPnpNamespaces.ProtocolInfo);
+                var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
 
                 if (info != null && !string.IsNullOrWhiteSpace(info.Value))
                 {
@@ -871,10 +893,6 @@ namespace Emby.Dlna.PlayTo
             return new string[4];
         }
 
-        #endregion
-
-        #region From XML
-
         private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
         {
             if (AvCommands != null)
@@ -895,7 +913,7 @@ namespace Emby.Dlna.PlayTo
 
             string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
 
-            var httpClient = new SsdpHttpClient(_httpClient);
+            var httpClient = new SsdpHttpClient(_httpClientFactory);
 
             var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
 
@@ -923,7 +941,7 @@ namespace Emby.Dlna.PlayTo
 
             string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
 
-            var httpClient = new SsdpHttpClient(_httpClient);
+            var httpClient = new SsdpHttpClient(_httpClientFactory);
             _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
             var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
 
@@ -939,12 +957,12 @@ namespace Emby.Dlna.PlayTo
                 return url;
             }
 
-            if (!url.Contains("/"))
+            if (!url.Contains('/', StringComparison.Ordinal))
             {
                 url = "/dmr/" + url;
             }
 
-            if (!url.StartsWith("/"))
+            if (!url.StartsWith("/", StringComparison.Ordinal))
             {
                 url = "/" + url;
             }
@@ -952,25 +970,21 @@ namespace Emby.Dlna.PlayTo
             return baseUrl + url;
         }
 
-        private TransportCommands AvCommands { get; set; }
-
-        private TransportCommands RendererCommands { get; set; }
-
-        public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, CancellationToken cancellationToken)
+        public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
         {
-            var ssdpHttpClient = new SsdpHttpClient(httpClient);
+            var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
 
             var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
 
             var friendlyNames = new List<string>();
 
-            var name = document.Descendants(uPnpNamespaces.ud.GetName("friendlyName")).FirstOrDefault();
+            var name = document.Descendants(UPnpNamespaces.Ud.GetName("friendlyName")).FirstOrDefault();
             if (name != null && !string.IsNullOrWhiteSpace(name.Value))
             {
                 friendlyNames.Add(name.Value);
             }
 
-            var room = document.Descendants(uPnpNamespaces.ud.GetName("roomName")).FirstOrDefault();
+            var room = document.Descendants(UPnpNamespaces.Ud.GetName("roomName")).FirstOrDefault();
             if (room != null && !string.IsNullOrWhiteSpace(room.Value))
             {
                 friendlyNames.Add(room.Value);
@@ -979,77 +993,77 @@ namespace Emby.Dlna.PlayTo
             var deviceProperties = new DeviceInfo()
             {
                 Name = string.Join(" ", friendlyNames),
-                BaseUrl = string.Format("http://{0}:{1}", url.Host, url.Port)
+                BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
             };
 
-            var model = document.Descendants(uPnpNamespaces.ud.GetName("modelName")).FirstOrDefault();
+            var model = document.Descendants(UPnpNamespaces.Ud.GetName("modelName")).FirstOrDefault();
             if (model != null)
             {
                 deviceProperties.ModelName = model.Value;
             }
 
-            var modelNumber = document.Descendants(uPnpNamespaces.ud.GetName("modelNumber")).FirstOrDefault();
+            var modelNumber = document.Descendants(UPnpNamespaces.Ud.GetName("modelNumber")).FirstOrDefault();
             if (modelNumber != null)
             {
                 deviceProperties.ModelNumber = modelNumber.Value;
             }
 
-            var uuid = document.Descendants(uPnpNamespaces.ud.GetName("UDN")).FirstOrDefault();
+            var uuid = document.Descendants(UPnpNamespaces.Ud.GetName("UDN")).FirstOrDefault();
             if (uuid != null)
             {
                 deviceProperties.UUID = uuid.Value;
             }
 
-            var manufacturer = document.Descendants(uPnpNamespaces.ud.GetName("manufacturer")).FirstOrDefault();
+            var manufacturer = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturer")).FirstOrDefault();
             if (manufacturer != null)
             {
                 deviceProperties.Manufacturer = manufacturer.Value;
             }
 
-            var manufacturerUrl = document.Descendants(uPnpNamespaces.ud.GetName("manufacturerURL")).FirstOrDefault();
+            var manufacturerUrl = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturerURL")).FirstOrDefault();
             if (manufacturerUrl != null)
             {
                 deviceProperties.ManufacturerUrl = manufacturerUrl.Value;
             }
 
-            var presentationUrl = document.Descendants(uPnpNamespaces.ud.GetName("presentationURL")).FirstOrDefault();
+            var presentationUrl = document.Descendants(UPnpNamespaces.Ud.GetName("presentationURL")).FirstOrDefault();
             if (presentationUrl != null)
             {
                 deviceProperties.PresentationUrl = presentationUrl.Value;
             }
 
-            var modelUrl = document.Descendants(uPnpNamespaces.ud.GetName("modelURL")).FirstOrDefault();
+            var modelUrl = document.Descendants(UPnpNamespaces.Ud.GetName("modelURL")).FirstOrDefault();
             if (modelUrl != null)
             {
                 deviceProperties.ModelUrl = modelUrl.Value;
             }
 
-            var serialNumber = document.Descendants(uPnpNamespaces.ud.GetName("serialNumber")).FirstOrDefault();
+            var serialNumber = document.Descendants(UPnpNamespaces.Ud.GetName("serialNumber")).FirstOrDefault();
             if (serialNumber != null)
             {
                 deviceProperties.SerialNumber = serialNumber.Value;
             }
 
-            var modelDescription = document.Descendants(uPnpNamespaces.ud.GetName("modelDescription")).FirstOrDefault();
+            var modelDescription = document.Descendants(UPnpNamespaces.Ud.GetName("modelDescription")).FirstOrDefault();
             if (modelDescription != null)
             {
                 deviceProperties.ModelDescription = modelDescription.Value;
             }
 
-            var icon = document.Descendants(uPnpNamespaces.ud.GetName("icon")).FirstOrDefault();
+            var icon = document.Descendants(UPnpNamespaces.Ud.GetName("icon")).FirstOrDefault();
             if (icon != null)
             {
                 deviceProperties.Icon = CreateIcon(icon);
             }
 
-            foreach (var services in document.Descendants(uPnpNamespaces.ud.GetName("serviceList")))
+            foreach (var services in document.Descendants(UPnpNamespaces.Ud.GetName("serviceList")))
             {
                 if (services == null)
                 {
                     continue;
                 }
 
-                var servicesList = services.Descendants(uPnpNamespaces.ud.GetName("service"));
+                var servicesList = services.Descendants(UPnpNamespaces.Ud.GetName("service"));
                 if (servicesList == null)
                 {
                     continue;
@@ -1066,12 +1080,9 @@ namespace Emby.Dlna.PlayTo
                 }
             }
 
-            return new Device(deviceProperties, httpClient, logger, config);
+            return new Device(deviceProperties, httpClientFactory, logger);
         }
 
-        #endregion
-
-        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
         private static DeviceIcon CreateIcon(XElement element)
         {
             if (element == null)
@@ -1079,11 +1090,11 @@ namespace Emby.Dlna.PlayTo
                 throw new ArgumentNullException(nameof(element));
             }
 
-            var mimeType = element.GetDescendantValue(uPnpNamespaces.ud.GetName("mimetype"));
-            var width = element.GetDescendantValue(uPnpNamespaces.ud.GetName("width"));
-            var height = element.GetDescendantValue(uPnpNamespaces.ud.GetName("height"));
-            var depth = element.GetDescendantValue(uPnpNamespaces.ud.GetName("depth"));
-            var url = element.GetDescendantValue(uPnpNamespaces.ud.GetName("url"));
+            var mimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype"));
+            var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width"));
+            var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height"));
+            var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
+            var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
 
             var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
             var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
@@ -1100,11 +1111,11 @@ namespace Emby.Dlna.PlayTo
 
         private static DeviceService Create(XElement element)
         {
-            var type = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceType"));
-            var id = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceId"));
-            var scpdUrl = element.GetDescendantValue(uPnpNamespaces.ud.GetName("SCPDURL"));
-            var controlURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("controlURL"));
-            var eventSubURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("eventSubURL"));
+            var type = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType"));
+            var id = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId"));
+            var scpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL"));
+            var controlURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL"));
+            var eventSubURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL"));
 
             return new DeviceService
             {
@@ -1116,14 +1127,7 @@ namespace Emby.Dlna.PlayTo
             };
         }
 
-        public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
-        public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
-        public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
-        public event EventHandler<MediaChangedEventArgs> MediaChanged;
-
-        public uBaseObject CurrentMediaInfo { get; private set; }
-
-        private void UpdateMediaInfo(uBaseObject mediaInfo, TRANSPORTSTATE state)
+        private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state)
         {
             TransportState = state;
 
@@ -1132,7 +1136,7 @@ namespace Emby.Dlna.PlayTo
 
             if (previousMediaInfo == null && mediaInfo != null)
             {
-                if (state != TRANSPORTSTATE.STOPPED)
+                if (state != TransportState.Stopped)
                 {
                     OnPlaybackStart(mediaInfo);
                 }
@@ -1151,7 +1155,7 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        private void OnPlaybackStart(uBaseObject mediaInfo)
+        private void OnPlaybackStart(UBaseObject mediaInfo)
         {
             if (string.IsNullOrWhiteSpace(mediaInfo.Url))
             {
@@ -1164,7 +1168,7 @@ namespace Emby.Dlna.PlayTo
             });
         }
 
-        private void OnPlaybackProgress(uBaseObject mediaInfo)
+        private void OnPlaybackProgress(UBaseObject mediaInfo)
         {
             if (string.IsNullOrWhiteSpace(mediaInfo.Url))
             {
@@ -1177,7 +1181,7 @@ namespace Emby.Dlna.PlayTo
             });
         }
 
-        private void OnPlaybackStop(uBaseObject mediaInfo)
+        private void OnPlaybackStop(UBaseObject mediaInfo)
         {
             PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
             {
@@ -1185,7 +1189,7 @@ namespace Emby.Dlna.PlayTo
             });
         }
 
-        private void OnMediaChanged(uBaseObject old, uBaseObject newMedia)
+        private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
         {
             MediaChanged?.Invoke(this, new MediaChangedEventArgs
             {
@@ -1194,16 +1198,17 @@ namespace Emby.Dlna.PlayTo
             });
         }
 
-        #region IDisposable
-
-        bool _disposed;
-
+        /// <inheritdoc />
         public void Dispose()
         {
             Dispose(true);
             GC.SuppressFinalize(this);
         }
 
+        /// <summary>
+        /// Releases unmanaged and optionally managed resources.
+        /// </summary>
+        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
         protected virtual void Dispose(bool disposing)
         {
             if (_disposed)
@@ -1222,11 +1227,10 @@ namespace Emby.Dlna.PlayTo
             _disposed = true;
         }
 
-        #endregion
-
+        /// <inheritdoc />
         public override string ToString()
         {
-            return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);
+            return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl);
         }
     }
 }

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

@@ -8,6 +8,9 @@ namespace Emby.Dlna.PlayTo
 {
     public class DeviceInfo
     {
+        private readonly List<DeviceService> _services = new List<DeviceService>();
+        private string _baseUrl = string.Empty;
+
         public DeviceInfo()
         {
             Name = "Generic Device";
@@ -33,7 +36,6 @@ namespace Emby.Dlna.PlayTo
 
         public string PresentationUrl { get; set; }
 
-        private string _baseUrl = string.Empty;
         public string BaseUrl
         {
             get => _baseUrl;
@@ -42,7 +44,6 @@ namespace Emby.Dlna.PlayTo
 
         public DeviceIcon Icon { get; set; }
 
-        private readonly List<DeviceService> _services = new List<DeviceService>();
         public List<DeviceService> Services => _services;
 
         public DeviceIdentification ToDeviceIdentification()

+ 13 - 0
Emby.Dlna/PlayTo/MediaChangedEventArgs.cs

@@ -0,0 +1,13 @@
+#pragma warning disable CS1591
+
+using System;
+
+namespace Emby.Dlna.PlayTo
+{
+    public class MediaChangedEventArgs : EventArgs
+    {
+        public UBaseObject OldMediaInfo { get; set; }
+
+        public UBaseObject NewMediaInfo { get; set; }
+    }
+}

+ 152 - 128
Emby.Dlna/PlayTo/PlayToController.cs

@@ -7,6 +7,8 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Emby.Dlna.Didl;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
@@ -17,11 +19,11 @@ using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Session;
 using Microsoft.AspNetCore.WebUtilities;
 using Microsoft.Extensions.Logging;
+using Photo = MediaBrowser.Controller.Entities.Photo;
 
 namespace Emby.Dlna.PlayTo
 {
@@ -29,7 +31,6 @@ namespace Emby.Dlna.PlayTo
     {
         private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
 
-        private Device _device;
         private readonly SessionInfo _session;
         private readonly ISessionManager _sessionManager;
         private readonly ILibraryManager _libraryManager;
@@ -48,6 +49,7 @@ namespace Emby.Dlna.PlayTo
         private readonly string _accessToken;
 
         private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
+        private Device _device;
         private int _currentPlaylistIndex;
 
         private bool _disposed;
@@ -146,11 +148,14 @@ namespace Emby.Dlna.PlayTo
                 {
                     var positionTicks = GetProgressPositionTicks(streamInfo);
 
-                    ReportPlaybackStopped(streamInfo, positionTicks);
+                    await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
                 }
 
                 streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager);
-                if (streamInfo.Item == null) return;
+                if (streamInfo.Item == null)
+                {
+                    return;
+                }
 
                 var newItemProgress = GetProgressInfo(streamInfo);
 
@@ -173,11 +178,14 @@ namespace Emby.Dlna.PlayTo
             {
                 var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
 
-                if (streamInfo.Item == null) return;
+                if (streamInfo.Item == null)
+                {
+                    return;
+                }
 
                 var positionTicks = GetProgressPositionTicks(streamInfo);
 
-                ReportPlaybackStopped(streamInfo, positionTicks);
+                await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
 
                 var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
 
@@ -185,7 +193,7 @@ namespace Emby.Dlna.PlayTo
                     (_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) :
                     mediaSource.RunTimeTicks;
 
-                var playedToCompletion = (positionTicks.HasValue && positionTicks.Value == 0);
+                var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
 
                 if (!playedToCompletion && duration.HasValue && positionTicks.HasValue)
                 {
@@ -210,7 +218,7 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        private async void ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks)
+        private async Task ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks)
         {
             try
             {
@@ -220,7 +228,6 @@ namespace Emby.Dlna.PlayTo
                     SessionId = _session.Id,
                     PositionTicks = positionTicks,
                     MediaSourceId = streamInfo.MediaSourceId
-
                 }).ConfigureAwait(false);
             }
             catch (Exception ex)
@@ -365,8 +372,13 @@ namespace Emby.Dlna.PlayTo
 
             if (!command.ControllingUserId.Equals(Guid.Empty))
             {
-                _sessionManager.LogSessionActivity(_session.Client, _session.ApplicationVersion, _session.DeviceId,
-                       _session.DeviceName, _session.RemoteEndPoint, user);
+                _sessionManager.LogSessionActivity(
+                    _session.Client,
+                    _session.ApplicationVersion,
+                    _session.DeviceId,
+                    _session.DeviceName,
+                    _session.RemoteEndPoint,
+                    user);
             }
 
             return PlayItems(playlist, cancellationToken);
@@ -418,6 +430,7 @@ namespace Emby.Dlna.PlayTo
                     await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
                     return;
                 }
+
                 await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
             }
         }
@@ -441,7 +454,13 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        private PlaylistItem CreatePlaylistItem(BaseItem item, User user, long startPostionTicks, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
+        private PlaylistItem CreatePlaylistItem(
+            BaseItem item,
+            User user,
+            long startPostionTicks,
+            string mediaSourceId,
+            int? audioStreamIndex,
+            int? subtitleStreamIndex)
         {
             var deviceInfo = _device.Properties;
 
@@ -484,42 +503,44 @@ namespace Emby.Dlna.PlayTo
             if (streamInfo.MediaType == DlnaProfileType.Audio)
             {
                 return new ContentFeatureBuilder(profile)
-                    .BuildAudioHeader(streamInfo.Container,
-                    streamInfo.TargetAudioCodec.FirstOrDefault(),
-                    streamInfo.TargetAudioBitrate,
-                    streamInfo.TargetAudioSampleRate,
-                    streamInfo.TargetAudioChannels,
-                    streamInfo.TargetAudioBitDepth,
-                    streamInfo.IsDirectStream,
-                    streamInfo.RunTimeTicks ?? 0,
-                    streamInfo.TranscodeSeekInfo);
+                    .BuildAudioHeader(
+                        streamInfo.Container,
+                        streamInfo.TargetAudioCodec.FirstOrDefault(),
+                        streamInfo.TargetAudioBitrate,
+                        streamInfo.TargetAudioSampleRate,
+                        streamInfo.TargetAudioChannels,
+                        streamInfo.TargetAudioBitDepth,
+                        streamInfo.IsDirectStream,
+                        streamInfo.RunTimeTicks ?? 0,
+                        streamInfo.TranscodeSeekInfo);
             }
 
             if (streamInfo.MediaType == DlnaProfileType.Video)
             {
                 var list = new ContentFeatureBuilder(profile)
-                    .BuildVideoHeader(streamInfo.Container,
-                    streamInfo.TargetVideoCodec.FirstOrDefault(),
-                    streamInfo.TargetAudioCodec.FirstOrDefault(),
-                    streamInfo.TargetWidth,
-                    streamInfo.TargetHeight,
-                    streamInfo.TargetVideoBitDepth,
-                    streamInfo.TargetVideoBitrate,
-                    streamInfo.TargetTimestamp,
-                    streamInfo.IsDirectStream,
-                    streamInfo.RunTimeTicks ?? 0,
-                    streamInfo.TargetVideoProfile,
-                    streamInfo.TargetVideoLevel,
-                    streamInfo.TargetFramerate ?? 0,
-                    streamInfo.TargetPacketLength,
-                    streamInfo.TranscodeSeekInfo,
-                    streamInfo.IsTargetAnamorphic,
-                    streamInfo.IsTargetInterlaced,
-                    streamInfo.TargetRefFrames,
-                    streamInfo.TargetVideoStreamCount,
-                    streamInfo.TargetAudioStreamCount,
-                    streamInfo.TargetVideoCodecTag,
-                    streamInfo.IsTargetAVC);
+                    .BuildVideoHeader(
+                        streamInfo.Container,
+                        streamInfo.TargetVideoCodec.FirstOrDefault(),
+                        streamInfo.TargetAudioCodec.FirstOrDefault(),
+                        streamInfo.TargetWidth,
+                        streamInfo.TargetHeight,
+                        streamInfo.TargetVideoBitDepth,
+                        streamInfo.TargetVideoBitrate,
+                        streamInfo.TargetTimestamp,
+                        streamInfo.IsDirectStream,
+                        streamInfo.RunTimeTicks ?? 0,
+                        streamInfo.TargetVideoProfile,
+                        streamInfo.TargetVideoLevel,
+                        streamInfo.TargetFramerate ?? 0,
+                        streamInfo.TargetPacketLength,
+                        streamInfo.TranscodeSeekInfo,
+                        streamInfo.IsTargetAnamorphic,
+                        streamInfo.IsTargetInterlaced,
+                        streamInfo.TargetRefFrames,
+                        streamInfo.TargetVideoStreamCount,
+                        streamInfo.TargetAudioStreamCount,
+                        streamInfo.TargetVideoCodecTag,
+                        streamInfo.IsTargetAVC);
 
                 return list.Count == 0 ? null : list[0];
             }
@@ -619,6 +640,10 @@ namespace Emby.Dlna.PlayTo
             GC.SuppressFinalize(this);
         }
 
+        /// <summary>
+        /// Releases unmanaged and optionally managed resources.
+        /// </summary>
+        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
         protected virtual void Dispose(bool disposing)
         {
             if (_disposed)
@@ -659,47 +684,41 @@ namespace Emby.Dlna.PlayTo
                     case GeneralCommandType.ToggleMute:
                         return _device.ToggleMute(cancellationToken);
                     case GeneralCommandType.SetAudioStreamIndex:
+                        if (command.Arguments.TryGetValue("Index", out string index))
                         {
-                            if (command.Arguments.TryGetValue("Index", out string arg))
+                            if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
                             {
-                                if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val))
-                                {
-                                    return SetAudioStreamIndex(val);
-                                }
-
-                                throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
+                                return SetAudioStreamIndex(val);
                             }
 
-                            throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
+                            throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
                         }
+
+                        throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
                     case GeneralCommandType.SetSubtitleStreamIndex:
+                        if (command.Arguments.TryGetValue("Index", out index))
                         {
-                            if (command.Arguments.TryGetValue("Index", out string arg))
+                            if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
                             {
-                                if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val))
-                                {
-                                    return SetSubtitleStreamIndex(val);
-                                }
-
-                                throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
+                                return SetSubtitleStreamIndex(val);
                             }
 
-                            throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
+                            throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
                         }
+
+                        throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
                     case GeneralCommandType.SetVolume:
+                        if (command.Arguments.TryGetValue("Volume", out string vol))
                         {
-                            if (command.Arguments.TryGetValue("Volume", out string arg))
+                            if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
                             {
-                                if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var volume))
-                                {
-                                    return _device.SetVolume(volume, cancellationToken);
-                                }
-
-                                throw new ArgumentException("Unsupported volume value supplied.");
+                                return _device.SetVolume(volume, cancellationToken);
                             }
 
-                            throw new ArgumentException("Volume argument cannot be null");
+                            throw new ArgumentException("Unsupported volume value supplied.");
                         }
+
+                        throw new ArgumentException("Volume argument cannot be null");
                     default:
                         return Task.CompletedTask;
                 }
@@ -763,7 +782,7 @@ namespace Emby.Dlna.PlayTo
             const int maxWait = 15000000;
             const int interval = 500;
             var currentWait = 0;
-            while (_device.TransportState != TRANSPORTSTATE.PLAYING && currentWait < maxWait)
+            while (_device.TransportState != TransportState.Playing && currentWait < maxWait)
             {
                 await Task.Delay(interval).ConfigureAwait(false);
                 currentWait += interval;
@@ -772,8 +791,67 @@ namespace Emby.Dlna.PlayTo
             await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false);
         }
 
+        private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
+        {
+            var value = values.GetValueOrDefault(name);
+
+            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+            {
+                return result;
+            }
+
+            return null;
+        }
+
+        private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
+        {
+            var value = values.GetValueOrDefault(name);
+
+            if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+            {
+                return result;
+            }
+
+            return 0;
+        }
+
+        /// <inheritdoc />
+        public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
+        {
+            if (_disposed)
+            {
+                throw new ObjectDisposedException(GetType().Name);
+            }
+
+            if (_device == null)
+            {
+                return Task.CompletedTask;
+            }
+
+            if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
+            {
+                return SendPlayCommand(data as PlayRequest, cancellationToken);
+            }
+
+            if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
+            {
+                return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
+            }
+
+            if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
+            {
+                return SendGeneralCommand(data as GeneralCommand, cancellationToken);
+            }
+
+            // Not supported or needed right now
+            return Task.CompletedTask;
+        }
+
         private class StreamParams
         {
+            private MediaSourceInfo mediaSource;
+            private IMediaSourceManager _mediaSourceManager;
+
             public Guid ItemId { get; set; }
 
             public bool IsDirectStream { get; set; }
@@ -785,21 +863,20 @@ namespace Emby.Dlna.PlayTo
             public int? SubtitleStreamIndex { get; set; }
 
             public string DeviceProfileId { get; set; }
+
             public string DeviceId { get; set; }
 
             public string MediaSourceId { get; set; }
+
             public string LiveStreamId { get; set; }
 
             public BaseItem Item { get; set; }
-            private MediaSourceInfo MediaSource;
-
-            private IMediaSourceManager _mediaSourceManager;
 
             public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
             {
-                if (MediaSource != null)
+                if (mediaSource != null)
                 {
-                    return MediaSource;
+                    return mediaSource;
                 }
 
                 var hasMediaSources = Item as IHasMediaSources;
@@ -809,9 +886,9 @@ namespace Emby.Dlna.PlayTo
                     return null;
                 }
 
-                MediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
+                mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
 
-                return MediaSource;
+                return mediaSource;
             }
 
             private static Guid GetItemId(string url)
@@ -883,58 +960,5 @@ namespace Emby.Dlna.PlayTo
                 return request;
             }
         }
-
-        private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
-        {
-            var value = values.GetValueOrDefault(name);
-
-            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
-            {
-                return result;
-            }
-
-            return null;
-        }
-
-        private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
-        {
-            var value = values.GetValueOrDefault(name);
-
-            if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
-            {
-                return result;
-            }
-
-            return 0;
-        }
-
-        public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
-        {
-            if (_disposed)
-            {
-                throw new ObjectDisposedException(GetType().Name);
-            }
-
-            if (_device == null)
-            {
-                return Task.CompletedTask;
-            }
-
-            if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
-            {
-                return SendPlayCommand(data as PlayRequest, cancellationToken);
-            }
-            if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
-            {
-                return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
-            }
-            if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
-            {
-                return SendGeneralCommand(data as GeneralCommand, cancellationToken);
-            }
-
-            // Not supported or needed right now
-            return Task.CompletedTask;
-        }
     }
 }

+ 53 - 49
Emby.Dlna/PlayTo/PlayToManager.cs

@@ -4,8 +4,10 @@ using System;
 using System.Globalization;
 using System.Linq;
 using System.Net;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
@@ -16,7 +18,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
@@ -33,7 +34,7 @@ namespace Emby.Dlna.PlayTo
         private readonly IDlnaManager _dlnaManager;
         private readonly IServerApplicationHost _appHost;
         private readonly IImageProcessor _imageProcessor;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerConfigurationManager _config;
         private readonly IUserDataManager _userDataManager;
         private readonly ILocalizationManager _localization;
@@ -46,7 +47,7 @@ namespace Emby.Dlna.PlayTo
         private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
         private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
 
-        public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
+        public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
         {
             _logger = logger;
             _sessionManager = sessionManager;
@@ -56,7 +57,7 @@ namespace Emby.Dlna.PlayTo
             _appHost = appHost;
             _imageProcessor = imageProcessor;
             _deviceDiscovery = deviceDiscovery;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _config = config;
             _userDataManager = userDataManager;
             _localization = localization;
@@ -78,17 +79,23 @@ namespace Emby.Dlna.PlayTo
 
             var info = e.Argument;
 
-            if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty;
+            if (!info.Headers.TryGetValue("USN", out string usn))
+            {
+                usn = string.Empty;
+            }
 
-            if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty;
+            if (!info.Headers.TryGetValue("NT", out string nt))
+            {
+                nt = string.Empty;
+            }
 
             string location = info.Location.ToString();
 
             // It has to report that it's a media renderer
             if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 &&
-                     nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
+                nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
             {
-                //_logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location);
+                // _logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location);
                 return;
             }
 
@@ -112,7 +119,6 @@ namespace Emby.Dlna.PlayTo
             }
             catch (OperationCanceledException)
             {
-
             }
             catch (Exception ex)
             {
@@ -124,24 +130,21 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        private string GetUuid(string usn)
+        private static string GetUuid(string usn)
         {
-            var found = false;
-            var index = usn.IndexOf("uuid:", StringComparison.OrdinalIgnoreCase);
-            if (index != -1)
-            {
-                usn = usn.Substring(index);
-                found = true;
-            }
-            index = usn.IndexOf("::", StringComparison.OrdinalIgnoreCase);
+            const string UuidStr = "uuid:";
+            const string UuidColonStr = "::";
+
+            var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase);
             if (index != -1)
             {
-                usn = usn.Substring(0, index);
+                return usn.Substring(index + UuidStr.Length);
             }
 
-            if (found)
+            index = usn.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase);
+            if (index != -1)
             {
-                return usn;
+                usn = usn.Substring(0, index + UuidColonStr.Length);
             }
 
             return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture);
@@ -168,7 +171,7 @@ namespace Emby.Dlna.PlayTo
 
             if (controller == null)
             {
-                var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger, cancellationToken).ConfigureAwait(false);
+                var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
 
                 string deviceName = device.Properties.Name;
 
@@ -184,21 +187,22 @@ namespace Emby.Dlna.PlayTo
                     serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress);
                 }
 
-                controller = new PlayToController(sessionInfo,
-                   _sessionManager,
-                   _libraryManager,
-                   _logger,
-                   _dlnaManager,
-                   _userManager,
-                   _imageProcessor,
-                   serverAddress,
-                   null,
-                   _deviceDiscovery,
-                   _userDataManager,
-                   _localization,
-                   _mediaSourceManager,
-                   _config,
-                   _mediaEncoder);
+                controller = new PlayToController(
+                    sessionInfo,
+                    _sessionManager,
+                    _libraryManager,
+                    _logger,
+                    _dlnaManager,
+                    _userManager,
+                    _imageProcessor,
+                    serverAddress,
+                    null,
+                    _deviceDiscovery,
+                    _userDataManager,
+                    _localization,
+                    _mediaSourceManager,
+                    _config,
+                    _mediaEncoder);
 
                 sessionInfo.AddController(controller);
 
@@ -211,17 +215,17 @@ namespace Emby.Dlna.PlayTo
                 {
                     PlayableMediaTypes = profile.GetSupportedMediaTypes(),
 
-                    SupportedCommands = new string[]
+                    SupportedCommands = new[]
                     {
-                            GeneralCommandType.VolumeDown.ToString(),
-                            GeneralCommandType.VolumeUp.ToString(),
-                            GeneralCommandType.Mute.ToString(),
-                            GeneralCommandType.Unmute.ToString(),
-                            GeneralCommandType.ToggleMute.ToString(),
-                            GeneralCommandType.SetVolume.ToString(),
-                            GeneralCommandType.SetAudioStreamIndex.ToString(),
-                            GeneralCommandType.SetSubtitleStreamIndex.ToString(),
-                            GeneralCommandType.PlayMediaSource.ToString()
+                        GeneralCommandType.VolumeDown.ToString(),
+                        GeneralCommandType.VolumeUp.ToString(),
+                        GeneralCommandType.Mute.ToString(),
+                        GeneralCommandType.Unmute.ToString(),
+                        GeneralCommandType.ToggleMute.ToString(),
+                        GeneralCommandType.SetVolume.ToString(),
+                        GeneralCommandType.SetAudioStreamIndex.ToString(),
+                        GeneralCommandType.SetSubtitleStreamIndex.ToString(),
+                        GeneralCommandType.PlayMediaSource.ToString()
                     },
 
                     SupportsMediaControl = true
@@ -240,9 +244,9 @@ namespace Emby.Dlna.PlayTo
             {
                 _disposeCancellationTokenSource.Cancel();
             }
-            catch
+            catch (Exception ex)
             {
-
+                _logger.LogDebug(ex, "Error while disposing PlayToManager");
             }
 
             _sessionLock.Dispose();

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

@@ -6,6 +6,6 @@ namespace Emby.Dlna.PlayTo
 {
     public class PlaybackProgressEventArgs : EventArgs
     {
-        public uBaseObject MediaInfo { get; set; }
+        public UBaseObject MediaInfo { get; set; }
     }
 }

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

@@ -6,6 +6,6 @@ namespace Emby.Dlna.PlayTo
 {
     public class PlaybackStartEventArgs : EventArgs
     {
-        public uBaseObject MediaInfo { get; set; }
+        public UBaseObject MediaInfo { get; set; }
     }
 }

+ 1 - 7
Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs

@@ -6,12 +6,6 @@ namespace Emby.Dlna.PlayTo
 {
     public class PlaybackStoppedEventArgs : EventArgs
     {
-        public uBaseObject MediaInfo { get; set; }
-    }
-
-    public class MediaChangedEventArgs : EventArgs
-    {
-        public uBaseObject OldMediaInfo { get; set; }
-        public uBaseObject NewMediaInfo { get; set; }
+        public UBaseObject MediaInfo { get; set; }
     }
 }

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

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

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

@@ -1,13 +0,0 @@
-#pragma warning disable CS1591
-
-namespace Emby.Dlna.PlayTo
-{
-    public enum TRANSPORTSTATE
-    {
-        STOPPED,
-        PLAYING,
-        TRANSITIONING,
-        PAUSED_PLAYBACK,
-        PAUSED
-    }
-}

+ 31 - 39
Emby.Dlna/PlayTo/TransportCommands.cs

@@ -2,6 +2,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Linq;
 using System.Xml.Linq;
 using Emby.Dlna.Common;
@@ -11,36 +12,30 @@ namespace Emby.Dlna.PlayTo
 {
     public class TransportCommands
     {
+        private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
         private List<StateVariable> _stateVariables = new List<StateVariable>();
-        public List<StateVariable> StateVariables
-        {
-            get => _stateVariables;
-            set => _stateVariables = value;
-        }
-
         private List<ServiceAction> _serviceActions = new List<ServiceAction>();
-        public List<ServiceAction> ServiceActions
-        {
-            get => _serviceActions;
-            set => _serviceActions = value;
-        }
+
+        public List<StateVariable> StateVariables => _stateVariables;
+
+        public List<ServiceAction> ServiceActions => _serviceActions;
 
         public static TransportCommands Create(XDocument document)
         {
             var command = new TransportCommands();
 
-            var actionList = document.Descendants(uPnpNamespaces.svc + "actionList");
+            var actionList = document.Descendants(UPnpNamespaces.Svc + "actionList");
 
-            foreach (var container in actionList.Descendants(uPnpNamespaces.svc + "action"))
+            foreach (var container in actionList.Descendants(UPnpNamespaces.Svc + "action"))
             {
                 command.ServiceActions.Add(ServiceActionFromXml(container));
             }
 
-            var stateValues = document.Descendants(uPnpNamespaces.ServiceStateTable).FirstOrDefault();
+            var stateValues = document.Descendants(UPnpNamespaces.ServiceStateTable).FirstOrDefault();
 
             if (stateValues != null)
             {
-                foreach (var container in stateValues.Elements(uPnpNamespaces.svc + "stateVariable"))
+                foreach (var container in stateValues.Elements(UPnpNamespaces.Svc + "stateVariable"))
                 {
                     command.StateVariables.Add(FromXml(container));
                 }
@@ -51,19 +46,19 @@ namespace Emby.Dlna.PlayTo
 
         private static ServiceAction ServiceActionFromXml(XElement container)
         {
-            var argumentList = new List<Argument>();
+            var serviceAction = new ServiceAction
+            {
+                Name = container.GetValue(UPnpNamespaces.Svc + "name"),
+            };
 
-            foreach (var arg in container.Descendants(uPnpNamespaces.svc + "argument"))
+            var argumentList = serviceAction.ArgumentList;
+
+            foreach (var arg in container.Descendants(UPnpNamespaces.Svc + "argument"))
             {
                 argumentList.Add(ArgumentFromXml(arg));
             }
 
-            return new ServiceAction
-            {
-                Name = container.GetValue(uPnpNamespaces.svc + "name"),
-
-                ArgumentList = argumentList
-            };
+            return serviceAction;
         }
 
         private static Argument ArgumentFromXml(XElement container)
@@ -75,29 +70,29 @@ namespace Emby.Dlna.PlayTo
 
             return new Argument
             {
-                Name = container.GetValue(uPnpNamespaces.svc + "name"),
-                Direction = container.GetValue(uPnpNamespaces.svc + "direction"),
-                RelatedStateVariable = container.GetValue(uPnpNamespaces.svc + "relatedStateVariable")
+                Name = container.GetValue(UPnpNamespaces.Svc + "name"),
+                Direction = container.GetValue(UPnpNamespaces.Svc + "direction"),
+                RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable")
             };
         }
 
         private static StateVariable FromXml(XElement container)
         {
             var allowedValues = new List<string>();
-            var element = container.Descendants(uPnpNamespaces.svc + "allowedValueList")
+            var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList")
                 .FirstOrDefault();
 
             if (element != null)
             {
-                var values = element.Descendants(uPnpNamespaces.svc + "allowedValue");
+                var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue");
 
                 allowedValues.AddRange(values.Select(child => child.Value));
             }
 
             return new StateVariable
             {
-                Name = container.GetValue(uPnpNamespaces.svc + "name"),
-                DataType = container.GetValue(uPnpNamespaces.svc + "dataType"),
+                Name = container.GetValue(UPnpNamespaces.Svc + "name"),
+                DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
                 AllowedValues = allowedValues.ToArray()
             };
         }
@@ -123,7 +118,7 @@ namespace Emby.Dlna.PlayTo
                 }
             }
 
-            return string.Format(CommandBase, action.Name, xmlNamespace, stateString);
+            return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
         }
 
         public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "")
@@ -147,7 +142,7 @@ namespace Emby.Dlna.PlayTo
                 }
             }
 
-            return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
+            return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
         }
 
         public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary)
@@ -170,7 +165,7 @@ namespace Emby.Dlna.PlayTo
                 }
             }
 
-            return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
+            return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
         }
 
         private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
@@ -180,15 +175,12 @@ namespace Emby.Dlna.PlayTo
             if (state != null)
             {
                 var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
-                                 state.AllowedValues.FirstOrDefault() ??
-                                 value;
+                    (state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value);
 
-                return string.Format("<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
+                return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
             }
 
-            return string.Format("<{0}>{1}</{0}>", argument.Name, value);
+            return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value);
         }
-
-        private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
     }
 }

+ 14 - 0
Emby.Dlna/PlayTo/TransportState.cs

@@ -0,0 +1,14 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1602
+
+namespace Emby.Dlna.PlayTo
+{
+    public enum TransportState
+    {
+        Stopped,
+        Playing,
+        Transitioning,
+        PausedPlayback,
+        Paused
+    }
+}

+ 8 - 8
Emby.Dlna/PlayTo/UpnpContainer.cs

@@ -6,22 +6,22 @@ using Emby.Dlna.Ssdp;
 
 namespace Emby.Dlna.PlayTo
 {
-    public class UpnpContainer : uBaseObject
+    public class UpnpContainer : UBaseObject
     {
-        public static uBaseObject Create(XElement container)
+        public static UBaseObject Create(XElement container)
         {
             if (container == null)
             {
                 throw new ArgumentNullException(nameof(container));
             }
 
-            return new uBaseObject
+            return new UBaseObject
             {
-                Id = container.GetAttributeValue(uPnpNamespaces.Id),
-                ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
-                Title = container.GetValue(uPnpNamespaces.title),
-                IconUrl = container.GetValue(uPnpNamespaces.Artwork),
-                UpnpClass = container.GetValue(uPnpNamespaces.uClass)
+                Id = container.GetAttributeValue(UPnpNamespaces.Id),
+                ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId),
+                Title = container.GetValue(UPnpNamespaces.Title),
+                IconUrl = container.GetValue(UPnpNamespaces.Artwork),
+                UpnpClass = container.GetValue(UPnpNamespaces.Class)
             };
         }
     }

+ 15 - 12
Emby.Dlna/PlayTo/uBaseObject.cs

@@ -1,10 +1,11 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Collections.Generic;
 
 namespace Emby.Dlna.PlayTo
 {
-    public class uBaseObject
+    public class UBaseObject
     {
         public string Id { get; set; }
 
@@ -20,20 +21,10 @@ namespace Emby.Dlna.PlayTo
 
         public string Url { get; set; }
 
-        public string[] ProtocolInfo { get; set; }
+        public IReadOnlyList<string> ProtocolInfo { get; set; }
 
         public string UpnpClass { get; set; }
 
-        public bool Equals(uBaseObject obj)
-        {
-            if (obj == null)
-            {
-                throw new ArgumentNullException(nameof(obj));
-            }
-
-            return string.Equals(Id, obj.Id);
-        }
-
         public string MediaType
         {
             get
@@ -44,10 +35,12 @@ namespace Emby.Dlna.PlayTo
                 {
                     return MediaBrowser.Model.Entities.MediaType.Audio;
                 }
+
                 if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1)
                 {
                     return MediaBrowser.Model.Entities.MediaType.Video;
                 }
+
                 if (classType.IndexOf("image", StringComparison.Ordinal) != -1)
                 {
                     return MediaBrowser.Model.Entities.MediaType.Photo;
@@ -56,5 +49,15 @@ namespace Emby.Dlna.PlayTo
                 return null;
             }
         }
+
+        public bool Equals(UBaseObject obj)
+        {
+            if (obj == null)
+            {
+                throw new ArgumentNullException(nameof(obj));
+            }
+
+            return string.Equals(Id, obj.Id, StringComparison.Ordinal);
+        }
     }
 }

+ 58 - 32
Emby.Dlna/PlayTo/uPnpNamespaces.cs

@@ -4,38 +4,64 @@ using System.Xml.Linq;
 
 namespace Emby.Dlna.PlayTo
 {
-    public class uPnpNamespaces
+    public static class UPnpNamespaces
     {
-        public static XNamespace dc = "http://purl.org/dc/elements/1.1/";
-        public static XNamespace ns = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
-        public static XNamespace svc = "urn:schemas-upnp-org:service-1-0";
-        public static XNamespace ud = "urn:schemas-upnp-org:device-1-0";
-        public static XNamespace upnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
-        public static XNamespace RenderingControl = "urn:schemas-upnp-org:service:RenderingControl:1";
-        public static XNamespace AvTransport = "urn:schemas-upnp-org:service:AVTransport:1";
-        public static XNamespace ContentDirectory = "urn:schemas-upnp-org:service:ContentDirectory:1";
-
-        public static XName containers = ns + "container";
-        public static XName items = ns + "item";
-        public static XName title = dc + "title";
-        public static XName creator = dc + "creator";
-        public static XName artist = upnp + "artist";
-        public static XName Id = "id";
-        public static XName ParentId = "parentID";
-        public static XName uClass = upnp + "class";
-        public static XName Artwork = upnp + "albumArtURI";
-        public static XName Description = dc + "description";
-        public static XName LongDescription = upnp + "longDescription";
-        public static XName Album = upnp + "album";
-        public static XName Author = upnp + "author";
-        public static XName Director = upnp + "director";
-        public static XName PlayCount = upnp + "playbackCount";
-        public static XName Tracknumber = upnp + "originalTrackNumber";
-        public static XName Res = ns + "res";
-        public static XName Duration = "duration";
-        public static XName ProtocolInfo = "protocolInfo";
-
-        public static XName ServiceStateTable = svc + "serviceStateTable";
-        public static XName StateVariable = svc + "stateVariable";
+        public static XNamespace Dc { get; } = "http://purl.org/dc/elements/1.1/";
+
+        public static XNamespace Ns { get; } = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
+
+        public static XNamespace Svc { get; } = "urn:schemas-upnp-org:service-1-0";
+
+        public static XNamespace Ud { get; } = "urn:schemas-upnp-org:device-1-0";
+
+        public static XNamespace UPnp { get; } = "urn:schemas-upnp-org:metadata-1-0/upnp/";
+
+        public static XNamespace RenderingControl { get; } = "urn:schemas-upnp-org:service:RenderingControl:1";
+
+        public static XNamespace AvTransport { get; } = "urn:schemas-upnp-org:service:AVTransport:1";
+
+        public static XNamespace ContentDirectory { get; } = "urn:schemas-upnp-org:service:ContentDirectory:1";
+
+        public static XName Containers { get; } = Ns + "container";
+
+        public static XName Items { get; } = Ns + "item";
+
+        public static XName Title { get; } = Dc + "title";
+
+        public static XName Creator { get; } = Dc + "creator";
+
+        public static XName Artist { get; } = UPnp + "artist";
+
+        public static XName Id { get; } = "id";
+
+        public static XName ParentId { get; } = "parentID";
+
+        public static XName Class { get; } = UPnp + "class";
+
+        public static XName Artwork { get; } = UPnp + "albumArtURI";
+
+        public static XName Description { get; } = Dc + "description";
+
+        public static XName LongDescription { get; } = UPnp + "longDescription";
+
+        public static XName Album { get; } = UPnp + "album";
+
+        public static XName Author { get; } = UPnp + "author";
+
+        public static XName Director { get; } = UPnp + "director";
+
+        public static XName PlayCount { get; } = UPnp + "playbackCount";
+
+        public static XName Tracknumber { get; } = UPnp + "originalTrackNumber";
+
+        public static XName Res { get; } = Ns + "res";
+
+        public static XName Duration { get; } = "duration";
+
+        public static XName ProtocolInfo { get; } = "protocolInfo";
+
+        public static XName ServiceStateTable { get; } = Svc + "serviceStateTable";
+
+        public static XName StateVariable { get; } = Svc + "stateVariable";
     }
 }

+ 3 - 3
Emby.Dlna/Profiles/DefaultProfile.cs

@@ -64,14 +64,14 @@ namespace Emby.Dlna.Profiles
                 new DirectPlayProfile
                 {
                     // play all
-                    Container = "",
+                    Container = string.Empty,
                     Type = DlnaProfileType.Video
                 },
 
                 new DirectPlayProfile
                 {
                     // play all
-                    Container = "",
+                    Container = string.Empty,
                     Type = DlnaProfileType.Audio
                 }
             };
@@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
 
         public void AddXmlRootAttribute(string name, string value)
         {
-            var atts = XmlRootAttributes ?? new XmlAttribute[] { };
+            var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>();
             var list = atts.ToList();
 
             list.Add(new XmlAttribute

+ 1 - 1
Emby.Dlna/Profiles/DenonAvrProfile.cs

@@ -28,7 +28,7 @@ namespace Emby.Dlna.Profiles
                 },
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = System.Array.Empty<ResponseProfile>();
         }
     }
 }

+ 1 - 1
Emby.Dlna/Profiles/DirectTvProfile.cs

@@ -123,7 +123,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = System.Array.Empty<ResponseProfile>();
         }
     }
 }

+ 6 - 6
Emby.Dlna/Profiles/DishHopperJoeyProfile.cs

@@ -24,7 +24,7 @@ namespace Emby.Dlna.Profiles
                     {
                          Match = HeaderMatchType.Substring,
                          Name = "User-Agent",
-                         Value ="Zip_"
+                         Value = "Zip_"
                     }
                 }
             };
@@ -81,7 +81,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -124,7 +124,7 @@ namespace Emby.Dlna.Profiles
                 new CodecProfile
                 {
                     Type = CodecType.Video,
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -161,7 +161,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "ac3,he-aac",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -177,7 +177,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -192,7 +192,7 @@ namespace Emby.Dlna.Profiles
                 new CodecProfile
                 {
                     Type = CodecType.VideoAudio,
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         // The device does not have any audio switching capabilities
                         new ProfileCondition

+ 1 - 1
Emby.Dlna/Profiles/Foobar2000Profile.cs

@@ -72,7 +72,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = System.Array.Empty<ResponseProfile>();
         }
     }
 }

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

@@ -84,7 +84,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -191,7 +191,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[]
+            ResponseProfiles = new[]
             {
                 new ResponseProfile
                 {

+ 1 - 1
Emby.Dlna/Profiles/LinksysDMA2100Profile.cs

@@ -32,7 +32,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[]
+            ResponseProfiles = new[]
             {
                 new ResponseProfile
                 {

+ 1 - 1
Emby.Dlna/Profiles/MarantzProfile.cs

@@ -37,7 +37,7 @@ namespace Emby.Dlna.Profiles
                 },
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = System.Array.Empty<ResponseProfile>();
         }
     }
 }

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

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 
+using System;
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles
@@ -37,7 +38,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = Array.Empty<ResponseProfile>();
         }
     }
 }

+ 1 - 1
Emby.Dlna/Profiles/PanasonicVieraProfile.cs

@@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 7 - 7
Emby.Dlna/Profiles/PopcornHourProfile.cs

@@ -93,8 +93,8 @@ namespace Emby.Dlna.Profiles
                 new CodecProfile
                 {
                     Type = CodecType.Video,
-                    Codec="h264",
-                    Conditions = new []
+                    Codec = "h264",
+                    Conditions = new[]
                     {
                         new ProfileCondition(ProfileConditionType.EqualsAny, ProfileConditionValue.VideoProfile, "baseline|constrained baseline"),
                         new ProfileCondition
@@ -122,7 +122,7 @@ namespace Emby.Dlna.Profiles
                 new CodecProfile
                 {
                     Type = CodecType.Video,
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -150,7 +150,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -166,7 +166,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Audio,
                     Codec = "aac",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -182,7 +182,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Audio,
                     Codec = "mp3",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -202,7 +202,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[]
+            ResponseProfiles = new[]
             {
                 new ResponseProfile
                 {

+ 1 - 1
Emby.Dlna/Profiles/SamsungSmartTvProfile.cs

@@ -139,7 +139,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 5 - 4
Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 
+using System;
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles
@@ -149,7 +150,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -177,7 +178,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -196,7 +197,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -223,7 +224,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = Array.Empty<ResponseProfile>();
         }
     }
 }

+ 5 - 4
Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 
+using System;
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles
@@ -149,7 +150,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -177,7 +178,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -196,7 +197,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -223,7 +224,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = Array.Empty<ResponseProfile>();
         }
     }
 }

+ 5 - 4
Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 
+using System;
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles
@@ -137,7 +138,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -165,7 +166,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -184,7 +185,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -211,7 +212,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = Array.Empty<ResponseProfile>();
         }
     }
 }

+ 5 - 4
Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 
+using System;
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles
@@ -137,7 +138,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -165,7 +166,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -184,7 +185,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -211,7 +212,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = Array.Empty<ResponseProfile>();
         }
     }
 }

+ 5 - 5
Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs

@@ -114,7 +114,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -156,7 +156,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -172,7 +172,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -191,7 +191,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -217,7 +217,7 @@ namespace Emby.Dlna.Profiles
                     VideoCodec = "h264,mpeg4,vc1",
                     AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
+                    OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
                     Type = DlnaProfileType.Video
                 },
 

+ 22 - 22
Emby.Dlna/Profiles/SonyBravia2010Profile.cs

@@ -102,13 +102,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
+                    OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -128,13 +128,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/mpeg",
-                    OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
+                    OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -148,28 +148,28 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
+                    OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="mpeg2video",
+                    VideoCodec = "mpeg2video",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
+                    OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "mpeg",
-                    VideoCodec="mpeg1video,mpeg2video",
+                    VideoCodec = "mpeg1video,mpeg2video",
                     MimeType = "video/mpeg",
-                    OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
+                    OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
                     Type = DlnaProfileType.Video
                 }
             };
@@ -180,7 +180,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -204,7 +204,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -243,7 +243,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "mpeg2video",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -275,7 +275,7 @@ namespace Emby.Dlna.Profiles
                 new CodecProfile
                 {
                     Type = CodecType.Video,
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -303,7 +303,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -319,7 +319,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -341,7 +341,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "mp3,mp2",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 21 - 21
Emby.Dlna/Profiles/SonyBravia2011Profile.cs

@@ -120,7 +120,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -143,13 +143,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
+                    OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -169,13 +169,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/mpeg",
-                    OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
+                    OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -189,28 +189,28 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
+                    OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="mpeg2video",
+                    VideoCodec = "mpeg2video",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
+                    OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "mpeg",
-                    VideoCodec="mpeg1video,mpeg2video",
+                    VideoCodec = "mpeg1video,mpeg2video",
                     MimeType = "video/mpeg",
-                    OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
+                    OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
                     Type = DlnaProfileType.Video
                 },
                 new ResponseProfile
@@ -227,7 +227,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -266,7 +266,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "mpeg2video",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -298,7 +298,7 @@ namespace Emby.Dlna.Profiles
                 new CodecProfile
                 {
                     Type = CodecType.Video,
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -326,7 +326,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -364,7 +364,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "mp3,mp2",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 17 - 17
Emby.Dlna/Profiles/SonyBravia2012Profile.cs

@@ -131,13 +131,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
+                    OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -157,13 +157,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/mpeg",
-                    OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
+                    OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -177,28 +177,28 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
+                    OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="mpeg2video",
+                    VideoCodec = "mpeg2video",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
+                    OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "mpeg",
-                    VideoCodec="mpeg1video,mpeg2video",
+                    VideoCodec = "mpeg1video,mpeg2video",
                     MimeType = "video/mpeg",
-                    OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
+                    OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
                     Type = DlnaProfileType.Video
                 },
                 new ResponseProfile
@@ -215,7 +215,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -282,7 +282,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "mp3,mp2",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 18 - 19
Emby.Dlna/Profiles/SonyBravia2013Profile.cs

@@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -187,13 +187,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
+                    OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -213,13 +213,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/mpeg",
-                    OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
+                    OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -233,28 +233,28 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
+                    OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="mpeg2video",
+                    VideoCodec = "mpeg2video",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
+                    OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "mpeg",
-                    VideoCodec="mpeg1video,mpeg2video",
+                    VideoCodec = "mpeg1video,mpeg2video",
                     MimeType = "video/mpeg",
-                    OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
+                    OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
                     Type = DlnaProfileType.Video
                 },
                 new ResponseProfile
@@ -265,14 +265,13 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-
             CodecProfiles = new[]
             {
                 new CodecProfile
                 {
                     Type = CodecType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -300,7 +299,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "mp3,mp2",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 18 - 19
Emby.Dlna/Profiles/SonyBravia2014Profile.cs

@@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -187,13 +187,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
+                    OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -213,13 +213,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/mpeg",
-                    OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
+                    OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -233,28 +233,28 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
+                    OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="mpeg2video",
+                    VideoCodec = "mpeg2video",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
+                    OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "mpeg",
-                    VideoCodec="mpeg1video,mpeg2video",
+                    VideoCodec = "mpeg1video,mpeg2video",
                     MimeType = "video/mpeg",
-                    OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
+                    OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
                     Type = DlnaProfileType.Video
                 },
                 new ResponseProfile
@@ -265,14 +265,13 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-
             CodecProfiles = new[]
             {
                 new CodecProfile
                 {
                     Type = CodecType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -300,7 +299,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "mp3,mp2",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 7 - 7
Emby.Dlna/Profiles/SonyPs3Profile.cs

@@ -108,7 +108,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -133,7 +133,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.Video,
                     Codec = "h264",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -176,7 +176,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -201,7 +201,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "wmapro",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -217,7 +217,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -235,7 +235,7 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "mp4,mov",
-                    AudioCodec="aac",
+                    AudioCodec = "aac",
                     MimeType = "video/mp4",
                     Type = DlnaProfileType.Video
                 },
@@ -244,7 +244,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Container = "avi",
                     MimeType = "video/divx",
-                    OrgPn="AVI",
+                    OrgPn = "AVI",
                     Type = DlnaProfileType.Video
                 },
 

+ 7 - 7
Emby.Dlna/Profiles/SonyPs4Profile.cs

@@ -110,7 +110,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -135,7 +135,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.Video,
                     Codec = "h264",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -178,7 +178,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -203,7 +203,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "wmapro",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -219,7 +219,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -237,7 +237,7 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "mp4,mov",
-                    AudioCodec="aac",
+                    AudioCodec = "aac",
                     MimeType = "video/mp4",
                     Type = DlnaProfileType.Video
                 },
@@ -246,7 +246,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Container = "avi",
                     MimeType = "video/divx",
-                    OrgPn="AVI",
+                    OrgPn = "AVI",
                     Type = DlnaProfileType.Video
                 },
 

+ 4 - 4
Emby.Dlna/Profiles/WdtvLiveProfile.cs

@@ -20,7 +20,7 @@ namespace Emby.Dlna.Profiles
 
                 Headers = new[]
                 {
-                    new HttpHeaderInfo {Name = "User-Agent", Value = "alphanetworks", Match = HeaderMatchType.Substring},
+                    new HttpHeaderInfo { Name = "User-Agent", Value = "alphanetworks", Match = HeaderMatchType.Substring },
                     new HttpHeaderInfo
                     {
                         Name = "User-Agent",
@@ -168,7 +168,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -193,7 +193,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.Video,
                     Codec = "h264",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -221,7 +221,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 7 - 7
Emby.Dlna/Profiles/XboxOneProfile.cs

@@ -119,7 +119,7 @@ namespace Emby.Dlna.Profiles
                     Type = DlnaProfileType.Video,
                     Container = "mp4,mov",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "mpeg4",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -187,7 +187,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -236,7 +236,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "wmv2,wmv3,vc1",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -284,7 +284,7 @@ namespace Emby.Dlna.Profiles
                 new CodecProfile
                 {
                     Type = CodecType.Video,
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -307,7 +307,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "ac3,wmav2,wmapro",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -323,7 +323,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 75 - 102
Emby.Dlna/Server/DescriptionXmlBuilder.cs

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Security;
 using System.Text;
 using Emby.Dlna.Common;
 using MediaBrowser.Model.Dlna;
@@ -64,10 +65,10 @@ namespace Emby.Dlna.Server
 
             foreach (var att in attributes)
             {
-                builder.AppendFormat(" {0}=\"{1}\"", att.Name, att.Value);
+                builder.AppendFormat(CultureInfo.InvariantCulture, " {0}=\"{1}\"", att.Name, att.Value);
             }
 
-            builder.Append(">");
+            builder.Append('>');
 
             builder.Append("<specVersion>");
             builder.Append("<major>1</major>");
@@ -76,7 +77,9 @@ namespace Emby.Dlna.Server
 
             if (!EnableAbsoluteUrls)
             {
-                builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>");
+                builder.Append("<URLBase>")
+                    .Append(SecurityElement.Escape(_serverAddress))
+                    .Append("</URLBase>");
             }
 
             AppendDeviceInfo(builder);
@@ -93,86 +96,14 @@ namespace Emby.Dlna.Server
 
             AppendIconList(builder);
 
-            builder.Append("<presentationURL>" + Escape(_serverAddress) + "/web/index.html</presentationURL>");
+            builder.Append("<presentationURL>")
+                .Append(SecurityElement.Escape(_serverAddress))
+                .Append("/web/index.html</presentationURL>");
 
             AppendServiceList(builder);
             builder.Append("</device>");
         }
 
-        private static readonly char[] s_escapeChars = new char[]
-        {
-            '<',
-            '>',
-            '"',
-            '\'',
-            '&'
-        };
-
-        private static readonly string[] s_escapeStringPairs = new[]
-        {
-            "<",
-            "&lt;",
-            ">",
-            "&gt;",
-            "\"",
-            "&quot;",
-            "'",
-            "&apos;",
-            "&",
-            "&amp;"
-        };
-
-        private static string GetEscapeSequence(char c)
-        {
-            int num = s_escapeStringPairs.Length;
-            for (int i = 0; i < num; i += 2)
-            {
-                string text = s_escapeStringPairs[i];
-                string result = s_escapeStringPairs[i + 1];
-                if (text[0] == c)
-                {
-                    return result;
-                }
-            }
-            return c.ToString(CultureInfo.InvariantCulture);
-        }
-
-        /// <summary>Replaces invalid XML characters in a string with their valid XML equivalent.</summary>
-        /// <returns>The input string with invalid characters replaced.</returns>
-        /// <param name="str">The string within which to escape invalid characters. </param>
-        public static string Escape(string str)
-        {
-            if (str == null)
-            {
-                return null;
-            }
-
-            StringBuilder stringBuilder = null;
-            int length = str.Length;
-            int num = 0;
-            while (true)
-            {
-                int num2 = str.IndexOfAny(s_escapeChars, num);
-                if (num2 == -1)
-                {
-                    break;
-                }
-                if (stringBuilder == null)
-                {
-                    stringBuilder = new StringBuilder();
-                }
-                stringBuilder.Append(str, num, num2 - num);
-                stringBuilder.Append(GetEscapeSequence(str[num2]));
-                num = num2 + 1;
-            }
-            if (stringBuilder == null)
-            {
-                return str;
-            }
-            stringBuilder.Append(str, num, length - num);
-            return stringBuilder.ToString();
-        }
-
         private void AppendDeviceProperties(StringBuilder builder)
         {
             builder.Append("<dlna:X_DLNACAP/>");
@@ -182,32 +113,54 @@ namespace Emby.Dlna.Server
 
             builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>");
 
-            builder.Append("<friendlyName>" + Escape(GetFriendlyName()) + "</friendlyName>");
-            builder.Append("<manufacturer>" + Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>");
-            builder.Append("<manufacturerURL>" + Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>");
-
-            builder.Append("<modelDescription>" + Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>");
-            builder.Append("<modelName>" + Escape(_profile.ModelName ?? string.Empty) + "</modelName>");
-
-            builder.Append("<modelNumber>" + Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>");
-            builder.Append("<modelURL>" + Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>");
+            builder.Append("<friendlyName>")
+                .Append(SecurityElement.Escape(GetFriendlyName()))
+                .Append("</friendlyName>");
+            builder.Append("<manufacturer>")
+                .Append(SecurityElement.Escape(_profile.Manufacturer ?? string.Empty))
+                .Append("</manufacturer>");
+            builder.Append("<manufacturerURL>")
+                .Append(SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty))
+                .Append("</manufacturerURL>");
+
+            builder.Append("<modelDescription>")
+                .Append(SecurityElement.Escape(_profile.ModelDescription ?? string.Empty))
+                .Append("</modelDescription>");
+            builder.Append("<modelName>")
+                .Append(SecurityElement.Escape(_profile.ModelName ?? string.Empty))
+                .Append("</modelName>");
+
+            builder.Append("<modelNumber>")
+                .Append(SecurityElement.Escape(_profile.ModelNumber ?? string.Empty))
+                .Append("</modelNumber>");
+            builder.Append("<modelURL>")
+                .Append(SecurityElement.Escape(_profile.ModelUrl ?? string.Empty))
+                .Append("</modelURL>");
 
             if (string.IsNullOrEmpty(_profile.SerialNumber))
             {
-                builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>");
+                builder.Append("<serialNumber>")
+                    .Append(SecurityElement.Escape(_serverId))
+                    .Append("</serialNumber>");
             }
             else
             {
-                builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>");
+                builder.Append("<serialNumber>")
+                    .Append(SecurityElement.Escape(_profile.SerialNumber))
+                    .Append("</serialNumber>");
             }
 
             builder.Append("<UPC/>");
 
-            builder.Append("<UDN>uuid:" + Escape(_serverUdn) + "</UDN>");
+            builder.Append("<UDN>uuid:")
+                .Append(SecurityElement.Escape(_serverUdn))
+                .Append("</UDN>");
 
             if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags))
             {
-                builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">" + Escape(_profile.SonyAggregationFlags) + "</av:aggregationFlags>");
+                builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">")
+                    .Append(SecurityElement.Escape(_profile.SonyAggregationFlags))
+                    .Append("</av:aggregationFlags>");
             }
         }
 
@@ -245,11 +198,21 @@ namespace Emby.Dlna.Server
             {
                 builder.Append("<icon>");
 
-                builder.Append("<mimetype>" + Escape(icon.MimeType ?? string.Empty) + "</mimetype>");
-                builder.Append("<width>" + Escape(icon.Width.ToString(_usCulture)) + "</width>");
-                builder.Append("<height>" + Escape(icon.Height.ToString(_usCulture)) + "</height>");
-                builder.Append("<depth>" + Escape(icon.Depth ?? string.Empty) + "</depth>");
-                builder.Append("<url>" + BuildUrl(icon.Url) + "</url>");
+                builder.Append("<mimetype>")
+                    .Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
+                    .Append("</mimetype>");
+                builder.Append("<width>")
+                    .Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
+                    .Append("</width>");
+                builder.Append("<height>")
+                    .Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
+                    .Append("</height>");
+                builder.Append("<depth>")
+                    .Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
+                    .Append("</depth>");
+                builder.Append("<url>")
+                    .Append(BuildUrl(icon.Url))
+                    .Append("</url>");
 
                 builder.Append("</icon>");
             }
@@ -265,11 +228,21 @@ namespace Emby.Dlna.Server
             {
                 builder.Append("<service>");
 
-                builder.Append("<serviceType>" + Escape(service.ServiceType ?? string.Empty) + "</serviceType>");
-                builder.Append("<serviceId>" + Escape(service.ServiceId ?? string.Empty) + "</serviceId>");
-                builder.Append("<SCPDURL>" + BuildUrl(service.ScpdUrl) + "</SCPDURL>");
-                builder.Append("<controlURL>" + BuildUrl(service.ControlUrl) + "</controlURL>");
-                builder.Append("<eventSubURL>" + BuildUrl(service.EventSubUrl) + "</eventSubURL>");
+                builder.Append("<serviceType>")
+                    .Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
+                    .Append("</serviceType>");
+                builder.Append("<serviceId>")
+                    .Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
+                    .Append("</serviceId>");
+                builder.Append("<SCPDURL>")
+                    .Append(BuildUrl(service.ScpdUrl))
+                    .Append("</SCPDURL>");
+                builder.Append("<controlURL>")
+                    .Append(BuildUrl(service.ControlUrl))
+                    .Append("</controlURL>");
+                builder.Append("<eventSubURL>")
+                    .Append(BuildUrl(service.EventSubUrl))
+                    .Append("</eventSubURL>");
 
                 builder.Append("</service>");
             }
@@ -293,7 +266,7 @@ namespace Emby.Dlna.Server
                 url = _serverAddress.TrimEnd('/') + url;
             }
 
-            return Escape(url);
+            return SecurityElement.Escape(url);
         }
 
         private IEnumerable<DeviceIcon> GetIcons()

+ 18 - 14
Emby.Dlna/Service/BaseControlHandler.cs

@@ -15,10 +15,7 @@ namespace Emby.Dlna.Service
 {
     public abstract class BaseControlHandler
     {
-        private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
-
-        protected IServerConfigurationManager Config { get; }
-        protected ILogger Logger { get; }
+        private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
 
         protected BaseControlHandler(IServerConfigurationManager config, ILogger logger)
         {
@@ -26,6 +23,10 @@ namespace Emby.Dlna.Service
             Logger = logger;
         }
 
+        protected IServerConfigurationManager Config { get; }
+
+        protected ILogger Logger { get; }
+
         public async Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
         {
             try
@@ -79,10 +80,10 @@ namespace Emby.Dlna.Service
             {
                 writer.WriteStartDocument(true);
 
-                writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV);
-                writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/");
+                writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
+                writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
 
-                writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV);
+                writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
                 writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI);
 
                 WriteResult(requestInfo.LocalName, requestInfo.Headers, writer);
@@ -135,6 +136,7 @@ namespace Emby.Dlna.Service
 
                                 break;
                             }
+
                         default:
                             {
                                 await reader.SkipAsync().ConfigureAwait(false);
@@ -208,13 +210,6 @@ namespace Emby.Dlna.Service
             }
         }
 
-        private class ControlRequestInfo
-        {
-            public string LocalName { get; set; }
-            public string NamespaceURI { get; set; }
-            public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-        }
-
         protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter);
 
         private void LogRequest(ControlRequest request)
@@ -236,5 +231,14 @@ namespace Emby.Dlna.Service
 
             Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml);
         }
+
+        private class ControlRequestInfo
+        {
+            public string LocalName { get; set; }
+
+            public string NamespaceURI { get; set; }
+
+            public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+        }
     }
 }

+ 8 - 10
Emby.Dlna/Service/BaseService.cs

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

+ 5 - 5
Emby.Dlna/Service/ControlErrorHandler.cs

@@ -10,7 +10,7 @@ namespace Emby.Dlna.Service
 {
     public static class ControlErrorHandler
     {
-        private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
+        private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
 
         public static ControlResponse GetResponse(Exception ex)
         {
@@ -26,11 +26,11 @@ namespace Emby.Dlna.Service
             {
                 writer.WriteStartDocument(true);
 
-                writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV);
-                writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/");
+                writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
+                writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
 
-                writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV);
-                writer.WriteStartElement("SOAP-ENV", "Fault", NS_SOAPENV);
+                writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
+                writer.WriteStartElement("SOAP-ENV", "Fault", NsSoapEnv);
 
                 writer.WriteElementString("faultcode", "500");
                 writer.WriteElementString("faultstring", ex.Message);

+ 27 - 10
Emby.Dlna/Service/ServiceXmlBuilder.cs

@@ -1,9 +1,9 @@
 #pragma warning disable CS1591
 
 using System.Collections.Generic;
+using System.Security;
 using System.Text;
 using Emby.Dlna.Common;
-using Emby.Dlna.Server;
 
 namespace Emby.Dlna.Service
 {
@@ -37,7 +37,9 @@ namespace Emby.Dlna.Service
             {
                 builder.Append("<action>");
 
-                builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>");
+                builder.Append("<name>")
+                    .Append(SecurityElement.Escape(item.Name ?? string.Empty))
+                    .Append("</name>");
 
                 builder.Append("<argumentList>");
 
@@ -45,9 +47,15 @@ namespace Emby.Dlna.Service
                 {
                     builder.Append("<argument>");
 
-                    builder.Append("<name>" + DescriptionXmlBuilder.Escape(argument.Name ?? string.Empty) + "</name>");
-                    builder.Append("<direction>" + DescriptionXmlBuilder.Escape(argument.Direction ?? string.Empty) + "</direction>");
-                    builder.Append("<relatedStateVariable>" + DescriptionXmlBuilder.Escape(argument.RelatedStateVariable ?? string.Empty) + "</relatedStateVariable>");
+                    builder.Append("<name>")
+                        .Append(SecurityElement.Escape(argument.Name ?? string.Empty))
+                        .Append("</name>");
+                    builder.Append("<direction>")
+                        .Append(SecurityElement.Escape(argument.Direction ?? string.Empty))
+                        .Append("</direction>");
+                    builder.Append("<relatedStateVariable>")
+                        .Append(SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty))
+                        .Append("</relatedStateVariable>");
 
                     builder.Append("</argument>");
                 }
@@ -68,18 +76,27 @@ namespace Emby.Dlna.Service
             {
                 var sendEvents = item.SendsEvents ? "yes" : "no";
 
-                builder.Append("<stateVariable sendEvents=\"" + sendEvents + "\">");
+                builder.Append("<stateVariable sendEvents=\"")
+                    .Append(sendEvents)
+                    .Append("\">");
 
-                builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>");
-                builder.Append("<dataType>" + DescriptionXmlBuilder.Escape(item.DataType ?? string.Empty) + "</dataType>");
+                builder.Append("<name>")
+                    .Append(SecurityElement.Escape(item.Name ?? string.Empty))
+                    .Append("</name>");
+                builder.Append("<dataType>")
+                    .Append(SecurityElement.Escape(item.DataType ?? string.Empty))
+                    .Append("</dataType>");
 
-                if (item.AllowedValues.Length > 0)
+                if (item.AllowedValues.Count > 0)
                 {
                     builder.Append("<allowedValueList>");
                     foreach (var allowedValue in item.AllowedValues)
                     {
-                        builder.Append("<allowedValue>" + DescriptionXmlBuilder.Escape(allowedValue) + "</allowedValue>");
+                        builder.Append("<allowedValue>")
+                            .Append(SecurityElement.Escape(allowedValue))
+                            .Append("</allowedValue>");
                     }
+
                     builder.Append("</allowedValueList>");
                 }
 

+ 16 - 21
Emby.Dlna/Ssdp/DeviceDiscovery.cs

@@ -3,9 +3,9 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Events;
 using Rssdp;
 using Rssdp.Infrastructure;
 
@@ -17,9 +17,17 @@ namespace Emby.Dlna.Ssdp
 
         private readonly IServerConfigurationManager _config;
 
+        private SsdpDeviceLocator _deviceLocator;
+        private ISsdpCommunicationsServer _commsServer;
+
         private int _listenerCount;
         private bool _disposed;
 
+        public DeviceDiscovery(IServerConfigurationManager config)
+        {
+            _config = config;
+        }
+
         private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal;
 
         /// <inheritdoc />
@@ -49,15 +57,6 @@ namespace Emby.Dlna.Ssdp
         /// <inheritdoc />
         public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft;
 
-        private SsdpDeviceLocator _deviceLocator;
-
-        private ISsdpCommunicationsServer _commsServer;
-
-        public DeviceDiscovery(IServerConfigurationManager config)
-        {
-            _config = config;
-        }
-
         // Call this method from somewhere in your code to start the search.
         public void Start(ISsdpCommunicationsServer communicationsServer)
         {
@@ -77,7 +76,7 @@ namespace Emby.Dlna.Ssdp
                     // (Optional) Set the filter so we only see notifications for devices we care about
                     // (can be any search target value i.e device type, uuid value etc - any value that appears in the
                     // DiscoverdSsdpDevice.NotificationType property or that is used with the searchTarget parameter of the Search method).
-                    //_DeviceLocator.NotificationFilter = "upnp:rootdevice";
+                    // _DeviceLocator.NotificationFilter = "upnp:rootdevice";
 
                     // Connect our event handler so we process devices as they are found
                     _deviceLocator.DeviceAvailable += OnDeviceLocatorDeviceAvailable;
@@ -100,15 +99,13 @@ namespace Emby.Dlna.Ssdp
 
             var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
 
-            var args = new GenericEventArgs<UpnpDeviceInfo>
-            {
-                Argument = new UpnpDeviceInfo
+            var args = new GenericEventArgs<UpnpDeviceInfo>(
+                new UpnpDeviceInfo
                 {
                     Location = e.DiscoveredDevice.DescriptionLocation,
                     Headers = headers,
                     LocalIpAddress = e.LocalIpAddress
-                }
-            };
+                });
 
             DeviceDiscoveredInternal?.Invoke(this, args);
         }
@@ -121,14 +118,12 @@ namespace Emby.Dlna.Ssdp
 
             var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
 
-            var args = new GenericEventArgs<UpnpDeviceInfo>
-            {
-                Argument = new UpnpDeviceInfo
+            var args = new GenericEventArgs<UpnpDeviceInfo>(
+                new UpnpDeviceInfo
                 {
                     Location = e.DiscoveredDevice.DescriptionLocation,
                     Headers = headers
-                }
-            };
+                });
 
             DeviceLeft?.Invoke(this, args);
         }

+ 5 - 11
Emby.Dlna/Ssdp/Extensions.cs → Emby.Dlna/Ssdp/SsdpExtensions.cs

@@ -1,33 +1,27 @@
 #pragma warning disable CS1591
 
+using System.Linq;
 using System.Xml.Linq;
 
 namespace Emby.Dlna.Ssdp
 {
-    public static class Extensions
+    public static class SsdpExtensions
     {
         public static string GetValue(this XElement container, XName name)
         {
             var node = container.Element(name);
 
-            return node == null ? null : node.Value;
+            return node?.Value;
         }
 
         public static string GetAttributeValue(this XElement container, XName name)
         {
             var node = container.Attribute(name);
 
-            return node == null ? null : node.Value;
+            return node?.Value;
         }
 
         public static string GetDescendantValue(this XElement container, XName name)
-        {
-            foreach (var node in container.Descendants(name))
-            {
-                return node.Value;
-            }
-
-            return null;
-        }
+            => container.Descendants(name).FirstOrDefault()?.Value;
     }
 }

+ 41 - 11
Emby.Drawing/ImageProcessor.cs

@@ -4,6 +4,7 @@ using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Drawing;
@@ -14,6 +15,7 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using Microsoft.Extensions.Logging;
+using Photo = MediaBrowser.Controller.Entities.Photo;
 
 namespace Emby.Drawing
 {
@@ -28,7 +30,7 @@ namespace Emby.Drawing
         private static readonly HashSet<string> _transparentImageTypes
             = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
 
-        private readonly ILogger _logger;
+        private readonly ILogger<ImageProcessor> _logger;
         private readonly IFileSystem _fileSystem;
         private readonly IServerApplicationPaths _appPaths;
         private readonly IImageEncoder _imageEncoder;
@@ -114,7 +116,7 @@ namespace Emby.Drawing
             => _transparentImageTypes.Contains(Path.GetExtension(path));
 
         /// <inheritdoc />
-        public async Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options)
+        public async Task<(string path, string? mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options)
         {
             ItemImageInfo originalImage = options.Image;
             BaseItem item = options.Item;
@@ -230,7 +232,7 @@ namespace Emby.Drawing
             return ImageFormat.Jpg;
         }
 
-        private string GetMimeType(ImageFormat format, string path)
+        private string? GetMimeType(ImageFormat format, string path)
             => format switch
             {
                 ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
@@ -300,7 +302,7 @@ namespace Emby.Drawing
             }
 
             string path = info.Path;
-            _logger.LogInformation("Getting image size for item {ItemType} {Path}", item.GetType().Name, path);
+            _logger.LogDebug("Getting image size for item {ItemType} {Path}", item.GetType().Name, path);
 
             ImageDimensions size = GetImageDimensions(path);
             info.Width = size.Width;
@@ -313,6 +315,27 @@ namespace Emby.Drawing
         public ImageDimensions GetImageDimensions(string path)
             => _imageEncoder.GetImageSize(path);
 
+        /// <inheritdoc />
+        public string GetImageBlurHash(string path)
+        {
+            var size = GetImageDimensions(path);
+            if (size.Width <= 0 || size.Height <= 0)
+            {
+                return string.Empty;
+            }
+
+            // We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance.
+            // One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width.
+            // See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components
+            float xCompF = MathF.Sqrt(16.0f * size.Width / size.Height);
+            float yCompF = xCompF * size.Height / size.Width;
+
+            int xComp = Math.Min((int)xCompF + 1, 9);
+            int yComp = Math.Min((int)yCompF + 1, 9);
+
+            return _imageEncoder.GetImageBlurHash(xComp, yComp, path);
+        }
+
         /// <inheritdoc />
         public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
             => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
@@ -328,6 +351,13 @@ namespace Emby.Drawing
             });
         }
 
+        /// <inheritdoc />
+        public string GetImageCacheTag(User user)
+        {
+            return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
+                .ToString("N", CultureInfo.InvariantCulture);
+        }
+
         private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
         {
             var inputFormat = Path.GetExtension(originalImagePath)
@@ -418,21 +448,21 @@ namespace Emby.Drawing
         /// or
         /// filename.
         /// </exception>
-        public string GetCachePath(string path, string filename)
+        public string GetCachePath(ReadOnlySpan<char> path, ReadOnlySpan<char> filename)
         {
-            if (string.IsNullOrEmpty(path))
+            if (path.IsEmpty)
             {
-                throw new ArgumentNullException(nameof(path));
+                throw new ArgumentException("Path can't be empty.", nameof(path));
             }
 
-            if (string.IsNullOrEmpty(filename))
+            if (path.IsEmpty)
             {
-                throw new ArgumentNullException(nameof(filename));
+                throw new ArgumentException("Filename can't be empty.", nameof(filename));
             }
 
-            var prefix = filename.Substring(0, 1);
+            var prefix = filename.Slice(0, 1);
 
-            return Path.Combine(path, prefix, filename);
+            return Path.Join(path, prefix, filename);
         }
 
         /// <inheritdoc />

+ 6 - 0
Emby.Drawing/NullImageEncoder.cs

@@ -42,5 +42,11 @@ namespace Emby.Drawing
         {
             throw new NotImplementedException();
         }
+
+        /// <inheritdoc />
+        public string GetImageBlurHash(int xComp, int yComp, string path)
+        {
+            throw new NotImplementedException();
+        }
     }
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов