Procházet zdrojové kódy

Merge pull request from jellyfin/master

Nyanmisaka před 5 roky
rodič
revize
912946a427
100 změnil soubory, kde provedl 1335 přidání a 1700 odebrání
  1. 21 28
      .ci/azure-pipelines-abi.yml
  2. 42 49
      .ci/azure-pipelines-main.yml
  3. 131 0
      .ci/azure-pipelines-package.yml
  4. 46 18
      .ci/azure-pipelines-test.yml
  5. 14 12
      .ci/azure-pipelines.yml
  6. 0 59
      .copr/Makefile
  7. 1 0
      .copr/Makefile
  8. 7 10
      .editorconfig
  9. 3 0
      .github/CODEOWNERS
  10. 9 0
      .github/dependabot.yml
  11. 12 8
      .gitignore
  12. 14 0
      .vscode/extensions.json
  13. 6 6
      .vscode/launch.json
  14. 17 2
      .vscode/tasks.json
  15. 4 0
      CONTRIBUTORS.md
  16. 11 19
      Dockerfile
  17. 2 2
      Dockerfile.arm
  18. 5 10
      DvdLib/BigEndianBinaryReader.cs
  19. 6 4
      DvdLib/DvdLib.csproj
  20. 3 0
      DvdLib/Ifo/Cell.cs
  21. 2 0
      DvdLib/Ifo/CellPlaybackInfo.cs
  22. 2 0
      DvdLib/Ifo/CellPositionInfo.cs
  23. 4 0
      DvdLib/Ifo/Chapter.cs
  24. 18 10
      DvdLib/Ifo/Dvd.cs
  25. 10 2
      DvdLib/Ifo/DvdTime.cs
  26. 3 1
      DvdLib/Ifo/Program.cs
  27. 15 2
      DvdLib/Ifo/ProgramChain.cs
  28. 10 1
      DvdLib/Ifo/Title.cs
  29. 2 0
      DvdLib/Ifo/UserOperation.cs
  30. 1 0
      Emby.Dlna/ConfigurationExtension.cs
  31. 7 9
      Emby.Dlna/ContentDirectory/ContentDirectory.cs
  32. 24 16
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  33. 138 51
      Emby.Dlna/Didl/DidlBuilder.cs
  34. 1 2
      Emby.Dlna/Didl/Filter.cs
  35. 28 8
      Emby.Dlna/DlnaManager.cs
  36. 5 0
      Emby.Dlna/Emby.Dlna.csproj
  37. 18 10
      Emby.Dlna/Eventing/EventManager.cs
  38. 3 0
      Emby.Dlna/Eventing/EventSubscription.cs
  39. 37 29
      Emby.Dlna/Main/DlnaEntryPoint.cs
  40. 31 35
      Emby.Dlna/PlayTo/Device.cs
  41. 30 9
      Emby.Dlna/PlayTo/PlayToController.cs
  42. 12 6
      Emby.Dlna/PlayTo/PlayToManager.cs
  43. 1 0
      Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
  44. 0 1
      Emby.Dlna/PlayTo/SsdpHttpClient.cs
  45. 2 0
      Emby.Dlna/PlayTo/uBaseObject.cs
  46. 1 1
      Emby.Dlna/Profiles/DefaultProfile.cs
  47. 1 1
      Emby.Dlna/Profiles/Xml/Default.xml
  48. 1 1
      Emby.Dlna/Profiles/Xml/Denon AVR.xml
  49. 1 1
      Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml
  50. 1 1
      Emby.Dlna/Profiles/Xml/LG Smart TV.xml
  51. 1 1
      Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml
  52. 1 1
      Emby.Dlna/Profiles/Xml/Marantz.xml
  53. 1 1
      Emby.Dlna/Profiles/Xml/MediaMonkey.xml
  54. 1 1
      Emby.Dlna/Profiles/Xml/Panasonic Viera.xml
  55. 1 1
      Emby.Dlna/Profiles/Xml/Popcorn Hour.xml
  56. 1 1
      Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml
  57. 1 1
      Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml
  58. 1 1
      Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml
  59. 1 1
      Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml
  60. 1 1
      Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml
  61. 1 1
      Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml
  62. 1 1
      Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml
  63. 1 1
      Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml
  64. 1 1
      Emby.Dlna/Profiles/Xml/WDTV Live.xml
  65. 1 1
      Emby.Dlna/Profiles/Xml/Xbox One.xml
  66. 1 1
      Emby.Dlna/Profiles/Xml/foobar2000.xml
  67. 5 0
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  68. 4 0
      Emby.Dlna/Service/BaseControlHandler.cs
  69. 1 1
      Emby.Dlna/Service/BaseService.cs
  70. 1 0
      Emby.Dlna/Service/ServiceXmlBuilder.cs
  71. 7 11
      Emby.Dlna/Ssdp/DeviceDiscovery.cs
  72. 4 10
      Emby.Dlna/Ssdp/Extensions.cs
  73. 6 0
      Emby.Drawing/Emby.Drawing.csproj
  74. 43 51
      Emby.Drawing/ImageProcessor.cs
  75. 6 0
      Emby.Drawing/NullImageEncoder.cs
  76. 10 7
      Emby.Naming/Audio/AlbumParser.cs
  77. 2 1
      Emby.Naming/Audio/AudioFileParser.cs
  78. 1 0
      Emby.Naming/AudioBook/AudioBookFilePathParser.cs
  79. 1 6
      Emby.Naming/Common/EpisodeExpression.cs
  80. 3 3
      Emby.Naming/Common/MediaType.cs
  81. 3 2
      Emby.Naming/Common/NamingOptions.cs
  82. 5 0
      Emby.Naming/Emby.Naming.csproj
  83. 4 8
      Emby.Naming/Subtitles/SubtitleParser.cs
  84. 1 1
      Emby.Naming/Video/VideoListResolver.cs
  85. 3 3
      Emby.Naming/Video/VideoResolver.cs
  86. 6 4
      Emby.Notifications/Api/NotificationsService.cs
  87. 5 0
      Emby.Notifications/Emby.Notifications.csproj
  88. 2 2
      Emby.Notifications/NotificationEntryPoint.cs
  89. 6 4
      Emby.Notifications/NotificationManager.cs
  90. 6 0
      Emby.Photos/Emby.Photos.csproj
  91. 3 3
      Emby.Photos/PhotoProvider.cs
  92. 158 210
      Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
  93. 0 67
      Emby.Server.Implementations/Activity/ActivityManager.cs
  94. 0 313
      Emby.Server.Implementations/Activity/ActivityRepository.cs
  95. 5 0
      Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
  96. 2 2
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  97. 12 14
      Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
  98. 184 458
      Emby.Server.Implementations/ApplicationHost.cs
  99. 53 77
      Emby.Server.Implementations/Archiving/ZipClient.cs
  100. 4 2
      Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs

+ 21 - 28
.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
@@ -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)

+ 42 - 49
.ci/azure-pipelines-main.yml

@@ -1,6 +1,6 @@
 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:
@@ -13,88 +13,81 @@ jobs:
         Debug:
         Debug:
           BuildConfiguration: Debug
           BuildConfiguration: Debug
     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 Branch"
-        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 Target"
-        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: "12.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'

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

@@ -0,0 +1,131 @@
+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'
+    condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), 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="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'
+    condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
+    inputs:
+      targetPath: '$(Build.SourcesDirectory)/deployment/dist'
+      artifactName: 'jellyfin-server-$(BuildConfiguration)'
+
+  - task: CopyFilesOverSSH@0
+    displayName: 'Upload artifacts to repository server'
+    condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
+    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'
+
+  - 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)'

+ 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

+ 14 - 12
.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
@@ -17,17 +17,17 @@ trigger:
 jobs:
 jobs:
   - template: azure-pipelines-main.yml
   - template: azure-pipelines-main.yml
     parameters:
     parameters:
-      LinuxImage: "ubuntu-latest"
+      LinuxImage: 'ubuntu-latest'
       RestoreBuildProjects: $(RestoreBuildProjects)
       RestoreBuildProjects: $(RestoreBuildProjects)
 
 
   - 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-compat.yml
+  - template: azure-pipelines-abi.yml
     parameters:
     parameters:
       Packages:
       Packages:
         Naming:
         Naming:
@@ -42,4 +42,6 @@ jobs:
         Common:
         Common:
           NugetPackageName: Jellyfin.Common
           NugetPackageName: Jellyfin.Common
           AssemblyFileName: MediaBrowser.Common.dll
           AssemblyFileName: MediaBrowser.Common.dll
-      LinuxImage: "ubuntu-latest"
+      LinuxImage: 'ubuntu-latest'
+
+  - 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 - 8
.gitignore

@@ -39,7 +39,6 @@ ProgramData*/
 CorePlugins*/
 CorePlugins*/
 ProgramData-Server*/
 ProgramData-Server*/
 ProgramData-UI*/
 ProgramData-UI*/
-MediaBrowser.WebDashboard/jellyfin-web/**
 
 
 #################
 #################
 ## Visual Studio
 ## Visual Studio
@@ -245,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/
@@ -272,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

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

+ 18 - 10
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)
@@ -150,6 +158,7 @@ namespace Emby.Dlna.Eventing
                 builder.Append("</" + key + ">");
                 builder.Append("</" + key + ">");
                 builder.Append("</e:property>");
                 builder.Append("</e:property>");
             }
             }
+
             builder.Append("</e:propertyset>");
             builder.Append("</e:propertyset>");
 
 
             var options = new HttpRequestOptions
             var options = new HttpRequestOptions
@@ -169,7 +178,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;
             }
             }
         }
         }
     }
     }

+ 31 - 35
Emby.Dlna/PlayTo/Device.cs

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

+ 1 - 1
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";

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

+ 5 - 0
Emby.Dlna/Server/DescriptionXmlBuilder.cs

@@ -134,6 +134,7 @@ namespace Emby.Dlna.Server
                     return result;
                     return result;
                 }
                 }
             }
             }
+
             return c.ToString(CultureInfo.InvariantCulture);
             return c.ToString(CultureInfo.InvariantCulture);
         }
         }
 
 
@@ -157,18 +158,22 @@ namespace Emby.Dlna.Server
                 {
                 {
                     break;
                     break;
                 }
                 }
+
                 if (stringBuilder == null)
                 if (stringBuilder == null)
                 {
                 {
                     stringBuilder = new StringBuilder();
                     stringBuilder = new StringBuilder();
                 }
                 }
+
                 stringBuilder.Append(str, num, num2 - num);
                 stringBuilder.Append(str, num, num2 - num);
                 stringBuilder.Append(GetEscapeSequence(str[num2]));
                 stringBuilder.Append(GetEscapeSequence(str[num2]));
                 num = num2 + 1;
                 num = num2 + 1;
             }
             }
+
             if (stringBuilder == null)
             if (stringBuilder == null)
             {
             {
                 return str;
                 return str;
             }
             }
+
             stringBuilder.Append(str, num, length - num);
             stringBuilder.Append(str, num, length - num);
             return stringBuilder.ToString();
             return stringBuilder.ToString();
         }
         }

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

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

@@ -80,6 +80,7 @@ namespace Emby.Dlna.Service
                     {
                     {
                         builder.Append("<allowedValue>" + DescriptionXmlBuilder.Escape(allowedValue) + "</allowedValue>");
                         builder.Append("<allowedValue>" + DescriptionXmlBuilder.Escape(allowedValue) + "</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
     }
     }

+ 3 - 2
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\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*",
+                @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{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|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
                 @"(\[.*\])"
                 @"(\[.*\])"
             };
             };
 
 

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

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

+ 5 - 0
Emby.Notifications/Emby.Notifications.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>{2E030C33-6923-4530-9E54-FA29FA6AD1A9}</ProjectGuid>
+  </PropertyGroup>
+
   <PropertyGroup>
   <PropertyGroup>
     <TargetFramework>netstandard2.1</TargetFramework>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>

+ 2 - 2
Emby.Notifications/NotificationEntryPoint.cs

@@ -25,7 +25,7 @@ namespace Emby.Notifications
     /// </summary>
     /// </summary>
     public class NotificationEntryPoint : IServerEntryPoint
     public class NotificationEntryPoint : IServerEntryPoint
     {
     {
-        private readonly ILogger _logger;
+        private readonly ILogger<NotificationEntryPoint> _logger;
         private readonly IActivityManager _activityManager;
         private readonly IActivityManager _activityManager;
         private readonly ILocalizationManager _localization;
         private readonly ILocalizationManager _localization;
         private readonly INotificationManager _notificationManager;
         private readonly INotificationManager _notificationManager;
@@ -143,7 +143,7 @@ namespace Emby.Notifications
 
 
             var notification = new NotificationRequest
             var notification = new NotificationRequest
             {
             {
-                Description = "Please see jellyfin.media for details.",
+                Description = "Please see jellyfin.org for details.",
                 NotificationType = type,
                 NotificationType = type,
                 Name = _localization.GetLocalizedString("NewVersionIsAvailable")
                 Name = _localization.GetLocalizedString("NewVersionIsAvailable")
             };
             };

+ 6 - 4
Emby.Notifications/NotificationManager.cs

@@ -4,6 +4,8 @@ using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
@@ -21,7 +23,7 @@ namespace Emby.Notifications
     /// </summary>
     /// </summary>
     public class NotificationManager : INotificationManager
     public class NotificationManager : INotificationManager
     {
     {
-        private readonly ILogger _logger;
+        private readonly ILogger<NotificationManager> _logger;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
 
 
@@ -101,7 +103,7 @@ namespace Emby.Notifications
                 switch (request.SendToUserMode.Value)
                 switch (request.SendToUserMode.Value)
                 {
                 {
                     case SendToUserType.Admins:
                     case SendToUserType.Admins:
-                        return _userManager.Users.Where(i => i.Policy.IsAdministrator)
+                        return _userManager.Users.Where(i => i.HasPermission(PermissionKind.IsAdministrator))
                                 .Select(i => i.Id);
                                 .Select(i => i.Id);
                     case SendToUserType.All:
                     case SendToUserType.All:
                         return _userManager.UsersIds;
                         return _userManager.UsersIds;
@@ -117,7 +119,7 @@ namespace Emby.Notifications
                 var config = GetConfiguration();
                 var config = GetConfiguration();
 
 
                 return _userManager.Users
                 return _userManager.Users
-                    .Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i.Policy))
+                    .Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i))
                     .Select(i => i.Id);
                     .Select(i => i.Id);
             }
             }
 
 
@@ -142,7 +144,7 @@ namespace Emby.Notifications
                 User = user
                 User = user
             };
             };
 
 
-            _logger.LogDebug("Sending notification via {0} to user {1}", service.Name, user.Name);
+            _logger.LogDebug("Sending notification via {0} to user {1}", service.Name, user.Username);
 
 
             try
             try
             {
             {

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

@@ -1,4 +1,10 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
+
+  <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
+  <PropertyGroup>
+    <ProjectGuid>{89AB4548-770D-41FD-A891-8DAFF44F452C}</ProjectGuid>
+  </PropertyGroup>
+
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
     <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
     <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />

+ 3 - 3
Emby.Photos/PhotoProvider.cs

@@ -22,7 +22,7 @@ namespace Emby.Photos
     /// </summary>
     /// </summary>
     public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
     public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
     {
     {
-        private readonly ILogger _logger;
+        private readonly ILogger<PhotoProvider> _logger;
         private readonly IImageProcessor _imageProcessor;
         private readonly IImageProcessor _imageProcessor;
 
 
         // These are causing taglib to hang
         // These are causing taglib to hang
@@ -104,7 +104,7 @@ namespace Emby.Photos
                             item.Overview = image.ImageTag.Comment;
                             item.Overview = image.ImageTag.Comment;
 
 
                             if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)
                             if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)
-                                && !item.LockedFields.Contains(MetadataFields.Name))
+                                && !item.LockedFields.Contains(MetadataField.Name))
                             {
                             {
                                 item.Name = image.ImageTag.Title;
                                 item.Name = image.ImageTag.Title;
                             }
                             }
@@ -160,7 +160,7 @@ namespace Emby.Photos
 
 
                 try
                 try
                 {
                 {
-                    var size = _imageProcessor.GetImageDimensions(item, img, false);
+                    var size = _imageProcessor.GetImageDimensions(item, img);
 
 
                     if (size.Width > 0 && size.Height > 0)
                     if (size.Width > 0 && size.Height > 0)
                     {
                     {

+ 158 - 210
Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs

@@ -1,16 +1,13 @@
-#pragma warning disable CS1591
-
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Text;
 using System.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Session;
@@ -27,9 +24,12 @@ using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.Activity
 namespace Emby.Server.Implementations.Activity
 {
 {
+    /// <summary>
+    /// Entry point for the activity logger.
+    /// </summary>
     public sealed class ActivityLogEntryPoint : IServerEntryPoint
     public sealed class ActivityLogEntryPoint : IServerEntryPoint
     {
     {
-        private readonly ILogger _logger;
+        private readonly ILogger<ActivityLogEntryPoint> _logger;
         private readonly IInstallationManager _installationManager;
         private readonly IInstallationManager _installationManager;
         private readonly ISessionManager _sessionManager;
         private readonly ISessionManager _sessionManager;
         private readonly ITaskManager _taskManager;
         private readonly ITaskManager _taskManager;
@@ -37,25 +37,21 @@ namespace Emby.Server.Implementations.Activity
         private readonly ILocalizationManager _localization;
         private readonly ILocalizationManager _localization;
         private readonly ISubtitleManager _subManager;
         private readonly ISubtitleManager _subManager;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
-        private readonly IDeviceManager _deviceManager;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
         /// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
         /// </summary>
         /// </summary>
-        /// <param name="logger"></param>
-        /// <param name="sessionManager"></param>
-        /// <param name="deviceManager"></param>
-        /// <param name="taskManager"></param>
-        /// <param name="activityManager"></param>
-        /// <param name="localization"></param>
-        /// <param name="installationManager"></param>
-        /// <param name="subManager"></param>
-        /// <param name="userManager"></param>
-        /// <param name="appHost"></param>
+        /// <param name="logger">The logger.</param>
+        /// <param name="sessionManager">The session manager.</param>
+        /// <param name="taskManager">The task manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        /// <param name="localization">The localization manager.</param>
+        /// <param name="installationManager">The installation manager.</param>
+        /// <param name="subManager">The subtitle manager.</param>
+        /// <param name="userManager">The user manager.</param>
         public ActivityLogEntryPoint(
         public ActivityLogEntryPoint(
             ILogger<ActivityLogEntryPoint> logger,
             ILogger<ActivityLogEntryPoint> logger,
             ISessionManager sessionManager,
             ISessionManager sessionManager,
-            IDeviceManager deviceManager,
             ITaskManager taskManager,
             ITaskManager taskManager,
             IActivityManager activityManager,
             IActivityManager activityManager,
             ILocalizationManager localization,
             ILocalizationManager localization,
@@ -65,7 +61,6 @@ namespace Emby.Server.Implementations.Activity
         {
         {
             _logger = logger;
             _logger = logger;
             _sessionManager = sessionManager;
             _sessionManager = sessionManager;
-            _deviceManager = deviceManager;
             _taskManager = taskManager;
             _taskManager = taskManager;
             _activityManager = activityManager;
             _activityManager = activityManager;
             _localization = localization;
             _localization = localization;
@@ -74,6 +69,7 @@ namespace Emby.Server.Implementations.Activity
             _userManager = userManager;
             _userManager = userManager;
         }
         }
 
 
+        /// <inheritdoc />
         public Task RunAsync()
         public Task RunAsync()
         {
         {
             _taskManager.TaskCompleted += OnTaskCompleted;
             _taskManager.TaskCompleted += OnTaskCompleted;
@@ -92,58 +88,45 @@ namespace Emby.Server.Implementations.Activity
 
 
             _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure;
             _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure;
 
 
-            _userManager.UserCreated += OnUserCreated;
-            _userManager.UserPasswordChanged += OnUserPasswordChanged;
-            _userManager.UserDeleted += OnUserDeleted;
-            _userManager.UserPolicyUpdated += OnUserPolicyUpdated;
-            _userManager.UserLockedOut += OnUserLockedOut;
-
-            _deviceManager.CameraImageUploaded += OnCameraImageUploaded;
+            _userManager.OnUserCreated += OnUserCreated;
+            _userManager.OnUserPasswordChanged += OnUserPasswordChanged;
+            _userManager.OnUserDeleted += OnUserDeleted;
+            _userManager.OnUserLockedOut += OnUserLockedOut;
 
 
             return Task.CompletedTask;
             return Task.CompletedTask;
         }
         }
 
 
-        private void OnCameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e)
-        {
-            CreateLogEntry(new ActivityLogEntry
-            {
-                Name = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("CameraImageUploadedFrom"),
-                    e.Argument.Device.Name),
-                Type = NotificationType.CameraImageUploaded.ToString()
-            });
-        }
-
-        private void OnUserLockedOut(object sender, GenericEventArgs<User> e)
+        private async void OnUserLockedOut(object sender, GenericEventArgs<User> e)
         {
         {
-            CreateLogEntry(new ActivityLogEntry
+            await CreateLogEntry(new ActivityLog(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        _localization.GetLocalizedString("UserLockedOutWithName"),
+                        e.Argument.Username),
+                    NotificationType.UserLockedOut.ToString(),
+                    e.Argument.Id)
             {
             {
-                Name = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserLockedOutWithName"),
-                    e.Argument.Name),
-                Type = NotificationType.UserLockedOut.ToString(),
-                UserId = e.Argument.Id
-            });
+                LogSeverity = LogLevel.Error
+            }).ConfigureAwait(false);
         }
         }
 
 
-        private void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
+        private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
         {
         {
-            CreateLogEntry(new ActivityLogEntry
-            {
-                Name = string.Format(
+            await CreateLogEntry(new ActivityLog(
+                string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
                     _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
                     e.Provider,
                     e.Provider,
-                    Emby.Notifications.NotificationEntryPoint.GetItemName(e.Item)),
-                Type = "SubtitleDownloadFailure",
+                    Notifications.NotificationEntryPoint.GetItemName(e.Item)),
+                "SubtitleDownloadFailure",
+                Guid.Empty)
+            {
                 ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
                 ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
                 ShortOverview = e.Exception.Message
                 ShortOverview = e.Exception.Message
-            });
+            }).ConfigureAwait(false);
         }
         }
 
 
-        private void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
+        private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
         {
         {
             var item = e.MediaInfo;
             var item = e.MediaInfo;
 
 
@@ -166,15 +149,19 @@ namespace Emby.Server.Implementations.Activity
 
 
             var user = e.Users[0];
             var user = e.Users[0];
 
 
-            CreateLogEntry(new ActivityLogEntry
-            {
-                Name = string.Format(_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName),
-                Type = GetPlaybackStoppedNotificationType(item.MediaType),
-                UserId = user.Id
-            });
+            await CreateLogEntry(new ActivityLog(
+                string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
+                    user.Username,
+                    GetItemName(item),
+                    e.DeviceName),
+                GetPlaybackStoppedNotificationType(item.MediaType),
+                user.Id))
+                .ConfigureAwait(false);
         }
         }
 
 
-        private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
+        private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
         {
         {
             var item = e.MediaInfo;
             var item = e.MediaInfo;
 
 
@@ -197,17 +184,16 @@ namespace Emby.Server.Implementations.Activity
 
 
             var user = e.Users.First();
             var user = e.Users.First();
 
 
-            CreateLogEntry(new ActivityLogEntry
-            {
-                Name = string.Format(
+            await CreateLogEntry(new ActivityLog(
+                string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
                     _localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
-                    user.Name,
+                    user.Username,
                     GetItemName(item),
                     GetItemName(item),
                     e.DeviceName),
                     e.DeviceName),
-                Type = GetPlaybackNotificationType(item.MediaType),
-                UserId = user.Id
-            });
+                GetPlaybackNotificationType(item.MediaType),
+                user.Id))
+                .ConfigureAwait(false);
         }
         }
 
 
         private static string GetItemName(BaseItemDto item)
         private static string GetItemName(BaseItemDto item)
@@ -257,236 +243,203 @@ namespace Emby.Server.Implementations.Activity
             return null;
             return null;
         }
         }
 
 
-        private void OnSessionEnded(object sender, SessionEventArgs e)
+        private async void OnSessionEnded(object sender, SessionEventArgs e)
         {
         {
-            string name;
             var session = e.SessionInfo;
             var session = e.SessionInfo;
 
 
             if (string.IsNullOrEmpty(session.UserName))
             if (string.IsNullOrEmpty(session.UserName))
             {
             {
-                name = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("DeviceOfflineWithName"),
-                    session.DeviceName);
-
-                // Causing too much spam for now
                 return;
                 return;
             }
             }
-            else
-            {
-                name = string.Format(
+
+            await CreateLogEntry(new ActivityLog(
+                string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserOfflineFromDevice"),
                     _localization.GetLocalizedString("UserOfflineFromDevice"),
                     session.UserName,
                     session.UserName,
-                    session.DeviceName);
-            }
-
-            CreateLogEntry(new ActivityLogEntry
+                    session.DeviceName),
+                "SessionEnded",
+                session.UserId)
             {
             {
-                Name = name,
-                Type = "SessionEnded",
                 ShortOverview = string.Format(
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("LabelIpAddressValue"),
                     _localization.GetLocalizedString("LabelIpAddressValue"),
                     session.RemoteEndPoint),
                     session.RemoteEndPoint),
-                UserId = session.UserId
-            });
+            }).ConfigureAwait(false);
         }
         }
 
 
-        private void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
+        private async void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
         {
         {
             var user = e.Argument.User;
             var user = e.Argument.User;
 
 
-            CreateLogEntry(new ActivityLogEntry
-            {
-                Name = string.Format(
+            await CreateLogEntry(new ActivityLog(
+                string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
                     _localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
                     user.Name),
                     user.Name),
-                Type = "AuthenticationSucceeded",
+                "AuthenticationSucceeded",
+                user.Id)
+            {
                 ShortOverview = string.Format(
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("LabelIpAddressValue"),
                     _localization.GetLocalizedString("LabelIpAddressValue"),
                     e.Argument.SessionInfo.RemoteEndPoint),
                     e.Argument.SessionInfo.RemoteEndPoint),
-                UserId = user.Id
-            });
+            }).ConfigureAwait(false);
         }
         }
 
 
-        private void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
+        private async void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
         {
         {
-            CreateLogEntry(new ActivityLogEntry
-            {
-                Name = string.Format(
+            await CreateLogEntry(new ActivityLog(
+                string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
                     _localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
                     e.Argument.Username),
                     e.Argument.Username),
-                Type = "AuthenticationFailed",
+                "AuthenticationFailed",
+                Guid.Empty)
+            {
+                LogSeverity = LogLevel.Error,
                 ShortOverview = string.Format(
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("LabelIpAddressValue"),
                     _localization.GetLocalizedString("LabelIpAddressValue"),
                     e.Argument.RemoteEndPoint),
                     e.Argument.RemoteEndPoint),
-                Severity = LogLevel.Error
-            });
+            }).ConfigureAwait(false);
         }
         }
 
 
-        private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
+        private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
         {
         {
-            CreateLogEntry(new ActivityLogEntry
-            {
-                Name = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserPolicyUpdatedWithName"),
-                    e.Argument.Name),
-                Type = "UserPolicyUpdated",
-                UserId = e.Argument.Id
-            });
-        }
-
-        private void OnUserDeleted(object sender, GenericEventArgs<User> e)
-        {
-            CreateLogEntry(new ActivityLogEntry
-            {
-                Name = string.Format(
+            await CreateLogEntry(new ActivityLog(
+                string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserDeletedWithName"),
                     _localization.GetLocalizedString("UserDeletedWithName"),
-                    e.Argument.Name),
-                Type = "UserDeleted"
-            });
+                    e.Argument.Username),
+                "UserDeleted",
+                Guid.Empty))
+                .ConfigureAwait(false);
         }
         }
 
 
-        private void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
+        private async void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
         {
         {
-            CreateLogEntry(new ActivityLogEntry
-            {
-                Name = string.Format(
+            await CreateLogEntry(new ActivityLog(
+                string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserPasswordChangedWithName"),
                     _localization.GetLocalizedString("UserPasswordChangedWithName"),
-                    e.Argument.Name),
-                Type = "UserPasswordChanged",
-                UserId = e.Argument.Id
-            });
+                    e.Argument.Username),
+                "UserPasswordChanged",
+                e.Argument.Id))
+                .ConfigureAwait(false);
         }
         }
 
 
-        private void OnUserCreated(object sender, GenericEventArgs<User> e)
+        private async void OnUserCreated(object sender, GenericEventArgs<User> e)
         {
         {
-            CreateLogEntry(new ActivityLogEntry
-            {
-                Name = string.Format(
+            await CreateLogEntry(new ActivityLog(
+                string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserCreatedWithName"),
                     _localization.GetLocalizedString("UserCreatedWithName"),
-                    e.Argument.Name),
-                Type = "UserCreated",
-                UserId = e.Argument.Id
-            });
+                    e.Argument.Username),
+                "UserCreated",
+                e.Argument.Id))
+                .ConfigureAwait(false);
         }
         }
 
 
-        private void OnSessionStarted(object sender, SessionEventArgs e)
+        private async void OnSessionStarted(object sender, SessionEventArgs e)
         {
         {
-            string name;
             var session = e.SessionInfo;
             var session = e.SessionInfo;
 
 
             if (string.IsNullOrEmpty(session.UserName))
             if (string.IsNullOrEmpty(session.UserName))
             {
             {
-                name = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("DeviceOnlineWithName"),
-                    session.DeviceName);
-
-                // Causing too much spam for now
                 return;
                 return;
             }
             }
-            else
-            {
-                name = string.Format(
+
+            await CreateLogEntry(new ActivityLog(
+                string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserOnlineFromDevice"),
                     _localization.GetLocalizedString("UserOnlineFromDevice"),
                     session.UserName,
                     session.UserName,
-                    session.DeviceName);
-            }
-
-            CreateLogEntry(new ActivityLogEntry
+                    session.DeviceName),
+                "SessionStarted",
+                session.UserId)
             {
             {
-                Name = name,
-                Type = "SessionStarted",
                 ShortOverview = string.Format(
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("LabelIpAddressValue"),
                     _localization.GetLocalizedString("LabelIpAddressValue"),
-                    session.RemoteEndPoint),
-                UserId = session.UserId
-            });
+                    session.RemoteEndPoint)
+            }).ConfigureAwait(false);
         }
         }
 
 
-        private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, PackageVersionInfo)> e)
+        private async void OnPluginUpdated(object sender, InstallationInfo e)
         {
         {
-            CreateLogEntry(new ActivityLogEntry
-            {
-                Name = string.Format(
+            await CreateLogEntry(new ActivityLog(
+                string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("PluginUpdatedWithName"),
                     _localization.GetLocalizedString("PluginUpdatedWithName"),
-                    e.Argument.Item1.Name),
-                Type = NotificationType.PluginUpdateInstalled.ToString(),
+                    e.Name),
+                NotificationType.PluginUpdateInstalled.ToString(),
+                Guid.Empty)
+            {
                 ShortOverview = string.Format(
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("VersionNumber"),
                     _localization.GetLocalizedString("VersionNumber"),
-                    e.Argument.Item2.versionStr),
-                Overview = e.Argument.Item2.description
-            });
+                    e.Version),
+                Overview = e.Changelog
+            }).ConfigureAwait(false);
         }
         }
 
 
-        private void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
+        private async void OnPluginUninstalled(object sender, IPlugin e)
         {
         {
-            CreateLogEntry(new ActivityLogEntry
-            {
-                Name = string.Format(
+            await CreateLogEntry(new ActivityLog(
+                string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("PluginUninstalledWithName"),
                     _localization.GetLocalizedString("PluginUninstalledWithName"),
-                    e.Argument.Name),
-                Type = NotificationType.PluginUninstalled.ToString()
-            });
+                    e.Name),
+                NotificationType.PluginUninstalled.ToString(),
+                Guid.Empty))
+                .ConfigureAwait(false);
         }
         }
 
 
-        private void OnPluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> e)
+        private async void OnPluginInstalled(object sender, InstallationInfo e)
         {
         {
-            CreateLogEntry(new ActivityLogEntry
-            {
-                Name = string.Format(
+            await CreateLogEntry(new ActivityLog(
+                string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("PluginInstalledWithName"),
                     _localization.GetLocalizedString("PluginInstalledWithName"),
-                    e.Argument.name),
-                Type = NotificationType.PluginInstalled.ToString(),
+                    e.Name),
+                NotificationType.PluginInstalled.ToString(),
+                Guid.Empty)
+            {
                 ShortOverview = string.Format(
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("VersionNumber"),
                     _localization.GetLocalizedString("VersionNumber"),
-                    e.Argument.versionStr)
-            });
+                    e.Version)
+            }).ConfigureAwait(false);
         }
         }
 
 
-        private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
+        private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
         {
         {
             var installationInfo = e.InstallationInfo;
             var installationInfo = e.InstallationInfo;
 
 
-            CreateLogEntry(new ActivityLogEntry
-            {
-                Name = string.Format(
+            await CreateLogEntry(new ActivityLog(
+                string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("NameInstallFailed"),
                     _localization.GetLocalizedString("NameInstallFailed"),
                     installationInfo.Name),
                     installationInfo.Name),
-                Type = NotificationType.InstallationFailed.ToString(),
+                NotificationType.InstallationFailed.ToString(),
+                Guid.Empty)
+            {
                 ShortOverview = string.Format(
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("VersionNumber"),
                     _localization.GetLocalizedString("VersionNumber"),
                     installationInfo.Version),
                     installationInfo.Version),
                 Overview = e.Exception.Message
                 Overview = e.Exception.Message
-            });
+            }).ConfigureAwait(false);
         }
         }
 
 
-        private void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
+        private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
         {
         {
             var result = e.Result;
             var result = e.Result;
             var task = e.Task;
             var task = e.Task;
 
 
-            var activityTask = task.ScheduledTask as IConfigurableScheduledTask;
-            if (activityTask != null && !activityTask.IsLogged)
+            if (task.ScheduledTask is IConfigurableScheduledTask activityTask
+                && !activityTask.IsLogged)
             {
             {
                 return;
                 return;
             }
             }
@@ -511,22 +464,20 @@ namespace Emby.Server.Implementations.Activity
                     vals.Add(e.Result.LongErrorMessage);
                     vals.Add(e.Result.LongErrorMessage);
                 }
                 }
 
 
-                CreateLogEntry(new ActivityLogEntry
+                await CreateLogEntry(new ActivityLog(
+                    string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
+                    NotificationType.TaskFailed.ToString(),
+                    Guid.Empty)
                 {
                 {
-                    Name = string.Format(
-                        CultureInfo.InvariantCulture,
-                        _localization.GetLocalizedString("ScheduledTaskFailedWithName"),
-                        task.Name),
-                    Type = NotificationType.TaskFailed.ToString(),
+                    LogSeverity = LogLevel.Error,
                     Overview = string.Join(Environment.NewLine, vals),
                     Overview = string.Join(Environment.NewLine, vals),
-                    ShortOverview = runningTime,
-                    Severity = LogLevel.Error
-                });
+                    ShortOverview = runningTime
+                }).ConfigureAwait(false);
             }
             }
         }
         }
 
 
-        private void CreateLogEntry(ActivityLogEntry entry)
-            => _activityManager.Create(entry);
+        private async Task CreateLogEntry(ActivityLog entry)
+            => await _activityManager.CreateAsync(entry).ConfigureAwait(false);
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public void Dispose()
         public void Dispose()
@@ -548,19 +499,16 @@ namespace Emby.Server.Implementations.Activity
 
 
             _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure;
             _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure;
 
 
-            _userManager.UserCreated -= OnUserCreated;
-            _userManager.UserPasswordChanged -= OnUserPasswordChanged;
-            _userManager.UserDeleted -= OnUserDeleted;
-            _userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
-            _userManager.UserLockedOut -= OnUserLockedOut;
-
-            _deviceManager.CameraImageUploaded -= OnCameraImageUploaded;
+            _userManager.OnUserCreated -= OnUserCreated;
+            _userManager.OnUserPasswordChanged -= OnUserPasswordChanged;
+            _userManager.OnUserDeleted -= OnUserDeleted;
+            _userManager.OnUserLockedOut -= OnUserLockedOut;
         }
         }
 
 
         /// <summary>
         /// <summary>
         /// Constructs a user-friendly string for this TimeSpan instance.
         /// Constructs a user-friendly string for this TimeSpan instance.
         /// </summary>
         /// </summary>
-        public static string ToUserFriendlyString(TimeSpan span)
+        private static string ToUserFriendlyString(TimeSpan span)
         {
         {
             const int DaysInYear = 365;
             const int DaysInYear = 365;
             const int DaysInMonth = 30;
             const int DaysInMonth = 30;
@@ -574,7 +522,7 @@ namespace Emby.Server.Implementations.Activity
             {
             {
                 int years = days / DaysInYear;
                 int years = days / DaysInYear;
                 values.Add(CreateValueString(years, "year"));
                 values.Add(CreateValueString(years, "year"));
-                days = days % DaysInYear;
+                days %= DaysInYear;
             }
             }
 
 
             // Number of months
             // Number of months

+ 0 - 67
Emby.Server.Implementations/Activity/ActivityManager.cs

@@ -1,67 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Querying;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Activity
-{
-    public class ActivityManager : IActivityManager
-    {
-        public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
-
-        private readonly IActivityRepository _repo;
-        private readonly ILogger _logger;
-        private readonly IUserManager _userManager;
-
-        public ActivityManager(
-            ILoggerFactory loggerFactory,
-            IActivityRepository repo,
-            IUserManager userManager)
-        {
-            _logger = loggerFactory.CreateLogger(nameof(ActivityManager));
-            _repo = repo;
-            _userManager = userManager;
-        }
-
-        public void Create(ActivityLogEntry entry)
-        {
-            entry.Date = DateTime.UtcNow;
-
-            _repo.Create(entry);
-
-            EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(entry));
-        }
-
-        public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit)
-        {
-            var result = _repo.GetActivityLogEntries(minDate, hasUserId, startIndex, limit);
-
-            foreach (var item in result.Items)
-            {
-                if (item.UserId == Guid.Empty)
-                {
-                    continue;
-                }
-
-                var user = _userManager.GetUserById(item.UserId);
-
-                if (user != null)
-                {
-                    var dto = _userManager.GetUserDto(user);
-                    item.UserPrimaryImageTag = dto.PrimaryImageTag;
-                }
-            }
-
-            return result;
-        }
-
-        public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit)
-        {
-            return GetActivityLogEntries(minDate, null, startIndex, limit);
-        }
-    }
-}

+ 0 - 313
Emby.Server.Implementations/Activity/ActivityRepository.cs

@@ -1,313 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using Emby.Server.Implementations.Data;
-using MediaBrowser.Controller;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Querying;
-using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Activity
-{
-    public class ActivityRepository : BaseSqliteRepository, IActivityRepository
-    {
-        private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
-        private readonly IFileSystem _fileSystem;
-
-        public ActivityRepository(ILoggerFactory loggerFactory, IServerApplicationPaths appPaths, IFileSystem fileSystem)
-            : base(loggerFactory.CreateLogger(nameof(ActivityRepository)))
-        {
-            DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db");
-            _fileSystem = fileSystem;
-        }
-
-        public void Initialize()
-        {
-            try
-            {
-                InitializeInternal();
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error loading database file. Will reset and retry.");
-
-                _fileSystem.DeleteFile(DbFilePath);
-
-                InitializeInternal();
-            }
-        }
-
-        private void InitializeInternal()
-        {
-            using (var connection = GetConnection())
-            {
-                connection.RunQueries(new[]
-                {
-                    "create table if not exists ActivityLog (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, Overview TEXT, ShortOverview TEXT, Type TEXT NOT NULL, ItemId TEXT, UserId TEXT, DateCreated DATETIME NOT NULL, LogSeverity TEXT NOT NULL)",
-                    "drop index if exists idx_ActivityLogEntries"
-                });
-
-                TryMigrate(connection);
-            }
-        }
-
-        private void TryMigrate(ManagedConnection connection)
-        {
-            try
-            {
-                if (TableExists(connection, "ActivityLogEntries"))
-                {
-                    connection.RunQueries(new[]
-                    {
-                        "INSERT INTO ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) SELECT Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity FROM ActivityLogEntries",
-                        "drop table if exists ActivityLogEntries"
-                    });
-                }
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error migrating activity log database");
-            }
-        }
-
-        private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLog";
-
-        public void Create(ActivityLogEntry entry)
-        {
-            if (entry == null)
-            {
-                throw new ArgumentNullException(nameof(entry));
-            }
-
-            using (var connection = GetConnection())
-            {
-                connection.RunInTransaction(db =>
-                {
-                    using (var statement = db.PrepareStatement("insert into ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)"))
-                    {
-                        statement.TryBind("@Name", entry.Name);
-
-                        statement.TryBind("@Overview", entry.Overview);
-                        statement.TryBind("@ShortOverview", entry.ShortOverview);
-                        statement.TryBind("@Type", entry.Type);
-                        statement.TryBind("@ItemId", entry.ItemId);
-
-                        if (entry.UserId.Equals(Guid.Empty))
-                        {
-                            statement.TryBindNull("@UserId");
-                        }
-                        else
-                        {
-                            statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture));
-                        }
-
-                        statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue());
-                        statement.TryBind("@LogSeverity", entry.Severity.ToString());
-
-                        statement.MoveNext();
-                    }
-                }, TransactionMode);
-            }
-        }
-
-        public void Update(ActivityLogEntry entry)
-        {
-            if (entry == null)
-            {
-                throw new ArgumentNullException(nameof(entry));
-            }
-
-            using (var connection = GetConnection())
-            {
-                connection.RunInTransaction(db =>
-                {
-                    using (var statement = db.PrepareStatement("Update ActivityLog set Name=@Name,Overview=@Overview,ShortOverview=@ShortOverview,Type=@Type,ItemId=@ItemId,UserId=@UserId,DateCreated=@DateCreated,LogSeverity=@LogSeverity where Id=@Id"))
-                    {
-                        statement.TryBind("@Id", entry.Id);
-
-                        statement.TryBind("@Name", entry.Name);
-                        statement.TryBind("@Overview", entry.Overview);
-                        statement.TryBind("@ShortOverview", entry.ShortOverview);
-                        statement.TryBind("@Type", entry.Type);
-                        statement.TryBind("@ItemId", entry.ItemId);
-
-                        if (entry.UserId.Equals(Guid.Empty))
-                        {
-                            statement.TryBindNull("@UserId");
-                        }
-                        else
-                        {
-                            statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture));
-                        }
-
-                        statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue());
-                        statement.TryBind("@LogSeverity", entry.Severity.ToString());
-
-                        statement.MoveNext();
-                    }
-                }, TransactionMode);
-            }
-        }
-
-        public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit)
-        {
-            var commandText = BaseActivitySelectText;
-            var whereClauses = new List<string>();
-
-            if (minDate.HasValue)
-            {
-                whereClauses.Add("DateCreated>=@DateCreated");
-            }
-            if (hasUserId.HasValue)
-            {
-                if (hasUserId.Value)
-                {
-                    whereClauses.Add("UserId not null");
-                }
-                else
-                {
-                    whereClauses.Add("UserId is null");
-                }
-            }
-
-            var whereTextWithoutPaging = whereClauses.Count == 0 ?
-              string.Empty :
-              " where " + string.Join(" AND ", whereClauses.ToArray());
-
-            if (startIndex.HasValue && startIndex.Value > 0)
-            {
-                var pagingWhereText = whereClauses.Count == 0 ?
-                    string.Empty :
-                    " where " + string.Join(" AND ", whereClauses.ToArray());
-
-                whereClauses.Add(
-                    string.Format(
-                        CultureInfo.InvariantCulture,
-                        "Id NOT IN (SELECT Id FROM ActivityLog {0} ORDER BY DateCreated DESC LIMIT {1})",
-                        pagingWhereText,
-                        startIndex.Value));
-            }
-
-            var whereText = whereClauses.Count == 0 ?
-                string.Empty :
-                " where " + string.Join(" AND ", whereClauses.ToArray());
-
-            commandText += whereText;
-
-            commandText += " ORDER BY DateCreated DESC";
-
-            if (limit.HasValue)
-            {
-                commandText += " LIMIT " + limit.Value.ToString(_usCulture);
-            }
-
-            var statementTexts = new[]
-            {
-                commandText,
-                "select count (Id) from ActivityLog" + whereTextWithoutPaging
-            };
-
-            var list = new List<ActivityLogEntry>();
-            var result = new QueryResult<ActivityLogEntry>();
-
-            using (var connection = GetConnection(true))
-            {
-                connection.RunInTransaction(
-                    db =>
-                    {
-                        var statements = PrepareAll(db, statementTexts).ToList();
-
-                        using (var statement = statements[0])
-                        {
-                            if (minDate.HasValue)
-                            {
-                                statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue());
-                            }
-
-                            foreach (var row in statement.ExecuteQuery())
-                            {
-                                list.Add(GetEntry(row));
-                            }
-                        }
-
-                        using (var statement = statements[1])
-                        {
-                            if (minDate.HasValue)
-                            {
-                                statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue());
-                            }
-
-                            result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
-                        }
-                    },
-                    ReadTransactionMode);
-            }
-
-            result.Items = list;
-            return result;
-        }
-
-        private static ActivityLogEntry GetEntry(IReadOnlyList<IResultSetValue> reader)
-        {
-            var index = 0;
-
-            var info = new ActivityLogEntry
-            {
-                Id = reader[index].ToInt64()
-            };
-
-            index++;
-            if (reader[index].SQLiteType != SQLiteType.Null)
-            {
-                info.Name = reader[index].ToString();
-            }
-
-            index++;
-            if (reader[index].SQLiteType != SQLiteType.Null)
-            {
-                info.Overview = reader[index].ToString();
-            }
-
-            index++;
-            if (reader[index].SQLiteType != SQLiteType.Null)
-            {
-                info.ShortOverview = reader[index].ToString();
-            }
-
-            index++;
-            if (reader[index].SQLiteType != SQLiteType.Null)
-            {
-                info.Type = reader[index].ToString();
-            }
-
-            index++;
-            if (reader[index].SQLiteType != SQLiteType.Null)
-            {
-                info.ItemId = reader[index].ToString();
-            }
-
-            index++;
-            if (reader[index].SQLiteType != SQLiteType.Null)
-            {
-                info.UserId = new Guid(reader[index].ToString());
-            }
-
-            index++;
-            info.Date = reader[index].ReadDateTime();
-
-            index++;
-            if (reader[index].SQLiteType != SQLiteType.Null)
-            {
-                info.Severity = (LogLevel)Enum.Parse(typeof(LogLevel), reader[index].ToString(), true);
-            }
-
-            return info;
-        }
-    }
-}

+ 5 - 0
Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs

@@ -15,6 +15,11 @@ namespace Emby.Server.Implementations.AppBase
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
         /// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
         /// </summary>
         /// </summary>
+        /// <param name="programDataPath">The program data path.</param>
+        /// <param name="logDirectoryPath">The log directory path.</param>
+        /// <param name="configurationDirectoryPath">The configuration directory path.</param>
+        /// <param name="cacheDirectoryPath">The cache directory path.</param>
+        /// <param name="webDirectoryPath">The web directory path.</param>
         protected BaseApplicationPaths(
         protected BaseApplicationPaths(
             string programDataPath,
             string programDataPath,
             string logDirectoryPath,
             string logDirectoryPath,

+ 2 - 2
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.AppBase
             CommonApplicationPaths = applicationPaths;
             CommonApplicationPaths = applicationPaths;
             XmlSerializer = xmlSerializer;
             XmlSerializer = xmlSerializer;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
-            Logger = loggerFactory.CreateLogger(GetType().Name);
+            Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
 
 
             UpdateCachePath();
             UpdateCachePath();
         }
         }
@@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.AppBase
         /// Gets the logger.
         /// Gets the logger.
         /// </summary>
         /// </summary>
         /// <value>The logger.</value>
         /// <value>The logger.</value>
-        protected ILogger Logger { get; private set; }
+        protected ILogger<BaseConfigurationManager> Logger { get; private set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the XML serializer.
         /// Gets the XML serializer.

+ 12 - 14
Emby.Server.Implementations/AppBase/ConfigurationHelper.cs

@@ -36,24 +36,22 @@ namespace Emby.Server.Implementations.AppBase
                 configuration = Activator.CreateInstance(type);
                 configuration = Activator.CreateInstance(type);
             }
             }
 
 
-            using (var stream = new MemoryStream())
-            {
-                xmlSerializer.SerializeToStream(configuration, stream);
-
-                // Take the object we just got and serialize it back to bytes
-                var newBytes = stream.ToArray();
+            using var stream = new MemoryStream();
+            xmlSerializer.SerializeToStream(configuration, stream);
 
 
-                // If the file didn't exist before, or if something has changed, re-save
-                if (buffer == null || !buffer.SequenceEqual(newBytes))
-                {
-                    Directory.CreateDirectory(Path.GetDirectoryName(path));
+            // Take the object we just got and serialize it back to bytes
+            var newBytes = stream.ToArray();
 
 
-                    // Save it after load in case we got new items
-                    File.WriteAllBytes(path, newBytes);
-                }
+            // If the file didn't exist before, or if something has changed, re-save
+            if (buffer == null || !buffer.SequenceEqual(newBytes))
+            {
+                Directory.CreateDirectory(Path.GetDirectoryName(path));
 
 
-                return configuration;
+                // Save it after load in case we got new items
+                File.WriteAllBytes(path, newBytes);
             }
             }
+
+            return configuration;
         }
         }
     }
     }
 }
 }

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 184 - 458
Emby.Server.Implementations/ApplicationHost.cs


+ 53 - 77
Emby.Server.Implementations/Archiving/ZipClient.cs

@@ -22,10 +22,8 @@ namespace Emby.Server.Implementations.Archiving
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles)
         public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles)
         {
         {
-            using (var fileStream = File.OpenRead(sourceFile))
-            {
-                ExtractAll(fileStream, targetPath, overwriteExistingFiles);
-            }
+            using var fileStream = File.OpenRead(sourceFile);
+            ExtractAll(fileStream, targetPath, overwriteExistingFiles);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -36,67 +34,61 @@ namespace Emby.Server.Implementations.Archiving
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles)
         public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles)
         {
         {
-            using (var reader = ReaderFactory.Open(source))
+            using var reader = ReaderFactory.Open(source);
+            var options = new ExtractionOptions
             {
             {
-                var options = new ExtractionOptions();
-                options.ExtractFullPath = true;
-
-                if (overwriteExistingFiles)
-                {
-                    options.Overwrite = true;
-                }
+                ExtractFullPath = true
+            };
 
 
-                reader.WriteAllToDirectory(targetPath, options);
+            if (overwriteExistingFiles)
+            {
+                options.Overwrite = true;
             }
             }
+
+            reader.WriteAllToDirectory(targetPath, options);
         }
         }
 
 
+        /// <inheritdoc />
         public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles)
         public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles)
         {
         {
-            using (var reader = ZipReader.Open(source))
+            using var reader = ZipReader.Open(source);
+            var options = new ExtractionOptions
             {
             {
-                var options = new ExtractionOptions();
-                options.ExtractFullPath = true;
+                ExtractFullPath = true,
+                Overwrite = overwriteExistingFiles
+            };
 
 
-                if (overwriteExistingFiles)
-                {
-                    options.Overwrite = true;
-                }
-
-                reader.WriteAllToDirectory(targetPath, options);
-            }
+            reader.WriteAllToDirectory(targetPath, options);
         }
         }
 
 
+        /// <inheritdoc />
         public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
         public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
         {
         {
-            using (var reader = GZipReader.Open(source))
+            using var reader = GZipReader.Open(source);
+            var options = new ExtractionOptions
             {
             {
-                var options = new ExtractionOptions();
-                options.ExtractFullPath = true;
+                ExtractFullPath = true,
+                Overwrite = overwriteExistingFiles
+            };
 
 
-                if (overwriteExistingFiles)
-                {
-                    options.Overwrite = true;
-                }
-
-                reader.WriteAllToDirectory(targetPath, options);
-            }
+            reader.WriteAllToDirectory(targetPath, options);
         }
         }
 
 
+        /// <inheritdoc />
         public void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName)
         public void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName)
         {
         {
-            using (var reader = GZipReader.Open(source))
+            using var reader = GZipReader.Open(source);
+            if (reader.MoveToNextEntry())
             {
             {
-                if (reader.MoveToNextEntry())
+                var entry = reader.Entry;
+
+                var filename = entry.Key;
+                if (string.IsNullOrWhiteSpace(filename))
                 {
                 {
-                    var entry = reader.Entry;
-
-                    var filename = entry.Key;
-                    if (string.IsNullOrWhiteSpace(filename))
-                    {
-                        filename = defaultFileName;
-                    }
-                    reader.WriteEntryToFile(Path.Combine(targetPath, filename));
+                    filename = defaultFileName;
                 }
                 }
+
+                reader.WriteEntryToFile(Path.Combine(targetPath, filename));
             }
             }
         }
         }
 
 
@@ -108,10 +100,8 @@ namespace Emby.Server.Implementations.Archiving
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles)
         public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles)
         {
         {
-            using (var fileStream = File.OpenRead(sourceFile))
-            {
-                ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles);
-            }
+            using var fileStream = File.OpenRead(sourceFile);
+            ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -122,21 +112,15 @@ namespace Emby.Server.Implementations.Archiving
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         public void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles)
         public void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles)
         {
         {
-            using (var archive = SevenZipArchive.Open(source))
+            using var archive = SevenZipArchive.Open(source);
+            using var reader = archive.ExtractAllEntries();
+            var options = new ExtractionOptions
             {
             {
-                using (var reader = archive.ExtractAllEntries())
-                {
-                    var options = new ExtractionOptions();
-                    options.ExtractFullPath = true;
-
-                    if (overwriteExistingFiles)
-                    {
-                        options.Overwrite = true;
-                    }
+                ExtractFullPath = true,
+                Overwrite = overwriteExistingFiles
+            };
 
 
-                    reader.WriteAllToDirectory(targetPath, options);
-                }
-            }
+            reader.WriteAllToDirectory(targetPath, options);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -147,10 +131,8 @@ namespace Emby.Server.Implementations.Archiving
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         public void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles)
         public void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles)
         {
         {
-            using (var fileStream = File.OpenRead(sourceFile))
-            {
-                ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles);
-            }
+            using var fileStream = File.OpenRead(sourceFile);
+            ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -161,21 +143,15 @@ namespace Emby.Server.Implementations.Archiving
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
         public void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles)
         public void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles)
         {
         {
-            using (var archive = TarArchive.Open(source))
+            using var archive = TarArchive.Open(source);
+            using var reader = archive.ExtractAllEntries();
+            var options = new ExtractionOptions
             {
             {
-                using (var reader = archive.ExtractAllEntries())
-                {
-                    var options = new ExtractionOptions();
-                    options.ExtractFullPath = true;
+                ExtractFullPath = true,
+                Overwrite = overwriteExistingFiles
+            };
 
 
-                    if (overwriteExistingFiles)
-                    {
-                        options.Overwrite = true;
-                    }
-
-                    reader.WriteAllToDirectory(targetPath, options);
-                }
-            }
+            reader.WriteAllToDirectory(targetPath, options);
         }
         }
     }
     }
 }
 }

+ 4 - 2
Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs

@@ -1,13 +1,15 @@
-#pragma warning disable CS1591
-
 using System.Collections.Generic;
 using System.Collections.Generic;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.Branding;
 using MediaBrowser.Model.Branding;
 
 
 namespace Emby.Server.Implementations.Branding
 namespace Emby.Server.Implementations.Branding
 {
 {
+    /// <summary>
+    /// A configuration factory for <see cref="BrandingOptions"/>.
+    /// </summary>
     public class BrandingConfigurationFactory : IConfigurationFactory
     public class BrandingConfigurationFactory : IConfigurationFactory
     {
     {
+        /// <inheritdoc />
         public IEnumerable<ConfigurationStore> GetConfigurations()
         public IEnumerable<ConfigurationStore> GetConfigurations()
         {
         {
             return new[]
             return new[]

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů