فهرست منبع

Merge remote-tracking branch 'upstream/master' into external-id-type

Mark Monteiro 5 سال پیش
والد
کامیت
0e9164351b
100فایلهای تغییر یافته به همراه1767 افزوده شده و 1242 حذف شده
  1. 12 19
      .ci/azure-pipelines-abi.yml
  2. 22 22
      .ci/azure-pipelines-main.yml
  3. 131 0
      .ci/azure-pipelines-package.yml
  4. 5 1
      .ci/azure-pipelines-test.yml
  5. 10 8
      .ci/azure-pipelines.yml
  6. 9 0
      .github/dependabot.yml
  7. 1 2
      .gitignore
  8. 6 6
      .vscode/launch.json
  9. 17 2
      .vscode/tasks.json
  10. 2 0
      CONTRIBUTORS.md
  11. 1 1
      Dockerfile
  12. 1 1
      Dockerfile.arm
  13. 1 0
      DvdLib/Ifo/Cell.cs
  14. 2 0
      DvdLib/Ifo/Chapter.cs
  15. 13 3
      DvdLib/Ifo/Dvd.cs
  16. 8 2
      DvdLib/Ifo/DvdTime.cs
  17. 1 1
      DvdLib/Ifo/Program.cs
  18. 13 2
      DvdLib/Ifo/ProgramChain.cs
  19. 8 1
      DvdLib/Ifo/Title.cs
  20. 7 9
      Emby.Dlna/ContentDirectory/ContentDirectory.cs
  21. 24 16
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  22. 21 15
      Emby.Dlna/Didl/DidlBuilder.cs
  23. 1 2
      Emby.Dlna/Didl/Filter.cs
  24. 28 8
      Emby.Dlna/DlnaManager.cs
  25. 18 10
      Emby.Dlna/Eventing/EventManager.cs
  26. 3 0
      Emby.Dlna/Eventing/EventSubscription.cs
  27. 37 29
      Emby.Dlna/Main/DlnaEntryPoint.cs
  28. 31 35
      Emby.Dlna/PlayTo/Device.cs
  29. 26 8
      Emby.Dlna/PlayTo/PlayToController.cs
  30. 12 6
      Emby.Dlna/PlayTo/PlayToManager.cs
  31. 1 0
      Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
  32. 0 1
      Emby.Dlna/PlayTo/SsdpHttpClient.cs
  33. 2 0
      Emby.Dlna/PlayTo/uBaseObject.cs
  34. 5 0
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  35. 4 0
      Emby.Dlna/Service/BaseControlHandler.cs
  36. 1 1
      Emby.Dlna/Service/BaseService.cs
  37. 1 0
      Emby.Dlna/Service/ServiceXmlBuilder.cs
  38. 7 11
      Emby.Dlna/Ssdp/DeviceDiscovery.cs
  39. 4 10
      Emby.Dlna/Ssdp/Extensions.cs
  40. 34 4
      Emby.Drawing/ImageProcessor.cs
  41. 6 0
      Emby.Drawing/NullImageEncoder.cs
  42. 1 0
      Emby.Naming/AudioBook/AudioBookFilePathParser.cs
  43. 3 3
      Emby.Naming/Common/MediaType.cs
  44. 1 1
      Emby.Naming/Common/NamingOptions.cs
  45. 6 4
      Emby.Notifications/Api/NotificationsService.cs
  46. 1 1
      Emby.Notifications/NotificationEntryPoint.cs
  47. 6 4
      Emby.Notifications/NotificationManager.cs
  48. 2 2
      Emby.Photos/PhotoProvider.cs
  49. 31 43
      Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
  50. 2 2
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  51. 8 12
      Emby.Server.Implementations/ApplicationHost.cs
  52. 1 1
      Emby.Server.Implementations/Browser/BrowserLauncher.cs
  53. 12 7
      Emby.Server.Implementations/Channels/ChannelManager.cs
  54. 1 1
      Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
  55. 4 3
      Emby.Server.Implementations/Collections/CollectionManager.cs
  56. 1 1
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  57. 0 1
      Emby.Server.Implementations/ConfigurationOptions.cs
  58. 2 2
      Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
  59. 4 5
      Emby.Server.Implementations/Data/BaseSqliteRepository.cs
  60. 1 2
      Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
  61. 4 4
      Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
  62. 211 39
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  63. 8 4
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  64. 0 240
      Emby.Server.Implementations/Data/SqliteUserRepository.cs
  65. 2 2
      Emby.Server.Implementations/Devices/DeviceId.cs
  66. 12 28
      Emby.Server.Implementations/Devices/DeviceManager.cs
  67. 116 36
      Emby.Server.Implementations/Dto/DtoService.cs
  68. 9 7
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  69. 1 1
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  70. 4 4
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  71. 12 11
      Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
  72. 0 77
      Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs
  73. 26 45
      Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs
  74. 9 6
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  75. 2 2
      Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
  76. 2 2
      Emby.Server.Implementations/HttpServer/FileWriter.cs
  77. 30 17
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  78. 19 12
      Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
  79. 84 97
      Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
  80. 2 3
      Emby.Server.Implementations/HttpServer/ResponseFilter.cs
  81. 32 13
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  82. 71 31
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  83. 1 1
      Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
  84. 42 2
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  85. 3 41
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  86. 4 2
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  87. 6 2
      Emby.Server.Implementations/IStartupOptions.cs
  88. 60 0
      Emby.Server.Implementations/Images/ArtistImageProvider.cs
  89. 10 3
      Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
  90. 3 2
      Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
  91. 4 5
      Emby.Server.Implementations/Images/DynamicImageProvider.cs
  92. 6 8
      Emby.Server.Implementations/Images/FolderImageProvider.cs
  93. 25 54
      Emby.Server.Implementations/Images/GenreImageProvider.cs
  94. 66 0
      Emby.Server.Implementations/Images/PlaylistImageProvider.cs
  95. 15 46
      Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
  96. 2 0
      Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
  97. 74 0
      Emby.Server.Implementations/Library/IgnorePatterns.cs
  98. 133 22
      Emby.Server.Implementations/Library/LibraryManager.cs
  99. 2 5
      Emby.Server.Implementations/Library/LiveStreamHelper.cs
  100. 32 37
      Emby.Server.Implementations/Library/MediaSourceManager.cs

+ 12 - 19
.ci/azure-pipelines-compat.yml → .ci/azure-pipelines-abi.yml

@@ -33,6 +33,13 @@ jobs:
           packageType: sdk
           version: ${{ parameters.DotNetSdkVersion }}
 
+      - task: DotNetCoreCLI@2
+        displayName: 'Install ABI CompatibilityChecker tool'
+        inputs:
+          command: custom
+          custom: tool
+          arguments: 'update compatibilitychecker -g'
+
       - task: DownloadPipelineArtifact@2
         displayName: "Download New Assembly Build Artifact"
         inputs:
@@ -72,25 +79,11 @@ jobs:
           overWrite: true
           flattenFolders: true
 
-      - task: DownloadGitHubRelease@0
-        displayName: "Download ABI Compatibility Check Tool"
-        inputs:
-          connection: Jellyfin Release Download
-          userRepository: EraYaN/dotnet-compatibility
-          defaultVersionType: "latest"
-          itemPattern: "**-ci.zip"
-          downloadPath: "$(System.ArtifactsDirectory)"
-
-      - task: ExtractFiles@1
-        displayName: "Extract ABI Compatibility Check Tool"
-        inputs:
-          archiveFilePatterns: "$(System.ArtifactsDirectory)/*-ci.zip"
-          destinationFolder: $(System.ArtifactsDirectory)/tools
-          cleanDestinationFolder: true
-
       # The `--warnings-only` switch will swallow the return code and not emit any errors.
-      - task: CmdLine@2
-        displayName: "Execute ABI Compatibility Check Tool"
+      - task: DotNetCoreCLI@2
+        displayName: 'Execute ABI Compatibility Check Tool'
         inputs:
-          script: "dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only"
+          command: custom
+          custom: compat
+          arguments: 'current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only'
           workingDirectory: $(System.ArtifactsDirectory)

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

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

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

+ 5 - 1
.ci/azure-pipelines-test.yml

@@ -45,6 +45,7 @@ jobs:
       - task: SonarCloudPrepare@1
         displayName: 'Prepare analysis on SonarCloud'
         condition: eq(variables['ImageName'], 'ubuntu-latest')
+        enabled: false
         inputs:
           SonarCloud: 'Sonarcloud for Jellyfin'
           organization: 'jellyfin'
@@ -63,14 +64,17 @@ jobs:
       - task: SonarCloudAnalyze@1
         displayName: 'Run Code Analysis'
         condition: eq(variables['ImageName'], 'ubuntu-latest')
+        enabled: false
 
       - task: SonarCloudPublish@1
         displayName: 'Publish Quality Gate Result'
         condition: eq(variables['ImageName'], 'ubuntu-latest')
+        enabled: false
 
       - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
         displayName: 'Run ReportGenerator'
+        enabled: false
         inputs:
           reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
           targetdir: "$(Agent.TempDirectory)/merged/"
@@ -80,10 +84,10 @@ jobs:
       - task: PublishCodeCoverageResults@1
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
         displayName: 'Publish Code Coverage'
+        enabled: false
         inputs:
           codeCoverageTool: "cobertura"
           #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
           summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml"
           pathToSources: $(Build.SourcesDirectory)
           failIfCoverageEmpty: true
-

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

@@ -2,9 +2,9 @@ name: $(Date:yyyyMMdd)$(Rev:.r)
 
 variables:
 - name: TestProjects
-  value: "tests/**/*Tests.csproj"
+  value: 'tests/**/*Tests.csproj'
 - name: RestoreBuildProjects
-  value: "Jellyfin.Server/Jellyfin.Server.csproj"
+  value: 'Jellyfin.Server/Jellyfin.Server.csproj'
 - name: DotNetSdkVersion
   value: 3.1.100
 
@@ -17,17 +17,17 @@ trigger:
 jobs:
   - template: azure-pipelines-main.yml
     parameters:
-      LinuxImage: "ubuntu-latest"
+      LinuxImage: 'ubuntu-latest'
       RestoreBuildProjects: $(RestoreBuildProjects)
 
   - template: azure-pipelines-test.yml
     parameters:
       ImageNames:
-        Linux: "ubuntu-latest"
-        Windows: "windows-latest"
-        macOS: "macos-latest"
+        Linux: 'ubuntu-latest'
+        Windows: 'windows-latest'
+        macOS: 'macos-latest'
 
-  - template: azure-pipelines-compat.yml
+  - template: azure-pipelines-abi.yml
     parameters:
       Packages:
         Naming:
@@ -42,4 +42,6 @@ jobs:
         Common:
           NugetPackageName: Jellyfin.Common
           AssemblyFileName: MediaBrowser.Common.dll
-      LinuxImage: "ubuntu-latest"
+      LinuxImage: 'ubuntu-latest'
+
+  - template: azure-pipelines-package.yml

+ 9 - 0
.github/dependabot.yml

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

+ 1 - 2
.gitignore

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

+ 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)",
             "type": "coreclr",
@@ -24,5 +21,8 @@
             "request": "attach",
             "processId": "${command:pickProcess}"
         }
-    ,]
+    ],
+    "env": {
+        "DOTNET_CLI_TELEMETRY_OPTOUT": "1"
+    }
 }

+ 17 - 2
.vscode/tasks.json

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

+ 2 - 0
CONTRIBUTORS.md

@@ -7,6 +7,7 @@
  - [anthonylavado](https://github.com/anthonylavado)
  - [Artiume](https://github.com/Artiume)
  - [AThomsen](https://github.com/AThomsen)
+ - [barronpm](https://github.com/barronpm)
  - [bilde2910](https://github.com/bilde2910)
  - [bfayers](https://github.com/bfayers)
  - [BnMcG](https://github.com/BnMcG)
@@ -130,6 +131,7 @@
  - [XVicarious](https://github.com/XVicarious)
  - [YouKnowBlom](https://github.com/YouKnowBlom)
  - [KristupasSavickas](https://github.com/KristupasSavickas)
+ - [Pusta](https://github.com/pusta)
 
 # Emby Contributors
 

+ 1 - 1
Dockerfile

@@ -2,7 +2,7 @@ ARG DOTNET_VERSION=3.1
 
 FROM node:alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm \
+RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
  && yarn install \

+ 1 - 1
Dockerfile.arm

@@ -38,7 +38,7 @@ COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
 RUN apt-get update \
  && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
  curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
- curl -s https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \
+ curl -ks https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \
  echo 'deb [arch=armhf] https://repo.jellyfin.org/debian buster main' > /etc/apt/sources.list.d/jellyfin.list && \
  echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \
  apt-get update && \

+ 1 - 0
DvdLib/Ifo/Cell.cs

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

+ 2 - 0
DvdLib/Ifo/Chapter.cs

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

+ 13 - 3
DvdLib/Ifo/Dvd.cs

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

+ 8 - 2
DvdLib/Ifo/DvdTime.cs

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

+ 1 - 1
DvdLib/Ifo/Program.cs

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

+ 13 - 2
DvdLib/Ifo/ProgramChain.cs

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

+ 8 - 1
DvdLib/Ifo/Title.cs

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

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

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

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

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

+ 21 - 15
Emby.Dlna/Didl/DidlBuilder.cs

@@ -6,14 +6,13 @@ using System.IO;
 using System.Linq;
 using System.Text;
 using System.Xml;
-using Emby.Dlna.Configuration;
 using Emby.Dlna.ContentDirectory;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Playlists;
@@ -23,6 +22,13 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Net;
 using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
+using XmlAttribute = MediaBrowser.Model.Dlna.XmlAttribute;
 
 namespace Emby.Dlna.Didl
 {
@@ -92,21 +98,21 @@ namespace Emby.Dlna.Didl
             {
                 using (var writer = XmlWriter.Create(builder, settings))
                 {
-                    //writer.WriteStartDocument();
+                    // writer.WriteStartDocument();
 
                     writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
 
                     writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
                     writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
                     writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
-                    //didl.SetAttribute("xmlns:sec", NS_SEC);
+                    // didl.SetAttribute("xmlns:sec", NS_SEC);
 
                     WriteXmlRootAttributes(_profile, writer);
 
                     WriteItemElement(writer, item, user, context, null, deviceId, filter, streamInfo);
 
                     writer.WriteFullEndElement();
-                    //writer.WriteEndDocument();
+                    // writer.WriteEndDocument();
                 }
 
                 return builder.ToString();
@@ -421,7 +427,6 @@ namespace Emby.Dlna.Didl
                     case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows");
                     case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes");
                     case StubType.Series: return _localization.GetLocalizedString("Shows");
-                    default: break;
                 }
             }
 
@@ -670,7 +675,7 @@ namespace Emby.Dlna.Didl
                 return;
             }
 
-            MediaBrowser.Model.Dlna.XmlAttribute secAttribute = null;
+            XmlAttribute secAttribute = null;
             foreach (var attribute in _profile.XmlRootAttributes)
             {
                 if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
@@ -700,13 +705,13 @@ namespace Emby.Dlna.Didl
         }
 
         /// <summary>
-        /// Adds fields used by both items and folders
+        /// Adds fields used by both items and folders.
         /// </summary>
         private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
         {
             // Don't filter on dc:title because not all devices will include it in the filter
             // MediaMonkey for example won't display content without a title
-            //if (filter.Contains("dc:title"))
+            // if (filter.Contains("dc:title"))
             {
                 AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC);
             }
@@ -745,7 +750,7 @@ namespace Emby.Dlna.Didl
                         AddValue(writer, "dc", "description", desc, NS_DC);
                     }
                 }
-                //if (filter.Contains("upnp:longDescription"))
+                // if (filter.Contains("upnp:longDescription"))
                 //{
                 //    if (!string.IsNullOrWhiteSpace(item.Overview))
                 //    {
@@ -760,6 +765,7 @@ namespace Emby.Dlna.Didl
                 {
                     AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC);
                 }
+
                 if (filter.Contains("upnp:rating"))
                 {
                     AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP);
@@ -995,7 +1001,6 @@ namespace Emby.Dlna.Didl
             }
 
             AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
-
         }
 
         private void AddImageResElement(
@@ -1048,10 +1053,12 @@ namespace Emby.Dlna.Didl
             {
                 return GetImageInfo(item, ImageType.Primary);
             }
+
             if (item.HasImage(ImageType.Thumb))
             {
                 return GetImageInfo(item, ImageType.Thumb);
             }
+
             if (item.HasImage(ImageType.Backdrop))
             {
                 if (item is Channel)
@@ -1131,25 +1138,24 @@ namespace Emby.Dlna.Didl
 
             if (width == 0 || height == 0)
             {
-                //_imageProcessor.GetImageSize(item, imageInfo);
+                // _imageProcessor.GetImageSize(item, imageInfo);
                 width = null;
                 height = null;
             }
-
             else if (width == -1 || height == -1)
             {
                 width = null;
                 height = null;
             }
 
-            //try
+            // try
             //{
             //    var size = _imageProcessor.GetImageSize(imageInfo);
 
             //    width = size.Width;
             //    height = size.Height;
             //}
-            //catch
+            // catch
             //{
 
             //}

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

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

+ 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)
         {
             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)
@@ -150,6 +158,7 @@ namespace Emby.Dlna.Eventing
                 builder.Append("</" + key + ">");
                 builder.Append("</e:property>");
             }
+
             builder.Append("</e:propertyset>");
 
             var options = new HttpRequestOptions
@@ -169,7 +178,6 @@ namespace Emby.Dlna.Eventing
             {
                 using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false))
                 {
-
                 }
             }
             catch (OperationCanceledException)

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

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

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

@@ -33,10 +33,8 @@ namespace Emby.Dlna.Main
     public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
     {
         private readonly IServerConfigurationManager _config;
-        private readonly ILogger _logger;
+        private readonly ILogger<DlnaEntryPoint> _logger;
         private readonly IServerApplicationHost _appHost;
-
-        private PlayToManager _manager;
         private readonly ISessionManager _sessionManager;
         private readonly IHttpClient _httpClient;
         private readonly ILibraryManager _libraryManager;
@@ -47,14 +45,13 @@ namespace Emby.Dlna.Main
         private readonly ILocalizationManager _localization;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaEncoder _mediaEncoder;
-
         private readonly IDeviceDiscovery _deviceDiscovery;
-
-        private SsdpDevicePublisher _Publisher;
-
         private readonly ISocketFactory _socketFactory;
         private readonly INetworkManager _networkManager;
+        private readonly object _syncLock = new object();
 
+        private PlayToManager _manager;
+        private SsdpDevicePublisher _publisher;
         private ISsdpCommunicationsServer _communicationsServer;
 
         internal IContentDirectory ContentDirectory { get; private set; }
@@ -65,7 +62,8 @@ namespace Emby.Dlna.Main
 
         public static DlnaEntryPoint Current;
 
-        public DlnaEntryPoint(IServerConfigurationManager config,
+        public DlnaEntryPoint(
+            IServerConfigurationManager config,
             ILoggerFactory loggerFactory,
             IServerApplicationHost appHost,
             ISessionManager sessionManager,
@@ -99,7 +97,7 @@ namespace Emby.Dlna.Main
             _mediaEncoder = mediaEncoder;
             _socketFactory = socketFactory;
             _networkManager = networkManager;
-            _logger = loggerFactory.CreateLogger("Dlna");
+            _logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
 
             ContentDirectory = new ContentDirectory.ContentDirectory(
                 dlnaManager,
@@ -133,20 +131,20 @@ namespace Emby.Dlna.Main
         {
             await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
 
-            ReloadComponents();
+            await ReloadComponents().ConfigureAwait(false);
 
-            _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
+            _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
         }
 
-        void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
+        private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
         {
             if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
             {
-                ReloadComponents();
+                await ReloadComponents().ConfigureAwait(false);
             }
         }
 
-        private async void ReloadComponents()
+        private async Task ReloadComponents()
         {
             var options = _config.GetDlnaConfiguration();
 
@@ -180,7 +178,7 @@ namespace Emby.Dlna.Main
                     var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
                                                    OperatingSystem.Id == OperatingSystemId.Linux;
 
-                    _communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding)
+                    _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
                     {
                         IsShared = true
                     };
@@ -231,20 +229,22 @@ namespace Emby.Dlna.Main
                 return;
             }
 
-            if (_Publisher != null)
+            if (_publisher != null)
             {
                 return;
             }
 
             try
             {
-                _Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost);
-                _Publisher.LogFunction = LogMessage;
-                _Publisher.SupportPnpRootDevice = false;
+                _publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
+                {
+                    LogFunction = LogMessage,
+                    SupportPnpRootDevice = false
+                };
 
                 await RegisterServerEndpoints().ConfigureAwait(false);
 
-                _Publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
+                _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
             }
             catch (Exception ex)
             {
@@ -266,6 +266,12 @@ namespace Emby.Dlna.Main
                     continue;
                 }
 
+                // Limit to LAN addresses only
+                if (!_networkManager.IsAddressInSubnets(address, true, true))
+                {
+                    continue;
+                }
+
                 var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
 
                 _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
@@ -275,7 +281,7 @@ namespace Emby.Dlna.Main
 
                 var device = new SsdpRootDevice
                 {
-                    CacheLifetime = TimeSpan.FromSeconds(1800), //How long SSDP clients can cache this info.
+                    CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
                     Location = uri, // Must point to the URL that serves your devices UPnP description document.
                     Address = address,
                     SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
@@ -287,13 +293,13 @@ namespace Emby.Dlna.Main
                 };
 
                 SetProperies(device, fullService);
-                _Publisher.AddDevice(device);
+                _publisher.AddDevice(device);
 
                 var embeddedDevices = new[]
                 {
                     "urn:schemas-upnp-org:service:ContentDirectory:1",
                     "urn:schemas-upnp-org:service:ConnectionManager:1",
-                    //"urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
+                    // "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
                 };
 
                 foreach (var subDevice in embeddedDevices)
@@ -319,12 +325,13 @@ namespace Emby.Dlna.Main
             {
                 guid = text.GetMD5();
             }
+
             return guid.ToString("N", CultureInfo.InvariantCulture);
         }
 
         private void SetProperies(SsdpDevice device, string fullDeviceType)
         {
-            var service = fullDeviceType.Replace("urn:", string.Empty).Replace(":1", string.Empty);
+            var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase);
 
             var serviceParts = service.Split(':');
 
@@ -335,7 +342,6 @@ namespace Emby.Dlna.Main
             device.DeviceType = serviceParts[2];
         }
 
-        private readonly object _syncLock = new object();
         private void StartPlayToManager()
         {
             lock (_syncLock)
@@ -347,7 +353,8 @@ namespace Emby.Dlna.Main
 
                 try
                 {
-                    _manager = new PlayToManager(_logger,
+                    _manager = new PlayToManager(
+                        _logger,
                         _sessionManager,
                         _libraryManager,
                         _userManager,
@@ -386,6 +393,7 @@ namespace Emby.Dlna.Main
                     {
                         _logger.LogError(ex, "Error disposing PlayTo manager");
                     }
+
                     _manager = null;
                 }
             }
@@ -412,11 +420,11 @@ namespace Emby.Dlna.Main
 
         public void DisposeDevicePublisher()
         {
-            if (_Publisher != null)
+            if (_publisher != null)
             {
                 _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
     {
-        #region Fields & Properties
-
         private Timer _timer;
 
         public DeviceInfo Properties { get; set; }
@@ -34,9 +32,10 @@ namespace Emby.Dlna.PlayTo
         {
             get
             {
-                RefreshVolumeIfNeeded();
+                RefreshVolumeIfNeeded().GetAwaiter().GetResult();
                 return _volume;
             }
+
             set => _volume = value;
         }
 
@@ -52,10 +51,10 @@ namespace Emby.Dlna.PlayTo
 
         public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED;
 
-        #endregion
-
         private readonly IHttpClient _httpClient;
+
         private readonly ILogger _logger;
+
         private readonly IServerConfigurationManager _config;
 
         public Action OnDeviceUnavailable { get; set; }
@@ -76,24 +75,24 @@ namespace Emby.Dlna.PlayTo
 
         private DateTime _lastVolumeRefresh;
         private bool _volumeRefreshActive;
-        private void RefreshVolumeIfNeeded()
+        private Task RefreshVolumeIfNeeded()
         {
-            if (!_volumeRefreshActive)
-            {
-                return;
-            }
-
-            if (DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5))
+            if (_volumeRefreshActive
+                && DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5))
             {
                 _lastVolumeRefresh = DateTime.UtcNow;
-                RefreshVolume(CancellationToken.None);
+                return RefreshVolume();
             }
+
+            return Task.CompletedTask;
         }
 
-        private async void RefreshVolume(CancellationToken cancellationToken)
+        private async Task RefreshVolume(CancellationToken cancellationToken = default)
         {
             if (_disposed)
+            {
                 return;
+            }
 
             try
             {
@@ -141,8 +140,6 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        #region Commanding
-
         public Task VolumeDown(CancellationToken cancellationToken)
         {
             var sendVolume = Math.Max(Volume - 5, 0);
@@ -211,7 +208,9 @@ namespace Emby.Dlna.PlayTo
 
             var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
             if (command == null)
+            {
                 return false;
+            }
 
             var service = GetServiceRenderingControl();
 
@@ -232,7 +231,7 @@ namespace Emby.Dlna.PlayTo
         }
 
         /// <summary>
-        /// Sets volume on a scale of 0-100
+        /// Sets volume on a scale of 0-100.
         /// </summary>
         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");
             if (command == null)
+            {
                 return;
+            }
 
             var service = GetServiceRenderingControl();
 
@@ -263,7 +264,9 @@ namespace Emby.Dlna.PlayTo
 
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
             if (command == null)
+            {
                 return;
+            }
 
             var service = GetAvTransportService();
 
@@ -288,7 +291,9 @@ namespace Emby.Dlna.PlayTo
 
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
             if (command == null)
+            {
                 return;
+            }
 
             var dictionary = new Dictionary<string, string>
             {
@@ -401,11 +406,8 @@ namespace Emby.Dlna.PlayTo
             RestartTimer(true);
         }
 
-        #endregion
-
-        #region Get data
-
         private int _connectFailureCount;
+
         private async void TimerCallback(object sender)
         {
             if (_disposed)
@@ -458,7 +460,9 @@ namespace Emby.Dlna.PlayTo
                     _connectFailureCount = 0;
 
                     if (_disposed)
+                    {
                         return;
+                    }
 
                     // If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive
                     if (transportState.Value == TRANSPORTSTATE.STOPPED)
@@ -478,7 +482,9 @@ namespace Emby.Dlna.PlayTo
             catch (Exception ex)
             {
                 if (_disposed)
+                {
                     return;
+                }
 
                 _logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name);
 
@@ -494,6 +500,7 @@ namespace Emby.Dlna.PlayTo
                         return;
                     }
                 }
+
                 RestartTimerInactive();
             }
         }
@@ -578,7 +585,9 @@ namespace Emby.Dlna.PlayTo
                 cancellationToken: cancellationToken).ConfigureAwait(false);
 
             if (result == null || result.Document == null)
+            {
                 return;
+            }
 
             var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse")
                                             .Select(i => i.Element("CurrentMute"))
@@ -750,7 +759,7 @@ namespace Emby.Dlna.PlayTo
 
             if (track == null)
             {
-                //If track is null, some vendors do this, use GetMediaInfo instead
+                // If track is null, some vendors do this, use GetMediaInfo instead
                 return (true, null);
             }
 
@@ -794,7 +803,6 @@ namespace Emby.Dlna.PlayTo
             }
             catch (XmlException)
             {
-
             }
 
             // first try to add a root node with a dlna namesapce
@@ -806,7 +814,6 @@ namespace Emby.Dlna.PlayTo
             }
             catch (XmlException)
             {
-
             }
 
             // some devices send back invalid xml
@@ -816,7 +823,6 @@ namespace Emby.Dlna.PlayTo
             }
             catch (XmlException)
             {
-
             }
 
             return null;
@@ -871,10 +877,6 @@ namespace Emby.Dlna.PlayTo
             return new string[4];
         }
 
-        #endregion
-
-        #region From XML
-
         private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
         {
             if (AvCommands != null)
@@ -1069,8 +1071,6 @@ namespace Emby.Dlna.PlayTo
             return new Device(deviceProperties, httpClient, logger, config);
         }
 
-        #endregion
-
         private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
         private static DeviceIcon CreateIcon(XElement element)
         {
@@ -1194,8 +1194,6 @@ namespace Emby.Dlna.PlayTo
             });
         }
 
-        #region IDisposable
-
         bool _disposed;
 
         public void Dispose()
@@ -1222,8 +1220,6 @@ namespace Emby.Dlna.PlayTo
             _disposed = true;
         }
 
-        #endregion
-
         public override string ToString()
         {
             return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);

+ 26 - 8
Emby.Dlna/PlayTo/PlayToController.cs

@@ -7,6 +7,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Emby.Dlna.Didl;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
@@ -22,6 +23,7 @@ using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Session;
 using Microsoft.AspNetCore.WebUtilities;
 using Microsoft.Extensions.Logging;
+using Photo = MediaBrowser.Controller.Entities.Photo;
 
 namespace Emby.Dlna.PlayTo
 {
@@ -146,11 +148,14 @@ namespace Emby.Dlna.PlayTo
                 {
                     var positionTicks = GetProgressPositionTicks(streamInfo);
 
-                    ReportPlaybackStopped(streamInfo, positionTicks);
+                    await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
                 }
 
                 streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager);
-                if (streamInfo.Item == null) return;
+                if (streamInfo.Item == null)
+                {
+                    return;
+                }
 
                 var newItemProgress = GetProgressInfo(streamInfo);
 
@@ -173,11 +178,14 @@ namespace Emby.Dlna.PlayTo
             {
                 var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
 
-                if (streamInfo.Item == null) return;
+                if (streamInfo.Item == null)
+                {
+                    return;
+                }
 
                 var positionTicks = GetProgressPositionTicks(streamInfo);
 
-                ReportPlaybackStopped(streamInfo, positionTicks);
+                await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
 
                 var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
 
@@ -185,7 +193,7 @@ namespace Emby.Dlna.PlayTo
                     (_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) :
                     mediaSource.RunTimeTicks;
 
-                var playedToCompletion = (positionTicks.HasValue && positionTicks.Value == 0);
+                var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
 
                 if (!playedToCompletion && duration.HasValue && positionTicks.HasValue)
                 {
@@ -210,7 +218,7 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        private async void ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks)
+        private async Task ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks)
         {
             try
             {
@@ -220,7 +228,6 @@ namespace Emby.Dlna.PlayTo
                     SessionId = _session.Id,
                     PositionTicks = positionTicks,
                     MediaSourceId = streamInfo.MediaSourceId
-
                 }).ConfigureAwait(false);
             }
             catch (Exception ex)
@@ -418,6 +425,7 @@ namespace Emby.Dlna.PlayTo
                     await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
                     return;
                 }
+
                 await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
             }
         }
@@ -441,7 +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;
 
@@ -700,6 +714,7 @@ namespace Emby.Dlna.PlayTo
 
                             throw new ArgumentException("Volume argument cannot be null");
                         }
+
                     default:
                         return Task.CompletedTask;
                 }
@@ -785,12 +800,15 @@ namespace Emby.Dlna.PlayTo
             public int? SubtitleStreamIndex { get; set; }
 
             public string DeviceProfileId { get; set; }
+
             public string DeviceId { get; set; }
 
             public string MediaSourceId { get; set; }
+
             public string LiveStreamId { get; set; }
 
             public BaseItem Item { get; set; }
+
             private MediaSourceInfo MediaSource;
 
             private IMediaSourceManager _mediaSourceManager;

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

@@ -78,9 +78,15 @@ namespace Emby.Dlna.PlayTo
 
             var info = e.Argument;
 
-            if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty;
+            if (!info.Headers.TryGetValue("USN", out string usn))
+            {
+                usn = string.Empty;
+            }
 
-            if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty;
+            if (!info.Headers.TryGetValue("NT", out string nt))
+            {
+                nt = string.Empty;
+            }
 
             string location = info.Location.ToString();
 
@@ -88,7 +94,7 @@ namespace Emby.Dlna.PlayTo
             if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 &&
                      nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
             {
-                //_logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location);
+                // _logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location);
                 return;
             }
 
@@ -112,7 +118,6 @@ namespace Emby.Dlna.PlayTo
             }
             catch (OperationCanceledException)
             {
-
             }
             catch (Exception ex)
             {
@@ -133,6 +138,7 @@ namespace Emby.Dlna.PlayTo
                 usn = usn.Substring(index);
                 found = true;
             }
+
             index = usn.IndexOf("::", StringComparison.OrdinalIgnoreCase);
             if (index != -1)
             {
@@ -184,7 +190,8 @@ namespace Emby.Dlna.PlayTo
                     serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress);
                 }
 
-                controller = new PlayToController(sessionInfo,
+                controller = new PlayToController(
+                    sessionInfo,
                    _sessionManager,
                    _libraryManager,
                    _logger,
@@ -242,7 +249,6 @@ namespace Emby.Dlna.PlayTo
             }
             catch
             {
-
             }
 
             _sessionLock.Dispose();

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

@@ -12,6 +12,7 @@ namespace Emby.Dlna.PlayTo
     public class MediaChangedEventArgs : EventArgs
     {
         public uBaseObject OldMediaInfo { 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))
             {
-
             }
         }
 

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

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

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

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

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

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

@@ -17,7 +17,7 @@ namespace Emby.Dlna.Service
             Logger = logger;
             HttpClient = httpClient;
 
-            EventManager = new EventManager(Logger, HttpClient);
+            EventManager = new EventManager(logger, HttpClient);
         }
 
         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("</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
                     // (can be any search target value i.e device type, uuid value etc - any value that appears in the
                     // DiscoverdSsdpDevice.NotificationType property or that is used with the searchTarget parameter of the Search method).
-                    //_DeviceLocator.NotificationFilter = "upnp:rootdevice";
+                    // _DeviceLocator.NotificationFilter = "upnp:rootdevice";
 
                     // Connect our event handler so we process devices as they are found
                     _deviceLocator.DeviceAvailable += OnDeviceLocatorDeviceAvailable;
@@ -100,15 +100,13 @@ namespace Emby.Dlna.Ssdp
 
             var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
 
-            var args = new GenericEventArgs<UpnpDeviceInfo>
-            {
-                Argument = new UpnpDeviceInfo
+            var args = new GenericEventArgs<UpnpDeviceInfo>(
+                new UpnpDeviceInfo
                 {
                     Location = e.DiscoveredDevice.DescriptionLocation,
                     Headers = headers,
                     LocalIpAddress = e.LocalIpAddress
-                }
-            };
+                });
 
             DeviceDiscoveredInternal?.Invoke(this, args);
         }
@@ -121,14 +119,12 @@ namespace Emby.Dlna.Ssdp
 
             var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
 
-            var args = new GenericEventArgs<UpnpDeviceInfo>
-            {
-                Argument = new UpnpDeviceInfo
+            var args = new GenericEventArgs<UpnpDeviceInfo>(
+                new UpnpDeviceInfo
                 {
                     Location = e.DiscoveredDevice.DescriptionLocation,
                     Headers = headers
-                }
-            };
+                });
 
             DeviceLeft?.Invoke(this, args);
         }

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

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

+ 34 - 4
Emby.Drawing/ImageProcessor.cs

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

+ 6 - 0
Emby.Drawing/NullImageEncoder.cs

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

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

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

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

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

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

@@ -142,7 +142,7 @@ namespace Emby.Naming.Common
 
             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|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
                 @"(\[.*\])"
             };
 

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

@@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Notifications;
@@ -149,9 +150,7 @@ namespace Emby.Notifications.Api
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
         public object Get(GetNotificationsSummary request)
         {
-            return new NotificationsSummary
-            {
-            };
+            return new NotificationsSummary();
         }
 
         public Task Post(AddAdminNotification request)
@@ -164,7 +163,10 @@ namespace Emby.Notifications.Api
                 Level = request.Level,
                 Name = request.Name,
                 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);

+ 1 - 1
Emby.Notifications/NotificationEntryPoint.cs

@@ -25,7 +25,7 @@ namespace Emby.Notifications
     /// </summary>
     public class NotificationEntryPoint : IServerEntryPoint
     {
-        private readonly ILogger _logger;
+        private readonly ILogger<NotificationEntryPoint> _logger;
         private readonly IActivityManager _activityManager;
         private readonly ILocalizationManager _localization;
         private readonly INotificationManager _notificationManager;

+ 6 - 4
Emby.Notifications/NotificationManager.cs

@@ -4,6 +4,8 @@ using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
@@ -21,7 +23,7 @@ namespace Emby.Notifications
     /// </summary>
     public class NotificationManager : INotificationManager
     {
-        private readonly ILogger _logger;
+        private readonly ILogger<NotificationManager> _logger;
         private readonly IUserManager _userManager;
         private readonly IServerConfigurationManager _config;
 
@@ -101,7 +103,7 @@ namespace Emby.Notifications
                 switch (request.SendToUserMode.Value)
                 {
                     case SendToUserType.Admins:
-                        return _userManager.Users.Where(i => i.Policy.IsAdministrator)
+                        return _userManager.Users.Where(i => i.HasPermission(PermissionKind.IsAdministrator))
                                 .Select(i => i.Id);
                     case SendToUserType.All:
                         return _userManager.UsersIds;
@@ -117,7 +119,7 @@ namespace Emby.Notifications
                 var config = GetConfiguration();
 
                 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);
             }
 
@@ -142,7 +144,7 @@ namespace Emby.Notifications
                 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
             {

+ 2 - 2
Emby.Photos/PhotoProvider.cs

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

+ 31 - 43
Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs

@@ -88,25 +88,26 @@ namespace Emby.Server.Implementations.Activity
 
             _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure;
 
-            _userManager.UserCreated += OnUserCreated;
-            _userManager.UserPasswordChanged += OnUserPasswordChanged;
-            _userManager.UserDeleted += OnUserDeleted;
-            _userManager.UserPolicyUpdated += OnUserPolicyUpdated;
-            _userManager.UserLockedOut += OnUserLockedOut;
+            _userManager.OnUserCreated += OnUserCreated;
+            _userManager.OnUserPasswordChanged += OnUserPasswordChanged;
+            _userManager.OnUserDeleted += OnUserDeleted;
+            _userManager.OnUserLockedOut += OnUserLockedOut;
 
             return Task.CompletedTask;
         }
 
-        private async void OnUserLockedOut(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
+        private async void OnUserLockedOut(object sender, GenericEventArgs<User> e)
         {
             await CreateLogEntry(new ActivityLog(
                     string.Format(
                         CultureInfo.InvariantCulture,
                         _localization.GetLocalizedString("UserLockedOutWithName"),
-                        e.Argument.Name),
+                        e.Argument.Username),
                     NotificationType.UserLockedOut.ToString(),
-                    e.Argument.Id))
-                .ConfigureAwait(false);
+                    e.Argument.Id)
+            {
+                LogSeverity = LogLevel.Error
+            }).ConfigureAwait(false);
         }
 
         private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
@@ -152,7 +153,7 @@ namespace Emby.Server.Implementations.Activity
                 string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
-                    user.Name,
+                    user.Username,
                     GetItemName(item),
                     e.DeviceName),
                 GetPlaybackStoppedNotificationType(item.MediaType),
@@ -187,7 +188,7 @@ namespace Emby.Server.Implementations.Activity
                 string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
-                    user.Name,
+                    user.Username,
                     GetItemName(item),
                     e.DeviceName),
                 GetPlaybackNotificationType(item.MediaType),
@@ -304,49 +305,37 @@ namespace Emby.Server.Implementations.Activity
             }).ConfigureAwait(false);
         }
 
-        private async void OnUserPolicyUpdated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserPolicyUpdatedWithName"),
-                    e.Argument.Name),
-                "UserPolicyUpdated",
-                e.Argument.Id))
-                .ConfigureAwait(false);
-        }
-
-        private async void OnUserDeleted(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
+        private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
         {
             await CreateLogEntry(new ActivityLog(
                 string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserDeletedWithName"),
-                    e.Argument.Name),
+                    e.Argument.Username),
                 "UserDeleted",
                 Guid.Empty))
                 .ConfigureAwait(false);
         }
 
-        private async void OnUserPasswordChanged(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
+        private async void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
         {
             await CreateLogEntry(new ActivityLog(
                 string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserPasswordChangedWithName"),
-                    e.Argument.Name),
+                    e.Argument.Username),
                 "UserPasswordChanged",
                 e.Argument.Id))
                 .ConfigureAwait(false);
         }
 
-        private async void OnUserCreated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
+        private async void OnUserCreated(object sender, GenericEventArgs<User> e)
         {
             await CreateLogEntry(new ActivityLog(
                 string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserCreatedWithName"),
-                    e.Argument.Name),
+                    e.Argument.Username),
                 "UserCreated",
                 e.Argument.Id))
                 .ConfigureAwait(false);
@@ -377,50 +366,50 @@ namespace Emby.Server.Implementations.Activity
             }).ConfigureAwait(false);
         }
 
-        private async void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e)
+        private async void OnPluginUpdated(object sender, InstallationInfo e)
         {
             await CreateLogEntry(new ActivityLog(
                 string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("PluginUpdatedWithName"),
-                    e.Argument.Item1.Name),
+                    e.Name),
                 NotificationType.PluginUpdateInstalled.ToString(),
                 Guid.Empty)
             {
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("VersionNumber"),
-                    e.Argument.Item2.version),
-                Overview = e.Argument.Item2.changelog
+                    e.Version),
+                Overview = e.Changelog
             }).ConfigureAwait(false);
         }
 
-        private async void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
+        private async void OnPluginUninstalled(object sender, IPlugin e)
         {
             await CreateLogEntry(new ActivityLog(
                 string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("PluginUninstalledWithName"),
-                    e.Argument.Name),
+                    e.Name),
                 NotificationType.PluginUninstalled.ToString(),
                 Guid.Empty))
                 .ConfigureAwait(false);
         }
 
-        private async void OnPluginInstalled(object sender, GenericEventArgs<VersionInfo> e)
+        private async void OnPluginInstalled(object sender, InstallationInfo e)
         {
             await CreateLogEntry(new ActivityLog(
                 string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("PluginInstalledWithName"),
-                    e.Argument.name),
+                    e.Name),
                 NotificationType.PluginInstalled.ToString(),
                 Guid.Empty)
             {
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("VersionNumber"),
-                    e.Argument.version)
+                    e.Version)
             }).ConfigureAwait(false);
         }
 
@@ -510,11 +499,10 @@ namespace Emby.Server.Implementations.Activity
 
             _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure;
 
-            _userManager.UserCreated -= OnUserCreated;
-            _userManager.UserPasswordChanged -= OnUserPasswordChanged;
-            _userManager.UserDeleted -= OnUserDeleted;
-            _userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
-            _userManager.UserLockedOut -= OnUserLockedOut;
+            _userManager.OnUserCreated -= OnUserCreated;
+            _userManager.OnUserPasswordChanged -= OnUserPasswordChanged;
+            _userManager.OnUserDeleted -= OnUserDeleted;
+            _userManager.OnUserLockedOut -= OnUserLockedOut;
         }
 
         /// <summary>

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

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

+ 8 - 12
Emby.Server.Implementations/ApplicationHost.cs

@@ -45,6 +45,7 @@ using Emby.Server.Implementations.Services;
 using Emby.Server.Implementations.Session;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.Updates;
+using Emby.Server.Implementations.SyncPlay;
 using MediaBrowser.Api;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
@@ -78,6 +79,7 @@ using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Controller.TV;
+using MediaBrowser.Controller.SyncPlay;
 using MediaBrowser.LocalMetadata.Savers;
 using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.Model.Configuration;
@@ -171,7 +173,7 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// Gets the logger.
         /// </summary>
-        protected ILogger Logger { get; }
+        protected ILogger<ApplicationHost> Logger { get; }
 
         private IPlugin[] _plugins;
 
@@ -560,11 +562,8 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
 
-            serviceCollection.AddSingleton<IUserRepository, SqliteUserRepository>();
-
             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
             serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
-            serviceCollection.AddSingleton<IUserManager, UserManager>();
 
             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
             // TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation
@@ -613,6 +612,8 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
 
+            serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
+
             serviceCollection.AddSingleton<LiveTvDtoService>();
             serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
 
@@ -655,15 +656,11 @@ namespace Emby.Server.Implementations
 
             ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
-            ((SqliteUserRepository)Resolve<IUserRepository>()).Initialize();
 
             SetStaticProperties();
 
-            var userManager = (UserManager)Resolve<IUserManager>();
-            userManager.Initialize();
-
             var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
-            ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, userManager);
+            ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>());
 
             FindParts();
         }
@@ -746,7 +743,6 @@ namespace Emby.Server.Implementations
             BaseItem.ProviderManager = Resolve<IProviderManager>();
             BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
             BaseItem.ItemRepository = Resolve<IItemRepository>();
-            User.UserManager = Resolve<IUserManager>();
             BaseItem.FileSystem = _fileSystemManager;
             BaseItem.UserDataManager = Resolve<IUserDataManager>();
             BaseItem.ChannelManager = Resolve<IChannelManager>();
@@ -960,7 +956,7 @@ namespace Emby.Server.Implementations
         }
 
         /// <summary>
-        /// Notifies that the kernel that a change has been made that requires a restart
+        /// Notifies that the kernel that a change has been made that requires a restart.
         /// </summary>
         public void NotifyPendingRestart()
         {
@@ -1238,7 +1234,7 @@ namespace Emby.Server.Implementations
 
             if (addresses.Count == 0)
             {
-                addresses.AddRange(_networkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces));
+                addresses.AddRange(_networkManager.GetLocalIpAddresses());
             }
 
             var resultList = new List<IPAddress>();

+ 1 - 1
Emby.Server.Implementations/Browser/BrowserLauncher.cs

@@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.Browser
             }
             catch (Exception ex)
             {
-                var logger = appHost.Resolve<ILogger>();
+                var logger = appHost.Resolve<ILogger<IServerApplicationHost>>();
                 logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl);
             }
         }

+ 12 - 7
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -6,6 +6,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller.Channels;
@@ -13,8 +14,6 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Channels;
@@ -24,6 +23,11 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+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;
 
 namespace Emby.Server.Implementations.Channels
 {
@@ -36,7 +40,7 @@ namespace Emby.Server.Implementations.Channels
         private readonly IUserDataManager _userDataManager;
         private readonly IDtoService _dtoService;
         private readonly ILibraryManager _libraryManager;
-        private readonly ILogger _logger;
+        private readonly ILogger<ChannelManager> _logger;
         private readonly IServerConfigurationManager _config;
         private readonly IFileSystem _fileSystem;
         private readonly IJsonSerializer _jsonSerializer;
@@ -46,14 +50,14 @@ namespace Emby.Server.Implementations.Channels
             new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
 
         private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
-        
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ChannelManager"/> class.
         /// </summary>
         /// <param name="userManager">The user manager.</param>
         /// <param name="dtoService">The dto service.</param>
         /// <param name="libraryManager">The library manager.</param>
-        /// <param name="loggerFactory">The logger factory.</param>
+        /// <param name="logger">The logger.</param>
         /// <param name="config">The server configuration manager.</param>
         /// <param name="fileSystem">The filesystem.</param>
         /// <param name="userDataManager">The user data manager.</param>
@@ -791,7 +795,8 @@ namespace Emby.Server.Implementations.Channels
             return result;
         }
 
-        private async Task<ChannelItemResult> GetChannelItems(IChannel channel,
+        private async Task<ChannelItemResult> GetChannelItems(
+            IChannel channel,
             User user,
             string externalFolderId,
             ChannelItemSortField? sortField,
@@ -1067,7 +1072,7 @@ namespace Emby.Server.Implementations.Channels
             }
 
             // was used for status
-            //if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
+            // if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
             //{
             //    item.ExternalEtag = info.Etag;
             //    forceUpdate = true;

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

@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Channels
     public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask
     {
         private readonly IChannelManager _channelManager;
-        private readonly ILogger _logger;
+        private readonly ILogger<RefreshChannelsScheduledTask> _logger;
         private readonly ILibraryManager _libraryManager;
         private readonly ILocalizationManager _localization;
 

+ 4 - 3
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -5,6 +5,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Configuration;
@@ -29,7 +30,7 @@ namespace Emby.Server.Implementations.Collections
         private readonly ILibraryManager _libraryManager;
         private readonly IFileSystem _fileSystem;
         private readonly ILibraryMonitor _iLibraryMonitor;
-        private readonly ILogger _logger;
+        private readonly ILogger<CollectionManager> _logger;
         private readonly IProviderManager _providerManager;
         private readonly ILocalizationManager _localizationManager;
         private readonly IApplicationPaths _appPaths;
@@ -56,7 +57,7 @@ namespace Emby.Server.Implementations.Collections
             _libraryManager = libraryManager;
             _fileSystem = fileSystem;
             _iLibraryMonitor = iLibraryMonitor;
-            _logger = loggerFactory.CreateLogger(nameof(CollectionManager));
+            _logger = loggerFactory.CreateLogger<CollectionManager>();
             _providerManager = providerManager;
             _localizationManager = localizationManager;
             _appPaths = appPaths;
@@ -370,7 +371,7 @@ namespace Emby.Server.Implementations.Collections
     {
         private readonly CollectionManager _collectionManager;
         private readonly IServerConfigurationManager _config;
-        private readonly ILogger _logger;
+        private readonly ILogger<CollectionManagerEntryPoint> _logger;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="CollectionManagerEntryPoint"/> class.

+ 1 - 1
Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs

@@ -90,7 +90,7 @@ namespace Emby.Server.Implementations.Configuration
             ValidateMetadataPath(newConfig);
             ValidateSslCertificate(newConfig);
 
-            ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration> { Argument = newConfig });
+            ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
 
             base.ReplaceConfiguration(newConfiguration);
         }

+ 0 - 1
Emby.Server.Implementations/ConfigurationOptions.cs

@@ -17,7 +17,6 @@ namespace Emby.Server.Implementations
         {
             { HostWebClientKey, bool.TrueString },
             { HttpListenerHost.DefaultRedirectKey, "web/index.html" },
-            { InstallationManager.PluginManifestUrlKey, "https://repo.jellyfin.org/releases/plugin/manifest-stable.json" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegAnalyzeDurationKey, "200M" },
             { PlaylistsAllowDuplicatesKey, bool.TrueString }

+ 2 - 2
Emby.Server.Implementations/Cryptography/CryptographyProvider.cs

@@ -1,3 +1,5 @@
+#nullable enable
+
 using System;
 using System.Collections.Generic;
 using System.Security.Cryptography;
@@ -129,8 +131,6 @@ namespace Emby.Server.Implementations.Cryptography
                 _randomNumberGenerator.Dispose();
             }
 
-            _randomNumberGenerator = null;
-
             _disposed = true;
         }
     }

+ 4 - 5
Emby.Server.Implementations/Data/BaseSqliteRepository.cs

@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Data
         /// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
         /// </summary>
         /// <param name="logger">The logger.</param>
-        protected BaseSqliteRepository(ILogger logger)
+        protected BaseSqliteRepository(ILogger<BaseSqliteRepository> logger)
         {
             Logger = logger;
         }
@@ -32,7 +32,7 @@ namespace Emby.Server.Implementations.Data
         /// Gets the logger.
         /// </summary>
         /// <value>The logger.</value>
-        protected ILogger Logger { get; }
+        protected ILogger<BaseSqliteRepository> Logger { get; }
 
         /// <summary>
         /// Gets the default connection flags.
@@ -162,7 +162,6 @@ namespace Emby.Server.Implementations.Data
                 }
 
                 return false;
-
             }, ReadTransactionMode);
         }
 
@@ -248,12 +247,12 @@ namespace Emby.Server.Implementations.Data
     public enum SynchronousMode
     {
         /// <summary>
-        /// SQLite continues without syncing as soon as it has handed data off to the operating system
+        /// SQLite continues without syncing as soon as it has handed data off to the operating system.
         /// </summary>
         Off = 0,
 
         /// <summary>
-        /// SQLite database engine will still sync at the most critical moments
+        /// SQLite database engine will still sync at the most critical moments.
         /// </summary>
         Normal = 1,
 

+ 1 - 2
Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs

@@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Data
     public class CleanDatabaseScheduledTask : ILibraryPostScanTask
     {
         private readonly ILibraryManager _libraryManager;
-        private readonly ILogger _logger;
+        private readonly ILogger<CleanDatabaseScheduledTask> _logger;
 
         public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger<CleanDatabaseScheduledTask> logger)
         {
@@ -51,7 +51,6 @@ namespace Emby.Server.Implementations.Data
                     _libraryManager.DeleteItem(item, new DeleteOptions
                     {
                         DeleteFileLocation = false
-
                     });
                 }
 

+ 4 - 4
Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs

@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
 
 using System;
 using System.Collections.Generic;
@@ -59,7 +59,7 @@ namespace Emby.Server.Implementations.Data
         }
 
         /// <summary>
-        /// Opens the connection to the database
+        /// Opens the connection to the database.
         /// </summary>
         /// <returns>Task.</returns>
         private void InitializeInternal()
@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Data
         }
 
         /// <summary>
-        /// Save the display preferences associated with an item in the repo
+        /// Save the display preferences associated with an item in the repo.
         /// </summary>
         /// <param name="displayPreferences">The display preferences.</param>
         /// <param name="userId">The user id.</param>
@@ -122,7 +122,7 @@ namespace Emby.Server.Implementations.Data
         }
 
         /// <summary>
-        /// Save all display preferences associated with a user in the repo
+        /// Save all display preferences associated with a user in the repo.
         /// </summary>
         /// <param name="displayPreferences">The display preferences.</param>
         /// <param name="userId">The user id.</param>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 211 - 39
Emby.Server.Implementations/Data/SqliteItemRepository.cs


+ 8 - 4
Emby.Server.Implementations/Data/SqliteUserDataRepository.cs

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Threading;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -134,10 +135,12 @@ namespace Emby.Server.Implementations.Data
             {
                 throw new ArgumentNullException(nameof(userData));
             }
+
             if (internalUserId <= 0)
             {
                 throw new ArgumentNullException(nameof(internalUserId));
             }
+
             if (string.IsNullOrEmpty(key))
             {
                 throw new ArgumentNullException(nameof(key));
@@ -152,6 +155,7 @@ namespace Emby.Server.Implementations.Data
             {
                 throw new ArgumentNullException(nameof(userData));
             }
+
             if (internalUserId <= 0)
             {
                 throw new ArgumentNullException(nameof(internalUserId));
@@ -234,7 +238,7 @@ namespace Emby.Server.Implementations.Data
         }
 
         /// <summary>
-        /// Persist all user data for the specified user
+        /// Persist all user data for the specified user.
         /// </summary>
         private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken)
         {
@@ -308,7 +312,7 @@ namespace Emby.Server.Implementations.Data
         }
 
         /// <summary>
-        /// Return all user-data associated with the given user
+        /// Return all user-data associated with the given user.
         /// </summary>
         /// <param name="internalUserId"></param>
         /// <returns></returns>
@@ -338,7 +342,7 @@ namespace Emby.Server.Implementations.Data
         }
 
         /// <summary>
-        /// Read a row from the specified reader into the provided userData object
+        /// Read a row from the specified reader into the provided userData object.
         /// </summary>
         /// <param name="reader"></param>
         private UserItemData ReadRow(IReadOnlyList<IResultSetValue> reader)
@@ -346,7 +350,7 @@ namespace Emby.Server.Implementations.Data
             var userData = new UserItemData();
 
             userData.Key = reader[0].ToString();
-            //userData.UserId = reader[1].ReadGuidFromBlob();
+            // userData.UserId = reader[1].ReadGuidFromBlob();
 
             if (reader[2].SQLiteType != SQLiteType.Null)
             {

+ 0 - 240
Emby.Server.Implementations/Data/SqliteUserRepository.cs

@@ -1,240 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text.Json;
-using MediaBrowser.Common.Json;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Persistence;
-using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Data
-{
-    /// <summary>
-    /// Class SQLiteUserRepository
-    /// </summary>
-    public class SqliteUserRepository : BaseSqliteRepository, IUserRepository
-    {
-        private readonly JsonSerializerOptions _jsonOptions;
-
-        public SqliteUserRepository(
-            ILogger<SqliteUserRepository> logger,
-            IServerApplicationPaths appPaths)
-            : base(logger)
-        {
-            _jsonOptions = JsonDefaults.GetOptions();
-
-            DbFilePath = Path.Combine(appPaths.DataPath, "users.db");
-        }
-
-        /// <summary>
-        /// Gets the name of the repository
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name => "SQLite";
-
-        /// <summary>
-        /// Opens the connection to the database.
-        /// </summary>
-        public void Initialize()
-        {
-            using (var connection = GetConnection())
-            {
-                var localUsersTableExists = TableExists(connection, "LocalUsersv2");
-
-                connection.RunQueries(new[] {
-                    "create table if not exists LocalUsersv2 (Id INTEGER PRIMARY KEY, guid GUID NOT NULL, data BLOB NOT NULL)",
-                    "drop index if exists idx_users"
-                });
-
-                if (!localUsersTableExists && TableExists(connection, "Users"))
-                {
-                    TryMigrateToLocalUsersTable(connection);
-                }
-
-                RemoveEmptyPasswordHashes(connection);
-            }
-        }
-
-        private void TryMigrateToLocalUsersTable(ManagedConnection connection)
-        {
-            try
-            {
-                connection.RunQueries(new[]
-                {
-                    "INSERT INTO LocalUsersv2 (guid, data) SELECT guid,data from users"
-                });
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error migrating users database");
-            }
-        }
-
-        private void RemoveEmptyPasswordHashes(ManagedConnection connection)
-        {
-            foreach (var user in RetrieveAllUsers(connection))
-            {
-                // If the user password is the sha1 hash of the empty string, remove it
-                if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
-                    && !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
-                {
-                    continue;
-                }
-
-                user.Password = null;
-                var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions);
-
-                connection.RunInTransaction(db =>
-                {
-                    using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
-                    {
-                        statement.TryBind("@InternalId", user.InternalId);
-                        statement.TryBind("@data", serialized);
-                        statement.MoveNext();
-                    }
-                }, TransactionMode);
-            }
-        }
-
-        /// <summary>
-        /// Save a user in the repo
-        /// </summary>
-        public void CreateUser(User user)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions);
-
-            using (var connection = GetConnection())
-            {
-                connection.RunInTransaction(db =>
-                {
-                    using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)"))
-                    {
-                        statement.TryBind("@guid", user.Id.ToByteArray());
-                        statement.TryBind("@data", serialized);
-
-                        statement.MoveNext();
-                    }
-
-                    var createdUser = GetUser(user.Id, connection);
-
-                    if (createdUser == null)
-                    {
-                        throw new ApplicationException("created user should never be null");
-                    }
-
-                    user.InternalId = createdUser.InternalId;
-
-                }, TransactionMode);
-            }
-        }
-
-        public void UpdateUser(User user)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions);
-
-            using (var connection = GetConnection())
-            {
-                connection.RunInTransaction(db =>
-                {
-                    using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
-                    {
-                        statement.TryBind("@InternalId", user.InternalId);
-                        statement.TryBind("@data", serialized);
-                        statement.MoveNext();
-                    }
-
-                }, TransactionMode);
-            }
-        }
-
-        private User GetUser(Guid guid, ManagedConnection connection)
-        {
-            using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid"))
-            {
-                statement.TryBind("@guid", guid);
-
-                foreach (var row in statement.ExecuteQuery())
-                {
-                    return GetUser(row);
-                }
-            }
-
-            return null;
-        }
-
-        private User GetUser(IReadOnlyList<IResultSetValue> row)
-        {
-            var id = row[0].ToInt64();
-            var guid = row[1].ReadGuidFromBlob();
-
-            var user = JsonSerializer.Deserialize<User>(row[2].ToBlob(), _jsonOptions);
-            user.InternalId = id;
-            user.Id = guid;
-            return user;
-        }
-
-        /// <summary>
-        /// Retrieve all users from the database
-        /// </summary>
-        /// <returns>IEnumerable{User}.</returns>
-        public List<User> RetrieveAllUsers()
-        {
-            using (var connection = GetConnection(true))
-            {
-                return new List<User>(RetrieveAllUsers(connection));
-            }
-        }
-
-        /// <summary>
-        /// Retrieve all users from the database
-        /// </summary>
-        /// <returns>IEnumerable{User}.</returns>
-        private IEnumerable<User> RetrieveAllUsers(ManagedConnection connection)
-        {
-            foreach (var row in connection.Query("select id,guid,data from LocalUsersv2"))
-            {
-                yield return GetUser(row);
-            }
-        }
-
-        /// <summary>
-        /// Deletes the user.
-        /// </summary>
-        /// <param name="user">The user.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="ArgumentNullException">user</exception>
-        public void DeleteUser(User user)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            using (var connection = GetConnection())
-            {
-                connection.RunInTransaction(db =>
-                {
-                    using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id"))
-                    {
-                        statement.TryBind("@id", user.InternalId);
-                        statement.MoveNext();
-                    }
-                }, TransactionMode);
-            }
-        }
-    }
-}

+ 2 - 2
Emby.Server.Implementations/Devices/DeviceId.cs

@@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Devices
     public class DeviceId
     {
         private readonly IApplicationPaths _appPaths;
-        private readonly ILogger _logger;
+        private readonly ILogger<DeviceId> _logger;
 
         private readonly object _syncLock = new object();
 
@@ -90,7 +90,7 @@ namespace Emby.Server.Implementations.Devices
         public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
         {
             _appPaths = appPaths;
-            _logger = loggerFactory.CreateLogger("SystemId");
+            _logger = loggerFactory.CreateLogger<DeviceId>();
         }
 
         public string Value => _id ?? (_id = GetDeviceId());

+ 12 - 28
Emby.Server.Implementations/Devices/DeviceManager.cs

@@ -5,10 +5,11 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using Jellyfin.Data.Enums;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Model.Devices;
@@ -16,7 +17,6 @@ using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Session;
-using MediaBrowser.Model.Users;
 
 namespace Emby.Server.Implementations.Devices
 {
@@ -27,11 +27,10 @@ namespace Emby.Server.Implementations.Devices
         private readonly IServerConfigurationManager _config;
         private readonly IAuthenticationRepository _authRepo;
         private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
+        private readonly object _capabilitiesSyncLock = new object();
 
         public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
 
-        private readonly object _capabilitiesSyncLock = new object();
-
         public DeviceManager(
             IAuthenticationRepository authRepo,
             IJsonSerializer json,
@@ -62,13 +61,7 @@ namespace Emby.Server.Implementations.Devices
         {
             _authRepo.UpdateDeviceOptions(deviceId, options);
 
-            if (DeviceOptionsUpdated != null)
-            {
-                DeviceOptionsUpdated(this, new GenericEventArgs<Tuple<string, DeviceOptions>>()
-                {
-                    Argument = new Tuple<string, DeviceOptions>(deviceId, options)
-                });
-            }
+            DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, options)));
         }
 
         public DeviceOptions GetDeviceOptions(string deviceId)
@@ -119,7 +112,7 @@ namespace Emby.Server.Implementations.Devices
         {
             IEnumerable<AuthenticationInfo> sessions = _authRepo.Get(new AuthenticationInfoQuery
             {
-                //UserId = query.UserId
+                // UserId = query.UserId
                 HasUser = true
             }).Items;
 
@@ -176,12 +169,18 @@ namespace Emby.Server.Implementations.Devices
             {
                 throw new ArgumentException("user not found");
             }
+
             if (string.IsNullOrEmpty(deviceId))
             {
                 throw new ArgumentNullException(nameof(deviceId));
             }
 
-            if (!CanAccessDevice(user.Policy, deviceId))
+            if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator))
+            {
+                return true;
+            }
+
+            if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase))
             {
                 var capabilities = GetCapabilities(deviceId);
 
@@ -193,20 +192,5 @@ namespace Emby.Server.Implementations.Devices
 
             return true;
         }
-
-        private static bool CanAccessDevice(UserPolicy policy, string id)
-        {
-            if (policy.EnableAllDevices)
-            {
-                return true;
-            }
-
-            if (policy.IsAdministrator)
-            {
-                return true;
-            }
-
-            return policy.EnabledDevices.Contains(id, StringComparer.OrdinalIgnoreCase);
-        }
     }
 }

+ 116 - 36
Emby.Server.Implementations/Dto/DtoService.cs

@@ -6,14 +6,14 @@ using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Persistence;
@@ -24,12 +24,20 @@ using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using Microsoft.Extensions.Logging;
+using Book = MediaBrowser.Controller.Entities.Book;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Person = MediaBrowser.Controller.Entities.Person;
+using Photo = MediaBrowser.Controller.Entities.Photo;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
 
 namespace Emby.Server.Implementations.Dto
 {
     public class DtoService : IDtoService
     {
-        private readonly ILogger _logger;
+        private readonly ILogger<DtoService> _logger;
         private readonly ILibraryManager _libraryManager;
         private readonly IUserDataManager _userDataRepository;
         private readonly IItemRepository _itemRepo;
@@ -66,7 +74,7 @@ namespace Emby.Server.Implementations.Dto
         }
 
         /// <summary>
-        /// Converts a BaseItem to a DTOBaseItem
+        /// Converts a BaseItem to a DTOBaseItem.
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="fields">The fields.</param>
@@ -269,6 +277,7 @@ namespace Emby.Server.Implementations.Dto
                     dto.EpisodeTitle = dto.Name;
                     dto.Name = dto.SeriesName;
                 }
+
                 liveTvManager.AddInfoToRecordingDto(item, dto, activeRecording, user);
             }
 
@@ -284,6 +293,7 @@ namespace Emby.Server.Implementations.Dto
                 {
                     continue;
                 }
+
                 var containers = container.Split(new[] { ',' });
                 if (containers.Length < 2)
                 {
@@ -384,7 +394,7 @@ namespace Emby.Server.Implementations.Dto
 
                     if (options.ContainsField(ItemFields.ChildCount))
                     {
-                        dto.ChildCount = dto.ChildCount ?? GetChildCount(folder, user);
+                        dto.ChildCount ??= GetChildCount(folder, user);
                     }
                 }
 
@@ -398,7 +408,6 @@ namespace Emby.Server.Implementations.Dto
                     dto.DateLastMediaAdded = folder.DateLastMediaAdded;
                 }
             }
-
             else
             {
                 if (options.EnableUserData)
@@ -414,7 +423,7 @@ namespace Emby.Server.Implementations.Dto
 
             if (options.ContainsField(ItemFields.BasicSyncInfo))
             {
-                var userCanSync = user != null && user.Policy.EnableContentDownloading;
+                var userCanSync = user != null && user.HasPermission(PermissionKind.EnableContentDownloading);
                 if (userCanSync && item.SupportsExternalTransfer)
                 {
                     dto.SupportsSync = true;
@@ -435,7 +444,7 @@ namespace Emby.Server.Implementations.Dto
         }
 
         /// <summary>
-        /// Gets client-side Id of a server-side BaseItem
+        /// Gets client-side Id of a server-side BaseItem.
         /// </summary>
         /// <param name="item">The item.</param>
         /// <returns>System.String.</returns>
@@ -449,6 +458,7 @@ namespace Emby.Server.Implementations.Dto
         {
             dto.SeriesName = item.SeriesName;
         }
+
         private static void SetPhotoProperties(BaseItemDto dto, Photo item)
         {
             dto.CameraMake = item.CameraMake;
@@ -530,7 +540,7 @@ namespace Emby.Server.Implementations.Dto
         }
 
         /// <summary>
-        /// Attaches People DTO's to a DTOBaseItem
+        /// Attaches People DTO's to a DTOBaseItem.
         /// </summary>
         /// <param name="dto">The dto.</param>
         /// <param name="item">The item.</param>
@@ -547,22 +557,27 @@ namespace Emby.Server.Implementations.Dto
                     {
                         return 0;
                     }
+
                     if (i.IsType(PersonType.GuestStar))
                     {
                         return 1;
                     }
+
                     if (i.IsType(PersonType.Director))
                     {
                         return 2;
                     }
+
                     if (i.IsType(PersonType.Writer))
                     {
                         return 3;
                     }
+
                     if (i.IsType(PersonType.Producer))
                     {
                         return 4;
                     }
+
                     if (i.IsType(PersonType.Composer))
                     {
                         return 4;
@@ -586,7 +601,6 @@ namespace Emby.Server.Implementations.Dto
                         _logger.LogError(ex, "Error getting person {Name}", c);
                         return null;
                     }
-
                 }).Where(i => i != null)
                 .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
                 .Select(x => x.First())
@@ -605,8 +619,9 @@ namespace Emby.Server.Implementations.Dto
 
                 if (dictionary.TryGetValue(person.Name, out Person entity))
                 {
-                    baseItemPerson.PrimaryImageTag = GetImageCacheTag(entity, ImageType.Primary);
+                    baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
                     baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture);
+                    baseItemPerson.ImageBlurHashes = dto.ImageBlurHashes;
                     list.Add(baseItemPerson);
                 }
             }
@@ -654,8 +669,72 @@ namespace Emby.Server.Implementations.Dto
             return _libraryManager.GetGenreId(name);
         }
 
+        private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0)
+        {
+            var image = item.GetImageInfo(imageType, imageIndex);
+            if (image != null)
+            {
+                return GetTagAndFillBlurhash(dto, item, image);
+            }
+
+            return null;
+        }
+
+        private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image)
+        {
+            var tag = GetImageCacheTag(item, image);
+            if (!string.IsNullOrEmpty(image.BlurHash))
+            {
+                if (dto.ImageBlurHashes == null)
+                {
+                    dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+                }
+
+                if (!dto.ImageBlurHashes.ContainsKey(image.Type))
+                {
+                    dto.ImageBlurHashes[image.Type] = new Dictionary<string, string>();
+                }
+
+                dto.ImageBlurHashes[image.Type][tag] = image.BlurHash;
+            }
+
+            return tag;
+        }
+
+        private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, int limit)
+        {
+            return GetTagsAndFillBlurhashes(dto, item, imageType, item.GetImages(imageType).Take(limit).ToList());
+        }
+
+        private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, List<ItemImageInfo> images)
+        {
+            var tags = GetImageTags(item, images);
+            var hashes = new Dictionary<string, string>();
+            for (int i = 0; i < images.Count; i++)
+            {
+                var img = images[i];
+                if (!string.IsNullOrEmpty(img.BlurHash))
+                {
+                    var tag = tags[i];
+                    hashes[tag] = img.BlurHash;
+                }
+            }
+
+            if (hashes.Count > 0)
+            {
+                if (dto.ImageBlurHashes == null)
+                {
+                    dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+                }
+
+                dto.ImageBlurHashes[imageType] = hashes;
+            }
+
+            return tags;
+        }
+
         /// <summary>
-        /// Sets simple property values on a DTOBaseItem
+        /// Sets simple property values on a DTOBaseItem.
         /// </summary>
         /// <param name="dto">The dto.</param>
         /// <param name="item">The item.</param>
@@ -674,8 +753,8 @@ namespace Emby.Server.Implementations.Dto
                 dto.LockData = item.IsLocked;
                 dto.ForcedSortName = item.ForcedSortName;
             }
-            dto.Container = item.Container;
 
+            dto.Container = item.Container;
             dto.EndDate = item.EndDate;
 
             if (options.ContainsField(ItemFields.ExternalUrls))
@@ -694,10 +773,12 @@ namespace Emby.Server.Implementations.Dto
                 dto.AspectRatio = hasAspectRatio.AspectRatio;
             }
 
+            dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+
             var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
             if (backdropLimit > 0)
             {
-                dto.BackdropImageTags = GetImageTags(item, item.GetImages(ImageType.Backdrop).Take(backdropLimit).ToList());
+                dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit);
             }
 
             if (options.ContainsField(ItemFields.ScreenshotImageTags))
@@ -705,7 +786,7 @@ namespace Emby.Server.Implementations.Dto
                 var screenshotLimit = options.GetImageLimit(ImageType.Screenshot);
                 if (screenshotLimit > 0)
                 {
-                    dto.ScreenshotImageTags = GetImageTags(item, item.GetImages(ImageType.Screenshot).Take(screenshotLimit).ToList());
+                    dto.ScreenshotImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Screenshot, screenshotLimit);
                 }
             }
 
@@ -721,12 +802,11 @@ namespace Emby.Server.Implementations.Dto
 
                 // Prevent implicitly captured closure
                 var currentItem = item;
-                foreach (var image in currentItem.ImageInfos.Where(i => !currentItem.AllowsMultipleImages(i.Type))
-                    .ToList())
+                foreach (var image in currentItem.ImageInfos.Where(i => !currentItem.AllowsMultipleImages(i.Type)))
                 {
                     if (options.GetImageLimit(image.Type) > 0)
                     {
-                        var tag = GetImageCacheTag(item, image);
+                        var tag = GetTagAndFillBlurhash(dto, item, image);
 
                         if (tag != null)
                         {
@@ -871,11 +951,10 @@ namespace Emby.Server.Implementations.Dto
                 if (albumParent != null)
                 {
                     dto.AlbumId = albumParent.Id;
-
-                    dto.AlbumPrimaryImageTag = GetImageCacheTag(albumParent, ImageType.Primary);
+                    dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary);
                 }
 
-                //if (options.ContainsField(ItemFields.MediaSourceCount))
+                // if (options.ContainsField(ItemFields.MediaSourceCount))
                 //{
                 // Songs always have one
                 //}
@@ -885,13 +964,13 @@ namespace Emby.Server.Implementations.Dto
             {
                 dto.Artists = hasArtist.Artists;
 
-                //var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
+                // var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
                 //{
                 //    EnableTotalRecordCount = false,
                 //    ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
                 //});
 
-                //dto.ArtistItems = artistItems.Items
+                // dto.ArtistItems = artistItems.Items
                 //    .Select(i =>
                 //    {
                 //        var artist = i.Item1;
@@ -904,7 +983,7 @@ namespace Emby.Server.Implementations.Dto
                 //    .ToList();
 
                 // Include artists that are not in the database yet, e.g., just added via metadata editor
-                //var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
+                // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
                 dto.ArtistItems = hasArtist.Artists
                     //.Except(foundArtists, new DistinctNameComparer())
                     .Select(i =>
@@ -929,7 +1008,6 @@ namespace Emby.Server.Implementations.Dto
                         }
 
                         return null;
-
                     }).Where(i => i != null).ToArray();
             }
 
@@ -938,13 +1016,13 @@ namespace Emby.Server.Implementations.Dto
             {
                 dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
 
-                //var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
+                // var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
                 //{
                 //    EnableTotalRecordCount = false,
                 //    ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
                 //});
 
-                //dto.AlbumArtists = artistItems.Items
+                // dto.AlbumArtists = artistItems.Items
                 //    .Select(i =>
                 //    {
                 //        var artist = i.Item1;
@@ -980,7 +1058,6 @@ namespace Emby.Server.Implementations.Dto
                         }
 
                         return null;
-
                     }).Where(i => i != null).ToArray();
             }
 
@@ -1094,12 +1171,12 @@ namespace Emby.Server.Implementations.Dto
 
                 // this block will add the series poster for episodes without a poster
                 // TODO maybe remove the if statement entirely
-                //if (options.ContainsField(ItemFields.SeriesPrimaryImage))
+                // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
                 {
                     episodeSeries = episodeSeries ?? episode.Series;
                     if (episodeSeries != null)
                     {
-                        dto.SeriesPrimaryImageTag = GetImageCacheTag(episodeSeries, ImageType.Primary);
+                        dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
                     }
                 }
 
@@ -1140,12 +1217,12 @@ namespace Emby.Server.Implementations.Dto
 
                 // this block will add the series poster for seasons without a poster
                 // TODO maybe remove the if statement entirely
-                //if (options.ContainsField(ItemFields.SeriesPrimaryImage))
+                // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
                 {
                     series = series ?? season.Series;
                     if (series != null)
                     {
-                        dto.SeriesPrimaryImageTag = GetImageCacheTag(series, ImageType.Primary);
+                        dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
                     }
                 }
             }
@@ -1275,9 +1352,10 @@ namespace Emby.Server.Implementations.Dto
                     if (image != null)
                     {
                         dto.ParentLogoItemId = GetDtoId(parent);
-                        dto.ParentLogoImageTag = GetImageCacheTag(parent, image);
+                        dto.ParentLogoImageTag = GetTagAndFillBlurhash(dto, parent, image);
                     }
                 }
+
                 if (artLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId == null)
                 {
                     var image = allImages.FirstOrDefault(i => i.Type == ImageType.Art);
@@ -1285,9 +1363,10 @@ namespace Emby.Server.Implementations.Dto
                     if (image != null)
                     {
                         dto.ParentArtItemId = GetDtoId(parent);
-                        dto.ParentArtImageTag = GetImageCacheTag(parent, image);
+                        dto.ParentArtImageTag = GetTagAndFillBlurhash(dto, parent, image);
                     }
                 }
+
                 if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && !(parent is ICollectionFolder) && !(parent is UserView))
                 {
                     var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
@@ -1295,9 +1374,10 @@ namespace Emby.Server.Implementations.Dto
                     if (image != null)
                     {
                         dto.ParentThumbItemId = GetDtoId(parent);
-                        dto.ParentThumbImageTag = GetImageCacheTag(parent, image);
+                        dto.ParentThumbImageTag = GetTagAndFillBlurhash(dto, parent, image);
                     }
                 }
+
                 if (backdropLimit > 0 && !((dto.BackdropImageTags != null && dto.BackdropImageTags.Length > 0) || (dto.ParentBackdropImageTags != null && dto.ParentBackdropImageTags.Length > 0)))
                 {
                     var images = allImages.Where(i => i.Type == ImageType.Backdrop).Take(backdropLimit).ToList();
@@ -1305,7 +1385,7 @@ namespace Emby.Server.Implementations.Dto
                     if (images.Count > 0)
                     {
                         dto.ParentBackdropItemId = GetDtoId(parent);
-                        dto.ParentBackdropImageTags = GetImageTags(parent, images);
+                        dto.ParentBackdropImageTags = GetTagsAndFillBlurhashes(dto, parent, ImageType.Backdrop, images);
                     }
                 }
 

+ 9 - 7
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -24,7 +24,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="IPNetwork2" Version="2.4.0.126" />
+    <PackageReference Include="IPNetwork2" Version="2.5.211" />
     <PackageReference Include="Jellyfin.XmlTv" Version="10.4.3" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
@@ -34,15 +34,16 @@
     <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.3" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.3" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" />
-    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.3" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.5" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.5" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" />
+    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.5" />
     <PackageReference Include="Mono.Nat" Version="2.0.1" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
-    <PackageReference Include="ServiceStack.Text.Core" Version="5.8.0" />
-    <PackageReference Include="sharpcompress" Version="0.25.0" />
+    <PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />
+    <PackageReference Include="sharpcompress" Version="0.25.1" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
+    <PackageReference Include="DotNet.Glob" Version="3.0.9" />
   </ItemGroup>
 
   <ItemGroup>
@@ -53,6 +54,7 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
   </PropertyGroup>
 
   <!-- Code Analyzers-->

+ 1 - 1
Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs

@@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.EntryPoints
     public class ExternalPortForwarding : IServerEntryPoint
     {
         private readonly IServerApplicationHost _appHost;
-        private readonly ILogger _logger;
+        private readonly ILogger<ExternalPortForwarding> _logger;
         private readonly IServerConfigurationManager _config;
         private readonly IDeviceDiscovery _deviceDiscovery;
 

+ 4 - 4
Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

@@ -6,6 +6,7 @@ using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -28,7 +29,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
         private readonly ISessionManager _sessionManager;
         private readonly IUserManager _userManager;
-        private readonly ILogger _logger;
+        private readonly ILogger<LibraryChangedNotifier> _logger;
 
         /// <summary>
         /// The library changed sync lock.
@@ -131,7 +132,6 @@ namespace Emby.Server.Implementations.EntryPoints
                 }
                 catch
                 {
-
                 }
             }
         }
@@ -302,7 +302,7 @@ namespace Emby.Server.Implementations.EntryPoints
                                     .Select(x => x.First())
                                     .ToList();
 
-                SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None);
+                SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult();
 
                 if (LibraryUpdateTimer != null)
                 {
@@ -327,7 +327,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <param name="foldersAddedTo">The folders added to.</param>
         /// <param name="foldersRemovedFrom">The folders removed from.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        private async void SendChangeNotifications(List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, List<BaseItem> itemsRemoved, List<Folder> foldersAddedTo, List<Folder> foldersRemovedFrom, CancellationToken cancellationToken)
+        private async Task SendChangeNotifications(List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, List<BaseItem> itemsRemoved, List<Folder> foldersAddedTo, List<Folder> foldersRemovedFrom, CancellationToken cancellationToken)
         {
             var userIds = _sessionManager.Sessions
                 .Select(i => i.UserId)

+ 12 - 11
Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs

@@ -4,6 +4,7 @@ using System;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Plugins;
@@ -17,7 +18,7 @@ namespace Emby.Server.Implementations.EntryPoints
         private readonly ILiveTvManager _liveTvManager;
         private readonly ISessionManager _sessionManager;
         private readonly IUserManager _userManager;
-        private readonly ILogger _logger;
+        private readonly ILogger<RecordingNotifier> _logger;
 
         public RecordingNotifier(
             ISessionManager sessionManager,
@@ -42,29 +43,29 @@ namespace Emby.Server.Implementations.EntryPoints
             return Task.CompletedTask;
         }
 
-        private void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
-            SendMessage("SeriesTimerCreated", e.Argument);
+            await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false);
         }
 
-        private void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
-            SendMessage("TimerCreated", e.Argument);
+            await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false);
         }
 
-        private void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
-            SendMessage("SeriesTimerCancelled", e.Argument);
+            await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false);
         }
 
-        private void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
-            SendMessage("TimerCancelled", e.Argument);
+            await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false);
         }
 
-        private async void SendMessage(string name, TimerEventInfo info)
+        private async Task SendMessage(string name, TimerEventInfo info)
         {
-            var users = _userManager.Users.Where(i => i.Policy.EnableLiveTvAccess).Select(i => i.Id).ToList();
+            var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList();
 
             try
             {

+ 0 - 77
Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs

@@ -1,77 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Tasks;
-
-namespace Emby.Server.Implementations.EntryPoints
-{
-    /// <summary>
-    /// Class RefreshUsersMetadata.
-    /// </summary>
-    public class RefreshUsersMetadata : IScheduledTask, IConfigurableScheduledTask
-    {
-        /// <summary>
-        /// The user manager.
-        /// </summary>
-        private readonly IUserManager _userManager;
-        private readonly IFileSystem _fileSystem;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="RefreshUsersMetadata" /> class.
-        /// </summary>
-        public RefreshUsersMetadata(IUserManager userManager, IFileSystem fileSystem)
-        {
-            _userManager = userManager;
-            _fileSystem = fileSystem;
-        }
-
-        /// <inheritdoc />
-        public string Name => "Refresh Users";
-
-        /// <inheritdoc />
-        public string Key => "RefreshUsers";
-
-        /// <inheritdoc />
-        public string Description => "Refresh user infos";
-
-        /// <inheritdoc />
-        public string Category => "Library";
-
-        /// <inheritdoc />
-        public bool IsHidden => true;
-
-        /// <inheritdoc />
-        public bool IsEnabled => true;
-
-        /// <inheritdoc />
-        public bool IsLogged => true;
-
-        /// <inheritdoc />
-        public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
-        {
-            foreach (var user in _userManager.Users)
-            {
-                cancellationToken.ThrowIfCancellationRequested();
-
-                await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false);
-            }
-        }
-
-        /// <inheritdoc />
-        public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
-        {
-            return new[]
-            {
-                new TaskTriggerInfo
-                {
-                    IntervalTicks = TimeSpan.FromDays(1).Ticks,
-                    Type = TaskTriggerInfo.TriggerInterval
-                }
-            };
-        }
-    }
-}

+ 26 - 45
Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs

@@ -3,15 +3,16 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Tasks;
+using MediaBrowser.Model.Updates;
 
 namespace Emby.Server.Implementations.EntryPoints
 {
@@ -67,10 +68,8 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <inheritdoc />
         public Task RunAsync()
         {
-            _userManager.UserDeleted += OnUserDeleted;
-            _userManager.UserUpdated += OnUserUpdated;
-            _userManager.UserPolicyUpdated += OnUserPolicyUpdated;
-            _userManager.UserConfigurationUpdated += OnUserConfigurationUpdated;
+            _userManager.OnUserDeleted += OnUserDeleted;
+            _userManager.OnUserUpdated += OnUserUpdated;
 
             _appHost.HasPendingRestartChanged += OnHasPendingRestartChanged;
 
@@ -85,29 +84,29 @@ namespace Emby.Server.Implementations.EntryPoints
             return Task.CompletedTask;
         }
 
-        private void OnPackageInstalling(object sender, InstallationEventArgs e)
+        private async void OnPackageInstalling(object sender, InstallationInfo e)
         {
-            SendMessageToAdminSessions("PackageInstalling", e.InstallationInfo);
+            await SendMessageToAdminSessions("PackageInstalling", e).ConfigureAwait(false);
         }
 
-        private void OnPackageInstallationCancelled(object sender, InstallationEventArgs e)
+        private async void OnPackageInstallationCancelled(object sender, InstallationInfo e)
         {
-            SendMessageToAdminSessions("PackageInstallationCancelled", e.InstallationInfo);
+            await SendMessageToAdminSessions("PackageInstallationCancelled", e).ConfigureAwait(false);
         }
 
-        private void OnPackageInstallationCompleted(object sender, InstallationEventArgs e)
+        private async void OnPackageInstallationCompleted(object sender, InstallationInfo e)
         {
-            SendMessageToAdminSessions("PackageInstallationCompleted", e.InstallationInfo);
+            await SendMessageToAdminSessions("PackageInstallationCompleted", e).ConfigureAwait(false);
         }
 
-        private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
+        private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
         {
-            SendMessageToAdminSessions("PackageInstallationFailed", e.InstallationInfo);
+            await SendMessageToAdminSessions("PackageInstallationFailed", e.InstallationInfo).ConfigureAwait(false);
         }
 
-        private void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
+        private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
         {
-            SendMessageToAdminSessions("ScheduledTaskEnded", e.Result);
+            await SendMessageToAdminSessions("ScheduledTaskEnded", e.Result).ConfigureAwait(false);
         }
 
         /// <summary>
@@ -115,9 +114,9 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// <param name="sender">The sender.</param>
         /// <param name="e">The e.</param>
-        private void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
+        private async void OnPluginUninstalled(object sender, IPlugin e)
         {
-            SendMessageToAdminSessions("PluginUninstalled", e.Argument.GetPluginInfo());
+            await SendMessageToAdminSessions("PluginUninstalled", e).ConfigureAwait(false);
         }
 
         /// <summary>
@@ -125,9 +124,9 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// <param name="sender">The source of the event.</param>
         /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
-        private void OnHasPendingRestartChanged(object sender, EventArgs e)
+        private async void OnHasPendingRestartChanged(object sender, EventArgs e)
         {
-            _sessionManager.SendRestartRequiredNotification(CancellationToken.None);
+            await _sessionManager.SendRestartRequiredNotification(CancellationToken.None).ConfigureAwait(false);
         }
 
         /// <summary>
@@ -135,11 +134,11 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// <param name="sender">The sender.</param>
         /// <param name="e">The e.</param>
-        private void OnUserUpdated(object sender, GenericEventArgs<User> e)
+        private async void OnUserUpdated(object sender, GenericEventArgs<User> e)
         {
             var dto = _userManager.GetUserDto(e.Argument);
 
-            SendMessageToUserSession(e.Argument, "UserUpdated", dto);
+            await SendMessageToUserSession(e.Argument, "UserUpdated", dto).ConfigureAwait(false);
         }
 
         /// <summary>
@@ -147,26 +146,12 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// <param name="sender">The sender.</param>
         /// <param name="e">The e.</param>
-        private void OnUserDeleted(object sender, GenericEventArgs<User> e)
+        private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
         {
-            SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture));
+            await SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)).ConfigureAwait(false);
         }
 
-        private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
-        {
-            var dto = _userManager.GetUserDto(e.Argument);
-
-            SendMessageToUserSession(e.Argument, "UserPolicyUpdated", dto);
-        }
-
-        private void OnUserConfigurationUpdated(object sender, GenericEventArgs<User> e)
-        {
-            var dto = _userManager.GetUserDto(e.Argument);
-
-            SendMessageToUserSession(e.Argument, "UserConfigurationUpdated", dto);
-        }
-
-        private async void SendMessageToAdminSessions<T>(string name, T data)
+        private async Task SendMessageToAdminSessions<T>(string name, T data)
         {
             try
             {
@@ -174,11 +159,10 @@ namespace Emby.Server.Implementations.EntryPoints
             }
             catch (Exception)
             {
-
             }
         }
 
-        private async void SendMessageToUserSession<T>(User user, string name, T data)
+        private async Task SendMessageToUserSession<T>(User user, string name, T data)
         {
             try
             {
@@ -190,7 +174,6 @@ namespace Emby.Server.Implementations.EntryPoints
             }
             catch (Exception)
             {
-
             }
         }
 
@@ -209,10 +192,8 @@ namespace Emby.Server.Implementations.EntryPoints
         {
             if (dispose)
             {
-                _userManager.UserDeleted -= OnUserDeleted;
-                _userManager.UserUpdated -= OnUserUpdated;
-                _userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
-                _userManager.UserConfigurationUpdated -= OnUserConfigurationUpdated;
+                _userManager.OnUserDeleted -= OnUserDeleted;
+                _userManager.OnUserUpdated -= OnUserUpdated;
 
                 _installationManager.PluginUninstalled -= OnPluginUninstalled;
                 _installationManager.PackageInstalling -= OnPackageInstalling;

+ 9 - 6
Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
 using Emby.Server.Implementations.Udp;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Plugins;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints
@@ -20,8 +21,9 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <summary>
         /// The logger.
         /// </summary>
-        private readonly ILogger _logger;
+        private readonly ILogger<UdpServerEntryPoint> _logger;
         private readonly IServerApplicationHost _appHost;
+        private readonly IConfiguration _config;
 
         /// <summary>
         /// The UDP server.
@@ -35,19 +37,20 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         public UdpServerEntryPoint(
             ILogger<UdpServerEntryPoint> logger,
-            IServerApplicationHost appHost)
+            IServerApplicationHost appHost,
+            IConfiguration configuration)
         {
             _logger = logger;
             _appHost = appHost;
-
-
+            _config = configuration;
         }
 
         /// <inheritdoc />
-        public async Task RunAsync()
+        public Task RunAsync()
         {
-            _udpServer = new UdpServer(_logger, _appHost);
+            _udpServer = new UdpServer(_logger, _appHost, _config);
             _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+            return Task.CompletedTask;
         }
 
         /// <inheritdoc />

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

@@ -22,7 +22,7 @@ namespace Emby.Server.Implementations.HttpClientManager
     /// </summary>
     public class HttpClientManager : IHttpClient
     {
-        private readonly ILogger _logger;
+        private readonly ILogger<HttpClientManager> _logger;
         private readonly IApplicationPaths _appPaths;
         private readonly IFileSystem _fileSystem;
         private readonly IApplicationHost _appHost;
@@ -140,7 +140,7 @@ namespace Emby.Server.Implementations.HttpClientManager
             => SendAsync(options, HttpMethod.Get);
 
         /// <summary>
-        /// Performs a GET request and returns the resulting stream
+        /// Performs a GET request and returns the resulting stream.
         /// </summary>
         /// <param name="options">The options.</param>
         /// <returns>Task{Stream}.</returns>

+ 2 - 2
Emby.Server.Implementations/HttpServer/FileWriter.cs

@@ -32,12 +32,12 @@ namespace Emby.Server.Implementations.HttpServer
         private readonly IFileSystem _fileSystem;
 
         /// <summary>
-        /// The _options
+        /// The _options.
         /// </summary>
         private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
 
         /// <summary>
-        /// The _requested ranges
+        /// The _requested ranges.
         /// </summary>
         private List<KeyValuePair<long, long?>> _requestedRanges;
 

+ 30 - 17
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.HttpServer
         /// </summary>
         public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
 
-        private readonly ILogger _logger;
+        private readonly ILogger<HttpListenerHost> _logger;
         private readonly ILoggerFactory _loggerFactory;
         private readonly IServerConfigurationManager _config;
         private readonly INetworkManager _networkManager;
@@ -210,16 +210,8 @@ namespace Emby.Server.Implementations.HttpServer
             }
         }
 
-        private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog)
+        private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace)
         {
-            bool ignoreStackTrace =
-                ex is SocketException
-                || ex is IOException
-                || ex is OperationCanceledException
-                || ex is SecurityException
-                || ex is AuthenticationException
-                || ex is FileNotFoundException;
-
             if (ignoreStackTrace)
             {
                 _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
@@ -238,7 +230,9 @@ namespace Emby.Server.Implementations.HttpServer
 
             httpRes.StatusCode = statusCode;
 
-            var errContent = NormalizeExceptionMessage(ex) ?? string.Empty;
+            var errContent = _hostEnvironment.IsDevelopment()
+                    ? (NormalizeExceptionMessage(ex) ?? string.Empty)
+                    : "Error processing request.";
             httpRes.ContentType = "text/plain";
             httpRes.ContentLength = errContent.Length;
             await httpRes.WriteAsync(errContent).ConfigureAwait(false);
@@ -405,7 +399,7 @@ namespace Emby.Server.Implementations.HttpServer
             var response = context.Response;
             var localPath = context.Request.Path.ToString();
 
-            var req = new WebSocketSharpRequest(request, response, request.Path, _logger);
+            var req = new WebSocketSharpRequest(request, response, request.Path);
             return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
         }
 
@@ -459,6 +453,7 @@ namespace Emby.Server.Implementations.HttpServer
                     {
                         httpRes.Headers.Add(key, value);
                     }
+
                     httpRes.ContentType = "text/plain";
                     await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
                     return;
@@ -505,14 +500,32 @@ namespace Emby.Server.Implementations.HttpServer
                     var requestInnerEx = GetActualException(requestEx);
                     var statusCode = GetStatusCode(requestInnerEx);
 
-                    // Do not handle 500 server exceptions manually when in development mode
-                    // The framework-defined development exception page will be returned instead
-                    if (statusCode == 500 && _hostEnvironment.IsDevelopment())
+                    foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
+                    {
+                        if (!httpRes.Headers.ContainsKey(key))
+                        {
+                            httpRes.Headers.Add(key, value);
+                        }
+                    }
+
+                    bool ignoreStackTrace =
+                        requestInnerEx is SocketException
+                        || requestInnerEx is IOException
+                        || requestInnerEx is OperationCanceledException
+                        || requestInnerEx is SecurityException
+                        || requestInnerEx is AuthenticationException
+                        || requestInnerEx is FileNotFoundException;
+
+                    // Do not handle 500 server exceptions manually when in development mode.
+                    // Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware.
+                    // However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored,
+                    // because it will log the stack trace when it handles the exception.
+                    if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment())
                     {
                         throw;
                     }
 
-                    await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog).ConfigureAwait(false);
+                    await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
                 }
                 catch (Exception handlerException)
                 {
@@ -579,7 +592,7 @@ namespace Emby.Server.Implementations.HttpServer
         }
 
         /// <summary>
-        /// Get the default CORS headers
+        /// Get the default CORS headers.
         /// </summary>
         /// <param name="req"></param>
         /// <returns></returns>

+ 19 - 12
Emby.Server.Implementations/HttpServer/HttpResultFactory.cs

@@ -37,7 +37,7 @@ namespace Emby.Server.Implementations.HttpServer
         /// <summary>
         /// The logger.
         /// </summary>
-        private readonly ILogger _logger;
+        private readonly ILogger<HttpResultFactory> _logger;
         private readonly IFileSystem _fileSystem;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IStreamHelper _streamHelper;
@@ -50,12 +50,13 @@ namespace Emby.Server.Implementations.HttpServer
             _fileSystem = fileSystem;
             _jsonSerializer = jsonSerializer;
             _streamHelper = streamHelper;
-            _logger = loggerfactory.CreateLogger("HttpResultFactory");
+            _logger = loggerfactory.CreateLogger<HttpResultFactory>();
         }
 
         /// <summary>
         /// Gets the result.
         /// </summary>
+        /// <param name="requestContext">The request context.</param>
         /// <param name="content">The content.</param>
         /// <param name="contentType">Type of the content.</param>
         /// <param name="responseHeaders">The response headers.</param>
@@ -255,16 +256,20 @@ namespace Emby.Server.Implementations.HttpServer
         {
             var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString();
 
-            if (string.IsNullOrEmpty(acceptEncoding))
+            if (!string.IsNullOrEmpty(acceptEncoding))
             {
-                //if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
+                // if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
                 //    return "br";
 
-                if (acceptEncoding.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
+                if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase))
+                {
                     return "deflate";
+                }
 
-                if (acceptEncoding.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
+                if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase))
+                {
                     return "gzip";
+                }
             }
 
             return null;
@@ -421,7 +426,7 @@ namespace Emby.Server.Implementations.HttpServer
         /// </summary>
         private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options)
         {
-            bool noCache = (requestContext.Headers[HeaderNames.CacheControl].ToString()).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
+            bool noCache = requestContext.Headers[HeaderNames.CacheControl].ToString().IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
             AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
 
             if (!noCache)
@@ -575,13 +580,12 @@ namespace Emby.Server.Implementations.HttpServer
                 }
                 catch (NotSupportedException)
                 {
-
                 }
             }
 
             if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue)
             {
-                var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest, _logger)
+                var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest)
                 {
                     OnComplete = options.OnComplete
                 };
@@ -618,8 +622,11 @@ namespace Emby.Server.Implementations.HttpServer
         /// <summary>
         /// Adds the caching responseHeaders.
         /// </summary>
-        private void AddCachingHeaders(IDictionary<string, string> responseHeaders, TimeSpan? cacheDuration,
-            bool noCache, DateTime? lastModifiedDate)
+        private void AddCachingHeaders(
+            IDictionary<string, string> responseHeaders,
+            TimeSpan? cacheDuration,
+            bool noCache,
+            DateTime? lastModifiedDate)
         {
             if (noCache)
             {
@@ -688,7 +695,7 @@ namespace Emby.Server.Implementations.HttpServer
 
 
         /// <summary>
-        /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
+        /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that.
         /// </summary>
         /// <param name="date">The date.</param>
         /// <returns>DateTime.</returns>

+ 84 - 97
Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Buffers;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
@@ -8,46 +9,17 @@ using System.Net;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
 using Microsoft.Net.Http.Headers;
 
 namespace Emby.Server.Implementations.HttpServer
 {
     public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult
     {
-        /// <summary>
-        /// Gets or sets the source stream.
-        /// </summary>
-        /// <value>The source stream.</value>
-        private Stream SourceStream { get; set; }
-        private string RangeHeader { get; set; }
-        private bool IsHeadRequest { get; set; }
-
-        private long RangeStart { get; set; }
-        private long RangeEnd { get; set; }
-        private long RangeLength { get; set; }
-        private long TotalContentLength { get; set; }
-
-        public Action OnComplete { get; set; }
-        private readonly ILogger _logger;
-
         private const int BufferSize = 81920;
 
-        /// <summary>
-        /// The _options
-        /// </summary>
         private readonly Dictionary<string, string> _options = new Dictionary<string, string>();
 
-        /// <summary>
-        /// The us culture
-        /// </summary>
-        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
-        /// <summary>
-        /// Additional HTTP Headers
-        /// </summary>
-        /// <value>The headers.</value>
-        public IDictionary<string, string> Headers => _options;
+        private List<KeyValuePair<long, long?>> _requestedRanges;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="RangeRequestWriter" /> class.
@@ -57,8 +29,7 @@ namespace Emby.Server.Implementations.HttpServer
         /// <param name="source">The source.</param>
         /// <param name="contentType">Type of the content.</param>
         /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
-        /// <param name="logger">The logger instance.</param>
-        public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest, ILogger logger)
+        public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest)
         {
             if (string.IsNullOrEmpty(contentType))
             {
@@ -68,7 +39,6 @@ namespace Emby.Server.Implementations.HttpServer
             RangeHeader = rangeHeader;
             SourceStream = source;
             IsHeadRequest = isHeadRequest;
-            this._logger = logger;
 
             ContentType = contentType;
             Headers[HeaderNames.ContentType] = contentType;
@@ -79,40 +49,26 @@ namespace Emby.Server.Implementations.HttpServer
         }
 
         /// <summary>
-        /// Sets the range values.
+        /// Gets or sets the source stream.
         /// </summary>
-        private void SetRangeValues(long contentLength)
-        {
-            var requestedRange = RequestedRanges[0];
-
-            TotalContentLength = contentLength;
-
-            // If the requested range is "0-", we can optimize by just doing a stream copy
-            if (!requestedRange.Value.HasValue)
-            {
-                RangeEnd = TotalContentLength - 1;
-            }
-            else
-            {
-                RangeEnd = requestedRange.Value.Value;
-            }
-
-            RangeStart = requestedRange.Key;
-            RangeLength = 1 + RangeEnd - RangeStart;
+        /// <value>The source stream.</value>
+        private Stream SourceStream { get; set; }
+        private string RangeHeader { get; set; }
+        private bool IsHeadRequest { get; set; }
 
-            Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
-            Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
+        private long RangeStart { get; set; }
+        private long RangeEnd { get; set; }
+        private long RangeLength { get; set; }
+        private long TotalContentLength { get; set; }
 
-            if (RangeStart > 0 && SourceStream.CanSeek)
-            {
-                SourceStream.Position = RangeStart;
-            }
-        }
+        public Action OnComplete { get; set; }
 
         /// <summary>
-        /// The _requested ranges
+        /// Additional HTTP Headers
         /// </summary>
-        private List<KeyValuePair<long, long?>> _requestedRanges;
+        /// <value>The headers.</value>
+        public IDictionary<string, string> Headers => _options;
+
         /// <summary>
         /// Gets the requested ranges.
         /// </summary>
@@ -137,11 +93,12 @@ namespace Emby.Server.Implementations.HttpServer
 
                         if (!string.IsNullOrEmpty(vals[0]))
                         {
-                            start = long.Parse(vals[0], UsCulture);
+                            start = long.Parse(vals[0], CultureInfo.InvariantCulture);
                         }
+
                         if (!string.IsNullOrEmpty(vals[1]))
                         {
-                            end = long.Parse(vals[1], UsCulture);
+                            end = long.Parse(vals[1], CultureInfo.InvariantCulture);
                         }
 
                         _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
@@ -152,6 +109,51 @@ namespace Emby.Server.Implementations.HttpServer
             }
         }
 
+        public string ContentType { get; set; }
+
+        public IRequest RequestContext { get; set; }
+
+        public object Response { get; set; }
+
+        public int Status { get; set; }
+
+        public HttpStatusCode StatusCode
+        {
+            get => (HttpStatusCode)Status;
+            set => Status = (int)value;
+        }
+
+        /// <summary>
+        /// Sets the range values.
+        /// </summary>
+        private void SetRangeValues(long contentLength)
+        {
+            var requestedRange = RequestedRanges[0];
+
+            TotalContentLength = contentLength;
+
+            // If the requested range is "0-", we can optimize by just doing a stream copy
+            if (!requestedRange.Value.HasValue)
+            {
+                RangeEnd = TotalContentLength - 1;
+            }
+            else
+            {
+                RangeEnd = requestedRange.Value.Value;
+            }
+
+            RangeStart = requestedRange.Key;
+            RangeLength = 1 + RangeEnd - RangeStart;
+
+            Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
+            Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
+
+            if (RangeStart > 0 && SourceStream.CanSeek)
+            {
+                SourceStream.Position = RangeStart;
+            }
+        }
+
         public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
         {
             try
@@ -167,59 +169,44 @@ namespace Emby.Server.Implementations.HttpServer
                     // If the requested range is "0-", we can optimize by just doing a stream copy
                     if (RangeEnd >= TotalContentLength - 1)
                     {
-                        await source.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false);
+                        await source.CopyToAsync(responseStream, BufferSize, cancellationToken).ConfigureAwait(false);
                     }
                     else
                     {
-                        await CopyToInternalAsync(source, responseStream, RangeLength).ConfigureAwait(false);
+                        await CopyToInternalAsync(source, responseStream, RangeLength, cancellationToken).ConfigureAwait(false);
                     }
                 }
             }
             finally
             {
-                if (OnComplete != null)
-                {
-                    OnComplete();
-                }
+                OnComplete?.Invoke();
             }
         }
 
-        private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength)
+        private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
         {
-            var array = new byte[BufferSize];
-            int bytesRead;
-            while ((bytesRead = await source.ReadAsync(array, 0, array.Length).ConfigureAwait(false)) != 0)
+            var array = ArrayPool<byte>.Shared.Rent(BufferSize);
+            try
             {
-                if (bytesRead == 0)
+                int bytesRead;
+                while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
                 {
-                    break;
-                }
+                    var bytesToCopy = Math.Min(bytesRead, copyLength);
 
-                var bytesToCopy = Math.Min(bytesRead, copyLength);
+                    await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy), cancellationToken).ConfigureAwait(false);
 
-                await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy)).ConfigureAwait(false);
+                    copyLength -= bytesToCopy;
 
-                copyLength -= bytesToCopy;
-
-                if (copyLength <= 0)
-                {
-                    break;
+                    if (copyLength <= 0)
+                    {
+                        break;
+                    }
                 }
             }
-        }
-
-        public string ContentType { get; set; }
-
-        public IRequest RequestContext { get; set; }
-
-        public object Response { get; set; }
-
-        public int Status { get; set; }
-
-        public HttpStatusCode StatusCode
-        {
-            get => (HttpStatusCode)Status;
-            set => Status = (int)value;
+            finally
+            {
+                ArrayPool<byte>.Shared.Return(array);
+            }
         }
     }
 }

+ 2 - 3
Emby.Server.Implementations/HttpServer/ResponseFilter.cs

@@ -1,5 +1,4 @@
 using System;
-using System.Collections.Generic;
 using System.Globalization;
 using System.Text;
 using MediaBrowser.Controller.Net;
@@ -42,11 +41,11 @@ namespace Emby.Server.Implementations.HttpServer
                 res.Headers.Add(key, value);
             }
             // Try to prevent compatibility view
-            res.Headers["Access-Control-Allow-Headers"] = ("Accept, Accept-Language, Authorization, Cache-Control, " +
+            res.Headers["Access-Control-Allow-Headers"] = "Accept, Accept-Language, Authorization, Cache-Control, " +
                 "Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " +
                 "Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " +
                 "Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " +
-                "X-Emby-Authorization");
+                "X-Emby-Authorization";
 
             if (dto is Exception exception)
             {

+ 32 - 13
Emby.Server.Implementations/HttpServer/Security/AuthService.cs

@@ -2,11 +2,12 @@
 
 using System;
 using System.Linq;
-using System.Security.Authentication;
 using Emby.Server.Implementations.SocketSharp;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
@@ -45,11 +46,27 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
         public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
         {
-            var req = new WebSocketSharpRequest(request, null, request.Path, _logger);
+            var req = new WebSocketSharpRequest(request, null, request.Path);
             var user = ValidateUser(req, authAttributes);
             return user;
         }
 
+        public AuthorizationInfo Authenticate(HttpRequest request)
+        {
+            var auth = _authorizationContext.GetAuthorizationInfo(request);
+            if (auth?.User == null)
+            {
+                return null;
+            }
+
+            if (auth.User.HasPermission(PermissionKind.IsDisabled))
+            {
+                throw new SecurityException("User account has been disabled.");
+            }
+
+            return auth;
+        }
+
         private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
         {
             // This code is executed before the service
@@ -90,7 +107,8 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 !string.IsNullOrEmpty(auth.Client) &&
                 !string.IsNullOrEmpty(auth.Device))
             {
-                _sessionManager.LogSessionActivity(auth.Client,
+                _sessionManager.LogSessionActivity(
+                    auth.Client,
                     auth.Version,
                     auth.DeviceId,
                     auth.Device,
@@ -104,21 +122,21 @@ namespace Emby.Server.Implementations.HttpServer.Security
         private void ValidateUserAccess(
             User user,
             IRequest request,
-            IAuthenticationAttributes authAttribtues,
+            IAuthenticationAttributes authAttributes,
             AuthorizationInfo auth)
         {
-            if (user.Policy.IsDisabled)
+            if (user.HasPermission(PermissionKind.IsDisabled))
             {
                 throw new SecurityException("User account has been disabled.");
             }
 
-            if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(request.RemoteIp))
+            if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp))
             {
                 throw new SecurityException("User account has been disabled.");
             }
 
-            if (!user.Policy.IsAdministrator
-                && !authAttribtues.EscapeParentalControl
+            if (!user.HasPermission(PermissionKind.IsAdministrator)
+                && !authAttributes.EscapeParentalControl
                 && !user.IsParentalScheduleAllowed())
             {
                 request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl");
@@ -138,6 +156,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
             {
                 return true;
             }
+
             if (authAttribtues.AllowLocalOnly && request.IsLocal)
             {
                 return true;
@@ -180,7 +199,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
         {
             if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase))
             {
-                if (user == null || !user.Policy.IsAdministrator)
+                if (user == null || !user.HasPermission(PermissionKind.IsAdministrator))
                 {
                     throw new SecurityException("User does not have admin access.");
                 }
@@ -188,7 +207,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
             if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase))
             {
-                if (user == null || !user.Policy.EnableContentDeletion)
+                if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion))
                 {
                     throw new SecurityException("User does not have delete access.");
                 }
@@ -196,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
             if (roles.Contains("download", StringComparer.OrdinalIgnoreCase))
             {
-                if (user == null || !user.Policy.EnableContentDownloading)
+                if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading))
                 {
                     throw new SecurityException("User does not have download access.");
                 }
@@ -223,7 +242,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 throw new AuthenticationException("Access token is invalid or expired.");
             }
 
-            //if (!string.IsNullOrEmpty(info.UserId))
+            // if (!string.IsNullOrEmpty(info.UserId))
             //{
             //    var user = _userManager.GetUserById(info.UserId);
 

+ 71 - 31
Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs

@@ -8,6 +8,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
 using Microsoft.Net.Http.Headers;
 
 namespace Emby.Server.Implementations.HttpServer.Security
@@ -38,6 +39,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
             return GetAuthorization(requestContext);
         }
 
+        public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
+        {
+            var auth = GetAuthorizationDictionary(requestContext);
+            var (authInfo, _) =
+                GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
+            return authInfo;
+        }
+
         /// <summary>
         /// Gets the authorization.
         /// </summary>
@@ -46,7 +55,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
         private AuthorizationInfo GetAuthorization(IRequest httpReq)
         {
             var auth = GetAuthorizationDictionary(httpReq);
+            var (authInfo, originalAuthInfo) =
+                GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
+
+            if (originalAuthInfo != null)
+            {
+                httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
+            }
+
+            httpReq.Items["AuthorizationInfo"] = authInfo;
+            return authInfo;
+        }
 
+        private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
+            in Dictionary<string, string> auth,
+            in IHeaderDictionary headers,
+            in IQueryCollection queryString)
+        {
             string deviceId = null;
             string device = null;
             string client = null;
@@ -64,19 +89,20 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
             if (string.IsNullOrEmpty(token))
             {
-                token = httpReq.Headers["X-Emby-Token"];
+                token = headers["X-Emby-Token"];
             }
 
             if (string.IsNullOrEmpty(token))
             {
-                token = httpReq.Headers["X-MediaBrowser-Token"];
+                token = headers["X-MediaBrowser-Token"];
             }
+
             if (string.IsNullOrEmpty(token))
             {
-                token = httpReq.QueryString["api_key"];
+                token = queryString["api_key"];
             }
 
-            var info = new AuthorizationInfo
+            var authInfo = new AuthorizationInfo
             {
                 Client = client,
                 Device = device,
@@ -85,6 +111,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 Token = token
             };
 
+            AuthenticationInfo originalAuthenticationInfo = null;
             if (!string.IsNullOrWhiteSpace(token))
             {
                 var result = _authRepo.Get(new AuthenticationInfoQuery
@@ -92,81 +119,77 @@ namespace Emby.Server.Implementations.HttpServer.Security
                     AccessToken = token
                 });
 
-                var tokenInfo = result.Items.Count > 0 ? result.Items[0] : null;
+                originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
 
-                if (tokenInfo != null)
+                if (originalAuthenticationInfo != null)
                 {
                     var updateToken = false;
 
                     // TODO: Remove these checks for IsNullOrWhiteSpace
-                    if (string.IsNullOrWhiteSpace(info.Client))
+                    if (string.IsNullOrWhiteSpace(authInfo.Client))
                     {
-                        info.Client = tokenInfo.AppName;
+                        authInfo.Client = originalAuthenticationInfo.AppName;
                     }
 
-                    if (string.IsNullOrWhiteSpace(info.DeviceId))
+                    if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
                     {
-                        info.DeviceId = tokenInfo.DeviceId;
+                        authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
                     }
 
                     // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
-                    var allowTokenInfoUpdate = info.Client == null || info.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
+                    var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
 
-                    if (string.IsNullOrWhiteSpace(info.Device))
+                    if (string.IsNullOrWhiteSpace(authInfo.Device))
                     {
-                        info.Device = tokenInfo.DeviceName;
+                        authInfo.Device = originalAuthenticationInfo.DeviceName;
                     }
-
-                    else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
+                    else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
                     {
                         if (allowTokenInfoUpdate)
                         {
                             updateToken = true;
-                            tokenInfo.DeviceName = info.Device;
+                            originalAuthenticationInfo.DeviceName = authInfo.Device;
                         }
                     }
 
-                    if (string.IsNullOrWhiteSpace(info.Version))
+                    if (string.IsNullOrWhiteSpace(authInfo.Version))
                     {
-                        info.Version = tokenInfo.AppVersion;
+                        authInfo.Version = originalAuthenticationInfo.AppVersion;
                     }
-                    else if (!string.Equals(info.Version, tokenInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
+                    else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
                     {
                         if (allowTokenInfoUpdate)
                         {
                             updateToken = true;
-                            tokenInfo.AppVersion = info.Version;
+                            originalAuthenticationInfo.AppVersion = authInfo.Version;
                         }
                     }
 
-                    if ((DateTime.UtcNow - tokenInfo.DateLastActivity).TotalMinutes > 3)
+                    if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
                     {
-                        tokenInfo.DateLastActivity = DateTime.UtcNow;
+                        originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
                         updateToken = true;
                     }
 
-                    if (!tokenInfo.UserId.Equals(Guid.Empty))
+                    if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
                     {
-                        info.User = _userManager.GetUserById(tokenInfo.UserId);
+                        authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
 
-                        if (info.User != null && !string.Equals(info.User.Name, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
+                        if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
                         {
-                            tokenInfo.UserName = info.User.Name;
+                            originalAuthenticationInfo.UserName = authInfo.User.Username;
                             updateToken = true;
                         }
                     }
 
                     if (updateToken)
                     {
-                        _authRepo.Update(tokenInfo);
+                        _authRepo.Update(originalAuthenticationInfo);
                     }
                 }
-                httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo;
             }
 
-            httpReq.Items["AuthorizationInfo"] = info;
-
-            return info;
+            return (authInfo, originalAuthenticationInfo);
         }
 
         /// <summary>
@@ -186,6 +209,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
             return GetAuthorization(auth);
         }
 
+        /// <summary>
+        /// Gets the auth.
+        /// </summary>
+        /// <param name="httpReq">The HTTP req.</param>
+        /// <returns>Dictionary{System.StringSystem.String}.</returns>
+        private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
+        {
+            var auth = httpReq.Headers["X-Emby-Authorization"];
+
+            if (string.IsNullOrEmpty(auth))
+            {
+                auth = httpReq.Headers[HeaderNames.Authorization];
+            }
+
+            return GetAuthorization(auth);
+        }
+
         /// <summary>
         /// Gets the authorization.
         /// </summary>

+ 1 - 1
Emby.Server.Implementations/HttpServer/Security/SessionContext.cs

@@ -1,7 +1,7 @@
 #pragma warning disable CS1591
 
 using System;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;

+ 42 - 2
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.HttpServer
         /// <summary>
         /// The logger.
         /// </summary>
-        private readonly ILogger _logger;
+        private readonly ILogger<WebSocketConnection> _logger;
 
         /// <summary>
         /// The json serializer options.
@@ -78,6 +78,9 @@ namespace Emby.Server.Implementations.HttpServer
         /// <value>The last activity date.</value>
         public DateTime LastActivityDate { get; private set; }
 
+        /// <inheritdoc />
+        public DateTime LastKeepAliveDate { get; set; }
+
         /// <summary>
         /// Gets or sets the query string.
         /// </summary>
@@ -218,7 +221,44 @@ namespace Emby.Server.Implementations.HttpServer
                 Connection = this
             };
 
-            await OnReceive(info).ConfigureAwait(false);
+            if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
+            {
+                await SendKeepAliveResponse();
+            }
+            else
+            {
+                await OnReceive(info).ConfigureAwait(false);
+            }
+        }
+
+        private Task SendKeepAliveResponse()
+        {
+            LastKeepAliveDate = DateTime.UtcNow;
+            return SendAsync(
+                new WebSocketMessage<string>
+                {
+                    MessageId = Guid.NewGuid(),
+                    MessageType = "KeepAlive"
+                }, CancellationToken.None);
+        }
+
+        /// <inheritdoc />
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        /// <summary>
+        /// Releases unmanaged and - optionally - managed resources.
+        /// </summary>
+        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected virtual void Dispose(bool dispose)
+        {
+            if (dispose)
+            {
+                _socket.Dispose();
+            }
         }
     }
 }

+ 3 - 41
Emby.Server.Implementations/IO/LibraryMonitor.cs

@@ -11,13 +11,14 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Model.IO;
+using Emby.Server.Implementations.Library;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.IO
 {
     public class LibraryMonitor : ILibraryMonitor
     {
-        private readonly ILogger _logger;
+        private readonly ILogger<LibraryMonitor> _logger;
         private readonly ILibraryManager _libraryManager;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IFileSystem _fileSystem;
@@ -37,38 +38,6 @@ namespace Emby.Server.Implementations.IO
         /// </summary>
         private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
-        /// <summary>
-        /// Any file name ending in any of these will be ignored by the watchers.
-        /// </summary>
-        private static readonly HashSet<string> _alwaysIgnoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
-        {
-            "small.jpg",
-            "albumart.jpg",
-
-            // WMC temp recording directories that will constantly be written to
-            "TempRec",
-            "TempSBE"
-        };
-
-        private static readonly string[] _alwaysIgnoreSubstrings = new string[]
-        {
-            // Synology
-            "eaDir",
-            "#recycle",
-            ".wd_tv",
-            ".actors"
-        };
-
-        private static readonly HashSet<string> _alwaysIgnoreExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
-        {
-            // thumbs.db
-            ".db",
-
-            // bts sync files
-            ".bts",
-            ".sync"
-        };
-
         /// <summary>
         /// Add the path to our temporary ignore list.  Use when writing to a path within our listening scope.
         /// </summary>
@@ -297,7 +266,6 @@ namespace Emby.Server.Implementations.IO
                     {
                         DisposeWatcher(newWatcher, false);
                     }
-
                 }
                 catch (Exception ex)
                 {
@@ -395,12 +363,7 @@ namespace Emby.Server.Implementations.IO
                 throw new ArgumentNullException(nameof(path));
             }
 
-            var filename = Path.GetFileName(path);
-
-            var monitorPath = !string.IsNullOrEmpty(filename) &&
-                !_alwaysIgnoreFiles.Contains(filename) &&
-                !_alwaysIgnoreExtensions.Contains(Path.GetExtension(path)) &&
-                _alwaysIgnoreSubstrings.All(i => path.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1);
+            var monitorPath = !IgnorePatterns.ShouldIgnore(path);
 
             // Ignore certain files
             var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList();
@@ -429,7 +392,6 @@ namespace Emby.Server.Implementations.IO
                 }
 
                 return false;
-
             }))
             {
                 monitorPath = false;

+ 4 - 2
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -20,7 +20,7 @@ namespace Emby.Server.Implementations.IO
     /// </summary>
     public class ManagedFileSystem : IFileSystem
     {
-        protected ILogger Logger;
+        protected ILogger<ManagedFileSystem> Logger;
 
         private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
         private readonly string _tempPath;
@@ -237,7 +237,7 @@ namespace Emby.Server.Implementations.IO
             {
                 result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
 
-                //if (!result.IsDirectory)
+                // if (!result.IsDirectory)
                 //{
                 //    result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
                 //}
@@ -628,6 +628,7 @@ namespace Emby.Server.Implementations.IO
                     {
                         return false;
                     }
+
                     return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
                 });
             }
@@ -682,6 +683,7 @@ namespace Emby.Server.Implementations.IO
                     {
                         return false;
                     }
+
                     return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
                 });
             }

+ 6 - 2
Emby.Server.Implementations/IStartupOptions.cs

@@ -1,3 +1,7 @@
+#pragma warning disable CS1591
+
+using System;
+
 namespace Emby.Server.Implementations
 {
     public interface IStartupOptions
@@ -33,8 +37,8 @@ namespace Emby.Server.Implementations
         string RestartArgs { get; }
 
         /// <summary>
-        /// Gets the value of the --plugin-manifest-url command line option.
+        /// Gets the value of the --published-server-url command line option.
         /// </summary>
-        string PluginManifestUrl { get; }
+        Uri PublishedServerUrl { get; }
     }
 }

+ 60 - 0
Emby.Server.Implementations/Images/ArtistImageProvider.cs

@@ -0,0 +1,60 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Emby.Server.Implementations.Images;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Images
+{
+    /// <summary>
+    /// Class ArtistImageProvider.
+    /// </summary>
+    public class ArtistImageProvider : BaseDynamicImageProvider<MusicArtist>
+    {
+        /// <summary>
+        /// The library manager.
+        /// </summary>
+        private readonly ILibraryManager _libraryManager;
+
+        public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+        {
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Get children objects used to create an artist image.
+        /// </summary>
+        /// <param name="item">The artist used to create the image.</param>
+        /// <returns>Any relevant children objects.</returns>
+        protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
+        {
+            return Array.Empty<BaseItem>();
+
+            // TODO enable this when BaseDynamicImageProvider objects are configurable
+            // return _libraryManager.GetItemList(new InternalItemsQuery
+            // {
+            //    ArtistIds = new[] { item.Id },
+            //    IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+            //    OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
+            //    Limit = 4,
+            //    Recursive = true,
+            //    ImageTypes = new[] { ImageType.Primary },
+            //    DtoOptions = new DtoOptions(false)
+            // });
+        }
+    }
+}

+ 10 - 3
Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs

@@ -194,7 +194,8 @@ namespace Emby.Server.Implementations.Images
             return outputPath;
         }
 
-        protected virtual string CreateImage(BaseItem item,
+        protected virtual string CreateImage(
+            BaseItem item,
             IReadOnlyCollection<BaseItem> itemsWithImages,
             string outputPathWithoutExtension,
             ImageType imageType,
@@ -214,7 +215,12 @@ namespace Emby.Server.Implementations.Images
 
             if (imageType == ImageType.Primary)
             {
-                if (item is UserView || item is Playlist || item is MusicGenre || item is Genre || item is PhotoAlbum)
+                if (item is UserView
+                    || item is Playlist
+                    || item is MusicGenre
+                    || item is Genre
+                    || item is PhotoAlbum
+                    || item is MusicArtist)
                 {
                     return CreateSquareCollage(item, itemsWithImages, outputPath);
                 }
@@ -225,7 +231,7 @@ namespace Emby.Server.Implementations.Images
             throw new ArgumentException("Unexpected image type", nameof(imageType));
         }
 
-        public bool HasChanged(BaseItem item, IDirectoryService directoryServicee)
+        public bool HasChanged(BaseItem item, IDirectoryService directoryService)
         {
             if (!Supports(item))
             {
@@ -236,6 +242,7 @@ namespace Emby.Server.Implementations.Images
             {
                 return true;
             }
+
             if (SupportedImages.Contains(ImageType.Thumb) && HasChanged(item, ImageType.Thumb))
             {
                 return true;

+ 3 - 2
Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs → Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.IO;
@@ -11,7 +13,7 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Querying;
 
-namespace Emby.Server.Implementations.UserViews
+namespace Emby.Server.Implementations.Images
 {
     public class CollectionFolderImageProvider : BaseDynamicImageProvider<CollectionFolder>
     {
@@ -69,7 +71,6 @@ namespace Emby.Server.Implementations.UserViews
                     new ValueTuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending)
                 },
                 IncludeItemTypes = includeItemTypes
-
             });
         }
 

+ 4 - 5
Emby.Server.Implementations/UserViews/DynamicImageProvider.cs → Emby.Server.Implementations/Images/DynamicImageProvider.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.IO;
@@ -14,18 +16,16 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 
-namespace Emby.Server.Implementations.UserViews
+namespace Emby.Server.Implementations.Images
 {
     public class DynamicImageProvider : BaseDynamicImageProvider<UserView>
     {
         private readonly IUserManager _userManager;
-        private readonly ILibraryManager _libraryManager;
 
-        public DynamicImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, IUserManager userManager, ILibraryManager libraryManager)
+        public DynamicImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, IUserManager userManager)
             : base(fileSystem, providerManager, applicationPaths, imageProcessor)
         {
             _userManager = userManager;
-            _libraryManager = libraryManager;
         }
 
         protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
@@ -78,7 +78,6 @@ namespace Emby.Server.Implementations.UserViews
                 }
 
                 return i;
-
             }).GroupBy(x => x.Id)
             .Select(x => x.First());
 

+ 6 - 8
Emby.Server.Implementations/UserViews/FolderImageProvider.cs → Emby.Server.Implementations/Images/FolderImageProvider.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using Emby.Server.Implementations.Images;
 using MediaBrowser.Common.Configuration;
@@ -11,7 +13,7 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Querying;
 
-namespace Emby.Server.Implementations.UserViews
+namespace Emby.Server.Implementations.Images
 {
     public abstract class BaseFolderImageProvider<T> : BaseDynamicImageProvider<T>
         where T : Folder, new()
@@ -75,16 +77,12 @@ namespace Emby.Server.Implementations.UserViews
                 return false;
             }
 
-            var folder = item as Folder;
-            if (folder != null)
+            if (item is Folder && item.IsTopParent)
             {
-                if (folder.IsTopParent)
-                {
-                    return false;
-                }
+                return false;
             }
+
             return true;
-            //return item.SourceType == SourceType.Library;
         }
     }
 

+ 25 - 54
Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs → Emby.Server.Implementations/Images/GenreImageProvider.cs

@@ -1,6 +1,6 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
-using System.Linq;
-using Emby.Server.Implementations.Images;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
@@ -9,66 +9,21 @@ using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Querying;
 
-namespace Emby.Server.Implementations.Playlists
+namespace Emby.Server.Implementations.Images
 {
-    public class PlaylistImageProvider : BaseDynamicImageProvider<Playlist>
-    {
-        public PlaylistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
-        {
-        }
-
-        protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
-        {
-            var playlist = (Playlist)item;
-
-            return playlist.GetManageableItems()
-                .Select(i =>
-                {
-                    var subItem = i.Item2;
-
-                    var episode = subItem as Episode;
-
-                    if (episode != null)
-                    {
-                        var series = episode.Series;
-                        if (series != null && series.HasImage(ImageType.Primary))
-                        {
-                            return series;
-                        }
-                    }
-
-                    if (subItem.HasImage(ImageType.Primary))
-                    {
-                        return subItem;
-                    }
-
-                    var parent = subItem.GetOwner() ?? subItem.GetParent();
-
-                    if (parent != null && parent.HasImage(ImageType.Primary))
-                    {
-                        if (parent is MusicAlbum)
-                        {
-                            return parent;
-                        }
-                    }
-
-                    return null;
-                })
-                .Where(i => i != null)
-                .GroupBy(x => x.Id)
-                .Select(x => x.First())
-                .ToList();
-        }
-    }
-
+    /// <summary>
+    /// Class MusicGenreImageProvider.
+    /// </summary>
     public class MusicGenreImageProvider : BaseDynamicImageProvider<MusicGenre>
     {
+        /// <summary>
+        /// The library manager.
+        /// </summary>
         private readonly ILibraryManager _libraryManager;
 
         public MusicGenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
@@ -76,6 +31,11 @@ namespace Emby.Server.Implementations.Playlists
             _libraryManager = libraryManager;
         }
 
+        /// <summary>
+        /// Get children objects used to create an music genre image.
+        /// </summary>
+        /// <param name="item">The music genre used to create the image.</param>
+        /// <returns>Any relevant children objects.</returns>
         protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
         {
             return _libraryManager.GetItemList(new InternalItemsQuery
@@ -91,8 +51,14 @@ namespace Emby.Server.Implementations.Playlists
         }
     }
 
+    /// <summary>
+    /// Class GenreImageProvider.
+    /// </summary>
     public class GenreImageProvider : BaseDynamicImageProvider<Genre>
     {
+        /// <summary>
+        /// The library manager.
+        /// </summary>
         private readonly ILibraryManager _libraryManager;
 
         public GenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
@@ -100,6 +66,11 @@ namespace Emby.Server.Implementations.Playlists
             _libraryManager = libraryManager;
         }
 
+        /// <summary>
+        /// Get children objects used to create an genre image.
+        /// </summary>
+        /// <param name="item">The genre used to create the image.</param>
+        /// <returns>Any relevant children objects.</returns>
         protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
         {
             return _libraryManager.GetItemList(new InternalItemsQuery

+ 66 - 0
Emby.Server.Implementations/Images/PlaylistImageProvider.cs

@@ -0,0 +1,66 @@
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Images
+{
+    public class PlaylistImageProvider : BaseDynamicImageProvider<Playlist>
+    {
+        public PlaylistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+        {
+        }
+
+        protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
+        {
+            var playlist = (Playlist)item;
+
+            return playlist.GetManageableItems()
+                .Select(i =>
+                {
+                    var subItem = i.Item2;
+
+                    var episode = subItem as Episode;
+
+                    if (episode != null)
+                    {
+                        var series = episode.Series;
+                        if (series != null && series.HasImage(ImageType.Primary))
+                        {
+                            return series;
+                        }
+                    }
+
+                    if (subItem.HasImage(ImageType.Primary))
+                    {
+                        return subItem;
+                    }
+
+                    var parent = subItem.GetOwner() ?? subItem.GetParent();
+
+                    if (parent != null && parent.HasImage(ImageType.Primary))
+                    {
+                        if (parent is MusicAlbum)
+                        {
+                            return parent;
+                        }
+                    }
+
+                    return null;
+                })
+                .Where(i => i != null)
+                .GroupBy(x => x.Id)
+                .Select(x => x.First())
+                .ToList();
+        }
+    }
+}

+ 15 - 46
Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs

@@ -1,7 +1,6 @@
 using System;
 using System.IO;
-using System.Linq;
-using System.Text.RegularExpressions;
+using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Resolvers;
@@ -10,73 +9,48 @@ using MediaBrowser.Model.IO;
 namespace Emby.Server.Implementations.Library
 {
     /// <summary>
-    /// Provides the core resolver ignore rules
+    /// Provides the core resolver ignore rules.
     /// </summary>
     public class CoreResolutionIgnoreRule : IResolverIgnoreRule
     {
         private readonly ILibraryManager _libraryManager;
-
-        /// <summary>
-        /// Any folder named in this list will be ignored
-        /// </summary>
-        private static readonly string[] _ignoreFolders =
-        {
-                "metadata",
-                "ps3_update",
-                "ps3_vprm",
-                "extrafanart",
-                "extrathumbs",
-                ".actors",
-                ".wd_tv",
-
-                // Synology
-                "@eaDir",
-                "eaDir",
-                "#recycle",
-
-                // Qnap
-                "@Recycle",
-                ".@__thumb",
-                "$RECYCLE.BIN",
-                "System Volume Information",
-                ".grab",
-        };
+        private readonly IServerApplicationPaths _serverApplicationPaths;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
         /// </summary>
         /// <param name="libraryManager">The library manager.</param>
-        public CoreResolutionIgnoreRule(ILibraryManager libraryManager)
+        /// <param name="serverApplicationPaths">The server application paths.</param>
+        public CoreResolutionIgnoreRule(ILibraryManager libraryManager, IServerApplicationPaths serverApplicationPaths)
         {
             _libraryManager = libraryManager;
+            _serverApplicationPaths = serverApplicationPaths;
         }
 
         /// <inheritdoc />
         public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent)
         {
+            // Don't ignore application folders
+            if (fileInfo.FullName.Contains(_serverApplicationPaths.RootFolderPath, StringComparison.InvariantCulture))
+            {
+                return false;
+            }
+
             // Don't ignore top level folders
             if (fileInfo.IsDirectory && parent is AggregateFolder)
             {
                 return false;
             }
 
-            var filename = fileInfo.Name;
-
-            // Ignore hidden files on UNIX
-            if (Environment.OSVersion.Platform != PlatformID.Win32NT
-                && filename[0] == '.')
+            if (IgnorePatterns.ShouldIgnore(fileInfo.FullName))
             {
                 return true;
             }
 
+            var filename = fileInfo.Name;
+
             if (fileInfo.IsDirectory)
             {
-                // Ignore any folders in our list
-                if (_ignoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase))
-                {
-                    return true;
-                }
-
                 if (parent != null)
                 {
                     // Ignore trailer folders but allow it at the collection level
@@ -109,11 +83,6 @@ namespace Emby.Server.Implementations.Library
                         return true;
                     }
                 }
-
-                // Ignore samples
-                Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase);
-
-                return m.Success;
             }
 
             return false;

+ 2 - 0
Emby.Server.Implementations/Library/ExclusiveLiveStream.cs

@@ -12,11 +12,13 @@ namespace Emby.Server.Implementations.Library
     public class ExclusiveLiveStream : ILiveStream
     {
         public int ConsumerCount { get; set; }
+
         public string OriginalStreamId { get; set; }
 
         public string TunerHostId => null;
 
         public bool EnableStreamSharing { get; set; }
+
         public MediaSourceInfo MediaSource { get; set; }
 
         public string UniqueId { get; private set; }

+ 74 - 0
Emby.Server.Implementations/Library/IgnorePatterns.cs

@@ -0,0 +1,74 @@
+using System.Linq;
+using DotNet.Globbing;
+
+namespace Emby.Server.Implementations.Library
+{
+    /// <summary>
+    /// Glob patterns for files to ignore.
+    /// </summary>
+    public static class IgnorePatterns
+    {
+        /// <summary>
+        /// Files matching these glob patterns will be ignored.
+        /// </summary>
+        public static readonly string[] Patterns = new string[]
+        {
+            "**/small.jpg",
+            "**/albumart.jpg",
+            "**/*sample*",
+
+            // Directories
+            "**/metadata/**",
+            "**/ps3_update/**",
+            "**/ps3_vprm/**",
+            "**/extrafanart/**",
+            "**/extrathumbs/**",
+            "**/.actors/**",
+            "**/.wd_tv/**",
+            "**/lost+found/**",
+
+            // WMC temp recording directories that will constantly be written to
+            "**/TempRec/**",
+            "**/TempSBE/**",
+
+            // Synology
+            "**/eaDir/**",
+            "**/@eaDir/**",
+            "**/#recycle/**",
+
+            // Qnap
+            "**/@Recycle/**",
+            "**/.@__thumb/**",
+            "**/$RECYCLE.BIN/**",
+            "**/System Volume Information/**",
+            "**/.grab/**",
+
+            // Unix hidden files and directories
+            "**/.*/**",
+
+            // thumbs.db
+            "**/thumbs.db",
+
+            // bts sync files
+            "**/*.bts",
+            "**/*.sync",
+        };
+
+        private static readonly GlobOptions _globOptions = new GlobOptions
+        {
+            Evaluation = {
+                CaseInsensitive = true
+            }
+        };
+
+        private static readonly Glob[] _globs = Patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray();
+
+        /// <summary>
+        /// Returns true if the supplied path should be ignored.
+        /// </summary>
+        public static bool ShouldIgnore(string path)
+        {
+            return _globs.Any(g => g.IsMatch(path));
+        }
+    }
+}

+ 133 - 22
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -17,14 +17,16 @@ using Emby.Server.Implementations.Library.Resolvers;
 using Emby.Server.Implementations.Library.Validators;
 using Emby.Server.Implementations.Playlists;
 using Emby.Server.Implementations.ScheduledTasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
@@ -35,6 +37,7 @@ using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
@@ -44,17 +47,20 @@ using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Providers.MediaInfo;
 using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Person = MediaBrowser.Controller.Entities.Person;
 using SortOrder = MediaBrowser.Model.Entities.SortOrder;
 using VideoResolver = Emby.Naming.Video.VideoResolver;
 
 namespace Emby.Server.Implementations.Library
 {
     /// <summary>
-    /// Class LibraryManager
+    /// Class LibraryManager.
     /// </summary>
     public class LibraryManager : ILibraryManager
     {
-        private readonly ILogger _logger;
+        private readonly ILogger<LibraryManager> _logger;
         private readonly ITaskManager _taskManager;
         private readonly IUserManager _userManager;
         private readonly IUserDataManager _userDataRepository;
@@ -67,6 +73,7 @@ namespace Emby.Server.Implementations.Library
         private readonly IFileSystem _fileSystem;
         private readonly IItemRepository _itemRepository;
         private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
+        private readonly IImageProcessor _imageProcessor;
 
         private NamingOptions _namingOptions;
         private string[] _videoFileExtensions;
@@ -90,13 +97,13 @@ namespace Emby.Server.Implementations.Library
         private IIntroProvider[] IntroProviders { get; set; }
 
         /// <summary>
-        /// Gets or sets the list of entity resolution ignore rules
+        /// Gets or sets the list of entity resolution ignore rules.
         /// </summary>
         /// <value>The entity resolution ignore rules.</value>
         private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
 
         /// <summary>
-        /// Gets or sets the list of currently registered entity resolvers
+        /// Gets or sets the list of currently registered entity resolvers.
         /// </summary>
         /// <value>The entity resolvers enumerable.</value>
         private IItemResolver[] EntityResolvers { get; set; }
@@ -129,12 +136,19 @@ namespace Emby.Server.Implementations.Library
         /// <summary>
         /// Initializes a new instance of the <see cref="LibraryManager" /> class.
         /// </summary>
-        /// <param name="appHost">The application host</param>
+        /// <param name="appHost">The application host.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="taskManager">The task manager.</param>
         /// <param name="userManager">The user manager.</param>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="userDataRepository">The user data repository.</param>
+        /// <param name="libraryMonitorFactory">The library monitor.</param>
+        /// <param name="fileSystem">The file system.</param>
+        /// <param name="providerManagerFactory">The provider manager.</param>
+        /// <param name="userviewManagerFactory">The userview manager.</param>
+        /// <param name="mediaEncoder">The media encoder.</param>
+        /// <param name="itemRepository">The item repository.</param>
+        /// <param name="imageProcessor">The image processor.</param>
         public LibraryManager(
             IServerApplicationHost appHost,
             ILogger<LibraryManager> logger,
@@ -147,7 +161,8 @@ namespace Emby.Server.Implementations.Library
             Lazy<IProviderManager> providerManagerFactory,
             Lazy<IUserViewManager> userviewManagerFactory,
             IMediaEncoder mediaEncoder,
-            IItemRepository itemRepository)
+            IItemRepository itemRepository,
+            IImageProcessor imageProcessor)
         {
             _appHost = appHost;
             _logger = logger;
@@ -161,6 +176,7 @@ namespace Emby.Server.Implementations.Library
             _userviewManagerFactory = userviewManagerFactory;
             _mediaEncoder = mediaEncoder;
             _itemRepository = itemRepository;
+            _imageProcessor = imageProcessor;
 
             _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
 
@@ -193,12 +209,12 @@ namespace Emby.Server.Implementations.Library
         }
 
         /// <summary>
-        /// The _root folder
+        /// The _root folder.
         /// </summary>
         private volatile AggregateFolder _rootFolder;
 
         /// <summary>
-        /// The _root folder sync lock
+        /// The _root folder sync lock.
         /// </summary>
         private readonly object _rootFolderSyncLock = new object();
 
@@ -610,7 +626,7 @@ namespace Emby.Server.Implementations.Library
         }
 
         /// <summary>
-        /// Determines whether a path should be ignored based on its contents - called after the contents have been read
+        /// Determines whether a path should be ignored based on its contents - called after the contents have been read.
         /// </summary>
         /// <param name="args">The args.</param>
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
@@ -695,7 +711,9 @@ namespace Emby.Server.Implementations.Library
 
             Directory.CreateDirectory(rootFolderPath);
 
-            var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath))).DeepCopy<Folder, AggregateFolder>();
+            var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
+                             ((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)))
+                             .DeepCopy<Folder, AggregateFolder>();
 
             // In case program data folder was moved
             if (!string.Equals(rootFolder.Path, rootFolderPath, StringComparison.Ordinal))
@@ -890,7 +908,7 @@ namespace Emby.Server.Implementations.Library
         }
 
         /// <summary>
-        /// Gets a Genre
+        /// Gets a Genre.
         /// </summary>
         /// <param name="name">The name.</param>
         /// <returns>Task{Genre}.</returns>
@@ -971,7 +989,7 @@ namespace Emby.Server.Implementations.Library
         }
 
         /// <summary>
-        /// Reloads the root media folder
+        /// Reloads the root media folder.
         /// </summary>
         /// <param name="progress">The progress.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
@@ -1524,7 +1542,8 @@ namespace Emby.Server.Implementations.Library
                 }
 
                 // Handle grouping
-                if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0)
+                if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType)
+                    && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
                 {
                     return GetUserRootFolder()
                         .GetChildren(user, true)
@@ -1773,7 +1792,7 @@ namespace Emby.Server.Implementations.Library
         /// Creates the items.
         /// </summary>
         /// <param name="items">The items.</param>
-        /// <param name="parent">The parent item</param>
+        /// <param name="parent">The parent item.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         public void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken)
         {
@@ -1815,10 +1834,100 @@ namespace Emby.Server.Implementations.Library
             }
         }
 
-        public void UpdateImages(BaseItem item)
+        private bool ImageNeedsRefresh(ItemImageInfo image)
         {
-            _itemRepository.SaveImages(item);
+            if (image.Path != null && image.IsLocalFile)
+            {
+                if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash))
+                {
+                    return true;
+                }
+
+                try
+                {
+                    return _fileSystem.GetLastWriteTimeUtc(image.Path) != image.DateModified;
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Cannot get file info for {0}", image.Path);
+                    return false;
+                }
+            }
+
+            return image.Path != null && !image.IsLocalFile;
+        }
+
+        public void UpdateImages(BaseItem item, bool forceUpdate = false)
+        {
+            if (item == null)
+            {
+                throw new ArgumentNullException(nameof(item));
+            }
+
+            var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
+            if (outdated.Length == 0)
+            {
+                RegisterItem(item);
+                return;
+            }
+
+            foreach (var img in outdated)
+            {
+                var image = img;
+                if (!img.IsLocalFile)
+                {
+                    try
+                    {
+                        var index = item.GetImageIndex(img);
+                        image = ConvertImageToLocal(item, img, index).ConfigureAwait(false).GetAwaiter().GetResult();
+                    }
+                    catch (ArgumentException)
+                    {
+                        _logger.LogWarning("Cannot get image index for {0}", img.Path);
+                        continue;
+                    }
+                    catch (InvalidOperationException)
+                    {
+                        _logger.LogWarning("Cannot fetch image from {0}", img.Path);
+                        continue;
+                    }
+                }
+
+                try
+                {
+                    ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
+                    image.Width = size.Width;
+                    image.Height = size.Height;
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Cannnot get image dimensions for {0}", image.Path);
+                    image.Width = 0;
+                    image.Height = 0;
+                    continue;
+                }
+
+                try
+                {
+                    image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path);
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Cannot compute blurhash for {0}", image.Path);
+                    image.BlurHash = string.Empty;
+                }
 
+                try
+                {
+                    image.DateModified = _fileSystem.GetLastWriteTimeUtc(image.Path);
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Cannot update DateModified for {0}", image.Path);
+                }
+            }
+
+            _itemRepository.SaveImages(item);
             RegisterItem(item);
         }
 
@@ -1839,7 +1948,7 @@ namespace Emby.Server.Implementations.Library
 
                 item.DateLastSaved = DateTime.UtcNow;
 
-                RegisterItem(item);
+                UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
             }
 
             _itemRepository.SaveItems(itemsList, cancellationToken);
@@ -2495,7 +2604,7 @@ namespace Emby.Server.Implementations.Library
                     Anime series don't generally have a season in their file name, however,
                     tvdb needs a season to correctly get the metadata.
                     Hence, a null season needs to be filled with something. */
-                    //FIXME perhaps this would be better for tvdb parser to ask for season 1 if no season is specified
+                    // FIXME perhaps this would be better for tvdb parser to ask for season 1 if no season is specified
                     episode.ParentIndexNumber = 1;
                 }
 
@@ -2684,10 +2793,12 @@ namespace Emby.Server.Implementations.Library
             {
                 throw new ArgumentNullException(nameof(path));
             }
+
             if (string.IsNullOrWhiteSpace(from))
             {
                 throw new ArgumentNullException(nameof(from));
             }
+
             if (string.IsNullOrWhiteSpace(to))
             {
                 throw new ArgumentNullException(nameof(to));
@@ -2761,7 +2872,6 @@ namespace Emby.Server.Implementations.Library
                     _logger.LogError(ex, "Error getting person");
                     return null;
                 }
-
             }).Where(i => i != null).ToList();
         }
 
@@ -2796,7 +2906,8 @@ namespace Emby.Server.Implementations.Library
                 }
                 catch (HttpException ex)
                 {
-                    if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
+                    if (ex.StatusCode.HasValue
+                        && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden))
                     {
                         continue;
                     }
@@ -2891,7 +3002,7 @@ namespace Emby.Server.Implementations.Library
 
         private static bool ValidateNetworkPath(string path)
         {
-            //if (Environment.OSVersion.Platform == PlatformID.Win32NT)
+            // if (Environment.OSVersion.Platform == PlatformID.Win32NT)
             //{
             //    // We can't validate protocol-based paths, so just allow them
             //    if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1)

+ 2 - 5
Emby.Server.Implementations/Library/LiveStreamHelper.cs

@@ -50,7 +50,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     mediaInfo = _json.DeserializeFromFile<MediaInfo>(cacheFilePath);
 
-                    //_logger.LogDebug("Found cached media info");
+                    // _logger.LogDebug("Found cached media info");
                 }
                 catch
                 {
@@ -85,7 +85,7 @@ namespace Emby.Server.Implementations.Library
                     Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
                     _json.SerializeToFile(mediaInfo, cacheFilePath);
 
-                    //_logger.LogDebug("Saved media info to {0}", cacheFilePath);
+                    // _logger.LogDebug("Saved media info to {0}", cacheFilePath);
                 }
             }
 
@@ -148,17 +148,14 @@ namespace Emby.Server.Implementations.Library
                     {
                         videoStream.BitRate = 30000000;
                     }
-
                     else if (width >= 1900)
                     {
                         videoStream.BitRate = 20000000;
                     }
-
                     else if (width >= 1200)
                     {
                         videoStream.BitRate = 8000000;
                     }
-
                     else if (width >= 700)
                     {
                         videoStream.BitRate = 2000000;

+ 32 - 37
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -7,6 +7,8 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Entities;
@@ -14,7 +16,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
@@ -33,7 +34,7 @@ namespace Emby.Server.Implementations.Library
         private readonly ILibraryManager _libraryManager;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IFileSystem _fileSystem;
-        private readonly ILogger _logger;
+        private readonly ILogger<MediaSourceManager> _logger;
         private readonly IUserDataManager _userDataManager;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly ILocalizationManager _localizationManager;
@@ -190,10 +191,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
                     {
-                        if (!user.Policy.EnableAudioPlaybackTranscoding)
-                        {
-                            source.SupportsTranscoding = false;
-                        }
+                        source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
                     }
                 }
             }
@@ -207,22 +205,27 @@ namespace Emby.Server.Implementations.Library
             {
                 return MediaProtocol.Rtsp;
             }
+
             if (path.StartsWith("Rtmp", StringComparison.OrdinalIgnoreCase))
             {
                 return MediaProtocol.Rtmp;
             }
+
             if (path.StartsWith("Http", StringComparison.OrdinalIgnoreCase))
             {
                 return MediaProtocol.Http;
             }
+
             if (path.StartsWith("rtp", StringComparison.OrdinalIgnoreCase))
             {
                 return MediaProtocol.Rtp;
             }
+
             if (path.StartsWith("ftp", StringComparison.OrdinalIgnoreCase))
             {
                 return MediaProtocol.Ftp;
             }
+
             if (path.StartsWith("udp", StringComparison.OrdinalIgnoreCase))
             {
                 return MediaProtocol.Udp;
@@ -352,7 +355,9 @@ namespace Emby.Server.Implementations.Library
 
         private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
         {
-            if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections && user.Configuration.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection)
+            if (userData.SubtitleStreamIndex.HasValue
+                && user.RememberSubtitleSelections
+                && user.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection)
             {
                 var index = userData.SubtitleStreamIndex.Value;
                 // Make sure the saved index is still valid
@@ -363,26 +368,27 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference)
-                ? Array.Empty<string>() : NormalizeLanguage(user.Configuration.SubtitleLanguagePreference);
+
+            var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
+                ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
 
             var defaultAudioIndex = source.DefaultAudioStreamIndex;
             var audioLangage = defaultAudioIndex == null
                 ? null
                 : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault();
 
-            source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(source.MediaStreams,
+            source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(
+                source.MediaStreams,
                 preferredSubs,
-                user.Configuration.SubtitleMode,
+                user.SubtitleMode,
                 audioLangage);
 
-            MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs,
-                user.Configuration.SubtitleMode, audioLangage);
+            MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLangage);
         }
 
         private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
         {
-            if (userData.AudioStreamIndex.HasValue && user.Configuration.RememberAudioSelections && allowRememberingSelection)
+            if (userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection)
             {
                 var index = userData.AudioStreamIndex.Value;
                 // Make sure the saved index is still valid
@@ -393,11 +399,11 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference)
+            var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference)
                 ? Array.Empty<string>()
-                : NormalizeLanguage(user.Configuration.AudioLanguagePreference);
+                : NormalizeLanguage(user.AudioLanguagePreference);
 
-            source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack);
+            source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
         }
 
         public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user)
@@ -435,7 +441,6 @@ namespace Emby.Server.Implementations.Library
                 }
 
                 return 1;
-
             }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
             .ThenByDescending(i =>
             {
@@ -521,11 +526,7 @@ namespace Emby.Server.Implementations.Library
                 SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user);
             }
 
-            return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse
-            {
-                MediaSource = clone
-
-            }, liveStream as IDirectStreamProvider);
+            return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider);
         }
 
         private static void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio)
@@ -538,7 +539,7 @@ namespace Emby.Server.Implementations.Library
                 mediaSource.RunTimeTicks = null;
             }
 
-            var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio);
+            var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
 
             if (audioStream == null || audioStream.Index == -1)
             {
@@ -549,7 +550,7 @@ namespace Emby.Server.Implementations.Library
                 mediaSource.DefaultAudioStreamIndex = audioStream.Index;
             }
 
-            var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video);
+            var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
             if (videoStream != null)
             {
                 if (!videoStream.BitRate.HasValue)
@@ -560,17 +561,14 @@ namespace Emby.Server.Implementations.Library
                     {
                         videoStream.BitRate = 30000000;
                     }
-
                     else if (width >= 1900)
                     {
                         videoStream.BitRate = 20000000;
                     }
-
                     else if (width >= 1200)
                     {
                         videoStream.BitRate = 8000000;
                     }
-
                     else if (width >= 700)
                     {
                         videoStream.BitRate = 2000000;
@@ -626,7 +624,6 @@ namespace Emby.Server.Implementations.Library
                     MediaSource = mediaSource,
                     ExtractChapters = false,
                     MediaType = DlnaProfileType.Video
-
                 }, cancellationToken).ConfigureAwait(false);
 
                 mediaSource.MediaStreams = info.MediaStreams;
@@ -652,7 +649,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     mediaInfo = _jsonSerializer.DeserializeFromFile<MediaInfo>(cacheFilePath);
 
-                    //_logger.LogDebug("Found cached media info");
+                    // _logger.LogDebug("Found cached media info");
                 }
                 catch (Exception ex)
                 {
@@ -674,20 +671,21 @@ namespace Emby.Server.Implementations.Library
                     mediaSource.AnalyzeDurationMs = 3000;
                 }
 
-                mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
+                mediaInfo = await _mediaEncoder.GetMediaInfo(
+                    new MediaInfoRequest
                 {
                     MediaSource = mediaSource,
                     MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
                     ExtractChapters = false
-
-                }, cancellationToken).ConfigureAwait(false);
+                },
+                    cancellationToken).ConfigureAwait(false);
 
                 if (cacheFilePath != null)
                 {
                     Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
                     _jsonSerializer.SerializeToFile(mediaInfo, cacheFilePath);
 
-                    //_logger.LogDebug("Saved media info to {0}", cacheFilePath);
+                    // _logger.LogDebug("Saved media info to {0}", cacheFilePath);
                 }
             }
 
@@ -753,17 +751,14 @@ namespace Emby.Server.Implementations.Library
                     {
                         videoStream.BitRate = 30000000;
                     }
-
                     else if (width >= 1900)
                     {
                         videoStream.BitRate = 20000000;
                     }
-
                     else if (width >= 1200)
                     {
                         videoStream.BitRate = 8000000;
                     }
-
                     else if (width >= 700)
                     {
                         videoStream.BitRate = 2000000;

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است