浏览代码

Merge branch 'master' into more-track-titles

Pika 4 年之前
父节点
当前提交
3b21abd879
共有 100 个文件被更改,包括 1169 次插入808 次删除
  1. 22 29
      .ci/azure-pipelines-abi.yml
  2. 44 52
      .ci/azure-pipelines-main.yml
  3. 163 0
      .ci/azure-pipelines-package.yml
  4. 46 18
      .ci/azure-pipelines-test.yml
  5. 0 82
      .ci/azure-pipelines-windows.yml
  6. 18 17
      .ci/azure-pipelines.yml
  7. 0 59
      .copr/Makefile
  8. 1 0
      .copr/Makefile
  9. 7 10
      .editorconfig
  10. 3 0
      .github/CODEOWNERS
  11. 9 0
      .github/dependabot.yml
  12. 12 7
      .gitignore
  13. 14 0
      .vscode/extensions.json
  14. 6 6
      .vscode/launch.json
  15. 17 2
      .vscode/tasks.json
  16. 4 0
      CONTRIBUTORS.md
  17. 11 19
      Dockerfile
  18. 2 2
      Dockerfile.arm
  19. 5 10
      DvdLib/BigEndianBinaryReader.cs
  20. 6 4
      DvdLib/DvdLib.csproj
  21. 3 0
      DvdLib/Ifo/Cell.cs
  22. 2 0
      DvdLib/Ifo/CellPlaybackInfo.cs
  23. 2 0
      DvdLib/Ifo/CellPositionInfo.cs
  24. 4 0
      DvdLib/Ifo/Chapter.cs
  25. 18 10
      DvdLib/Ifo/Dvd.cs
  26. 10 2
      DvdLib/Ifo/DvdTime.cs
  27. 3 1
      DvdLib/Ifo/Program.cs
  28. 15 2
      DvdLib/Ifo/ProgramChain.cs
  29. 10 1
      DvdLib/Ifo/Title.cs
  30. 2 0
      DvdLib/Ifo/UserOperation.cs
  31. 5 2
      Emby.Dlna/Api/DlnaServerService.cs
  32. 1 0
      Emby.Dlna/ConfigurationExtension.cs
  33. 7 9
      Emby.Dlna/ContentDirectory/ContentDirectory.cs
  34. 24 16
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  35. 138 51
      Emby.Dlna/Didl/DidlBuilder.cs
  36. 1 2
      Emby.Dlna/Didl/Filter.cs
  37. 28 8
      Emby.Dlna/DlnaManager.cs
  38. 5 0
      Emby.Dlna/Emby.Dlna.csproj
  39. 27 15
      Emby.Dlna/Eventing/EventManager.cs
  40. 3 0
      Emby.Dlna/Eventing/EventSubscription.cs
  41. 37 29
      Emby.Dlna/Main/DlnaEntryPoint.cs
  42. 33 37
      Emby.Dlna/PlayTo/Device.cs
  43. 30 9
      Emby.Dlna/PlayTo/PlayToController.cs
  44. 12 6
      Emby.Dlna/PlayTo/PlayToManager.cs
  45. 1 0
      Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
  46. 0 1
      Emby.Dlna/PlayTo/SsdpHttpClient.cs
  47. 2 0
      Emby.Dlna/PlayTo/uBaseObject.cs
  48. 2 2
      Emby.Dlna/Profiles/DefaultProfile.cs
  49. 1 1
      Emby.Dlna/Profiles/DenonAvrProfile.cs
  50. 1 1
      Emby.Dlna/Profiles/DirectTvProfile.cs
  51. 1 1
      Emby.Dlna/Profiles/Foobar2000Profile.cs
  52. 1 1
      Emby.Dlna/Profiles/MarantzProfile.cs
  53. 2 1
      Emby.Dlna/Profiles/MediaMonkeyProfile.cs
  54. 2 1
      Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs
  55. 2 1
      Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs
  56. 2 1
      Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs
  57. 2 1
      Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs
  58. 1 1
      Emby.Dlna/Profiles/Xml/Default.xml
  59. 1 1
      Emby.Dlna/Profiles/Xml/Denon AVR.xml
  60. 1 1
      Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml
  61. 1 1
      Emby.Dlna/Profiles/Xml/LG Smart TV.xml
  62. 1 1
      Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml
  63. 1 1
      Emby.Dlna/Profiles/Xml/Marantz.xml
  64. 1 1
      Emby.Dlna/Profiles/Xml/MediaMonkey.xml
  65. 1 1
      Emby.Dlna/Profiles/Xml/Panasonic Viera.xml
  66. 1 1
      Emby.Dlna/Profiles/Xml/Popcorn Hour.xml
  67. 1 1
      Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml
  68. 1 1
      Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml
  69. 1 1
      Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml
  70. 1 1
      Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml
  71. 1 1
      Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml
  72. 1 1
      Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml
  73. 1 1
      Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml
  74. 1 1
      Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml
  75. 1 1
      Emby.Dlna/Profiles/Xml/WDTV Live.xml
  76. 1 1
      Emby.Dlna/Profiles/Xml/Xbox One.xml
  77. 1 1
      Emby.Dlna/Profiles/Xml/foobar2000.xml
  78. 75 102
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  79. 4 0
      Emby.Dlna/Service/BaseControlHandler.cs
  80. 1 1
      Emby.Dlna/Service/BaseService.cs
  81. 26 9
      Emby.Dlna/Service/ServiceXmlBuilder.cs
  82. 7 11
      Emby.Dlna/Ssdp/DeviceDiscovery.cs
  83. 4 10
      Emby.Dlna/Ssdp/Extensions.cs
  84. 6 0
      Emby.Drawing/Emby.Drawing.csproj
  85. 43 51
      Emby.Drawing/ImageProcessor.cs
  86. 6 0
      Emby.Drawing/NullImageEncoder.cs
  87. 10 7
      Emby.Naming/Audio/AlbumParser.cs
  88. 2 1
      Emby.Naming/Audio/AudioFileParser.cs
  89. 1 0
      Emby.Naming/AudioBook/AudioBookFilePathParser.cs
  90. 1 6
      Emby.Naming/Common/EpisodeExpression.cs
  91. 3 3
      Emby.Naming/Common/MediaType.cs
  92. 88 31
      Emby.Naming/Common/NamingOptions.cs
  93. 5 0
      Emby.Naming/Emby.Naming.csproj
  94. 4 8
      Emby.Naming/Subtitles/SubtitleParser.cs
  95. 9 0
      Emby.Naming/Video/ExtraResolver.cs
  96. 6 7
      Emby.Naming/Video/ExtraRule.cs
  97. 9 4
      Emby.Naming/Video/ExtraRuleType.cs
  98. 1 1
      Emby.Naming/Video/VideoListResolver.cs
  99. 3 3
      Emby.Naming/Video/VideoResolver.cs
  100. 6 4
      Emby.Notifications/Api/NotificationsService.cs

+ 22 - 29
.ci/azure-pipelines-compat.yml → .ci/azure-pipelines-abi.yml

@@ -1,13 +1,13 @@
 parameters:
 parameters:
-  - name: Packages
-    type: object
-    default: {}
-  - name: LinuxImage
-    type: string
-    default: "ubuntu-latest"
-  - name: DotNetSdkVersion
-    type: string
-    default: 3.1.100
+- name: Packages
+  type: object
+  default: {}
+- name: LinuxImage
+  type: string
+  default: "ubuntu-latest"
+- name: DotNetSdkVersion
+  type: string
+  default: 3.1.100
 
 
 jobs:
 jobs:
   - job: CompatibilityCheck
   - job: CompatibilityCheck
@@ -23,7 +23,7 @@ jobs:
             NugetPackageName: ${{ Package.value.NugetPackageName }}
             NugetPackageName: ${{ Package.value.NugetPackageName }}
             AssemblyFileName: ${{ Package.value.AssemblyFileName }}
             AssemblyFileName: ${{ Package.value.AssemblyFileName }}
       maxParallel: 2
       maxParallel: 2
-    dependsOn: MainBuild
+    dependsOn: Build
     steps:
     steps:
       - checkout: none
       - checkout: none
 
 
@@ -33,6 +33,13 @@ jobs:
           packageType: sdk
           packageType: sdk
           version: ${{ parameters.DotNetSdkVersion }}
           version: ${{ parameters.DotNetSdkVersion }}
 
 
+      - task: DotNetCoreCLI@2
+        displayName: 'Install ABI CompatibilityChecker tool'
+        inputs:
+          command: custom
+          custom: tool
+          arguments: 'update compatibilitychecker -g'
+
       - task: DownloadPipelineArtifact@2
       - task: DownloadPipelineArtifact@2
         displayName: "Download New Assembly Build Artifact"
         displayName: "Download New Assembly Build Artifact"
         inputs:
         inputs:
@@ -72,25 +79,11 @@ jobs:
           overWrite: true
           overWrite: true
           flattenFolders: 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.
       # 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:
         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)
           workingDirectory: $(System.ArtifactsDirectory)

+ 44 - 52
.ci/azure-pipelines-main.yml

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

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

@@ -0,0 +1,163 @@
+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')
+
+  - 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'
+
+  steps:
+  - 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')
+    inputs:
+      repository: 'jellyfin/jellyfin-server'
+      command: buildAndPush
+      buildContext: '.'
+      Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
+      containerRegistry: Docker Hub
+      tags: |
+        stable-$(Build.BuildNumber)-$(BuildConfiguration)
+        stable-$(BuildConfiguration)
+
+- job: CollectArtifacts
+  displayName: 'Collect Artifacts'
+  dependsOn:
+  - BuildPackage
+  - BuildDocker
+  condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
+
+  pool:
+    vmImage: 'ubuntu-latest'
+
+  steps:
+  - task: SSH@0
+    displayName: 'Update Unstable Repository'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+    inputs:
+      sshEndpoint: repository
+      runOptions: 'inline'
+      inline: |
+        sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
+        rm $0
+        exit
+
+  - task: SSH@0
+    displayName: 'Update Stable Repository'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+    inputs:
+      sshEndpoint: repository
+      runOptions: 'inline'
+      inline: |
+        sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
+        rm $0
+        exit
+
+- job: PublishNuget
+  displayName: 'Publish NuGet packages'
+  dependsOn:
+  - BuildPackage
+  condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags'))
+  
+  pool:
+    vmImage: 'ubuntu-latest'
+
+  steps:
+  - task: NuGetCommand@2
+    inputs:
+      command: 'pack'
+      packagesToPack: Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj
+      packDestination: '$(Build.ArtifactStagingDirectory)'
+
+  - task: NuGetCommand@2
+    inputs:
+      command: 'push'
+      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
+      includeNugetOrg: 'true'

+ 46 - 18
.ci/azure-pipelines-test.yml

@@ -1,26 +1,25 @@
 parameters:
 parameters:
-  - name: ImageNames
-    type: object
-    default:
-      Linux: "ubuntu-latest"
-      Windows: "windows-latest"
-      macOS: "macos-latest"
-  - name: TestProjects
-    type: string
-    default: "tests/**/*Tests.csproj"
-  - name: DotNetSdkVersion
-    type: string
-    default: 3.1.100
+- name: ImageNames
+  type: object
+  default:
+    Linux: "ubuntu-latest"
+    Windows: "windows-latest"
+    macOS: "macos-latest"
+- name: TestProjects
+  type: string
+  default: "tests/**/*Tests.csproj"
+- name: DotNetSdkVersion
+  type: string
+  default: 3.1.100
 
 
 jobs:
 jobs:
-  - job: MainTest
-    displayName: Main Test
+  - job: Test
+    displayName: Test
     strategy:
     strategy:
       matrix:
       matrix:
         ${{ each imageName in parameters.ImageNames }}:
         ${{ each imageName in parameters.ImageNames }}:
           ${{ imageName.key }}:
           ${{ imageName.key }}:
             ImageName: ${{ imageName.value }}
             ImageName: ${{ imageName.value }}
-      maxParallel: 3
     pool:
     pool:
       vmImage: "$(ImageName)"
       vmImage: "$(ImageName)"
     steps:
     steps:
@@ -29,14 +28,31 @@ jobs:
         submodules: true
         submodules: true
         persistCredentials: false
         persistCredentials: false
 
 
+      # This is required for the SonarCloud analyzer
+      - task: UseDotNet@2
+        displayName: "Install .NET Core SDK 2.1"
+        condition: eq(variables['ImageName'], 'ubuntu-latest')
+        inputs:
+          packageType: sdk
+          version: '2.1.805'
+
       - task: UseDotNet@2
       - task: UseDotNet@2
         displayName: "Update DotNet"
         displayName: "Update DotNet"
         inputs:
         inputs:
           packageType: sdk
           packageType: sdk
           version: ${{ parameters.DotNetSdkVersion }}
           version: ${{ parameters.DotNetSdkVersion }}
 
 
+      - task: SonarCloudPrepare@1
+        displayName: 'Prepare analysis on SonarCloud'
+        condition: eq(variables['ImageName'], 'ubuntu-latest')
+        enabled: false
+        inputs:
+          SonarCloud: 'Sonarcloud for Jellyfin'
+          organization: 'jellyfin'
+          projectKey: 'jellyfin_jellyfin'
+
       - task: DotNetCoreCLI@2
       - task: DotNetCoreCLI@2
-        displayName: Run .NET Core CLI tests
+        displayName: 'Run CLI Tests'
         inputs:
         inputs:
           command: "test"
           command: "test"
           projects: ${{ parameters.TestProjects }}
           projects: ${{ parameters.TestProjects }}
@@ -45,9 +61,20 @@ jobs:
           testRunTitle: $(Agent.JobName)
           testRunTitle: $(Agent.JobName)
           workingDirectory: "$(Build.SourcesDirectory)"
           workingDirectory: "$(Build.SourcesDirectory)"
 
 
+      - 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
       - 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
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
-        displayName: ReportGenerator (merge)
+        displayName: 'Run ReportGenerator'
+        enabled: false
         inputs:
         inputs:
           reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
           reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
           targetdir: "$(Agent.TempDirectory)/merged/"
           targetdir: "$(Agent.TempDirectory)/merged/"
@@ -56,7 +83,8 @@ jobs:
       ## V2 is already in the repository but it does not work "wrong number of segments" YAML error.
       ## V2 is already in the repository but it does not work "wrong number of segments" YAML error.
       - task: PublishCodeCoverageResults@1
       - task: PublishCodeCoverageResults@1
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
-        displayName: Publish Code Coverage
+        displayName: 'Publish Code Coverage'
+        enabled: false
         inputs:
         inputs:
           codeCoverageTool: "cobertura"
           codeCoverageTool: "cobertura"
           #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
           #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2

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

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

+ 18 - 17
.ci/azure-pipelines.yml

@@ -1,12 +1,12 @@
 name: $(Date:yyyyMMdd)$(Rev:.r)
 name: $(Date:yyyyMMdd)$(Rev:.r)
 
 
 variables:
 variables:
-  - name: TestProjects
-    value: "tests/**/*Tests.csproj"
-  - name: RestoreBuildProjects
-    value: "Jellyfin.Server/Jellyfin.Server.csproj"
-  - name: DotNetSdkVersion
-    value: 3.1.100
+- name: TestProjects
+  value: 'tests/**/*Tests.csproj'
+- name: RestoreBuildProjects
+  value: 'Jellyfin.Server/Jellyfin.Server.csproj'
+- name: DotNetSdkVersion
+  value: 3.1.100
 
 
 pr:
 pr:
   autoCancel: true
   autoCancel: true
@@ -15,24 +15,22 @@ trigger:
   batch: true
   batch: true
 
 
 jobs:
 jobs:
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
   - template: azure-pipelines-main.yml
   - template: azure-pipelines-main.yml
     parameters:
     parameters:
-      LinuxImage: "ubuntu-latest"
+      LinuxImage: 'ubuntu-latest'
       RestoreBuildProjects: $(RestoreBuildProjects)
       RestoreBuildProjects: $(RestoreBuildProjects)
 
 
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
   - template: azure-pipelines-test.yml
   - template: azure-pipelines-test.yml
     parameters:
     parameters:
       ImageNames:
       ImageNames:
-        Linux: "ubuntu-latest"
-        Windows: "windows-latest"
-        macOS: "macos-latest"
+        Linux: 'ubuntu-latest'
+        Windows: 'windows-latest'
+        macOS: 'macos-latest'
 
 
-  - template: azure-pipelines-windows.yml
-    parameters:
-      WindowsImage: "windows-latest"
-      TestProjects: $(TestProjects)
-
-  - template: azure-pipelines-compat.yml
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
+  - template: azure-pipelines-abi.yml
     parameters:
     parameters:
       Packages:
       Packages:
         Naming:
         Naming:
@@ -47,4 +45,7 @@ jobs:
         Common:
         Common:
           NugetPackageName: Jellyfin.Common
           NugetPackageName: Jellyfin.Common
           AssemblyFileName: MediaBrowser.Common.dll
           AssemblyFileName: MediaBrowser.Common.dll
-      LinuxImage: "ubuntu-latest"
+      LinuxImage: 'ubuntu-latest'
+
+- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
+  - template: azure-pipelines-package.yml

+ 0 - 59
.copr/Makefile

@@ -1,59 +0,0 @@
-VERSION := $(shell sed -ne '/^Version:/s/.*  *//p'                      \
-                   deployment/fedora-package-x64/pkg-src/jellyfin.spec)
-
-deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz:
-	curl -f -L -o deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz \
-         https://github.com/jellyfin/jellyfin-web/archive/v$(VERSION).tar.gz \
-	|| curl -f -L -o deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz \
-         https://github.com/jellyfin/jellyfin-web/archive/master.tar.gz \
-
-srpm: deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz
-	cd deployment/fedora-package-x64;                                             \
-    SOURCE_DIR=../..                                                              \
-    WORKDIR="$${PWD}";                                                            \
-    package_temporary_dir="$${WORKDIR}/pkg-dist-tmp";                             \
-    pkg_src_dir="$${WORKDIR}/pkg-src";                                            \
-    GNU_TAR=1;                                                                    \
-    tar                                                                           \
-    --transform "s,^\.,jellyfin-$(VERSION),"                                      \
-    --exclude='.git*'                                                             \
-    --exclude='**/.git'                                                           \
-    --exclude='**/.hg'                                                            \
-    --exclude='**/.vs'                                                            \
-    --exclude='**/.vscode'                                                        \
-    --exclude='deployment'                                                        \
-    --exclude='**/bin'                                                            \
-    --exclude='**/obj'                                                            \
-    --exclude='**/.nuget'                                                         \
-    --exclude='*.deb'                                                             \
-    --exclude='*.rpm'                                                             \
-    -czf "pkg-src/jellyfin-$(VERSION).tar.gz"                                     \
-    -C $${SOURCE_DIR} ./ || GNU_TAR=0;                                            \
-    if [ $${GNU_TAR} -eq 0 ]; then                                                \
-        package_temporary_dir="$$(mktemp -d)";                                    \
-        mkdir -p "$${package_temporary_dir}/jellyfin";                            \
-        tar                                                                       \
-        --exclude='.git*'                                                         \
-        --exclude='**/.git'                                                       \
-        --exclude='**/.hg'                                                        \
-        --exclude='**/.vs'                                                        \
-        --exclude='**/.vscode'                                                    \
-        --exclude='deployment'                                                    \
-        --exclude='**/bin'                                                        \
-        --exclude='**/obj'                                                        \
-        --exclude='**/.nuget'                                                     \
-        --exclude='*.deb'                                                         \
-        --exclude='*.rpm'                                                         \
-        -czf "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz"      \
-        -C $${SOURCE_DIR} ./;                                                     \
-        mkdir -p "$${package_temporary_dir}/jellyfin-$(VERSION)";                 \
-        tar -xzf "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz"  \
-            -C "$${package_temporary_dir}/jellyfin-$(VERSION);                    \
-        rm -f "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz";    \
-        tar -czf "$${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-$(VERSION).tar.gz"      \
-            -C "$${package_temporary_dir}" "jellyfin-$(VERSION);                  \
-        rm -rf $${package_temporary_dir};                                         \
-	fi;                                                                           \
-	rpmbuild -bs pkg-src/jellyfin.spec                                            \
-	         --define "_sourcedir $$PWD/pkg-src/"                                 \
-	         --define "_srcrpmdir $(outdir)"

+ 1 - 0
.copr/Makefile

@@ -0,0 +1 @@
+../fedora/Makefile

+ 7 - 10
.editorconfig

@@ -13,7 +13,7 @@ charset = utf-8
 trim_trailing_whitespace = true
 trim_trailing_whitespace = true
 insert_final_newline = true
 insert_final_newline = true
 end_of_line = lf
 end_of_line = lf
-max_line_length = null
+max_line_length = off
 
 
 # YAML indentation
 # YAML indentation
 [*.{yml,yaml}]
 [*.{yml,yaml}]
@@ -22,6 +22,7 @@ indent_size = 2
 # XML indentation
 # XML indentation
 [*.{csproj,xml}]
 [*.{csproj,xml}]
 indent_size = 2
 indent_size = 2
+
 ###############################
 ###############################
 # .NET Coding Conventions     #
 # .NET Coding Conventions     #
 ###############################
 ###############################
@@ -51,11 +52,12 @@ dotnet_style_explicit_tuple_names = true:suggestion
 dotnet_style_null_propagation = true:suggestion
 dotnet_style_null_propagation = true:suggestion
 dotnet_style_coalesce_expression = true:suggestion
 dotnet_style_coalesce_expression = true:suggestion
 dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
 dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
-dotnet_prefer_inferred_tuple_names = true:suggestion
-dotnet_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
 dotnet_style_prefer_auto_properties = true:silent
 dotnet_style_prefer_auto_properties = true:silent
 dotnet_style_prefer_conditional_expression_over_assignment = true:silent
 dotnet_style_prefer_conditional_expression_over_assignment = true:silent
 dotnet_style_prefer_conditional_expression_over_return = true:silent
 dotnet_style_prefer_conditional_expression_over_return = true:silent
+
 ###############################
 ###############################
 # Naming Conventions          #
 # Naming Conventions          #
 ###############################
 ###############################
@@ -67,7 +69,7 @@ dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non
 dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
 dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
 
 
 dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
 dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
-dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected
+dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
 dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
 dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
 
 
 dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
 dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
@@ -159,6 +161,7 @@ csharp_style_deconstructed_variable_declaration = true:suggestion
 csharp_prefer_simple_default_expression = true:suggestion
 csharp_prefer_simple_default_expression = true:suggestion
 csharp_style_pattern_local_over_anonymous_function = true:suggestion
 csharp_style_pattern_local_over_anonymous_function = true:suggestion
 csharp_style_inlined_variable_declaration = true:suggestion
 csharp_style_inlined_variable_declaration = true:suggestion
+
 ###############################
 ###############################
 # C# Formatting Rules         #
 # C# Formatting Rules         #
 ###############################
 ###############################
@@ -189,9 +192,3 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false
 # Wrapping preferences
 # Wrapping preferences
 csharp_preserve_single_line_statements = true
 csharp_preserve_single_line_statements = true
 csharp_preserve_single_line_blocks = true
 csharp_preserve_single_line_blocks = true
-###############################
-# VB Coding Conventions       #
-###############################
-[*.vb]
-# Modifier preferences
-visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion

+ 3 - 0
.github/CODEOWNERS

@@ -0,0 +1,3 @@
+# Joshua must review all changes to deployment and build.sh
+deployment/*    @joshuaboniface
+build.sh        @joshuaboniface

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

+ 12 - 7
.gitignore

@@ -244,14 +244,14 @@ pip-log.txt
 #########################
 #########################
 
 
 # Artifacts for debian-x64
 # Artifacts for debian-x64
-deployment/debian-package-x64/pkg-src/.debhelper/
-deployment/debian-package-x64/pkg-src/*.debhelper
-deployment/debian-package-x64/pkg-src/debhelper-build-stamp
-deployment/debian-package-x64/pkg-src/files
-deployment/debian-package-x64/pkg-src/jellyfin.substvars
-deployment/debian-package-x64/pkg-src/jellyfin/
+debian/.debhelper/
+debian/*.debhelper
+debian/debhelper-build-stamp
+debian/files
+debian/jellyfin.substvars
+debian/jellyfin/
 # Don't ignore the debian/bin folder
 # Don't ignore the debian/bin folder
-!deployment/debian-package-x64/pkg-src/bin/
+!debian/bin/
 
 
 deployment/**/dist/
 deployment/**/dist/
 deployment/**/pkg-dist/
 deployment/**/pkg-dist/
@@ -271,3 +271,8 @@ dist
 
 
 # BenchmarkDotNet artifacts
 # BenchmarkDotNet artifacts
 BenchmarkDotNet.Artifacts
 BenchmarkDotNet.Artifacts
+
+# Ignore web artifacts from native builds
+web/
+web-src.*
+MediaBrowser.WebDashboard/jellyfin-web

+ 14 - 0
.vscode/extensions.json

@@ -0,0 +1,14 @@
+{
+	// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
+	// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
+
+	// List of extensions which should be recommended for users of this workspace.
+	"recommendations": [
+        "ms-dotnettools.csharp",
+        "editorconfig.editorconfig"
+	],
+	// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
+	"unwantedRecommendations": [
+
+	]
+}

+ 6 - 6
.vscode/launch.json

@@ -1,9 +1,6 @@
 {
 {
-   // 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)",
             "name": ".NET Core Launch (console)",
             "type": "coreclr",
             "type": "coreclr",
@@ -24,5 +21,8 @@
             "request": "attach",
             "request": "attach",
             "processId": "${command:pickProcess}"
             "processId": "${command:pickProcess}"
         }
         }
-    ,]
+    ],
+    "env": {
+        "DOTNET_CLI_TELEMETRY_OPTOUT": "1"
+    }
 }
 }

+ 17 - 2
.vscode/tasks.json

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

+ 4 - 0
CONTRIBUTORS.md

@@ -7,6 +7,7 @@
  - [anthonylavado](https://github.com/anthonylavado)
  - [anthonylavado](https://github.com/anthonylavado)
  - [Artiume](https://github.com/Artiume)
  - [Artiume](https://github.com/Artiume)
  - [AThomsen](https://github.com/AThomsen)
  - [AThomsen](https://github.com/AThomsen)
+ - [barronpm](https://github.com/barronpm)
  - [bilde2910](https://github.com/bilde2910)
  - [bilde2910](https://github.com/bilde2910)
  - [bfayers](https://github.com/bfayers)
  - [bfayers](https://github.com/bfayers)
  - [BnMcG](https://github.com/BnMcG)
  - [BnMcG](https://github.com/BnMcG)
@@ -22,6 +23,7 @@
  - [cvium](https://github.com/cvium)
  - [cvium](https://github.com/cvium)
  - [dannymichel](https://github.com/dannymichel)
  - [dannymichel](https://github.com/dannymichel)
  - [DaveChild](https://github.com/DaveChild)
  - [DaveChild](https://github.com/DaveChild)
+ - [Delgan](https://github.com/Delgan)
  - [dcrdev](https://github.com/dcrdev)
  - [dcrdev](https://github.com/dcrdev)
  - [dhartung](https://github.com/dhartung)
  - [dhartung](https://github.com/dhartung)
  - [dinki](https://github.com/dinki)
  - [dinki](https://github.com/dinki)
@@ -128,6 +130,8 @@
  - [xosdy](https://github.com/xosdy)
  - [xosdy](https://github.com/xosdy)
  - [XVicarious](https://github.com/XVicarious)
  - [XVicarious](https://github.com/XVicarious)
  - [YouKnowBlom](https://github.com/YouKnowBlom)
  - [YouKnowBlom](https://github.com/YouKnowBlom)
+ - [KristupasSavickas](https://github.com/KristupasSavickas)
+ - [Pusta](https://github.com/pusta)
 
 
 # Emby Contributors
 # Emby Contributors
 
 

+ 11 - 19
Dockerfile

@@ -1,9 +1,8 @@
 ARG DOTNET_VERSION=3.1
 ARG DOTNET_VERSION=3.1
-ARG FFMPEG_VERSION=latest
 
 
 FROM node:alpine as web-builder
 FROM node:alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
 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 - \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
  && cd jellyfin-web-* \
  && yarn install \
  && yarn install \
@@ -17,7 +16,6 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
 # 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:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
 
 
-FROM jellyfin/ffmpeg:${FFMPEG_VERSION} as ffmpeg
 FROM debian:buster-slim
 FROM debian:buster-slim
 
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
@@ -27,32 +25,26 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
 # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
 # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
 ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
 ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
 
 
-COPY --from=ffmpeg /opt/ffmpeg /opt/ffmpeg
 COPY --from=builder /jellyfin /jellyfin
 COPY --from=builder /jellyfin /jellyfin
 COPY --from=web-builder /dist /jellyfin/jellyfin-web
 COPY --from=web-builder /dist /jellyfin/jellyfin-web
 # Install dependencies:
 # Install dependencies:
-#   libfontconfig1: needed for Skia
-#   libgomp1: needed for ffmpeg
-#   libva-drm2: needed for ffmpeg
-#   mesa-va-drivers: needed for VAAPI
+#   mesa-va-drivers: needed for AMD VAAPI
 RUN apt-get update \
 RUN apt-get update \
+ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \
+ && wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
+ && echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
+ && apt-get update \
  && apt-get install --no-install-recommends --no-install-suggests -y \
  && apt-get install --no-install-recommends --no-install-suggests -y \
-   libfontconfig1 \
-   libgomp1 \
-   libva-drm2 \
    mesa-va-drivers \
    mesa-va-drivers \
+   jellyfin-ffmpeg \
    openssl \
    openssl \
-   ca-certificates \
-   vainfo \
-   i965-va-driver \
    locales \
    locales \
- && apt-get clean autoclean -y\
- && apt-get autoremove -y\
+ && apt-get remove gnupg wget apt-transport-https -y \
+ && apt-get clean autoclean -y \
+ && apt-get autoremove -y \
  && rm -rf /var/lib/apt/lists/* \
  && rm -rf /var/lib/apt/lists/* \
  && mkdir -p /cache /config /media \
  && mkdir -p /cache /config /media \
  && chmod 777 /cache /config /media \
  && chmod 777 /cache /config /media \
- && ln -s /opt/ffmpeg/bin/ffmpeg /usr/local/bin \
- && ln -s /opt/ffmpeg/bin/ffprobe /usr/local/bin \
  && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
  && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
 
 
 ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
 ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
@@ -65,4 +57,4 @@ VOLUME /cache /config /media
 ENTRYPOINT ["./jellyfin/jellyfin", \
 ENTRYPOINT ["./jellyfin/jellyfin", \
     "--datadir", "/config", \
     "--datadir", "/config", \
     "--cachedir", "/cache", \
     "--cachedir", "/cache", \
-    "--ffmpeg", "/usr/local/bin/ffmpeg"]
+    "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]

+ 2 - 2
Dockerfile.arm

@@ -38,7 +38,7 @@ COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
 RUN apt-get update \
 RUN apt-get update \
  && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
  && 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 -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 [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 && \
  echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \
  apt-get update && \
  apt-get update && \
@@ -74,4 +74,4 @@ VOLUME /cache /config /media
 ENTRYPOINT ["./jellyfin/jellyfin", \
 ENTRYPOINT ["./jellyfin/jellyfin", \
     "--datadir", "/config", \
     "--datadir", "/config", \
     "--cachedir", "/cache", \
     "--cachedir", "/cache", \
-    "--ffmpeg", "/usr/lib/jellyfin-ffmpeg"]
+    "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]

+ 5 - 10
DvdLib/BigEndianBinaryReader.cs

@@ -1,4 +1,6 @@
-using System;
+#pragma warning disable CS1591
+
+using System.Buffers.Binary;
 using System.IO;
 using System.IO;
 
 
 namespace DvdLib
 namespace DvdLib
@@ -12,19 +14,12 @@ namespace DvdLib
 
 
         public override ushort ReadUInt16()
         public override ushort ReadUInt16()
         {
         {
-            return BitConverter.ToUInt16(ReadAndReverseBytes(2), 0);
+            return BinaryPrimitives.ReadUInt16BigEndian(base.ReadBytes(2));
         }
         }
 
 
         public override uint ReadUInt32()
         public override uint ReadUInt32()
         {
         {
-            return BitConverter.ToUInt32(ReadAndReverseBytes(4), 0);
-        }
-
-        private byte[] ReadAndReverseBytes(int count)
-        {
-            byte[] val = base.ReadBytes(count);
-            Array.Reverse(val, 0, count);
-            return val;
+            return BinaryPrimitives.ReadUInt32BigEndian(base.ReadBytes(4));
         }
         }
     }
     }
 }
 }

+ 6 - 4
DvdLib/DvdLib.csproj

@@ -1,17 +1,19 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
 
 
-  <ItemGroup>
-    <Compile Include="..\SharedVersion.cs" />
-  </ItemGroup>
+  <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
+  <PropertyGroup>
+    <ProjectGuid>{713F42B5-878E-499D-A878-E4C652B1D5E8}</ProjectGuid>
+  </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
+    <Compile Include="..\SharedVersion.cs" />
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
     <TargetFramework>netstandard2.1</TargetFramework>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
   </PropertyGroup>
   </PropertyGroup>
 
 
 </Project>
 </Project>

+ 3 - 0
DvdLib/Ifo/Cell.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.IO;
 using System.IO;
 
 
 namespace DvdLib.Ifo
 namespace DvdLib.Ifo
@@ -5,6 +7,7 @@ namespace DvdLib.Ifo
     public class Cell
     public class Cell
     {
     {
         public CellPlaybackInfo PlaybackInfo { get; private set; }
         public CellPlaybackInfo PlaybackInfo { get; private set; }
+
         public CellPositionInfo PositionInfo { get; private set; }
         public CellPositionInfo PositionInfo { get; private set; }
 
 
         internal void ParsePlayback(BinaryReader br)
         internal void ParsePlayback(BinaryReader br)

+ 2 - 0
DvdLib/Ifo/CellPlaybackInfo.cs

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

+ 2 - 0
DvdLib/Ifo/CellPositionInfo.cs

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

+ 4 - 0
DvdLib/Ifo/Chapter.cs

@@ -1,9 +1,13 @@
+#pragma warning disable CS1591
+
 namespace DvdLib.Ifo
 namespace DvdLib.Ifo
 {
 {
     public class Chapter
     public class Chapter
     {
     {
         public ushort ProgramChainNumber { get; private set; }
         public ushort ProgramChainNumber { get; private set; }
+
         public ushort ProgramNumber { get; private set; }
         public ushort ProgramNumber { get; private set; }
+
         public uint ChapterNumber { get; private set; }
         public uint ChapterNumber { get; private set; }
 
 
         public Chapter(ushort pgcNum, ushort programNum, uint chapterNum)
         public Chapter(ushort pgcNum, ushort programNum, uint chapterNum)

+ 18 - 10
DvdLib/Ifo/Dvd.cs

@@ -1,8 +1,9 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
-using MediaBrowser.Model.IO;
 
 
 namespace DvdLib.Ifo
 namespace DvdLib.Ifo
 {
 {
@@ -13,13 +14,10 @@ namespace DvdLib.Ifo
 
 
         private ushort _titleCount;
         private ushort _titleCount;
         public readonly Dictionary<ushort, string> VTSPaths = new Dictionary<ushort, string>();
         public readonly Dictionary<ushort, string> VTSPaths = new Dictionary<ushort, string>();
-        private readonly IFileSystem _fileSystem;
-
-        public Dvd(string path, IFileSystem fileSystem)
+        public Dvd(string path)
         {
         {
-            _fileSystem = fileSystem;
             Titles = new List<Title>();
             Titles = new List<Title>();
-            var allFiles = _fileSystem.GetFiles(path, true).ToList();
+            var allFiles = new DirectoryInfo(path).GetFiles(path, SearchOption.AllDirectories);
 
 
             var vmgPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.IFO", StringComparison.OrdinalIgnoreCase)) ??
             var vmgPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.IFO", StringComparison.OrdinalIgnoreCase)) ??
                 allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.BUP", StringComparison.OrdinalIgnoreCase));
                 allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.BUP", StringComparison.OrdinalIgnoreCase));
@@ -76,7 +74,7 @@ namespace DvdLib.Ifo
             }
             }
         }
         }
 
 
-        private void ReadVTS(ushort vtsNum, IEnumerable<FileSystemMetadata> allFiles)
+        private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
         {
         {
             var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
             var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
 
 
@@ -119,12 +117,19 @@ namespace DvdLib.Ifo
                         uint chapNum = 1;
                         uint chapNum = 1;
                         vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin);
                         vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin);
                         var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1));
                         var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1));
-                        if (t == null) continue;
+                        if (t == null)
+                        {
+                            continue;
+                        }
 
 
                         do
                         do
                         {
                         {
                             t.Chapters.Add(new Chapter(vtsRead.ReadUInt16(), vtsRead.ReadUInt16(), chapNum));
                             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++;
                             chapNum++;
                         }
                         }
                         while (vtsFs.Position < (baseAddr + endaddr));
                         while (vtsFs.Position < (baseAddr + endaddr));
@@ -149,7 +154,10 @@ namespace DvdLib.Ifo
                         uint vtsPgcOffset = vtsRead.ReadUInt32();
                         uint vtsPgcOffset = vtsRead.ReadUInt32();
 
 
                         var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum));
                         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);
+                        }
                     }
                     }
                 }
                 }
             }
             }

+ 10 - 2
DvdLib/Ifo/DvdTime.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 
 
 namespace DvdLib.Ifo
 namespace DvdLib.Ifo
@@ -13,8 +15,14 @@ namespace DvdLib.Ifo
             Second = GetBCDValue(data[2]);
             Second = GetBCDValue(data[2]);
             Frames = GetBCDValue((byte)(data[3] & 0x3F));
             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)
         private static byte GetBCDValue(byte data)

+ 3 - 1
DvdLib/Ifo/Program.cs

@@ -1,10 +1,12 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using System.Collections.Generic;
 
 
 namespace DvdLib.Ifo
 namespace DvdLib.Ifo
 {
 {
     public class Program
     public class Program
     {
     {
-        public readonly List<Cell> Cells;
+        public IReadOnlyList<Cell> Cells { get; }
 
 
         public Program(List<Cell> cells)
         public Program(List<Cell> cells)
         {
         {

+ 15 - 2
DvdLib/Ifo/ProgramChain.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
@@ -20,7 +22,9 @@ namespace DvdLib.Ifo
         public readonly List<Cell> Cells;
         public readonly List<Cell> Cells;
 
 
         public DvdTime PlaybackTime { get; private set; }
         public DvdTime PlaybackTime { get; private set; }
+
         public UserOperation ProhibitedUserOperations { get; private set; }
         public UserOperation ProhibitedUserOperations { get; private set; }
+
         public byte[] AudioStreamControl { get; private set; } // 8*2 entries
         public byte[] AudioStreamControl { get; private set; } // 8*2 entries
         public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries
         public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries
 
 
@@ -31,9 +35,11 @@ namespace DvdLib.Ifo
         private ushort _goupProgramNumber;
         private ushort _goupProgramNumber;
 
 
         public ProgramPlaybackMode PlaybackMode { get; private set; }
         public ProgramPlaybackMode PlaybackMode { get; private set; }
+
         public uint ProgramCount { get; private set; }
         public uint ProgramCount { get; private set; }
 
 
         public byte StillTime { get; private set; }
         public byte StillTime { get; private set; }
+
         public byte[] Palette { get; private set; } // 16*4 entries
         public byte[] Palette { get; private set; } // 16*4 entries
 
 
         private ushort _commandTableOffset;
         private ushort _commandTableOffset;
@@ -69,8 +75,15 @@ namespace DvdLib.Ifo
 
 
             StillTime = br.ReadByte();
             StillTime = br.ReadByte();
             byte pbMode = 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);
             ProgramCount = (uint)(pbMode & 0x7F);
 
 
             Palette = br.ReadBytes(64);
             Palette = br.ReadBytes(64);

+ 10 - 1
DvdLib/Ifo/Title.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 
 
@@ -6,8 +8,11 @@ namespace DvdLib.Ifo
     public class Title
     public class Title
     {
     {
         public uint TitleNumber { get; private set; }
         public uint TitleNumber { get; private set; }
+
         public uint AngleCount { get; private set; }
         public uint AngleCount { get; private set; }
+
         public ushort ChapterCount { get; private set; }
         public ushort ChapterCount { get; private set; }
+
         public byte VideoTitleSetNumber { get; private set; }
         public byte VideoTitleSetNumber { get; private set; }
 
 
         private ushort _parentalManagementMask;
         private ushort _parentalManagementMask;
@@ -15,6 +20,7 @@ namespace DvdLib.Ifo
         private uint _vtsStartSector; // relative to start of entire disk
         private uint _vtsStartSector; // relative to start of entire disk
 
 
         public ProgramChain EntryProgramChain { get; private set; }
         public ProgramChain EntryProgramChain { get; private set; }
+
         public readonly List<ProgramChain> ProgramChains;
         public readonly List<ProgramChain> ProgramChains;
 
 
         public readonly List<Chapter> Chapters;
         public readonly List<Chapter> Chapters;
@@ -53,7 +59,10 @@ namespace DvdLib.Ifo
             var pgc = new ProgramChain(pgcNum);
             var pgc = new ProgramChain(pgcNum);
             pgc.ParseHeader(br);
             pgc.ParseHeader(br);
             ProgramChains.Add(pgc);
             ProgramChains.Add(pgc);
-            if (entryPgc) EntryProgramChain = pgc;
+            if (entryPgc)
+            {
+                EntryProgramChain = pgc;
+            }
 
 
             br.BaseStream.Seek(curPos, SeekOrigin.Begin);
             br.BaseStream.Seek(curPos, SeekOrigin.Begin);
         }
         }

+ 2 - 0
DvdLib/Ifo/UserOperation.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 
 
 namespace DvdLib.Ifo
 namespace DvdLib.Ifo

+ 5 - 2
Emby.Dlna/Api/DlnaServerService.cs

@@ -11,6 +11,7 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Services;
 using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
 
 
 namespace Emby.Dlna.Api
 namespace Emby.Dlna.Api
 {
 {
@@ -108,7 +109,7 @@ namespace Emby.Dlna.Api
         public string Filename { get; set; }
         public string Filename { get; set; }
     }
     }
 
 
-    public class DlnaServerService : IService, IRequiresRequest
+    public class DlnaServerService : IService
     {
     {
         private const string XMLContentType = "text/xml; charset=UTF-8";
         private const string XMLContentType = "text/xml; charset=UTF-8";
 
 
@@ -127,11 +128,13 @@ namespace Emby.Dlna.Api
         public DlnaServerService(
         public DlnaServerService(
             IDlnaManager dlnaManager,
             IDlnaManager dlnaManager,
             IHttpResultFactory httpResultFactory,
             IHttpResultFactory httpResultFactory,
-            IServerConfigurationManager configurationManager)
+            IServerConfigurationManager configurationManager,
+            IHttpContextAccessor httpContextAccessor)
         {
         {
             _dlnaManager = dlnaManager;
             _dlnaManager = dlnaManager;
             _resultFactory = httpResultFactory;
             _resultFactory = httpResultFactory;
             _configurationManager = configurationManager;
             _configurationManager = configurationManager;
+            Request = httpContextAccessor?.HttpContext.GetServiceStackRequest() ?? throw new ArgumentNullException(nameof(httpContextAccessor));
         }
         }
 
 
         private string GetHeader(string name)
         private string GetHeader(string name)

+ 1 - 0
Emby.Dlna/ConfigurationExtension.cs

@@ -1,3 +1,4 @@
+#nullable enable
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 7 - 9
Emby.Dlna/ContentDirectory/ContentDirectory.cs

@@ -1,13 +1,15 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
+using System.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Dlna.Service;
 using Emby.Dlna.Service;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.TV;
 using MediaBrowser.Controller.TV;
@@ -31,7 +33,8 @@ namespace Emby.Dlna.ContentDirectory
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly ITVSeriesManager _tvSeriesManager;
         private readonly ITVSeriesManager _tvSeriesManager;
 
 
-        public ContentDirectory(IDlnaManager dlna,
+        public ContentDirectory(
+            IDlnaManager dlna,
             IUserDataManager userDataManager,
             IUserDataManager userDataManager,
             IImageProcessor imageProcessor,
             IImageProcessor imageProcessor,
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
@@ -130,18 +133,13 @@ namespace Emby.Dlna.ContentDirectory
 
 
             foreach (var user in _userManager.Users)
             foreach (var user in _userManager.Users)
             {
             {
-                if (user.Policy.IsAdministrator)
+                if (user.HasPermission(PermissionKind.IsAdministrator))
                 {
                 {
                     return user;
                     return user;
                 }
                 }
             }
             }
 
 
-            foreach (var user in _userManager.Users)
-            {
-                return user;
-            }
-
-            return null;
+            return _userManager.Users.FirstOrDefault();
         }
         }
     }
     }
 }
 }

+ 24 - 16
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -10,6 +10,7 @@ using System.Threading;
 using System.Xml;
 using System.Xml;
 using Emby.Dlna.Didl;
 using Emby.Dlna.Didl;
 using Emby.Dlna.Service;
 using Emby.Dlna.Service;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
@@ -17,7 +18,6 @@ using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
@@ -28,6 +28,12 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using Microsoft.Extensions.Logging;
 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
 namespace Emby.Dlna.ContentDirectory
 {
 {
@@ -460,12 +466,12 @@ namespace Emby.Dlna.ContentDirectory
             }
             }
             else if (search.SearchType == SearchType.Playlist)
             else if (search.SearchType == SearchType.Playlist)
             {
             {
-                //items = items.OfType<Playlist>();
+                // items = items.OfType<Playlist>();
                 isFolder = true;
                 isFolder = true;
             }
             }
             else if (search.SearchType == SearchType.MusicAlbum)
             else if (search.SearchType == SearchType.MusicAlbum)
             {
             {
-                //items = items.OfType<MusicAlbum>();
+                // items = items.OfType<MusicAlbum>();
                 isFolder = true;
                 isFolder = true;
             }
             }
 
 
@@ -731,7 +737,7 @@ namespace Emby.Dlna.ContentDirectory
                 return GetGenres(item, user, query);
                 return GetGenres(item, user, query);
             }
             }
 
 
-            var array = new ServerItem[]
+            var array = new[]
             {
             {
                 new ServerItem(item)
                 new ServerItem(item)
                 {
                 {
@@ -920,7 +926,7 @@ namespace Emby.Dlna.ContentDirectory
         private QueryResult<ServerItem> GetMovieCollections(User user, InternalItemsQuery query)
         private QueryResult<ServerItem> GetMovieCollections(User user, InternalItemsQuery query)
         {
         {
             query.Recursive = true;
             query.Recursive = true;
-            //query.Parent = parent;
+            // query.Parent = parent;
             query.SetUser(user);
             query.SetUser(user);
 
 
             query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
             query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
@@ -1115,7 +1121,7 @@ namespace Emby.Dlna.ContentDirectory
         private QueryResult<ServerItem> GetMusicPlaylists(User user, InternalItemsQuery query)
         private QueryResult<ServerItem> GetMusicPlaylists(User user, InternalItemsQuery query)
         {
         {
             query.Parent = null;
             query.Parent = null;
-            query.IncludeItemTypes = new[] { typeof(Playlist).Name };
+            query.IncludeItemTypes = new[] { nameof(Playlist) };
             query.SetUser(user);
             query.SetUser(user);
             query.Recursive = true;
             query.Recursive = true;
 
 
@@ -1132,10 +1138,9 @@ namespace Emby.Dlna.ContentDirectory
             {
             {
                 UserId = user.Id,
                 UserId = user.Id,
                 Limit = 50,
                 Limit = 50,
-                IncludeItemTypes = new[] { typeof(Audio).Name },
-                ParentId = parent == null ? Guid.Empty : parent.Id,
+                IncludeItemTypes = new[] { nameof(Audio) },
+                ParentId = parent?.Id ?? Guid.Empty,
                 GroupItems = true
                 GroupItems = true
-
             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
 
 
             return ToResult(items);
             return ToResult(items);
@@ -1150,7 +1155,6 @@ namespace Emby.Dlna.ContentDirectory
                 Limit = query.Limit,
                 Limit = query.Limit,
                 StartIndex = query.StartIndex,
                 StartIndex = query.StartIndex,
                 UserId = query.User.Id
                 UserId = query.User.Id
-
             }, new[] { parent }, query.DtoOptions);
             }, new[] { parent }, query.DtoOptions);
 
 
             return ToResult(result);
             return ToResult(result);
@@ -1167,7 +1171,6 @@ namespace Emby.Dlna.ContentDirectory
                 IncludeItemTypes = new[] { typeof(Episode).Name },
                 IncludeItemTypes = new[] { typeof(Episode).Name },
                 ParentId = parent == null ? Guid.Empty : parent.Id,
                 ParentId = parent == null ? Guid.Empty : parent.Id,
                 GroupItems = false
                 GroupItems = false
-
             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
 
 
             return ToResult(items);
             return ToResult(items);
@@ -1177,14 +1180,14 @@ namespace Emby.Dlna.ContentDirectory
         {
         {
             query.OrderBy = Array.Empty<(string, SortOrder)>();
             query.OrderBy = Array.Empty<(string, SortOrder)>();
 
 
-            var items = _userViewManager.GetLatestItems(new LatestItemsQuery
+            var items = _userViewManager.GetLatestItems(
+                new LatestItemsQuery
             {
             {
                 UserId = user.Id,
                 UserId = user.Id,
                 Limit = 50,
                 Limit = 50,
-                IncludeItemTypes = new[] { typeof(Movie).Name },
-                ParentId = parent == null ? Guid.Empty : parent.Id,
+                IncludeItemTypes = new[] { nameof(Movie) },
+                ParentId = parent?.Id ?? Guid.Empty,
                 GroupItems = true
                 GroupItems = true
-
             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
 
 
             return ToResult(items);
             return ToResult(items);
@@ -1217,7 +1220,11 @@ namespace Emby.Dlna.ContentDirectory
                 Recursive = true,
                 Recursive = true,
                 ParentId = parentId,
                 ParentId = parentId,
                 GenreIds = new[] { item.Id },
                 GenreIds = new[] { item.Id },
-                IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Series).Name },
+                IncludeItemTypes = new[]
+                {
+                    nameof(Movie),
+                    nameof(Series)
+                },
                 Limit = limit,
                 Limit = limit,
                 StartIndex = startIndex,
                 StartIndex = startIndex,
                 DtoOptions = GetDtoOptions()
                 DtoOptions = GetDtoOptions()
@@ -1350,6 +1357,7 @@ namespace Emby.Dlna.ContentDirectory
     internal class ServerItem
     internal class ServerItem
     {
     {
         public BaseItem Item { get; set; }
         public BaseItem Item { get; set; }
+
         public StubType? StubType { get; set; }
         public StubType? StubType { get; set; }
 
 
         public ServerItem(BaseItem item)
         public ServerItem(BaseItem item)

+ 138 - 51
Emby.Dlna/Didl/DidlBuilder.cs

@@ -6,14 +6,13 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Text;
 using System.Text;
 using System.Xml;
 using System.Xml;
-using Emby.Dlna.Configuration;
 using Emby.Dlna.ContentDirectory;
 using Emby.Dlna.ContentDirectory;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Playlists;
@@ -23,6 +22,13 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using Microsoft.Extensions.Logging;
 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
 namespace Emby.Dlna.Didl
 {
 {
@@ -92,21 +98,21 @@ namespace Emby.Dlna.Didl
             {
             {
                 using (var writer = XmlWriter.Create(builder, settings))
                 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", NS_DIDL);
 
 
                     writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
                     writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
                     writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
                     writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
                     writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
                     writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
-                    //didl.SetAttribute("xmlns:sec", NS_SEC);
+                    // didl.SetAttribute("xmlns:sec", NS_SEC);
 
 
                     WriteXmlRootAttributes(_profile, writer);
                     WriteXmlRootAttributes(_profile, writer);
 
 
                     WriteItemElement(writer, item, user, context, null, deviceId, filter, streamInfo);
                     WriteItemElement(writer, item, user, context, null, deviceId, filter, streamInfo);
 
 
                     writer.WriteFullEndElement();
                     writer.WriteFullEndElement();
-                    //writer.WriteEndDocument();
+                    // writer.WriteEndDocument();
                 }
                 }
 
 
                 return builder.ToString();
                 return builder.ToString();
@@ -421,61 +427,102 @@ namespace Emby.Dlna.Didl
                     case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows");
                     case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows");
                     case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes");
                     case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes");
                     case StubType.Series: return _localization.GetLocalizedString("Shows");
                     case StubType.Series: return _localization.GetLocalizedString("Shows");
-                    default: break;
                 }
                 }
             }
             }
 
 
-            if (item is Episode episode && context is Season season)
+            return item is Episode episode
+                ? GetEpisodeDisplayName(episode, context)
+                : item.Name;
+        }
+
+        /// <summary>
+        /// Gets episode display name appropriate for the given context.
+        /// </summary>
+        /// <remarks>
+        /// If context is a season, this will return a string containing just episode number and name.
+        /// Otherwise the result will include series nams and season number.
+        /// </remarks>
+        /// <param name="episode">The episode.</param>
+        /// <param name="context">Current context.</param>
+        /// <returns>Formatted name of the episode.</returns>
+        private string GetEpisodeDisplayName(Episode episode, BaseItem context)
+        {
+            string[] components;
+
+            if (context is Season season)
             {
             {
                 // This is a special embedded within a season
                 // This is a special embedded within a season
-                if (item.ParentIndexNumber.HasValue && item.ParentIndexNumber.Value == 0
+                if (episode.ParentIndexNumber.HasValue && episode.ParentIndexNumber.Value == 0
                     && season.IndexNumber.HasValue && season.IndexNumber.Value != 0)
                     && season.IndexNumber.HasValue && season.IndexNumber.Value != 0)
                 {
                 {
                     return string.Format(
                     return string.Format(
                         CultureInfo.InvariantCulture,
                         CultureInfo.InvariantCulture,
                         _localization.GetLocalizedString("ValueSpecialEpisodeName"),
                         _localization.GetLocalizedString("ValueSpecialEpisodeName"),
-                        item.Name);
+                        episode.Name);
                 }
                 }
 
 
-                if (item.IndexNumber.HasValue)
-                {
-                    var number = item.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
+                // inside a season use simple format (ex. '12 - Episode Name')
+                var epNumberName = GetEpisodeIndexFullName(episode);
+                components = new[] { epNumberName, episode.Name };
+            }
+            else
+            {
+                // outside a season include series and season details (ex. 'TV Show - S05E11 - Episode Name')
+                var epNumberName = GetEpisodeNumberDisplayName(episode);
+                components = new[] { episode.SeriesName, epNumberName, episode.Name };
+            }
 
 
-                    if (episode.IndexNumberEnd.HasValue)
-                    {
-                        number += "-" + episode.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture);
-                    }
+            return string.Join(" - ", components.Where(NotNullOrWhiteSpace));
+        }
 
 
-                    return number + " - " + item.Name;
-                }
-            }
-            else if (item is Episode ep)
+        /// <summary>
+        /// Gets complete episode number.
+        /// </summary>
+        /// <param name="episode">The episode.</param>
+        /// <returns>For single episodes returns just the number. For double episodes - current and ending numbers.</returns>
+        private string GetEpisodeIndexFullName(Episode episode)
+        {
+            var name = string.Empty;
+            if (episode.IndexNumber.HasValue)
             {
             {
-                var parent = ep.GetParent();
-                var name = parent.Name + " - ";
+                name += episode.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
 
 
-                if (ep.ParentIndexNumber.HasValue)
-                {
-                    name += "S" + ep.ParentIndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
-                }
-                else if (!item.IndexNumber.HasValue)
+                if (episode.IndexNumberEnd.HasValue)
                 {
                 {
-                    return name + " - " + item.Name;
+                    name += "-" + episode.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture);
                 }
                 }
+            }
 
 
-                name += "E" + ep.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
-                if (ep.IndexNumberEnd.HasValue)
-                {
-                    name += "-" + ep.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture);
-                }
+            return name;
+        }
+
+        /// <summary>
+        /// Gets episode number formatted as 'S##E##'.
+        /// </summary>
+        /// <param name="episode">The episode.</param>
+        /// <returns>Formatted episode number.</returns>
+        private string GetEpisodeNumberDisplayName(Episode episode)
+        {
+            var name = string.Empty;
+            var seasonNumber = episode.Season?.IndexNumber;
 
 
-                name += " - " + item.Name;
-                return name;
+            if (seasonNumber.HasValue)
+            {
+                name = "S" + seasonNumber.Value.ToString("00", CultureInfo.InvariantCulture);
             }
             }
 
 
-            return item.Name;
+            var indexName = GetEpisodeIndexFullName(episode);
+
+            if (!string.IsNullOrWhiteSpace(indexName))
+            {
+                name += "E" + indexName;
+            }
+
+            return name;
         }
         }
 
 
+        private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s);
+
         private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
         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", NS_DIDL);
@@ -628,7 +675,7 @@ namespace Emby.Dlna.Didl
                 return;
                 return;
             }
             }
 
 
-            MediaBrowser.Model.Dlna.XmlAttribute secAttribute = null;
+            XmlAttribute secAttribute = null;
             foreach (var attribute in _profile.XmlRootAttributes)
             foreach (var attribute in _profile.XmlRootAttributes)
             {
             {
                 if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
                 if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
@@ -658,13 +705,13 @@ namespace Emby.Dlna.Didl
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Adds fields used by both items and folders
+        /// Adds fields used by both items and folders.
         /// </summary>
         /// </summary>
         private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
         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
             // 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
             // 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), NS_DC);
             }
             }
@@ -703,7 +750,7 @@ namespace Emby.Dlna.Didl
                         AddValue(writer, "dc", "description", desc, NS_DC);
                         AddValue(writer, "dc", "description", desc, NS_DC);
                     }
                     }
                 }
                 }
-                //if (filter.Contains("upnp:longDescription"))
+                // if (filter.Contains("upnp:longDescription"))
                 //{
                 //{
                 //    if (!string.IsNullOrWhiteSpace(item.Overview))
                 //    if (!string.IsNullOrWhiteSpace(item.Overview))
                 //    {
                 //    {
@@ -718,6 +765,7 @@ namespace Emby.Dlna.Didl
                 {
                 {
                     AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC);
                     AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC);
                 }
                 }
+
                 if (filter.Contains("upnp:rating"))
                 if (filter.Contains("upnp:rating"))
                 {
                 {
                     AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP);
                     AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP);
@@ -953,7 +1001,6 @@ namespace Emby.Dlna.Didl
             }
             }
 
 
             AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
             AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
-
         }
         }
 
 
         private void AddImageResElement(
         private void AddImageResElement(
@@ -1006,10 +1053,12 @@ namespace Emby.Dlna.Didl
             {
             {
                 return GetImageInfo(item, ImageType.Primary);
                 return GetImageInfo(item, ImageType.Primary);
             }
             }
+
             if (item.HasImage(ImageType.Thumb))
             if (item.HasImage(ImageType.Thumb))
             {
             {
                 return GetImageInfo(item, ImageType.Thumb);
                 return GetImageInfo(item, ImageType.Thumb);
             }
             }
+
             if (item.HasImage(ImageType.Backdrop))
             if (item.HasImage(ImageType.Backdrop))
             {
             {
                 if (item is Channel)
                 if (item is Channel)
@@ -1018,19 +1067,58 @@ namespace Emby.Dlna.Didl
                 }
                 }
             }
             }
 
 
-            item = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary));
+            // For audio tracks without art use album art if available.
+            if (item is Audio audioItem)
+            {
+                var album = audioItem.AlbumEntity;
+                return album != null && album.HasImage(ImageType.Primary)
+                    ? GetImageInfo(album, ImageType.Primary)
+                    : null;
+            }
+
+            // Don't look beyond album/playlist level. Metadata service may assign an image from a different album/show to the parent folder.
+            if (item is MusicAlbum || item is Playlist)
+            {
+                return null;
+            }
 
 
-            if (item != null)
+            // For other item types check parents, but be aware that image retrieved from a parent may be not suitable for this media item.
+            var parentWithImage = GetFirstParentWithImageBelowUserRoot(item);
+            if (parentWithImage != null)
             {
             {
-                if (item.HasImage(ImageType.Primary))
-                {
-                    return GetImageInfo(item, ImageType.Primary);
-                }
+                return GetImageInfo(parentWithImage, ImageType.Primary);
             }
             }
 
 
             return null;
             return null;
         }
         }
 
 
+        private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item)
+        {
+            if (item == null)
+            {
+                return null;
+            }
+
+            if (item.HasImage(ImageType.Primary))
+            {
+                return item;
+            }
+
+            var parent = item.GetParent();
+            if (parent is UserRootFolder)
+            {
+                return null;
+            }
+
+            // terminate in case we went past user root folder (unlikely?)
+            if (parent is Folder folder && folder.IsRoot)
+            {
+                return null;
+            }
+
+            return GetFirstParentWithImageBelowUserRoot(parent);
+        }
+
         private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
         private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
         {
         {
             var imageInfo = item.GetImageInfo(type, 0);
             var imageInfo = item.GetImageInfo(type, 0);
@@ -1050,25 +1138,24 @@ namespace Emby.Dlna.Didl
 
 
             if (width == 0 || height == 0)
             if (width == 0 || height == 0)
             {
             {
-                //_imageProcessor.GetImageSize(item, imageInfo);
+                // _imageProcessor.GetImageSize(item, imageInfo);
                 width = null;
                 width = null;
                 height = null;
                 height = null;
             }
             }
-
             else if (width == -1 || height == -1)
             else if (width == -1 || height == -1)
             {
             {
                 width = null;
                 width = null;
                 height = null;
                 height = null;
             }
             }
 
 
-            //try
+            // try
             //{
             //{
             //    var size = _imageProcessor.GetImageSize(imageInfo);
             //    var size = _imageProcessor.GetImageSize(imageInfo);
 
 
             //    width = size.Width;
             //    width = size.Width;
             //    height = size.Height;
             //    height = size.Height;
             //}
             //}
-            //catch
+            // catch
             //{
             //{
 
 
             //}
             //}

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

@@ -12,7 +12,6 @@ namespace Emby.Dlna.Didl
         public Filter()
         public Filter()
             : this("*")
             : this("*")
         {
         {
-
         }
         }
 
 
         public Filter(string filter)
         public Filter(string filter)
@@ -26,7 +25,7 @@ namespace Emby.Dlna.Didl
         {
         {
             // 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.
             // 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 true;
-            //return _all || ListHelper.ContainsIgnoreCase(_fields, field);
+            // return _all || ListHelper.ContainsIgnoreCase(_fields, field);
         }
         }
     }
     }
 }
 }

+ 28 - 8
Emby.Dlna/DlnaManager.cs

@@ -31,7 +31,7 @@ namespace Emby.Dlna
         private readonly IApplicationPaths _appPaths;
         private readonly IApplicationPaths _appPaths;
         private readonly IXmlSerializer _xmlSerializer;
         private readonly IXmlSerializer _xmlSerializer;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
-        private readonly ILogger _logger;
+        private readonly ILogger<DlnaManager> _logger;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
         private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
         private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
@@ -49,7 +49,7 @@ namespace Emby.Dlna
             _xmlSerializer = xmlSerializer;
             _xmlSerializer = xmlSerializer;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _appPaths = appPaths;
             _appPaths = appPaths;
-            _logger = loggerFactory.CreateLogger("Dlna");
+            _logger = loggerFactory.CreateLogger<DlnaManager>();
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
             _appHost = appHost;
             _appHost = appHost;
         }
         }
@@ -88,7 +88,6 @@ namespace Emby.Dlna
                     .Select(i => i.Item2)
                     .Select(i => i.Item2)
                     .ToList();
                     .ToList();
             }
             }
-
         }
         }
 
 
         public DeviceProfile GetDefaultProfile()
         public DeviceProfile GetDefaultProfile()
@@ -141,55 +140,73 @@ namespace Emby.Dlna
             if (!string.IsNullOrEmpty(profileInfo.DeviceDescription))
             if (!string.IsNullOrEmpty(profileInfo.DeviceDescription))
             {
             {
                 if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
                 if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
+                {
                     return false;
                     return false;
+                }
             }
             }
 
 
             if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
             if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
             {
             {
                 if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
                 if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
+                {
                     return false;
                     return false;
+                }
             }
             }
 
 
             if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
             if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
             {
             {
                 if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
                 if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
+                {
                     return false;
                     return false;
+                }
             }
             }
 
 
             if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
             if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
             {
             {
                 if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
                 if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
+                {
                     return false;
                     return false;
+                }
             }
             }
 
 
             if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
             if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
             {
             {
                 if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
                 if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
+                {
                     return false;
                     return false;
+                }
             }
             }
 
 
             if (!string.IsNullOrEmpty(profileInfo.ModelName))
             if (!string.IsNullOrEmpty(profileInfo.ModelName))
             {
             {
                 if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName))
                 if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName))
+                {
                     return false;
                     return false;
+                }
             }
             }
 
 
             if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
             if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
             {
             {
                 if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
                 if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
+                {
                     return false;
                     return false;
+                }
             }
             }
 
 
             if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
             if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
             {
             {
                 if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
                 if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
+                {
                     return false;
                     return false;
+                }
             }
             }
 
 
             if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
             if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
             {
             {
                 if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
                 if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
+                {
                     return false;
                     return false;
+                }
             }
             }
 
 
             return true;
             return true;
@@ -251,7 +268,7 @@ namespace Emby.Dlna
                         return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
                         return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
                     case HeaderMatchType.Substring:
                     case HeaderMatchType.Substring:
                         var isMatch = value.ToString().IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1;
                         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;
                         return isMatch;
                     case HeaderMatchType.Regex:
                     case HeaderMatchType.Regex:
                         return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase);
                         return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase);
@@ -439,6 +456,7 @@ namespace Emby.Dlna
             {
             {
                 throw new ArgumentException("Profile is missing Id");
                 throw new ArgumentException("Profile is missing Id");
             }
             }
+
             if (string.IsNullOrEmpty(profile.Name))
             if (string.IsNullOrEmpty(profile.Name))
             {
             {
                 throw new ArgumentException("Profile is missing Name");
                 throw new ArgumentException("Profile is missing Name");
@@ -464,6 +482,7 @@ namespace Emby.Dlna
             {
             {
                 _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
                 _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
             }
             }
+
             SerializeToXml(profile, path);
             SerializeToXml(profile, path);
         }
         }
 
 
@@ -474,7 +493,7 @@ namespace Emby.Dlna
 
 
         /// <summary>
         /// <summary>
         /// Recreates the object using serialization, to ensure it's not a subclass.
         /// 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>
         /// </summary>
         /// <param name="profile"></param>
         /// <param name="profile"></param>
         /// <returns></returns>
         /// <returns></returns>
@@ -493,6 +512,7 @@ namespace Emby.Dlna
         class InternalProfileInfo
         class InternalProfileInfo
         {
         {
             internal DeviceProfileInfo Info { get; set; }
             internal DeviceProfileInfo Info { get; set; }
+
             internal string Path { get; set; }
             internal string Path { get; set; }
         }
         }
 
 
@@ -566,9 +586,9 @@ namespace Emby.Dlna
                 new Foobar2000Profile(),
                 new Foobar2000Profile(),
                 new SharpSmartTvProfile(),
                 new SharpSmartTvProfile(),
                 new MediaMonkeyProfile(),
                 new MediaMonkeyProfile(),
-                //new Windows81Profile(),
-                //new WindowsMediaCenterProfile(),
-                //new WindowsPhoneProfile(),
+                // new Windows81Profile(),
+                // new WindowsMediaCenterProfile(),
+                // new WindowsPhoneProfile(),
                 new DirectTvProfile(),
                 new DirectTvProfile(),
                 new DishHopperJoeyProfile(),
                 new DishHopperJoeyProfile(),
                 new DefaultProfile(),
                 new DefaultProfile(),

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

@@ -1,5 +1,10 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
 
 
+  <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
+  <PropertyGroup>
+    <ProjectGuid>{805844AB-E92F-45E6-9D99-4F6D48D129A5}</ProjectGuid>
+  </PropertyGroup>
+
   <ItemGroup>
   <ItemGroup>
     <Compile Include="..\SharedVersion.cs" />
     <Compile Include="..\SharedVersion.cs" />
   </ItemGroup>
   </ItemGroup>

+ 27 - 15
Emby.Dlna/Eventing/EventManager.cs

@@ -31,18 +31,26 @@ namespace Emby.Dlna.Eventing
         public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
         public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
         {
         {
             var subscription = GetSubscription(subscriptionId, false);
             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)
         public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
@@ -144,12 +152,17 @@ namespace Emby.Dlna.Eventing
             builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
             builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
             foreach (var key in stateVariables.Keys)
             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>");
             builder.Append("</e:propertyset>");
 
 
             var options = new HttpRequestOptions
             var options = new HttpRequestOptions
@@ -169,7 +182,6 @@ namespace Emby.Dlna.Eventing
             {
             {
                 using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false))
                 using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false))
                 {
                 {
-
                 }
                 }
             }
             }
             catch (OperationCanceledException)
             catch (OperationCanceledException)

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

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

+ 37 - 29
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -33,10 +33,8 @@ namespace Emby.Dlna.Main
     public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
     public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
     {
     {
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
-        private readonly ILogger _logger;
+        private readonly ILogger<DlnaEntryPoint> _logger;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
-
-        private PlayToManager _manager;
         private readonly ISessionManager _sessionManager;
         private readonly ISessionManager _sessionManager;
         private readonly IHttpClient _httpClient;
         private readonly IHttpClient _httpClient;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
@@ -47,14 +45,13 @@ namespace Emby.Dlna.Main
         private readonly ILocalizationManager _localization;
         private readonly ILocalizationManager _localization;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaEncoder _mediaEncoder;
-
         private readonly IDeviceDiscovery _deviceDiscovery;
         private readonly IDeviceDiscovery _deviceDiscovery;
-
-        private SsdpDevicePublisher _Publisher;
-
         private readonly ISocketFactory _socketFactory;
         private readonly ISocketFactory _socketFactory;
         private readonly INetworkManager _networkManager;
         private readonly INetworkManager _networkManager;
+        private readonly object _syncLock = new object();
 
 
+        private PlayToManager _manager;
+        private SsdpDevicePublisher _publisher;
         private ISsdpCommunicationsServer _communicationsServer;
         private ISsdpCommunicationsServer _communicationsServer;
 
 
         internal IContentDirectory ContentDirectory { get; private set; }
         internal IContentDirectory ContentDirectory { get; private set; }
@@ -65,7 +62,8 @@ namespace Emby.Dlna.Main
 
 
         public static DlnaEntryPoint Current;
         public static DlnaEntryPoint Current;
 
 
-        public DlnaEntryPoint(IServerConfigurationManager config,
+        public DlnaEntryPoint(
+            IServerConfigurationManager config,
             ILoggerFactory loggerFactory,
             ILoggerFactory loggerFactory,
             IServerApplicationHost appHost,
             IServerApplicationHost appHost,
             ISessionManager sessionManager,
             ISessionManager sessionManager,
@@ -99,7 +97,7 @@ namespace Emby.Dlna.Main
             _mediaEncoder = mediaEncoder;
             _mediaEncoder = mediaEncoder;
             _socketFactory = socketFactory;
             _socketFactory = socketFactory;
             _networkManager = networkManager;
             _networkManager = networkManager;
-            _logger = loggerFactory.CreateLogger("Dlna");
+            _logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
 
 
             ContentDirectory = new ContentDirectory.ContentDirectory(
             ContentDirectory = new ContentDirectory.ContentDirectory(
                 dlnaManager,
                 dlnaManager,
@@ -133,20 +131,20 @@ namespace Emby.Dlna.Main
         {
         {
             await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
             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))
             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();
             var options = _config.GetDlnaConfiguration();
 
 
@@ -180,7 +178,7 @@ namespace Emby.Dlna.Main
                     var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
                     var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
                                                    OperatingSystem.Id == OperatingSystemId.Linux;
                                                    OperatingSystem.Id == OperatingSystemId.Linux;
 
 
-                    _communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding)
+                    _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
                     {
                     {
                         IsShared = true
                         IsShared = true
                     };
                     };
@@ -231,20 +229,22 @@ namespace Emby.Dlna.Main
                 return;
                 return;
             }
             }
 
 
-            if (_Publisher != null)
+            if (_publisher != null)
             {
             {
                 return;
                 return;
             }
             }
 
 
             try
             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);
                 await RegisterServerEndpoints().ConfigureAwait(false);
 
 
-                _Publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
+                _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
@@ -266,6 +266,12 @@ namespace Emby.Dlna.Main
                     continue;
                     continue;
                 }
                 }
 
 
+                // Limit to LAN addresses only
+                if (!_networkManager.IsAddressInSubnets(address, true, true))
+                {
+                    continue;
+                }
+
                 var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
                 var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
 
 
                 _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
                 _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
@@ -275,7 +281,7 @@ namespace Emby.Dlna.Main
 
 
                 var device = new SsdpRootDevice
                 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.
                     Location = uri, // Must point to the URL that serves your devices UPnP description document.
                     Address = address,
                     Address = address,
                     SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
                     SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
@@ -287,13 +293,13 @@ namespace Emby.Dlna.Main
                 };
                 };
 
 
                 SetProperies(device, fullService);
                 SetProperies(device, fullService);
-                _Publisher.AddDevice(device);
+                _publisher.AddDevice(device);
 
 
                 var embeddedDevices = new[]
                 var embeddedDevices = new[]
                 {
                 {
                     "urn:schemas-upnp-org:service:ContentDirectory:1",
                     "urn:schemas-upnp-org:service:ContentDirectory:1",
                     "urn:schemas-upnp-org:service:ConnectionManager: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)
                 foreach (var subDevice in embeddedDevices)
@@ -319,12 +325,13 @@ namespace Emby.Dlna.Main
             {
             {
                 guid = text.GetMD5();
                 guid = text.GetMD5();
             }
             }
+
             return guid.ToString("N", CultureInfo.InvariantCulture);
             return guid.ToString("N", CultureInfo.InvariantCulture);
         }
         }
 
 
         private void SetProperies(SsdpDevice device, string fullDeviceType)
         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(':');
             var serviceParts = service.Split(':');
 
 
@@ -335,7 +342,6 @@ namespace Emby.Dlna.Main
             device.DeviceType = serviceParts[2];
             device.DeviceType = serviceParts[2];
         }
         }
 
 
-        private readonly object _syncLock = new object();
         private void StartPlayToManager()
         private void StartPlayToManager()
         {
         {
             lock (_syncLock)
             lock (_syncLock)
@@ -347,7 +353,8 @@ namespace Emby.Dlna.Main
 
 
                 try
                 try
                 {
                 {
-                    _manager = new PlayToManager(_logger,
+                    _manager = new PlayToManager(
+                        _logger,
                         _sessionManager,
                         _sessionManager,
                         _libraryManager,
                         _libraryManager,
                         _userManager,
                         _userManager,
@@ -386,6 +393,7 @@ namespace Emby.Dlna.Main
                     {
                     {
                         _logger.LogError(ex, "Error disposing PlayTo manager");
                         _logger.LogError(ex, "Error disposing PlayTo manager");
                     }
                     }
+
                     _manager = null;
                     _manager = null;
                 }
                 }
             }
             }
@@ -412,11 +420,11 @@ namespace Emby.Dlna.Main
 
 
         public void DisposeDevicePublisher()
         public void DisposeDevicePublisher()
         {
         {
-            if (_Publisher != null)
+            if (_publisher != null)
             {
             {
                 _logger.LogInformation("Disposing SsdpDevicePublisher");
                 _logger.LogInformation("Disposing SsdpDevicePublisher");
-                _Publisher.Dispose();
-                _Publisher = null;
+                _publisher.Dispose();
+                _publisher = null;
             }
             }
         }
         }
     }
     }

+ 33 - 37
Emby.Dlna/PlayTo/Device.cs

@@ -4,12 +4,12 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
+using System.Security;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using System.Xml;
 using System.Xml;
 using System.Xml.Linq;
 using System.Xml.Linq;
 using Emby.Dlna.Common;
 using Emby.Dlna.Common;
-using Emby.Dlna.Server;
 using Emby.Dlna.Ssdp;
 using Emby.Dlna.Ssdp;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
@@ -19,8 +19,6 @@ namespace Emby.Dlna.PlayTo
 {
 {
     public class Device : IDisposable
     public class Device : IDisposable
     {
     {
-        #region Fields & Properties
-
         private Timer _timer;
         private Timer _timer;
 
 
         public DeviceInfo Properties { get; set; }
         public DeviceInfo Properties { get; set; }
@@ -34,9 +32,10 @@ namespace Emby.Dlna.PlayTo
         {
         {
             get
             get
             {
             {
-                RefreshVolumeIfNeeded();
+                RefreshVolumeIfNeeded().GetAwaiter().GetResult();
                 return _volume;
                 return _volume;
             }
             }
+
             set => _volume = value;
             set => _volume = value;
         }
         }
 
 
@@ -52,10 +51,10 @@ namespace Emby.Dlna.PlayTo
 
 
         public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED;
         public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED;
 
 
-        #endregion
-
         private readonly IHttpClient _httpClient;
         private readonly IHttpClient _httpClient;
+
         private readonly ILogger _logger;
         private readonly ILogger _logger;
+
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
 
 
         public Action OnDeviceUnavailable { get; set; }
         public Action OnDeviceUnavailable { get; set; }
@@ -76,24 +75,24 @@ namespace Emby.Dlna.PlayTo
 
 
         private DateTime _lastVolumeRefresh;
         private DateTime _lastVolumeRefresh;
         private bool _volumeRefreshActive;
         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;
                 _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)
             if (_disposed)
+            {
                 return;
                 return;
+            }
 
 
             try
             try
             {
             {
@@ -141,8 +140,6 @@ namespace Emby.Dlna.PlayTo
             }
             }
         }
         }
 
 
-        #region Commanding
-
         public Task VolumeDown(CancellationToken cancellationToken)
         public Task VolumeDown(CancellationToken cancellationToken)
         {
         {
             var sendVolume = Math.Max(Volume - 5, 0);
             var sendVolume = Math.Max(Volume - 5, 0);
@@ -211,7 +208,9 @@ namespace Emby.Dlna.PlayTo
 
 
             var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
             var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
             if (command == null)
             if (command == null)
+            {
                 return false;
                 return false;
+            }
 
 
             var service = GetServiceRenderingControl();
             var service = GetServiceRenderingControl();
 
 
@@ -232,7 +231,7 @@ namespace Emby.Dlna.PlayTo
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Sets volume on a scale of 0-100
+        /// Sets volume on a scale of 0-100.
         /// </summary>
         /// </summary>
         public async Task SetVolume(int value, CancellationToken cancellationToken)
         public async Task SetVolume(int value, CancellationToken cancellationToken)
         {
         {
@@ -240,7 +239,9 @@ namespace Emby.Dlna.PlayTo
 
 
             var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
             var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
             if (command == null)
             if (command == null)
+            {
                 return;
                 return;
+            }
 
 
             var service = GetServiceRenderingControl();
             var service = GetServiceRenderingControl();
 
 
@@ -263,7 +264,9 @@ namespace Emby.Dlna.PlayTo
 
 
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
             if (command == null)
             if (command == null)
+            {
                 return;
                 return;
+            }
 
 
             var service = GetAvTransportService();
             var service = GetAvTransportService();
 
 
@@ -288,7 +291,9 @@ namespace Emby.Dlna.PlayTo
 
 
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
             if (command == null)
             if (command == null)
+            {
                 return;
                 return;
+            }
 
 
             var dictionary = new Dictionary<string, string>
             var dictionary = new Dictionary<string, string>
             {
             {
@@ -329,7 +334,7 @@ namespace Emby.Dlna.PlayTo
                 return string.Empty;
                 return string.Empty;
             }
             }
 
 
-            return DescriptionXmlBuilder.Escape(value);
+            return SecurityElement.Escape(value);
         }
         }
 
 
         private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
         private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
@@ -401,11 +406,8 @@ namespace Emby.Dlna.PlayTo
             RestartTimer(true);
             RestartTimer(true);
         }
         }
 
 
-        #endregion
-
-        #region Get data
-
         private int _connectFailureCount;
         private int _connectFailureCount;
+
         private async void TimerCallback(object sender)
         private async void TimerCallback(object sender)
         {
         {
             if (_disposed)
             if (_disposed)
@@ -458,7 +460,9 @@ namespace Emby.Dlna.PlayTo
                     _connectFailureCount = 0;
                     _connectFailureCount = 0;
 
 
                     if (_disposed)
                     if (_disposed)
+                    {
                         return;
                         return;
+                    }
 
 
                     // If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive
                     // 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)
@@ -478,7 +482,9 @@ namespace Emby.Dlna.PlayTo
             catch (Exception ex)
             catch (Exception ex)
             {
             {
                 if (_disposed)
                 if (_disposed)
+                {
                     return;
                     return;
+                }
 
 
                 _logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name);
                 _logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name);
 
 
@@ -494,6 +500,7 @@ namespace Emby.Dlna.PlayTo
                         return;
                         return;
                     }
                     }
                 }
                 }
+
                 RestartTimerInactive();
                 RestartTimerInactive();
             }
             }
         }
         }
@@ -578,7 +585,9 @@ namespace Emby.Dlna.PlayTo
                 cancellationToken: cancellationToken).ConfigureAwait(false);
                 cancellationToken: cancellationToken).ConfigureAwait(false);
 
 
             if (result == null || result.Document == null)
             if (result == null || result.Document == null)
+            {
                 return;
                 return;
+            }
 
 
             var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse")
             var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse")
                                             .Select(i => i.Element("CurrentMute"))
                                             .Select(i => i.Element("CurrentMute"))
@@ -750,7 +759,7 @@ namespace Emby.Dlna.PlayTo
 
 
             if (track == null)
             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);
                 return (true, null);
             }
             }
 
 
@@ -794,7 +803,6 @@ namespace Emby.Dlna.PlayTo
             }
             }
             catch (XmlException)
             catch (XmlException)
             {
             {
-
             }
             }
 
 
             // first try to add a root node with a dlna namesapce
             // first try to add a root node with a dlna namesapce
@@ -806,7 +814,6 @@ namespace Emby.Dlna.PlayTo
             }
             }
             catch (XmlException)
             catch (XmlException)
             {
             {
-
             }
             }
 
 
             // some devices send back invalid xml
             // some devices send back invalid xml
@@ -816,7 +823,6 @@ namespace Emby.Dlna.PlayTo
             }
             }
             catch (XmlException)
             catch (XmlException)
             {
             {
-
             }
             }
 
 
             return null;
             return null;
@@ -871,10 +877,6 @@ namespace Emby.Dlna.PlayTo
             return new string[4];
             return new string[4];
         }
         }
 
 
-        #endregion
-
-        #region From XML
-
         private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
         private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
         {
         {
             if (AvCommands != null)
             if (AvCommands != null)
@@ -1069,8 +1071,6 @@ namespace Emby.Dlna.PlayTo
             return new Device(deviceProperties, httpClient, logger, config);
             return new Device(deviceProperties, httpClient, logger, config);
         }
         }
 
 
-        #endregion
-
         private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
         private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
         private static DeviceIcon CreateIcon(XElement element)
         private static DeviceIcon CreateIcon(XElement element)
         {
         {
@@ -1194,8 +1194,6 @@ namespace Emby.Dlna.PlayTo
             });
             });
         }
         }
 
 
-        #region IDisposable
-
         bool _disposed;
         bool _disposed;
 
 
         public void Dispose()
         public void Dispose()
@@ -1222,8 +1220,6 @@ namespace Emby.Dlna.PlayTo
             _disposed = true;
             _disposed = true;
         }
         }
 
 
-        #endregion
-
         public override string ToString()
         public override string ToString()
         {
         {
             return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);
             return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);

+ 30 - 9
Emby.Dlna/PlayTo/PlayToController.cs

@@ -7,6 +7,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Dlna.Didl;
 using Emby.Dlna.Didl;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
@@ -22,6 +23,7 @@ using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Session;
 using Microsoft.AspNetCore.WebUtilities;
 using Microsoft.AspNetCore.WebUtilities;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
+using Photo = MediaBrowser.Controller.Entities.Photo;
 
 
 namespace Emby.Dlna.PlayTo
 namespace Emby.Dlna.PlayTo
 {
 {
@@ -146,11 +148,14 @@ namespace Emby.Dlna.PlayTo
                 {
                 {
                     var positionTicks = GetProgressPositionTicks(streamInfo);
                     var positionTicks = GetProgressPositionTicks(streamInfo);
 
 
-                    ReportPlaybackStopped(streamInfo, positionTicks);
+                    await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
                 }
                 }
 
 
                 streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager);
                 streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager);
-                if (streamInfo.Item == null) return;
+                if (streamInfo.Item == null)
+                {
+                    return;
+                }
 
 
                 var newItemProgress = GetProgressInfo(streamInfo);
                 var newItemProgress = GetProgressInfo(streamInfo);
 
 
@@ -173,11 +178,14 @@ namespace Emby.Dlna.PlayTo
             {
             {
                 var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
                 var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
 
 
-                if (streamInfo.Item == null) return;
+                if (streamInfo.Item == null)
+                {
+                    return;
+                }
 
 
                 var positionTicks = GetProgressPositionTicks(streamInfo);
                 var positionTicks = GetProgressPositionTicks(streamInfo);
 
 
-                ReportPlaybackStopped(streamInfo, positionTicks);
+                await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
 
 
                 var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).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) :
                     (_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) :
                     mediaSource.RunTimeTicks;
                     mediaSource.RunTimeTicks;
 
 
-                var playedToCompletion = (positionTicks.HasValue && positionTicks.Value == 0);
+                var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
 
 
                 if (!playedToCompletion && duration.HasValue && positionTicks.HasValue)
                 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
             try
             {
             {
@@ -220,7 +228,6 @@ namespace Emby.Dlna.PlayTo
                     SessionId = _session.Id,
                     SessionId = _session.Id,
                     PositionTicks = positionTicks,
                     PositionTicks = positionTicks,
                     MediaSourceId = streamInfo.MediaSourceId
                     MediaSourceId = streamInfo.MediaSourceId
-
                 }).ConfigureAwait(false);
                 }).ConfigureAwait(false);
             }
             }
             catch (Exception ex)
             catch (Exception ex)
@@ -418,6 +425,7 @@ namespace Emby.Dlna.PlayTo
                     await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
                     await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
                     return;
                     return;
                 }
                 }
+
                 await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
                 await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
             }
             }
         }
         }
@@ -441,7 +449,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;
             var deviceInfo = _device.Properties;
 
 
@@ -700,6 +714,7 @@ namespace Emby.Dlna.PlayTo
 
 
                             throw new ArgumentException("Volume argument cannot be null");
                             throw new ArgumentException("Volume argument cannot be null");
                         }
                         }
+
                     default:
                     default:
                         return Task.CompletedTask;
                         return Task.CompletedTask;
                 }
                 }
@@ -785,12 +800,15 @@ namespace Emby.Dlna.PlayTo
             public int? SubtitleStreamIndex { get; set; }
             public int? SubtitleStreamIndex { get; set; }
 
 
             public string DeviceProfileId { get; set; }
             public string DeviceProfileId { get; set; }
+
             public string DeviceId { get; set; }
             public string DeviceId { get; set; }
 
 
             public string MediaSourceId { get; set; }
             public string MediaSourceId { get; set; }
+
             public string LiveStreamId { get; set; }
             public string LiveStreamId { get; set; }
 
 
             public BaseItem Item { get; set; }
             public BaseItem Item { get; set; }
+
             private MediaSourceInfo MediaSource;
             private MediaSourceInfo MediaSource;
 
 
             private IMediaSourceManager _mediaSourceManager;
             private IMediaSourceManager _mediaSourceManager;
@@ -908,7 +926,8 @@ namespace Emby.Dlna.PlayTo
             return 0;
             return 0;
         }
         }
 
 
-        public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
+        /// <inheritdoc />
+        public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
         {
         {
             if (_disposed)
             if (_disposed)
             {
             {
@@ -924,10 +943,12 @@ namespace Emby.Dlna.PlayTo
             {
             {
                 return SendPlayCommand(data as PlayRequest, cancellationToken);
                 return SendPlayCommand(data as PlayRequest, cancellationToken);
             }
             }
+
             if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
             {
             {
                 return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
                 return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
             }
             }
+
             if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
             {
             {
                 return SendGeneralCommand(data as GeneralCommand, cancellationToken);
                 return SendGeneralCommand(data as GeneralCommand, cancellationToken);

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

@@ -78,9 +78,15 @@ namespace Emby.Dlna.PlayTo
 
 
             var info = e.Argument;
             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();
             string location = info.Location.ToString();
 
 
@@ -88,7 +94,7 @@ namespace Emby.Dlna.PlayTo
             if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 &&
             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;
                 return;
             }
             }
 
 
@@ -112,7 +118,6 @@ namespace Emby.Dlna.PlayTo
             }
             }
             catch (OperationCanceledException)
             catch (OperationCanceledException)
             {
             {
-
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
@@ -133,6 +138,7 @@ namespace Emby.Dlna.PlayTo
                 usn = usn.Substring(index);
                 usn = usn.Substring(index);
                 found = true;
                 found = true;
             }
             }
+
             index = usn.IndexOf("::", StringComparison.OrdinalIgnoreCase);
             index = usn.IndexOf("::", StringComparison.OrdinalIgnoreCase);
             if (index != -1)
             if (index != -1)
             {
             {
@@ -184,7 +190,8 @@ namespace Emby.Dlna.PlayTo
                     serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress);
                     serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress);
                 }
                 }
 
 
-                controller = new PlayToController(sessionInfo,
+                controller = new PlayToController(
+                    sessionInfo,
                    _sessionManager,
                    _sessionManager,
                    _libraryManager,
                    _libraryManager,
                    _logger,
                    _logger,
@@ -242,7 +249,6 @@ namespace Emby.Dlna.PlayTo
             }
             }
             catch
             catch
             {
             {
-
             }
             }
 
 
             _sessionLock.Dispose();
             _sessionLock.Dispose();

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

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

+ 0 - 1
Emby.Dlna/PlayTo/SsdpHttpClient.cs

@@ -91,7 +91,6 @@ namespace Emby.Dlna.PlayTo
 
 
             using (await _httpClient.SendAsync(options, new HttpMethod("SUBSCRIBE")).ConfigureAwait(false))
             using (await _httpClient.SendAsync(options, new HttpMethod("SUBSCRIBE")).ConfigureAwait(false))
             {
             {
-
             }
             }
         }
         }
 
 

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

@@ -44,10 +44,12 @@ namespace Emby.Dlna.PlayTo
                 {
                 {
                     return MediaBrowser.Model.Entities.MediaType.Audio;
                     return MediaBrowser.Model.Entities.MediaType.Audio;
                 }
                 }
+
                 if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1)
                 if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1)
                 {
                 {
                     return MediaBrowser.Model.Entities.MediaType.Video;
                     return MediaBrowser.Model.Entities.MediaType.Video;
                 }
                 }
+
                 if (classType.IndexOf("image", StringComparison.Ordinal) != -1)
                 if (classType.IndexOf("image", StringComparison.Ordinal) != -1)
                 {
                 {
                     return MediaBrowser.Model.Entities.MediaType.Photo;
                     return MediaBrowser.Model.Entities.MediaType.Photo;

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

@@ -12,7 +12,7 @@ namespace Emby.Dlna.Profiles
         {
         {
             Name = "Generic Device";
             Name = "Generic Device";
 
 
-            ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*";
+            ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";
 
 
             Manufacturer = "Jellyfin";
             Manufacturer = "Jellyfin";
             ModelDescription = "UPnP/AV 1.0 Compliant Media Server";
             ModelDescription = "UPnP/AV 1.0 Compliant Media Server";
@@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
 
 
         public void AddXmlRootAttribute(string name, string value)
         public void AddXmlRootAttribute(string name, string value)
         {
         {
-            var atts = XmlRootAttributes ?? new XmlAttribute[] { };
+            var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>();
             var list = atts.ToList();
             var list = atts.ToList();
 
 
             list.Add(new XmlAttribute
             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>();
         }
         }
     }
     }
 }
 }

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

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

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

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

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

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

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

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

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

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

+ 1 - 1
Emby.Dlna/Profiles/Xml/Default.xml

@@ -21,7 +21,7 @@
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/Denon AVR.xml

@@ -26,7 +26,7 @@
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml

@@ -27,7 +27,7 @@
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>10</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>10</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>true</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>true</RequiresPlainVideoItems>
   <RequiresPlainFolders>true</RequiresPlainFolders>
   <RequiresPlainFolders>true</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/LG Smart TV.xml

@@ -27,7 +27,7 @@
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>10</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>10</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml

@@ -25,7 +25,7 @@
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/Marantz.xml

@@ -27,7 +27,7 @@
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/MediaMonkey.xml

@@ -27,7 +27,7 @@
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/Panasonic Viera.xml

@@ -28,7 +28,7 @@
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>10</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>10</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/Popcorn Hour.xml

@@ -21,7 +21,7 @@
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml

@@ -27,7 +27,7 @@
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml

@@ -27,7 +27,7 @@
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>true</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>true</RequiresPlainVideoItems>
   <RequiresPlainFolders>true</RequiresPlainFolders>
   <RequiresPlainFolders>true</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml

@@ -29,7 +29,7 @@
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
   <SonyAggregationFlags>10</SonyAggregationFlags>
   <SonyAggregationFlags>10</SonyAggregationFlags>
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml

@@ -29,7 +29,7 @@
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
   <SonyAggregationFlags>10</SonyAggregationFlags>
   <SonyAggregationFlags>10</SonyAggregationFlags>
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml

@@ -29,7 +29,7 @@
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
   <SonyAggregationFlags>10</SonyAggregationFlags>
   <SonyAggregationFlags>10</SonyAggregationFlags>
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml

@@ -29,7 +29,7 @@
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
   <SonyAggregationFlags>10</SonyAggregationFlags>
   <SonyAggregationFlags>10</SonyAggregationFlags>
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml

@@ -29,7 +29,7 @@
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
   <SonyAggregationFlags>10</SonyAggregationFlags>
   <SonyAggregationFlags>10</SonyAggregationFlags>
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml

@@ -29,7 +29,7 @@
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
   <SonyAggregationFlags>10</SonyAggregationFlags>
   <SonyAggregationFlags>10</SonyAggregationFlags>
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/WDTV Live.xml

@@ -28,7 +28,7 @@
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>5</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>5</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/Xbox One.xml

@@ -28,7 +28,7 @@
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>40</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>40</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

+ 1 - 1
Emby.Dlna/Profiles/Xml/foobar2000.xml

@@ -27,7 +27,7 @@
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MaxStaticBitrate>140000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
   <MaxStaticMusicBitrate xsi:nil="true" />
   <MaxStaticMusicBitrate xsi:nil="true" />
-  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo>
+  <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
   <RequiresPlainFolders>false</RequiresPlainFolders>
   <RequiresPlainFolders>false</RequiresPlainFolders>

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

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
+using System.Security;
 using System.Text;
 using System.Text;
 using Emby.Dlna.Common;
 using Emby.Dlna.Common;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
@@ -64,10 +65,10 @@ namespace Emby.Dlna.Server
 
 
             foreach (var att in attributes)
             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("<specVersion>");
             builder.Append("<major>1</major>");
             builder.Append("<major>1</major>");
@@ -76,7 +77,9 @@ namespace Emby.Dlna.Server
 
 
             if (!EnableAbsoluteUrls)
             if (!EnableAbsoluteUrls)
             {
             {
-                builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>");
+                builder.Append("<URLBase>")
+                    .Append(SecurityElement.Escape(_serverAddress))
+                    .Append("</URLBase>");
             }
             }
 
 
             AppendDeviceInfo(builder);
             AppendDeviceInfo(builder);
@@ -93,86 +96,14 @@ namespace Emby.Dlna.Server
 
 
             AppendIconList(builder);
             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);
             AppendServiceList(builder);
             builder.Append("</device>");
             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)
         private void AppendDeviceProperties(StringBuilder builder)
         {
         {
             builder.Append("<dlna:X_DLNACAP/>");
             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("<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))
             if (string.IsNullOrEmpty(_profile.SerialNumber))
             {
             {
-                builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>");
+                builder.Append("<serialNumber>")
+                    .Append(SecurityElement.Escape(_serverId))
+                    .Append("</serialNumber>");
             }
             }
             else
             else
             {
             {
-                builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>");
+                builder.Append("<serialNumber>")
+                    .Append(SecurityElement.Escape(_profile.SerialNumber))
+                    .Append("</serialNumber>");
             }
             }
 
 
             builder.Append("<UPC/>");
             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))
             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("<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>");
                 builder.Append("</icon>");
             }
             }
@@ -265,11 +228,21 @@ namespace Emby.Dlna.Server
             {
             {
                 builder.Append("<service>");
                 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>");
                 builder.Append("</service>");
             }
             }
@@ -293,7 +266,7 @@ namespace Emby.Dlna.Server
                 url = _serverAddress.TrimEnd('/') + url;
                 url = _serverAddress.TrimEnd('/') + url;
             }
             }
 
 
-            return Escape(url);
+            return SecurityElement.Escape(url);
         }
         }
 
 
         private IEnumerable<DeviceIcon> GetIcons()
         private IEnumerable<DeviceIcon> GetIcons()

+ 4 - 0
Emby.Dlna/Service/BaseControlHandler.cs

@@ -18,6 +18,7 @@ namespace Emby.Dlna.Service
         private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
         private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
 
 
         protected IServerConfigurationManager Config { get; }
         protected IServerConfigurationManager Config { get; }
+
         protected ILogger Logger { get; }
         protected ILogger Logger { get; }
 
 
         protected BaseControlHandler(IServerConfigurationManager config, ILogger logger)
         protected BaseControlHandler(IServerConfigurationManager config, ILogger logger)
@@ -135,6 +136,7 @@ namespace Emby.Dlna.Service
 
 
                                 break;
                                 break;
                             }
                             }
+
                         default:
                         default:
                             {
                             {
                                 await reader.SkipAsync().ConfigureAwait(false);
                                 await reader.SkipAsync().ConfigureAwait(false);
@@ -211,7 +213,9 @@ namespace Emby.Dlna.Service
         private class ControlRequestInfo
         private class ControlRequestInfo
         {
         {
             public string LocalName { get; set; }
             public string LocalName { get; set; }
+
             public string NamespaceURI { get; set; }
             public string NamespaceURI { get; set; }
+
             public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
             public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
         }
         }
 
 

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

@@ -17,7 +17,7 @@ namespace Emby.Dlna.Service
             Logger = logger;
             Logger = logger;
             HttpClient = httpClient;
             HttpClient = httpClient;
 
 
-            EventManager = new EventManager(Logger, HttpClient);
+            EventManager = new EventManager(logger, HttpClient);
         }
         }
 
 
         public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)
         public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)

+ 26 - 9
Emby.Dlna/Service/ServiceXmlBuilder.cs

@@ -1,9 +1,9 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Security;
 using System.Text;
 using System.Text;
 using Emby.Dlna.Common;
 using Emby.Dlna.Common;
-using Emby.Dlna.Server;
 
 
 namespace Emby.Dlna.Service
 namespace Emby.Dlna.Service
 {
 {
@@ -37,7 +37,9 @@ namespace Emby.Dlna.Service
             {
             {
                 builder.Append("<action>");
                 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>");
                 builder.Append("<argumentList>");
 
 
@@ -45,9 +47,15 @@ namespace Emby.Dlna.Service
                 {
                 {
                     builder.Append("<argument>");
                     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>");
                     builder.Append("</argument>");
                 }
                 }
@@ -68,18 +76,27 @@ namespace Emby.Dlna.Service
             {
             {
                 var sendEvents = item.SendsEvents ? "yes" : "no";
                 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.Length > 0)
                 {
                 {
                     builder.Append("<allowedValueList>");
                     builder.Append("<allowedValueList>");
                     foreach (var allowedValue in item.AllowedValues)
                     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>");
                     builder.Append("</allowedValueList>");
                 }
                 }
 
 

+ 7 - 11
Emby.Dlna/Ssdp/DeviceDiscovery.cs

@@ -77,7 +77,7 @@ namespace Emby.Dlna.Ssdp
                     // (Optional) Set the filter so we only see notifications for devices we care about
                     // (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
                     // (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).
                     // 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
                     // Connect our event handler so we process devices as they are found
                     _deviceLocator.DeviceAvailable += OnDeviceLocatorDeviceAvailable;
                     _deviceLocator.DeviceAvailable += OnDeviceLocatorDeviceAvailable;
@@ -100,15 +100,13 @@ namespace Emby.Dlna.Ssdp
 
 
             var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
             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,
                     Location = e.DiscoveredDevice.DescriptionLocation,
                     Headers = headers,
                     Headers = headers,
                     LocalIpAddress = e.LocalIpAddress
                     LocalIpAddress = e.LocalIpAddress
-                }
-            };
+                });
 
 
             DeviceDiscoveredInternal?.Invoke(this, args);
             DeviceDiscoveredInternal?.Invoke(this, args);
         }
         }
@@ -121,14 +119,12 @@ namespace Emby.Dlna.Ssdp
 
 
             var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
             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,
                     Location = e.DiscoveredDevice.DescriptionLocation,
                     Headers = headers
                     Headers = headers
-                }
-            };
+                });
 
 
             DeviceLeft?.Invoke(this, args);
             DeviceLeft?.Invoke(this, args);
         }
         }

+ 4 - 10
Emby.Dlna/Ssdp/Extensions.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
+using System.Linq;
 using System.Xml.Linq;
 using System.Xml.Linq;
 
 
 namespace Emby.Dlna.Ssdp
 namespace Emby.Dlna.Ssdp
@@ -10,24 +11,17 @@ namespace Emby.Dlna.Ssdp
         {
         {
             var node = container.Element(name);
             var node = container.Element(name);
 
 
-            return node == null ? null : node.Value;
+            return node?.Value;
         }
         }
 
 
         public static string GetAttributeValue(this XElement container, XName name)
         public static string GetAttributeValue(this XElement container, XName name)
         {
         {
             var node = container.Attribute(name);
             var node = container.Attribute(name);
 
 
-            return node == null ? null : node.Value;
+            return node?.Value;
         }
         }
 
 
         public static string GetDescendantValue(this XElement container, XName name)
         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;
     }
     }
 }
 }

+ 6 - 0
Emby.Drawing/Emby.Drawing.csproj

@@ -1,10 +1,16 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
 
 
+  <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
+  <PropertyGroup>
+    <ProjectGuid>{08FFF49B-F175-4807-A2B5-73B0EBD9F716}</ProjectGuid>
+  </PropertyGroup>
+
   <PropertyGroup>
   <PropertyGroup>
     <TargetFramework>netstandard2.1</TargetFramework>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <Nullable>enable</Nullable>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 43 - 51
Emby.Drawing/ImageProcessor.cs

@@ -4,17 +4,18 @@ using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
+using Photo = MediaBrowser.Controller.Entities.Photo;
 
 
 namespace Emby.Drawing
 namespace Emby.Drawing
 {
 {
@@ -29,12 +30,11 @@ namespace Emby.Drawing
         private static readonly HashSet<string> _transparentImageTypes
         private static readonly HashSet<string> _transparentImageTypes
             = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
             = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
 
 
-        private readonly ILogger _logger;
+        private readonly ILogger<ImageProcessor> _logger;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IServerApplicationPaths _appPaths;
         private readonly IServerApplicationPaths _appPaths;
         private readonly IImageEncoder _imageEncoder;
         private readonly IImageEncoder _imageEncoder;
-        private readonly Func<ILibraryManager> _libraryManager;
-        private readonly Func<IMediaEncoder> _mediaEncoder;
+        private readonly IMediaEncoder _mediaEncoder;
 
 
         private bool _disposed = false;
         private bool _disposed = false;
 
 
@@ -45,20 +45,17 @@ namespace Emby.Drawing
         /// <param name="appPaths">The server application paths.</param>
         /// <param name="appPaths">The server application paths.</param>
         /// <param name="fileSystem">The filesystem.</param>
         /// <param name="fileSystem">The filesystem.</param>
         /// <param name="imageEncoder">The image encoder.</param>
         /// <param name="imageEncoder">The image encoder.</param>
-        /// <param name="libraryManager">The library manager.</param>
         /// <param name="mediaEncoder">The media encoder.</param>
         /// <param name="mediaEncoder">The media encoder.</param>
         public ImageProcessor(
         public ImageProcessor(
             ILogger<ImageProcessor> logger,
             ILogger<ImageProcessor> logger,
             IServerApplicationPaths appPaths,
             IServerApplicationPaths appPaths,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             IImageEncoder imageEncoder,
             IImageEncoder imageEncoder,
-            Func<ILibraryManager> libraryManager,
-            Func<IMediaEncoder> mediaEncoder)
+            IMediaEncoder mediaEncoder)
         {
         {
             _logger = logger;
             _logger = logger;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _imageEncoder = imageEncoder;
             _imageEncoder = imageEncoder;
-            _libraryManager = libraryManager;
             _mediaEncoder = mediaEncoder;
             _mediaEncoder = mediaEncoder;
             _appPaths = appPaths;
             _appPaths = appPaths;
         }
         }
@@ -119,28 +116,11 @@ namespace Emby.Drawing
             => _transparentImageTypes.Contains(Path.GetExtension(path));
             => _transparentImageTypes.Contains(Path.GetExtension(path));
 
 
         /// <inheritdoc />
         /// <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)
         {
         {
-            if (options == null)
-            {
-                throw new ArgumentNullException(nameof(options));
-            }
-
-            var libraryManager = _libraryManager();
-
             ItemImageInfo originalImage = options.Image;
             ItemImageInfo originalImage = options.Image;
             BaseItem item = options.Item;
             BaseItem item = options.Item;
 
 
-            if (!originalImage.IsLocalFile)
-            {
-                if (item == null)
-                {
-                    item = libraryManager.GetItemById(options.ItemId);
-                }
-
-                originalImage = await libraryManager.ConvertImageToLocal(item, originalImage, options.ImageIndex).ConfigureAwait(false);
-            }
-
             string originalImagePath = originalImage.Path;
             string originalImagePath = originalImage.Path;
             DateTime dateModified = originalImage.DateModified;
             DateTime dateModified = originalImage.DateModified;
             ImageDimensions? originalImageSize = null;
             ImageDimensions? originalImageSize = null;
@@ -252,7 +232,7 @@ namespace Emby.Drawing
             return ImageFormat.Jpg;
             return ImageFormat.Jpg;
         }
         }
 
 
-        private string GetMimeType(ImageFormat format, string path)
+        private string? GetMimeType(ImageFormat format, string path)
             => format switch
             => format switch
             {
             {
                 ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
                 ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
@@ -312,10 +292,6 @@ namespace Emby.Drawing
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info)
         public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info)
-            => GetImageDimensions(item, info, true);
-
-        /// <inheritdoc />
-        public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem)
         {
         {
             int width = info.Width;
             int width = info.Width;
             int height = info.Height;
             int height = info.Height;
@@ -326,17 +302,12 @@ namespace Emby.Drawing
             }
             }
 
 
             string path = info.Path;
             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);
             ImageDimensions size = GetImageDimensions(path);
             info.Width = size.Width;
             info.Width = size.Width;
             info.Height = size.Height;
             info.Height = size.Height;
 
 
-            if (updateItem)
-            {
-                _libraryManager().UpdateImages(item);
-            }
-
             return size;
             return size;
         }
         }
 
 
@@ -344,6 +315,27 @@ namespace Emby.Drawing
         public ImageDimensions GetImageDimensions(string path)
         public ImageDimensions GetImageDimensions(string path)
             => _imageEncoder.GetImageSize(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 />
         /// <inheritdoc />
         public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
         public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
             => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
             => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
@@ -351,19 +343,19 @@ namespace Emby.Drawing
         /// <inheritdoc />
         /// <inheritdoc />
         public string GetImageCacheTag(BaseItem item, ChapterInfo chapter)
         public string GetImageCacheTag(BaseItem item, ChapterInfo chapter)
         {
         {
-            try
+            return GetImageCacheTag(item, new ItemImageInfo
             {
             {
-                return GetImageCacheTag(item, new ItemImageInfo
-                {
-                    Path = chapter.ImagePath,
-                    Type = ImageType.Chapter,
-                    DateModified = chapter.ImageDateModified
-                });
-            }
-            catch
-            {
-                return null;
-            }
+                Path = chapter.ImagePath,
+                Type = ImageType.Chapter,
+                DateModified = chapter.ImageDateModified
+            });
+        }
+
+        /// <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)
         private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
@@ -384,13 +376,13 @@ namespace Emby.Drawing
                 {
                 {
                     string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
                     string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
 
 
-                    string cacheExtension = _mediaEncoder().SupportsEncoder("libwebp") ? ".webp" : ".png";
+                    string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
                     var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
                     var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
 
 
                     var file = _fileSystem.GetFileInfo(outputPath);
                     var file = _fileSystem.GetFileInfo(outputPath);
                     if (!file.Exists)
                     if (!file.Exists)
                     {
                     {
-                        await _mediaEncoder().ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
+                        await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
                         dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
                         dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
                     }
                     }
                     else
                     else

+ 6 - 0
Emby.Drawing/NullImageEncoder.cs

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

+ 10 - 7
Emby.Naming/Audio/AlbumParser.cs

@@ -1,9 +1,9 @@
+#nullable enable
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
-using System.Linq;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using Emby.Naming.Common;
 using Emby.Naming.Common;
 
 
@@ -21,8 +21,7 @@ namespace Emby.Naming.Audio
         public bool IsMultiPart(string path)
         public bool IsMultiPart(string path)
         {
         {
             var filename = Path.GetFileName(path);
             var filename = Path.GetFileName(path);
-
-            if (string.IsNullOrEmpty(filename))
+            if (filename.Length == 0)
             {
             {
                 return false;
                 return false;
             }
             }
@@ -39,18 +38,22 @@ namespace Emby.Naming.Audio
             filename = filename.Replace(')', ' ');
             filename = filename.Replace(')', ' ');
             filename = Regex.Replace(filename, @"\s+", " ");
             filename = Regex.Replace(filename, @"\s+", " ");
 
 
-            filename = filename.TrimStart();
+            ReadOnlySpan<char> trimmedFilename = filename.TrimStart();
 
 
             foreach (var prefix in _options.AlbumStackingPrefixes)
             foreach (var prefix in _options.AlbumStackingPrefixes)
             {
             {
-                if (filename.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) != 0)
+                if (!trimmedFilename.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     continue;
                     continue;
                 }
                 }
 
 
-                var tmp = filename.Substring(prefix.Length);
+                var tmp = trimmedFilename.Slice(prefix.Length).Trim();
 
 
-                tmp = tmp.Trim().Split(' ').FirstOrDefault() ?? string.Empty;
+                int index = tmp.IndexOf(' ');
+                if (index != -1)
+                {
+                    tmp = tmp.Slice(0, index);
+                }
 
 
                 if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
                 if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
                 {
                 {

+ 2 - 1
Emby.Naming/Audio/AudioFileParser.cs

@@ -1,3 +1,4 @@
+#nullable enable
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
@@ -11,7 +12,7 @@ namespace Emby.Naming.Audio
     {
     {
         public static bool IsAudioFile(string path, NamingOptions options)
         public static bool IsAudioFile(string path, NamingOptions options)
         {
         {
-            var extension = Path.GetExtension(path) ?? string.Empty;
+            var extension = Path.GetExtension(path);
             return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
             return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
         }
         }
     }
     }

+ 1 - 0
Emby.Naming/AudioBook/AudioBookFilePathParser.cs

@@ -64,6 +64,7 @@ namespace Emby.Naming.AudioBook
                 {
                 {
                     result.ChapterNumber = int.Parse(matches[0].Groups[0].Value);
                     result.ChapterNumber = int.Parse(matches[0].Groups[0].Value);
                 }
                 }
+
                 if (matches.Count > 1)
                 if (matches.Count > 1)
                 {
                 {
                     result.PartNumber = int.Parse(matches[matches.Count - 1].Groups[0].Value);
                     result.PartNumber = int.Parse(matches[matches.Count - 1].Groups[0].Value);

+ 1 - 6
Emby.Naming/Common/EpisodeExpression.cs

@@ -23,11 +23,6 @@ namespace Emby.Naming.Common
         {
         {
         }
         }
 
 
-        public EpisodeExpression()
-            : this(null)
-        {
-        }
-
         public string Expression
         public string Expression
         {
         {
             get => _expression;
             get => _expression;
@@ -48,6 +43,6 @@ namespace Emby.Naming.Common
 
 
         public string[] DateTimeFormats { get; set; }
         public string[] DateTimeFormats { get; set; }
 
 
-        public Regex Regex => _regex ?? (_regex = new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled));
+        public Regex Regex => _regex ??= new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled);
     }
     }
 }
 }

+ 3 - 3
Emby.Naming/Common/MediaType.cs

@@ -5,17 +5,17 @@ namespace Emby.Naming.Common
     public enum MediaType
     public enum MediaType
     {
     {
         /// <summary>
         /// <summary>
-        /// The audio
+        /// The audio.
         /// </summary>
         /// </summary>
         Audio = 0,
         Audio = 0,
 
 
         /// <summary>
         /// <summary>
-        /// The photo
+        /// The photo.
         /// </summary>
         /// </summary>
         Photo = 1,
         Photo = 1,
 
 
         /// <summary>
         /// <summary>
-        /// The video
+        /// The video.
         /// </summary>
         /// </summary>
         Video = 2
         Video = 2
     }
     }

+ 88 - 31
Emby.Naming/Common/NamingOptions.cs

@@ -136,12 +136,13 @@ namespace Emby.Naming.Common
 
 
             CleanDateTimes = new[]
             CleanDateTimes = new[]
             {
             {
-                @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*"
+                @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
+                @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
             };
             };
 
 
             CleanStrings = new[]
             CleanStrings = new[]
             {
             {
-                @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
+                @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
                 @"(\[.*\])"
                 @"(\[.*\])"
             };
             };
 
 
@@ -276,7 +277,7 @@ namespace Emby.Naming.Common
                 // This isn't a Kodi naming rule, but the expression below causes false positives,
                 // This isn't a Kodi naming rule, but the expression below causes false positives,
                 // so we make sure this one gets tested first.
                 // so we make sure this one gets tested first.
                 // "Foo Bar 889"
                 // "Foo Bar 889"
-                new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/x]*$")
+                new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
                 },
                 },
@@ -299,32 +300,32 @@ namespace Emby.Naming.Common
                 // *** End Kodi Standard Naming
                 // *** End Kodi Standard Naming
 
 
                 // [bar] Foo - 1 [baz]
                 // [bar] Foo - 1 [baz]
-                new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$")
+                new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
                 },
                 },
-                new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d+)[xX](?<epnumber>\d+)[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
-                new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>\d+)[x,X]?[eE](?<epnumber>\d+)[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>[0-9]+)[x,X]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
-                new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d+))[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]+))[^\\\/]*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
-                new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d+)[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
                 // "01.avi"
                 // "01.avi"
-                new EpisodeExpression(@".*[\\\/](?<epnumber>\d+)(-(?<endingepnumber>\d+))*\.\w+$")
+                new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))*\.\w+$")
                 {
                 {
                     IsOptimistic = true,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
@@ -334,34 +335,34 @@ namespace Emby.Naming.Common
                 new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
                 new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
 
 
                 // "01 - blah.avi", "01-blah.avi"
                 // "01 - blah.avi", "01-blah.avi"
-                new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\s?-\s?[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
                 {
                 {
                     IsOptimistic = true,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
                 // "01.blah.avi"
                 // "01.blah.avi"
-                new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\.[^\\\/]+$")
+                new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\.[^\\\/]+$")
                 {
                 {
                     IsOptimistic = true,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
                 // "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah"
                 // "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah"
-                new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
+                new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
                 {
                 {
                     IsOptimistic = true,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
                 // "01 episode title.avi"
                 // "01 episode title.avi"
-                new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>\d{1,3})([^\\\/]*)$")
+                new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>[0-9]{1,3})([^\\\/]*)$")
                 {
                 {
                     IsOptimistic = true,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
                 },
                 },
                 // "Episode 16", "Episode 16 - Title"
                 // "Episode 16", "Episode 16 - Title"
-                new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
+                new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
                 {
                 {
                     IsOptimistic = true,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
@@ -505,7 +506,63 @@ namespace Emby.Naming.Common
                     RuleType = ExtraRuleType.Suffix,
                     RuleType = ExtraRuleType.Suffix,
                     Token = "-short",
                     Token = "-short",
                     MediaType = MediaType.Video
                     MediaType = MediaType.Video
-                }
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.BehindTheScenes,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "behind the scenes",
+                    MediaType = MediaType.Video,
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.DeletedScene,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "deleted scenes",
+                    MediaType = MediaType.Video,
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.Interview,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "interviews",
+                    MediaType = MediaType.Video,
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.Scene,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "scenes",
+                    MediaType = MediaType.Video,
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.Sample,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "samples",
+                    MediaType = MediaType.Video,
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.Clip,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "shorts",
+                    MediaType = MediaType.Video,
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.Clip,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "featurettes",
+                    MediaType = MediaType.Video,
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.Unknown,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "extras",
+                    MediaType = MediaType.Video,
+                },
             };
             };
 
 
             Format3DRules = new[]
             Format3DRules = new[]
@@ -568,17 +625,17 @@ namespace Emby.Naming.Common
             AudioBookPartsExpressions = new[]
             AudioBookPartsExpressions = new[]
             {
             {
                 // Detect specified chapters, like CH 01
                 // Detect specified chapters, like CH 01
-                @"ch(?:apter)?[\s_-]?(?<chapter>\d+)",
+                @"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
                 // Detect specified parts, like Part 02
                 // Detect specified parts, like Part 02
-                @"p(?:ar)?t[\s_-]?(?<part>\d+)",
+                @"p(?:ar)?t[\s_-]?(?<part>[0-9]+)",
                 // Chapter is often beginning of filename
                 // Chapter is often beginning of filename
-                @"^(?<chapter>\d+)",
+                "^(?<chapter>[0-9]+)",
                 // Part if often ending of filename
                 // Part if often ending of filename
-                @"(?<part>\d+)$",
+                "(?<part>[0-9]+)$",
                 // Sometimes named as 0001_005 (chapter_part)
                 // Sometimes named as 0001_005 (chapter_part)
-                @"(?<chapter>\d+)_(?<part>\d+)",
+                "(?<chapter>[0-9]+)_(?<part>[0-9]+)",
                 // Some audiobooks are ripped from cd's, and will be named by disk number.
                 // Some audiobooks are ripped from cd's, and will be named by disk number.
-                @"dis(?:c|k)[\s_-]?(?<chapter>\d+)"
+                @"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
             };
             };
 
 
             var extensions = VideoFileExtensions.ToList();
             var extensions = VideoFileExtensions.ToList();
@@ -618,16 +675,16 @@ namespace Emby.Naming.Common
 
 
             MultipleEpisodeExpressions = new string[]
             MultipleEpisodeExpressions = new string[]
             {
             {
-                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[eExX](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})(-[xE]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$"
+                @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})(-[xE]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$"
             }.Select(i => new EpisodeExpression(i)
             }.Select(i => new EpisodeExpression(i)
             {
             {
                 IsNamed = true
                 IsNamed = true

+ 5 - 0
Emby.Naming/Emby.Naming.csproj

@@ -1,5 +1,10 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
 
 
+  <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
+  <PropertyGroup>
+    <ProjectGuid>{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}</ProjectGuid>
+  </PropertyGroup>
+
   <PropertyGroup>
   <PropertyGroup>
     <TargetFramework>netstandard2.1</TargetFramework>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>

+ 4 - 8
Emby.Naming/Subtitles/SubtitleParser.cs

@@ -1,3 +1,4 @@
+#nullable enable
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
@@ -16,11 +17,11 @@ namespace Emby.Naming.Subtitles
             _options = options;
             _options = options;
         }
         }
 
 
-        public SubtitleInfo ParseFile(string path)
+        public SubtitleInfo? ParseFile(string path)
         {
         {
-            if (string.IsNullOrEmpty(path))
+            if (path.Length == 0)
             {
             {
-                throw new ArgumentNullException(nameof(path));
+                throw new ArgumentException("File path can't be empty.", nameof(path));
             }
             }
 
 
             var extension = Path.GetExtension(path);
             var extension = Path.GetExtension(path);
@@ -52,11 +53,6 @@ namespace Emby.Naming.Subtitles
 
 
         private string[] GetFlags(string path)
         private string[] GetFlags(string path)
         {
         {
-            if (string.IsNullOrEmpty(path))
-            {
-                throw new ArgumentNullException(nameof(path));
-            }
-
             // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
             // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
 
 
             var file = Path.GetFileName(path);
             var file = Path.GetFileName(path);

+ 9 - 0
Emby.Naming/Video/ExtraResolver.cs

@@ -80,6 +80,15 @@ namespace Emby.Naming.Video
                     result.Rule = rule;
                     result.Rule = rule;
                 }
                 }
             }
             }
+            else if (rule.RuleType == ExtraRuleType.DirectoryName)
+            {
+                var directoryName = Path.GetFileName(Path.GetDirectoryName(path));
+                if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase))
+                {
+                    result.ExtraType = rule.ExtraType;
+                    result.Rule = rule;
+                }
+            }
 
 
             return result;
             return result;
         }
         }

+ 6 - 7
Emby.Naming/Video/ExtraRule.cs

@@ -5,30 +5,29 @@ using MediaType = Emby.Naming.Common.MediaType;
 
 
 namespace Emby.Naming.Video
 namespace Emby.Naming.Video
 {
 {
+    /// <summary>
+    /// A rule used to match a file path with an <see cref="MediaBrowser.Model.Entities.ExtraType"/>.
+    /// </summary>
     public class ExtraRule
     public class ExtraRule
     {
     {
         /// <summary>
         /// <summary>
-        /// Gets or sets the token.
+        /// Gets or sets the token to use for matching against the file path.
         /// </summary>
         /// </summary>
-        /// <value>The token.</value>
         public string Token { get; set; }
         public string Token { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets the type of the extra.
+        /// Gets or sets the type of the extra to return when matched.
         /// </summary>
         /// </summary>
-        /// <value>The type of the extra.</value>
         public ExtraType ExtraType { get; set; }
         public ExtraType ExtraType { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the type of the rule.
         /// Gets or sets the type of the rule.
         /// </summary>
         /// </summary>
-        /// <value>The type of the rule.</value>
         public ExtraRuleType RuleType { get; set; }
         public ExtraRuleType RuleType { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets the type of the media.
+        /// Gets or sets the type of the media to return when matched.
         /// </summary>
         /// </summary>
-        /// <value>The type of the media.</value>
         public MediaType MediaType { get; set; }
         public MediaType MediaType { get; set; }
     }
     }
 }
 }

+ 9 - 4
Emby.Naming/Video/ExtraRuleType.cs

@@ -5,18 +5,23 @@ namespace Emby.Naming.Video
     public enum ExtraRuleType
     public enum ExtraRuleType
     {
     {
         /// <summary>
         /// <summary>
-        /// The suffix
+        /// Match <see cref="ExtraRule.Token"/> against a suffix in the file name.
         /// </summary>
         /// </summary>
         Suffix = 0,
         Suffix = 0,
 
 
         /// <summary>
         /// <summary>
-        /// The filename
+        /// Match <see cref="ExtraRule.Token"/> against the file name, excluding the file extension.
         /// </summary>
         /// </summary>
         Filename = 1,
         Filename = 1,
 
 
         /// <summary>
         /// <summary>
-        /// The regex
+        /// Match <see cref="ExtraRule.Token"/> against the file name, including the file extension.
         /// </summary>
         /// </summary>
-        Regex = 2
+        Regex = 2,
+
+        /// <summary>
+        /// Match <see cref="ExtraRule.Token"/> against the name of the directory containing the file.
+        /// </summary>
+        DirectoryName = 3,
     }
     }
 }
 }

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

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

+ 3 - 3
Emby.Naming/Video/VideoResolver.cs

@@ -89,14 +89,14 @@ namespace Emby.Naming.Video
             if (parseName)
             if (parseName)
             {
             {
                 var cleanDateTimeResult = CleanDateTime(name);
                 var cleanDateTimeResult = CleanDateTime(name);
+                name = cleanDateTimeResult.Name;
+                year = cleanDateTimeResult.Year;
 
 
                 if (extraResult.ExtraType == null
                 if (extraResult.ExtraType == null
-                    && TryCleanString(cleanDateTimeResult.Name, out ReadOnlySpan<char> newName))
+                    && TryCleanString(name, out ReadOnlySpan<char> newName))
                 {
                 {
                     name = newName.ToString();
                     name = newName.ToString();
                 }
                 }
-
-                year = cleanDateTimeResult.Year;
             }
             }
 
 
             return new VideoFileInfo
             return new VideoFileInfo

+ 6 - 4
Emby.Notifications/Api/NotificationsService.cs

@@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Notifications;
 using MediaBrowser.Controller.Notifications;
@@ -149,9 +150,7 @@ namespace Emby.Notifications.Api
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
         public object Get(GetNotificationsSummary request)
         public object Get(GetNotificationsSummary request)
         {
         {
-            return new NotificationsSummary
-            {
-            };
+            return new NotificationsSummary();
         }
         }
 
 
         public Task Post(AddAdminNotification request)
         public Task Post(AddAdminNotification request)
@@ -164,7 +163,10 @@ namespace Emby.Notifications.Api
                 Level = request.Level,
                 Level = request.Level,
                 Name = request.Name,
                 Name = request.Name,
                 Url = request.Url,
                 Url = request.Url,
-                UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray()
+                UserIds = _userManager.Users
+                    .Where(user => user.HasPermission(PermissionKind.IsAdministrator))
+                    .Select(user => user.Id)
+                    .ToArray()
             };
             };
 
 
             return _notificationManager.SendNotification(notification, CancellationToken.None);
             return _notificationManager.SendNotification(notification, CancellationToken.None);

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