Browse Source

Merge branch 'master' into nullable4

Bond_009 5 years ago
parent
commit
118f30059c
100 changed files with 1978 additions and 2072 deletions
  1. 10 10
      .ci/azure-pipelines-compat.yml
  2. 30 38
      .ci/azure-pipelines-main.yml
  3. 42 18
      .ci/azure-pipelines-test.yml
  4. 0 82
      .ci/azure-pipelines-windows.yml
  5. 6 11
      .ci/azure-pipelines.yml
  6. 1 0
      .gitignore
  7. 14 0
      .vscode/extensions.json
  8. 2 0
      CONTRIBUTORS.md
  9. 10 18
      Dockerfile
  10. 1 1
      Dockerfile.arm
  11. 3 10
      DvdLib/BigEndianBinaryReader.cs
  12. 5 4
      DvdLib/DvdLib.csproj
  13. 3 7
      DvdLib/Ifo/Dvd.cs
  14. 45 6
      Emby.Dlna/Didl/DidlBuilder.cs
  15. 5 0
      Emby.Dlna/Emby.Dlna.csproj
  16. 5 0
      Emby.Drawing/Emby.Drawing.csproj
  17. 4 30
      Emby.Drawing/ImageProcessor.cs
  18. 59 2
      Emby.Naming/Common/NamingOptions.cs
  19. 5 0
      Emby.Naming/Emby.Naming.csproj
  20. 9 0
      Emby.Naming/Video/ExtraResolver.cs
  21. 6 7
      Emby.Naming/Video/ExtraRule.cs
  22. 9 4
      Emby.Naming/Video/ExtraRuleType.cs
  23. 5 0
      Emby.Notifications/Emby.Notifications.csproj
  24. 6 0
      Emby.Photos/Emby.Photos.csproj
  25. 1 1
      Emby.Photos/PhotoProvider.cs
  26. 27 21
      Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
  27. 3 8
      Emby.Server.Implementations/Activity/ActivityManager.cs
  28. 54 50
      Emby.Server.Implementations/Activity/ActivityRepository.cs
  29. 179 458
      Emby.Server.Implementations/ApplicationHost.cs
  30. 3 0
      Emby.Server.Implementations/Archiving/ZipClient.cs
  31. 2 2
      Emby.Server.Implementations/Channels/ChannelImageProvider.cs
  32. 85 83
      Emby.Server.Implementations/Channels/ChannelManager.cs
  33. 1 3
      Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
  34. 2 6
      Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
  35. 1 3
      Emby.Server.Implementations/Collections/CollectionImageProvider.cs
  36. 12 8
      Emby.Server.Implementations/Collections/CollectionManager.cs
  37. 2 0
      Emby.Server.Implementations/ConfigurationOptions.cs
  38. 4 25
      Emby.Server.Implementations/Data/SqliteExtensions.cs
  39. 142 273
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  40. 3 3
      Emby.Server.Implementations/Devices/DeviceManager.cs
  41. 0 152
      Emby.Server.Implementations/Diagnostics/CommonProcess.cs
  42. 0 14
      Emby.Server.Implementations/Diagnostics/ProcessFactory.cs
  43. 20 29
      Emby.Server.Implementations/Dto/DtoService.cs
  44. 8 3
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  45. 41 52
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  46. 31 14
      Emby.Server.Implementations/EntryPoints/StartupWizard.cs
  47. 5 4
      Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
  48. 68 53
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  49. 12 2
      Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
  50. 10 27
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  51. 26 32
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  52. 11 6
      Emby.Server.Implementations/IStartupOptions.cs
  53. 1 1
      Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
  54. 102 133
      Emby.Server.Implementations/Library/LibraryManager.cs
  55. 11 11
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  56. 1 1
      Emby.Server.Implementations/Library/PathExtensions.cs
  57. 3 4
      Emby.Server.Implementations/Library/SearchEngine.cs
  58. 18 19
      Emby.Server.Implementations/Library/UserDataManager.cs
  59. 21 25
      Emby.Server.Implementations/Library/UserManager.cs
  60. 16 18
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  61. 37 35
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  62. 12 14
      Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
  63. 28 19
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  64. 13 1
      Emby.Server.Implementations/Localization/Core/ar.json
  65. 5 5
      Emby.Server.Implementations/Localization/Core/ca.json
  66. 26 4
      Emby.Server.Implementations/Localization/Core/da.json
  67. 3 3
      Emby.Server.Implementations/Localization/Core/de.json
  68. 23 1
      Emby.Server.Implementations/Localization/Core/en-GB.json
  69. 1 1
      Emby.Server.Implementations/Localization/Core/es-AR.json
  70. 1 1
      Emby.Server.Implementations/Localization/Core/es_DO.json
  71. 24 2
      Emby.Server.Implementations/Localization/Core/fa.json
  72. 40 18
      Emby.Server.Implementations/Localization/Core/fi.json
  73. 9 1
      Emby.Server.Implementations/Localization/Core/fil.json
  74. 14 14
      Emby.Server.Implementations/Localization/Core/fr.json
  75. 1 1
      Emby.Server.Implementations/Localization/Core/hu.json
  76. 8 4
      Emby.Server.Implementations/Localization/Core/ja.json
  77. 61 0
      Emby.Server.Implementations/Localization/Core/mr.json
  78. 26 4
      Emby.Server.Implementations/Localization/Core/nl.json
  79. 24 2
      Emby.Server.Implementations/Localization/Core/pt-PT.json
  80. 13 1
      Emby.Server.Implementations/Localization/Core/pt.json
  81. 44 22
      Emby.Server.Implementations/Localization/Core/ru.json
  82. 22 1
      Emby.Server.Implementations/Localization/Core/sv.json
  83. 24 2
      Emby.Server.Implementations/Localization/Core/tr.json
  84. 117 0
      Emby.Server.Implementations/Localization/Core/ur_PK.json
  85. 29 7
      Emby.Server.Implementations/Localization/Core/zh-TW.json
  86. 0 3
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  87. 3 17
      Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
  88. 2 3
      Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
  89. 2 2
      Emby.Server.Implementations/Security/AuthenticationRepository.cs
  90. 1 1
      Emby.Server.Implementations/Session/SessionManager.cs
  91. 85 56
      Emby.Server.Implementations/Updates/InstallationManager.cs
  92. 5 0
      Jellyfin.Api/Jellyfin.Api.csproj
  93. 5 0
      Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
  94. 13 4
      Jellyfin.Drawing.Skia/SkiaEncoder.cs
  95. 22 5
      Jellyfin.Server/CoreAppHost.cs
  96. 5 0
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  97. 5 0
      Jellyfin.Server/Jellyfin.Server.csproj
  98. 2 23
      Jellyfin.Server/Program.cs
  99. 2 2
      Jellyfin.Server/Properties/launchSettings.json
  100. 10 1
      Jellyfin.Server/StartupOptions.cs

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

@@ -1,13 +1,13 @@
 parameters:
-  - name: Packages
-    type: object
-    default: {}
-  - name: LinuxImage
-    type: string
-    default: "ubuntu-latest"
-  - name: DotNetSdkVersion
-    type: string
-    default: 3.1.100
+- name: Packages
+  type: object
+  default: {}
+- name: LinuxImage
+  type: string
+  default: "ubuntu-latest"
+- name: DotNetSdkVersion
+  type: string
+  default: 3.1.100
 
 jobs:
   - job: CompatibilityCheck
@@ -23,7 +23,7 @@ jobs:
             NugetPackageName: ${{ Package.value.NugetPackageName }}
             AssemblyFileName: ${{ Package.value.AssemblyFileName }}
       maxParallel: 2
-    dependsOn: MainBuild
+    dependsOn: Build
     steps:
       - checkout: none
 

+ 30 - 38
.ci/azure-pipelines-main.yml

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

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

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

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

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

+ 6 - 11
.ci/azure-pipelines.yml

@@ -1,12 +1,12 @@
 name: $(Date:yyyyMMdd)$(Rev:.r)
 
 variables:
-  - name: TestProjects
-    value: "tests/**/*Tests.csproj"
-  - name: RestoreBuildProjects
-    value: "Jellyfin.Server/Jellyfin.Server.csproj"
-  - name: DotNetSdkVersion
-    value: 3.1.100
+- name: TestProjects
+  value: "tests/**/*Tests.csproj"
+- name: RestoreBuildProjects
+  value: "Jellyfin.Server/Jellyfin.Server.csproj"
+- name: DotNetSdkVersion
+  value: 3.1.100
 
 pr:
   autoCancel: true
@@ -27,11 +27,6 @@ jobs:
         Windows: "windows-latest"
         macOS: "macos-latest"
 
-  - template: azure-pipelines-windows.yml
-    parameters:
-      WindowsImage: "windows-latest"
-      TestProjects: $(TestProjects)
-
   - template: azure-pipelines-compat.yml
     parameters:
       Packages:

+ 1 - 0
.gitignore

@@ -39,6 +39,7 @@ ProgramData*/
 CorePlugins*/
 ProgramData-Server*/
 ProgramData-UI*/
+MediaBrowser.WebDashboard/jellyfin-web/**
 
 #################
 ## Visual Studio

+ 14 - 0
.vscode/extensions.json

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

+ 2 - 0
CONTRIBUTORS.md

@@ -22,6 +22,7 @@
  - [cvium](https://github.com/cvium)
  - [dannymichel](https://github.com/dannymichel)
  - [DaveChild](https://github.com/DaveChild)
+ - [Delgan](https://github.com/Delgan)
  - [dcrdev](https://github.com/dcrdev)
  - [dhartung](https://github.com/dhartung)
  - [dinki](https://github.com/dinki)
@@ -128,6 +129,7 @@
  - [xosdy](https://github.com/xosdy)
  - [XVicarious](https://github.com/XVicarious)
  - [YouKnowBlom](https://github.com/YouKnowBlom)
+ - [KristupasSavickas](https://github.com/KristupasSavickas)
 
 # Emby Contributors
 

+ 10 - 18
Dockerfile

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

+ 1 - 1
Dockerfile.arm

@@ -74,4 +74,4 @@ VOLUME /cache /config /media
 ENTRYPOINT ["./jellyfin/jellyfin", \
     "--datadir", "/config", \
     "--cachedir", "/cache", \
-    "--ffmpeg", "/usr/lib/jellyfin-ffmpeg"]
+    "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]

+ 3 - 10
DvdLib/BigEndianBinaryReader.cs

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

+ 5 - 4
DvdLib/DvdLib.csproj

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

+ 3 - 7
DvdLib/Ifo/Dvd.cs

@@ -2,7 +2,6 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
-using MediaBrowser.Model.IO;
 
 namespace DvdLib.Ifo
 {
@@ -13,13 +12,10 @@ namespace DvdLib.Ifo
 
         private ushort _titleCount;
         public readonly Dictionary<ushort, string> VTSPaths = new Dictionary<ushort, string>();
-        private readonly IFileSystem _fileSystem;
-
-        public Dvd(string path, IFileSystem fileSystem)
+        public Dvd(string path)
         {
-            _fileSystem = fileSystem;
             Titles = new List<Title>();
-            var allFiles = _fileSystem.GetFiles(path, true).ToList();
+            var allFiles = new DirectoryInfo(path).GetFiles(path, SearchOption.AllDirectories);
 
             var vmgPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.IFO", StringComparison.OrdinalIgnoreCase)) ??
                 allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.BUP", StringComparison.OrdinalIgnoreCase));
@@ -76,7 +72,7 @@ namespace DvdLib.Ifo
             }
         }
 
-        private void ReadVTS(ushort vtsNum, IEnumerable<FileSystemMetadata> allFiles)
+        private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
         {
             var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
 

+ 45 - 6
Emby.Dlna/Didl/DidlBuilder.cs

@@ -1018,19 +1018,58 @@ namespace Emby.Dlna.Didl
                 }
             }
 
-            item = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary));
+            // For audio tracks without art use album art if available.
+            if (item is Audio audioItem)
+            {
+                var album = audioItem.AlbumEntity;
+                return album != null && album.HasImage(ImageType.Primary)
+                    ? GetImageInfo(album, ImageType.Primary)
+                    : null;
+            }
 
-            if (item != null)
+            // Don't look beyond album/playlist level. Metadata service may assign an image from a different album/show to the parent folder.
+            if (item is MusicAlbum || item is Playlist)
             {
-                if (item.HasImage(ImageType.Primary))
-                {
-                    return GetImageInfo(item, ImageType.Primary);
-                }
+                return null;
+            }
+
+            // For other item types check parents, but be aware that image retrieved from a parent may be not suitable for this media item.
+            var parentWithImage = GetFirstParentWithImageBelowUserRoot(item);
+            if (parentWithImage != null)
+            {
+                return GetImageInfo(parentWithImage, ImageType.Primary);
             }
 
             return null;
         }
 
+        private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item)
+        {
+            if (item == null)
+            {
+                return null;
+            }
+
+            if (item.HasImage(ImageType.Primary))
+            {
+                return item;
+            }
+
+            var parent = item.GetParent();
+            if (parent is UserRootFolder)
+            {
+                return null;
+            }
+
+            // terminate in case we went past user root folder (unlikely?)
+            if (parent is Folder folder && folder.IsRoot)
+            {
+                return null;
+            }
+
+            return GetFirstParentWithImageBelowUserRoot(parent);
+        }
+
         private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
         {
             var imageInfo = item.GetImageInfo(type, 0);

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

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

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

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

+ 4 - 30
Emby.Drawing/ImageProcessor.cs

@@ -8,7 +8,6 @@ using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Entities;
@@ -33,8 +32,7 @@ namespace Emby.Drawing
         private readonly IFileSystem _fileSystem;
         private readonly IServerApplicationPaths _appPaths;
         private readonly IImageEncoder _imageEncoder;
-        private readonly Func<ILibraryManager> _libraryManager;
-        private readonly Func<IMediaEncoder> _mediaEncoder;
+        private readonly IMediaEncoder _mediaEncoder;
 
         private bool _disposed = false;
 
@@ -45,20 +43,17 @@ namespace Emby.Drawing
         /// <param name="appPaths">The server application paths.</param>
         /// <param name="fileSystem">The filesystem.</param>
         /// <param name="imageEncoder">The image encoder.</param>
-        /// <param name="libraryManager">The library manager.</param>
         /// <param name="mediaEncoder">The media encoder.</param>
         public ImageProcessor(
             ILogger<ImageProcessor> logger,
             IServerApplicationPaths appPaths,
             IFileSystem fileSystem,
             IImageEncoder imageEncoder,
-            Func<ILibraryManager> libraryManager,
-            Func<IMediaEncoder> mediaEncoder)
+            IMediaEncoder mediaEncoder)
         {
             _logger = logger;
             _fileSystem = fileSystem;
             _imageEncoder = imageEncoder;
-            _libraryManager = libraryManager;
             _mediaEncoder = mediaEncoder;
             _appPaths = appPaths;
         }
@@ -121,21 +116,9 @@ namespace Emby.Drawing
         /// <inheritdoc />
         public async Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options)
         {
-            var libraryManager = _libraryManager();
-
             ItemImageInfo originalImage = options.Image;
             BaseItem item = options.Item;
 
-            if (!originalImage.IsLocalFile)
-            {
-                if (item == null)
-                {
-                    item = libraryManager.GetItemById(options.ItemId);
-                }
-
-                originalImage = await libraryManager.ConvertImageToLocal(item, originalImage, options.ImageIndex).ConfigureAwait(false);
-            }
-
             string originalImagePath = originalImage.Path;
             DateTime dateModified = originalImage.DateModified;
             ImageDimensions? originalImageSize = null;
@@ -307,10 +290,6 @@ namespace Emby.Drawing
 
         /// <inheritdoc />
         public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info)
-            => GetImageDimensions(item, info, true);
-
-        /// <inheritdoc />
-        public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem)
         {
             int width = info.Width;
             int height = info.Height;
@@ -327,11 +306,6 @@ namespace Emby.Drawing
             info.Width = size.Width;
             info.Height = size.Height;
 
-            if (updateItem)
-            {
-                _libraryManager().UpdateImages(item);
-            }
-
             return size;
         }
 
@@ -372,13 +346,13 @@ namespace Emby.Drawing
                 {
                     string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
 
-                    string cacheExtension = _mediaEncoder().SupportsEncoder("libwebp") ? ".webp" : ".png";
+                    string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
                     var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
 
                     var file = _fileSystem.GetFileInfo(outputPath);
                     if (!file.Exists)
                     {
-                        await _mediaEncoder().ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
+                        await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
                         dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
                     }
                     else

+ 59 - 2
Emby.Naming/Common/NamingOptions.cs

@@ -136,7 +136,8 @@ namespace Emby.Naming.Common
 
             CleanDateTimes = new[]
             {
-                @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*"
+                @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*",
+                @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*"
             };
 
             CleanStrings = new[]
@@ -505,7 +506,63 @@ namespace Emby.Naming.Common
                     RuleType = ExtraRuleType.Suffix,
                     Token = "-short",
                     MediaType = MediaType.Video
-                }
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.BehindTheScenes,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "behind the scenes",
+                    MediaType = MediaType.Video,
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.DeletedScene,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "deleted scenes",
+                    MediaType = MediaType.Video,
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.Interview,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "interviews",
+                    MediaType = MediaType.Video,
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.Scene,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "scenes",
+                    MediaType = MediaType.Video,
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.Sample,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "samples",
+                    MediaType = MediaType.Video,
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.Clip,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "shorts",
+                    MediaType = MediaType.Video,
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.Clip,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "featurettes",
+                    MediaType = MediaType.Video,
+                },
+                new ExtraRule
+                {
+                    ExtraType = ExtraType.Unknown,
+                    RuleType = ExtraRuleType.DirectoryName,
+                    Token = "extras",
+                    MediaType = MediaType.Video,
+                },
             };
 
             Format3DRules = new[]

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

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

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

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

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

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

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

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

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

@@ -1,5 +1,10 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
+  <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
+  <PropertyGroup>
+    <ProjectGuid>{2E030C33-6923-4530-9E54-FA29FA6AD1A9}</ProjectGuid>
+  </PropertyGroup>
+
   <PropertyGroup>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>

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

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

+ 1 - 1
Emby.Photos/PhotoProvider.cs

@@ -160,7 +160,7 @@ namespace Emby.Photos
 
                 try
                 {
-                    var size = _imageProcessor.GetImageDimensions(item, img, false);
+                    var size = _imageProcessor.GetImageDimensions(item, img);
 
                     if (size.Width > 0 && size.Height > 0)
                     {

+ 27 - 21
Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs

@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -27,6 +25,9 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Activity
 {
+    /// <summary>
+    /// Entry point for the activity logger.
+    /// </summary>
     public sealed class ActivityLogEntryPoint : IServerEntryPoint
     {
         private readonly ILogger _logger;
@@ -42,16 +43,15 @@ namespace Emby.Server.Implementations.Activity
         /// <summary>
         /// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
         /// </summary>
-        /// <param name="logger"></param>
-        /// <param name="sessionManager"></param>
-        /// <param name="deviceManager"></param>
-        /// <param name="taskManager"></param>
-        /// <param name="activityManager"></param>
-        /// <param name="localization"></param>
-        /// <param name="installationManager"></param>
-        /// <param name="subManager"></param>
-        /// <param name="userManager"></param>
-        /// <param name="appHost"></param>
+        /// <param name="logger">The logger.</param>
+        /// <param name="sessionManager">The session manager.</param>
+        /// <param name="deviceManager">The device manager.</param>
+        /// <param name="taskManager">The task manager.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        /// <param name="localization">The localization manager.</param>
+        /// <param name="installationManager">The installation manager.</param>
+        /// <param name="subManager">The subtitle manager.</param>
+        /// <param name="userManager">The user manager.</param>
         public ActivityLogEntryPoint(
             ILogger<ActivityLogEntryPoint> logger,
             ISessionManager sessionManager,
@@ -74,6 +74,7 @@ namespace Emby.Server.Implementations.Activity
             _userManager = userManager;
         }
 
+        /// <inheritdoc />
         public Task RunAsync()
         {
             _taskManager.TaskCompleted += OnTaskCompleted;
@@ -168,7 +169,12 @@ namespace Emby.Server.Implementations.Activity
 
             CreateLogEntry(new ActivityLogEntry
             {
-                Name = string.Format(_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName),
+                Name = string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
+                    user.Name,
+                    GetItemName(item),
+                    e.DeviceName),
                 Type = GetPlaybackStoppedNotificationType(item.MediaType),
                 UserId = user.Id
             });
@@ -416,7 +422,7 @@ namespace Emby.Server.Implementations.Activity
             });
         }
 
-        private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, PackageVersionInfo)> e)
+        private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e)
         {
             CreateLogEntry(new ActivityLogEntry
             {
@@ -428,8 +434,8 @@ namespace Emby.Server.Implementations.Activity
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("VersionNumber"),
-                    e.Argument.Item2.versionStr),
-                Overview = e.Argument.Item2.description
+                    e.Argument.Item2.version),
+                Overview = e.Argument.Item2.changelog
             });
         }
 
@@ -445,7 +451,7 @@ namespace Emby.Server.Implementations.Activity
             });
         }
 
-        private void OnPluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> e)
+        private void OnPluginInstalled(object sender, GenericEventArgs<VersionInfo> e)
         {
             CreateLogEntry(new ActivityLogEntry
             {
@@ -457,7 +463,7 @@ namespace Emby.Server.Implementations.Activity
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("VersionNumber"),
-                    e.Argument.versionStr)
+                    e.Argument.version)
             });
         }
 
@@ -485,8 +491,8 @@ namespace Emby.Server.Implementations.Activity
             var result = e.Result;
             var task = e.Task;
 
-            var activityTask = task.ScheduledTask as IConfigurableScheduledTask;
-            if (activityTask != null && !activityTask.IsLogged)
+            if (task.ScheduledTask is IConfigurableScheduledTask activityTask
+                && !activityTask.IsLogged)
             {
                 return;
             }
@@ -560,7 +566,7 @@ namespace Emby.Server.Implementations.Activity
         /// <summary>
         /// Constructs a user-friendly string for this TimeSpan instance.
         /// </summary>
-        public static string ToUserFriendlyString(TimeSpan span)
+        private static string ToUserFriendlyString(TimeSpan span)
         {
             const int DaysInYear = 365;
             const int DaysInMonth = 30;

+ 3 - 8
Emby.Server.Implementations/Activity/ActivityManager.cs

@@ -11,22 +11,17 @@ namespace Emby.Server.Implementations.Activity
 {
     public class ActivityManager : IActivityManager
     {
-        public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
-
         private readonly IActivityRepository _repo;
-        private readonly ILogger _logger;
         private readonly IUserManager _userManager;
 
-        public ActivityManager(
-            ILoggerFactory loggerFactory,
-            IActivityRepository repo,
-            IUserManager userManager)
+        public ActivityManager(IActivityRepository repo, IUserManager userManager)
         {
-            _logger = loggerFactory.CreateLogger(nameof(ActivityManager));
             _repo = repo;
             _userManager = userManager;
         }
 
+        public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
+
         public void Create(ActivityLogEntry entry)
         {
             entry.Date = DateTime.UtcNow;

+ 54 - 50
Emby.Server.Implementations/Activity/ActivityRepository.cs

@@ -17,11 +17,12 @@ namespace Emby.Server.Implementations.Activity
 {
     public class ActivityRepository : BaseSqliteRepository, IActivityRepository
     {
-        private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
+        private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLog";
+
         private readonly IFileSystem _fileSystem;
 
-        public ActivityRepository(ILoggerFactory loggerFactory, IServerApplicationPaths appPaths, IFileSystem fileSystem)
-            : base(loggerFactory.CreateLogger(nameof(ActivityRepository)))
+        public ActivityRepository(ILogger<ActivityRepository> logger, IServerApplicationPaths appPaths, IFileSystem fileSystem)
+            : base(logger)
         {
             DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db");
             _fileSystem = fileSystem;
@@ -76,8 +77,6 @@ namespace Emby.Server.Implementations.Activity
             }
         }
 
-        private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLog";
-
         public void Create(ActivityLogEntry entry)
         {
             if (entry == null)
@@ -87,32 +86,34 @@ namespace Emby.Server.Implementations.Activity
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
-                {
-                    using (var statement = db.PrepareStatement("insert into ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)"))
+                connection.RunInTransaction(
+                    db =>
                     {
-                        statement.TryBind("@Name", entry.Name);
+                        using (var statement = db.PrepareStatement("insert into ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)"))
+                        {
+                            statement.TryBind("@Name", entry.Name);
 
-                        statement.TryBind("@Overview", entry.Overview);
-                        statement.TryBind("@ShortOverview", entry.ShortOverview);
-                        statement.TryBind("@Type", entry.Type);
-                        statement.TryBind("@ItemId", entry.ItemId);
+                            statement.TryBind("@Overview", entry.Overview);
+                            statement.TryBind("@ShortOverview", entry.ShortOverview);
+                            statement.TryBind("@Type", entry.Type);
+                            statement.TryBind("@ItemId", entry.ItemId);
 
-                        if (entry.UserId.Equals(Guid.Empty))
-                        {
-                            statement.TryBindNull("@UserId");
-                        }
-                        else
-                        {
-                            statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture));
-                        }
+                            if (entry.UserId.Equals(Guid.Empty))
+                            {
+                                statement.TryBindNull("@UserId");
+                            }
+                            else
+                            {
+                                statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture));
+                            }
 
-                        statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue());
-                        statement.TryBind("@LogSeverity", entry.Severity.ToString());
+                            statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue());
+                            statement.TryBind("@LogSeverity", entry.Severity.ToString());
 
-                        statement.MoveNext();
-                    }
-                }, TransactionMode);
+                            statement.MoveNext();
+                        }
+                    },
+                    TransactionMode);
             }
         }
 
@@ -125,33 +126,35 @@ namespace Emby.Server.Implementations.Activity
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
-                {
-                    using (var statement = db.PrepareStatement("Update ActivityLog set Name=@Name,Overview=@Overview,ShortOverview=@ShortOverview,Type=@Type,ItemId=@ItemId,UserId=@UserId,DateCreated=@DateCreated,LogSeverity=@LogSeverity where Id=@Id"))
+                connection.RunInTransaction(
+                    db =>
                     {
-                        statement.TryBind("@Id", entry.Id);
+                        using (var statement = db.PrepareStatement("Update ActivityLog set Name=@Name,Overview=@Overview,ShortOverview=@ShortOverview,Type=@Type,ItemId=@ItemId,UserId=@UserId,DateCreated=@DateCreated,LogSeverity=@LogSeverity where Id=@Id"))
+                        {
+                            statement.TryBind("@Id", entry.Id);
 
-                        statement.TryBind("@Name", entry.Name);
-                        statement.TryBind("@Overview", entry.Overview);
-                        statement.TryBind("@ShortOverview", entry.ShortOverview);
-                        statement.TryBind("@Type", entry.Type);
-                        statement.TryBind("@ItemId", entry.ItemId);
+                            statement.TryBind("@Name", entry.Name);
+                            statement.TryBind("@Overview", entry.Overview);
+                            statement.TryBind("@ShortOverview", entry.ShortOverview);
+                            statement.TryBind("@Type", entry.Type);
+                            statement.TryBind("@ItemId", entry.ItemId);
 
-                        if (entry.UserId.Equals(Guid.Empty))
-                        {
-                            statement.TryBindNull("@UserId");
-                        }
-                        else
-                        {
-                            statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture));
-                        }
+                            if (entry.UserId.Equals(Guid.Empty))
+                            {
+                                statement.TryBindNull("@UserId");
+                            }
+                            else
+                            {
+                                statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture));
+                            }
 
-                        statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue());
-                        statement.TryBind("@LogSeverity", entry.Severity.ToString());
+                            statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue());
+                            statement.TryBind("@LogSeverity", entry.Severity.ToString());
 
-                        statement.MoveNext();
-                    }
-                }, TransactionMode);
+                            statement.MoveNext();
+                        }
+                    },
+                    TransactionMode);
             }
         }
 
@@ -164,6 +167,7 @@ namespace Emby.Server.Implementations.Activity
             {
                 whereClauses.Add("DateCreated>=@DateCreated");
             }
+
             if (hasUserId.HasValue)
             {
                 if (hasUserId.Value)
@@ -204,7 +208,7 @@ namespace Emby.Server.Implementations.Activity
 
             if (limit.HasValue)
             {
-                commandText += " LIMIT " + limit.Value.ToString(_usCulture);
+                commandText += " LIMIT " + limit.Value.ToString(CultureInfo.InvariantCulture);
             }
 
             var statementTexts = new[]
@@ -304,7 +308,7 @@ namespace Emby.Server.Implementations.Activity
             index++;
             if (reader[index].SQLiteType != SQLiteType.Null)
             {
-                info.Severity = (LogLevel)Enum.Parse(typeof(LogLevel), reader[index].ToString(), true);
+                info.Severity = Enum.Parse<LogLevel>(reader[index].ToString(), true);
             }
 
             return info;

+ 179 - 458
Emby.Server.Implementations/ApplicationHost.cs

@@ -30,7 +30,6 @@ using Emby.Server.Implementations.Configuration;
 using Emby.Server.Implementations.Cryptography;
 using Emby.Server.Implementations.Data;
 using Emby.Server.Implementations.Devices;
-using Emby.Server.Implementations.Diagnostics;
 using Emby.Server.Implementations.Dto;
 using Emby.Server.Implementations.HttpServer;
 using Emby.Server.Implementations.HttpServer.Security;
@@ -86,9 +85,7 @@ using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Cryptography;
-using MediaBrowser.Model.Diagnostics;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
@@ -106,7 +103,6 @@ using MediaBrowser.WebDashboard.Api;
 using MediaBrowser.XbmcMetadata.Providers;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
@@ -123,14 +119,20 @@ namespace Emby.Server.Implementations
         /// </summary>
         private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
 
-        private SqliteUserRepository _userRepository;
-        private SqliteDisplayPreferencesRepository _displayPreferencesRepository;
+        private readonly IFileSystem _fileSystemManager;
+        private readonly INetworkManager _networkManager;
+        private readonly IXmlSerializer _xmlSerializer;
+        private readonly IStartupOptions _startupOptions;
+
+        private IMediaEncoder _mediaEncoder;
+        private ISessionManager _sessionManager;
+        private IHttpServer _httpServer;
+        private IHttpClient _httpClient;
 
         /// <summary>
         /// Gets a value indicating whether this instance can self restart.
         /// </summary>
-        /// <value><c>true</c> if this instance can self restart; otherwise, <c>false</c>.</value>
-        public abstract bool CanSelfRestart { get; }
+        public bool CanSelfRestart => _startupOptions.RestartPath != null;
 
         public virtual bool CanLaunchWebBrowser
         {
@@ -141,7 +143,7 @@ namespace Emby.Server.Implementations
                     return false;
                 }
 
-                if (StartupOptions.IsService)
+                if (_startupOptions.IsService)
                 {
                     return false;
                 }
@@ -211,21 +213,6 @@ namespace Emby.Server.Implementations
         /// <value>The configuration manager.</value>
         protected IConfigurationManager ConfigurationManager { get; set; }
 
-        public IFileSystem FileSystemManager { get; set; }
-
-        /// <inheritdoc />
-        public PackageVersionClass SystemUpdateLevel
-        {
-            get
-            {
-#if BETA
-                return PackageVersionClass.Beta;
-#else
-                return PackageVersionClass.Release;
-#endif
-            }
-        }
-
         /// <summary>
         /// Gets or sets the service provider.
         /// </summary>
@@ -247,112 +234,6 @@ namespace Emby.Server.Implementations
         /// <value>The server configuration manager.</value>
         public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
 
-        /// <summary>
-        /// Gets or sets the user manager.
-        /// </summary>
-        /// <value>The user manager.</value>
-        public IUserManager UserManager { get; set; }
-
-        /// <summary>
-        /// Gets or sets the library manager.
-        /// </summary>
-        /// <value>The library manager.</value>
-        internal ILibraryManager LibraryManager { get; set; }
-
-        /// <summary>
-        /// Gets or sets the directory watchers.
-        /// </summary>
-        /// <value>The directory watchers.</value>
-        private ILibraryMonitor LibraryMonitor { get; set; }
-
-        /// <summary>
-        /// Gets or sets the provider manager.
-        /// </summary>
-        /// <value>The provider manager.</value>
-        private IProviderManager ProviderManager { get; set; }
-
-        /// <summary>
-        /// Gets or sets the HTTP server.
-        /// </summary>
-        /// <value>The HTTP server.</value>
-        private IHttpServer HttpServer { get; set; }
-
-        private IDtoService DtoService { get; set; }
-
-        public IImageProcessor ImageProcessor { get; set; }
-
-        /// <summary>
-        /// Gets or sets the media encoder.
-        /// </summary>
-        /// <value>The media encoder.</value>
-        private IMediaEncoder MediaEncoder { get; set; }
-
-        private ISubtitleEncoder SubtitleEncoder { get; set; }
-
-        private ISessionManager SessionManager { get; set; }
-
-        private ILiveTvManager LiveTvManager { get; set; }
-
-        public LocalizationManager LocalizationManager { get; set; }
-
-        private IEncodingManager EncodingManager { get; set; }
-
-        private IChannelManager ChannelManager { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user data repository.
-        /// </summary>
-        /// <value>The user data repository.</value>
-        private IUserDataManager UserDataManager { get; set; }
-
-        internal SqliteItemRepository ItemRepository { get; set; }
-
-        private INotificationManager NotificationManager { get; set; }
-
-        private ISubtitleManager SubtitleManager { get; set; }
-
-        private IChapterManager ChapterManager { get; set; }
-
-        private IDeviceManager DeviceManager { get; set; }
-
-        internal IUserViewManager UserViewManager { get; set; }
-
-        private IAuthenticationRepository AuthenticationRepository { get; set; }
-
-        private ITVSeriesManager TVSeriesManager { get; set; }
-
-        private ICollectionManager CollectionManager { get; set; }
-
-        private IMediaSourceManager MediaSourceManager { get; set; }
-
-        /// <summary>
-        /// Gets the installation manager.
-        /// </summary>
-        /// <value>The installation manager.</value>
-        protected IInstallationManager InstallationManager { get; private set; }
-
-        protected IAuthService AuthService { get; private set; }
-
-        public IStartupOptions StartupOptions { get; }
-
-        internal IImageEncoder ImageEncoder { get; private set; }
-
-        protected IProcessFactory ProcessFactory { get; private set; }
-
-        protected readonly IXmlSerializer XmlSerializer;
-
-        protected ISocketFactory SocketFactory { get; private set; }
-
-        protected ITaskManager TaskManager { get; private set; }
-
-        public IHttpClient HttpClient { get; private set; }
-
-        protected INetworkManager NetworkManager { get; set; }
-
-        public IJsonSerializer JsonSerializer { get; private set; }
-
-        protected IIsoManager IsoManager { get; private set; }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="ApplicationHost" /> class.
         /// </summary>
@@ -361,29 +242,33 @@ namespace Emby.Server.Implementations
             ILoggerFactory loggerFactory,
             IStartupOptions options,
             IFileSystem fileSystem,
-            IImageEncoder imageEncoder,
             INetworkManager networkManager)
         {
-            XmlSerializer = new MyXmlSerializer();
+            _xmlSerializer = new MyXmlSerializer();
 
-            NetworkManager = networkManager;
+            _networkManager = networkManager;
             networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
 
             ApplicationPaths = applicationPaths;
             LoggerFactory = loggerFactory;
-            FileSystemManager = fileSystem;
-
-            ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, XmlSerializer, FileSystemManager);
+            _fileSystemManager = fileSystem;
 
-            Logger = LoggerFactory.CreateLogger("App");
+            ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
 
-            StartupOptions = options;
+            Logger = LoggerFactory.CreateLogger<ApplicationHost>();
 
-            ImageEncoder = imageEncoder;
+            _startupOptions = options;
 
             fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
 
-            NetworkManager.NetworkChanged += OnNetworkChanged;
+            _networkManager.NetworkChanged += OnNetworkChanged;
+
+            CertificateInfo = new CertificateInfo
+            {
+                Path = ServerConfigurationManager.Configuration.CertificatePath,
+                Password = ServerConfigurationManager.Configuration.CertificatePassword
+            };
+            Certificate = GetCertificate(CertificateInfo);
         }
 
         public string ExpandVirtualPath(string path)
@@ -451,10 +336,7 @@ namespace Emby.Server.Implementations
             }
         }
 
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
+        /// <inheritdoc/>
         public string Name => ApplicationProductName;
 
         /// <summary>
@@ -544,7 +426,7 @@ namespace Emby.Server.Implementations
 
             ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
 
-            MediaEncoder.SetFFmpegPath();
+            _mediaEncoder.SetFFmpegPath();
 
             Logger.LogInformation("ServerId: {0}", SystemId);
 
@@ -556,7 +438,7 @@ namespace Emby.Server.Implementations
             Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
 
             Logger.LogInformation("Core startup complete");
-            HttpServer.GlobalResponse = null;
+            _httpServer.GlobalResponse = null;
 
             stopWatch.Restart();
             await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
@@ -580,7 +462,7 @@ namespace Emby.Server.Implementations
         }
 
         /// <inheritdoc/>
-        public async Task InitAsync(IServiceCollection serviceCollection, IConfiguration startupConfig)
+        public void Init(IServiceCollection serviceCollection)
         {
             HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber;
             HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber;
@@ -592,8 +474,6 @@ namespace Emby.Server.Implementations
                 HttpsPort = ServerConfiguration.DefaultHttpsPort;
             }
 
-            JsonSerializer = new JsonSerializer();
-
             if (Plugins != null)
             {
                 var pluginBuilder = new StringBuilder();
@@ -613,7 +493,7 @@ namespace Emby.Server.Implementations
 
             DiscoverTypes();
 
-            await RegisterServices(serviceCollection, startupConfig).ConfigureAwait(false);
+            RegisterServices(serviceCollection);
         }
 
         public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next)
@@ -624,7 +504,7 @@ namespace Emby.Server.Implementations
                 return;
             }
 
-            await HttpServer.ProcessWebSocketRequest(context).ConfigureAwait(false);
+            await _httpServer.ProcessWebSocketRequest(context).ConfigureAwait(false);
         }
 
         public async Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
@@ -640,14 +520,16 @@ namespace Emby.Server.Implementations
             var localPath = context.Request.Path.ToString();
 
             var req = new WebSocketSharpRequest(request, response, request.Path, LoggerFactory.CreateLogger<WebSocketSharpRequest>());
-            await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false);
+            await _httpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false);
         }
 
         /// <summary>
         /// Registers services/resources with the service collection that will be available via DI.
         /// </summary>
-        protected async Task RegisterServices(IServiceCollection serviceCollection, IConfiguration startupConfig)
+        protected virtual void RegisterServices(IServiceCollection serviceCollection)
         {
+            serviceCollection.AddSingleton(_startupOptions);
+
             serviceCollection.AddMemoryCache();
 
             serviceCollection.AddSingleton(ConfigurationManager);
@@ -655,236 +537,169 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
 
-            serviceCollection.AddSingleton(JsonSerializer);
+            serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
 
-            // TODO: Support for injecting ILogger should be deprecated in favour of ILogger<T> and this removed
-            serviceCollection.AddSingleton<ILogger>(Logger);
+            // TODO: Remove support for injecting ILogger completely
+            serviceCollection.AddSingleton((provider) =>
+            {
+                Logger.LogWarning("Injecting ILogger directly is deprecated and should be replaced with ILogger<T>");
+                return Logger;
+            });
 
-            serviceCollection.AddSingleton(FileSystemManager);
+            serviceCollection.AddSingleton(_fileSystemManager);
             serviceCollection.AddSingleton<TvdbClientManager>();
 
-            HttpClient = new HttpClientManager.HttpClientManager(
-                ApplicationPaths,
-                LoggerFactory.CreateLogger<HttpClientManager.HttpClientManager>(),
-                FileSystemManager,
-                () => ApplicationUserAgent);
-            serviceCollection.AddSingleton(HttpClient);
+            serviceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
 
-            serviceCollection.AddSingleton(NetworkManager);
+            serviceCollection.AddSingleton(_networkManager);
 
-            IsoManager = new IsoManager();
-            serviceCollection.AddSingleton(IsoManager);
+            serviceCollection.AddSingleton<IIsoManager, IsoManager>();
 
-            TaskManager = new TaskManager(ApplicationPaths, JsonSerializer, LoggerFactory, FileSystemManager);
-            serviceCollection.AddSingleton(TaskManager);
+            serviceCollection.AddSingleton<ITaskManager, TaskManager>();
 
-            serviceCollection.AddSingleton(XmlSerializer);
+            serviceCollection.AddSingleton(_xmlSerializer);
 
-            ProcessFactory = new ProcessFactory();
-            serviceCollection.AddSingleton(ProcessFactory);
+            serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
 
-            serviceCollection.AddSingleton(typeof(IStreamHelper), typeof(StreamHelper));
+            serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
 
-            var cryptoProvider = new CryptographyProvider();
-            serviceCollection.AddSingleton<ICryptoProvider>(cryptoProvider);
+            serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
 
-            SocketFactory = new SocketFactory();
-            serviceCollection.AddSingleton(SocketFactory);
+            serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
 
-            serviceCollection.AddSingleton(typeof(IInstallationManager), typeof(InstallationManager));
+            serviceCollection.AddSingleton<IZipClient, ZipClient>();
 
-            serviceCollection.AddSingleton(typeof(IZipClient), typeof(ZipClient));
-
-            serviceCollection.AddSingleton(typeof(IHttpResultFactory), typeof(HttpResultFactory));
+            serviceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>();
 
             serviceCollection.AddSingleton<IServerApplicationHost>(this);
             serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
 
             serviceCollection.AddSingleton(ServerConfigurationManager);
 
-            LocalizationManager = new LocalizationManager(ServerConfigurationManager, JsonSerializer, LoggerFactory.CreateLogger<LocalizationManager>());
-            await LocalizationManager.LoadAll().ConfigureAwait(false);
-            serviceCollection.AddSingleton<ILocalizationManager>(LocalizationManager);
-
-            serviceCollection.AddSingleton<IBlurayExaminer>(new BdInfoExaminer(FileSystemManager));
+            serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
 
-            UserDataManager = new UserDataManager(LoggerFactory, ServerConfigurationManager, () => UserManager);
-            serviceCollection.AddSingleton(UserDataManager);
+            serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
 
-            _displayPreferencesRepository = new SqliteDisplayPreferencesRepository(
-                LoggerFactory.CreateLogger<SqliteDisplayPreferencesRepository>(),
-                ApplicationPaths,
-                FileSystemManager);
-            serviceCollection.AddSingleton<IDisplayPreferencesRepository>(_displayPreferencesRepository);
+            serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
+            serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
 
-            ItemRepository = new SqliteItemRepository(ServerConfigurationManager, this, LoggerFactory.CreateLogger<SqliteItemRepository>(), LocalizationManager);
-            serviceCollection.AddSingleton<IItemRepository>(ItemRepository);
+            serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
 
-            AuthenticationRepository = GetAuthenticationRepository();
-            serviceCollection.AddSingleton(AuthenticationRepository);
+            serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
 
-            _userRepository = GetUserRepository();
+            serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
 
-            UserManager = new UserManager(
-                LoggerFactory.CreateLogger<UserManager>(),
-                _userRepository,
-                XmlSerializer,
-                NetworkManager,
-                () => ImageProcessor,
-                () => DtoService,
-                this,
-                JsonSerializer,
-                FileSystemManager,
-                cryptoProvider);
+            serviceCollection.AddSingleton<IUserRepository, SqliteUserRepository>();
 
-            serviceCollection.AddSingleton(UserManager);
+            // 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>();
 
-            MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(
-                LoggerFactory.CreateLogger<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(),
-                ServerConfigurationManager,
-                FileSystemManager,
-                ProcessFactory,
-                LocalizationManager,
-                () => SubtitleEncoder,
-                startupConfig,
-                StartupOptions.FFmpegPath);
-            serviceCollection.AddSingleton(MediaEncoder);
+            // 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
+            serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
+            serviceCollection.AddSingleton<IMediaEncoder>(provider =>
+                ActivatorUtilities.CreateInstance<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(provider, _startupOptions.FFmpegPath ?? string.Empty));
 
-            LibraryManager = new LibraryManager(this, LoggerFactory, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager, MediaEncoder);
-            serviceCollection.AddSingleton(LibraryManager);
+            // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
+            serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
+            serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
+            serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
+            serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
 
-            var musicManager = new MusicManager(LibraryManager);
-            serviceCollection.AddSingleton<IMusicManager>(musicManager);
+            serviceCollection.AddSingleton<IMusicManager, MusicManager>();
 
-            LibraryMonitor = new LibraryMonitor(LoggerFactory, LibraryManager, ServerConfigurationManager, FileSystemManager);
-            serviceCollection.AddSingleton(LibraryMonitor);
+            serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
 
-            serviceCollection.AddSingleton<ISearchEngine>(new SearchEngine(LoggerFactory, LibraryManager, UserManager));
-
-            CertificateInfo = GetCertificateInfo(true);
-            Certificate = GetCertificate(CertificateInfo);
+            serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
 
             serviceCollection.AddSingleton<ServiceController>();
             serviceCollection.AddSingleton<IHttpListener, WebSocketSharpListener>();
             serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
 
-            ImageProcessor = new ImageProcessor(LoggerFactory.CreateLogger<ImageProcessor>(), ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
-            serviceCollection.AddSingleton(ImageProcessor);
+            serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
 
-            TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager);
-            serviceCollection.AddSingleton(TVSeriesManager);
+            serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
 
-            DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager);
-            serviceCollection.AddSingleton(DeviceManager);
+            serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
 
-            MediaSourceManager = new MediaSourceManager(ItemRepository, ApplicationPaths, LocalizationManager, UserManager, LibraryManager, LoggerFactory, JsonSerializer, FileSystemManager, UserDataManager, () => MediaEncoder);
-            serviceCollection.AddSingleton(MediaSourceManager);
+            serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
 
-            SubtitleManager = new SubtitleManager(LoggerFactory, FileSystemManager, LibraryMonitor, MediaSourceManager, LocalizationManager);
-            serviceCollection.AddSingleton(SubtitleManager);
+            serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
 
-            ProviderManager = new ProviderManager(HttpClient, SubtitleManager, ServerConfigurationManager, LibraryMonitor, LoggerFactory, FileSystemManager, ApplicationPaths, () => LibraryManager, JsonSerializer);
-            serviceCollection.AddSingleton(ProviderManager);
+            serviceCollection.AddSingleton<IProviderManager, ProviderManager>();
 
-            DtoService = new DtoService(LoggerFactory, LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ProviderManager, this, () => MediaSourceManager, () => LiveTvManager);
-            serviceCollection.AddSingleton(DtoService);
+            // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
+            serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
+            serviceCollection.AddSingleton<IDtoService, DtoService>();
 
-            ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LoggerFactory, ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, ProviderManager);
-            serviceCollection.AddSingleton(ChannelManager);
+            serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
 
-            SessionManager = new SessionManager(
-                LoggerFactory.CreateLogger<SessionManager>(),
-                UserDataManager,
-                LibraryManager,
-                UserManager,
-                musicManager,
-                DtoService,
-                ImageProcessor,
-                this,
-                AuthenticationRepository,
-                DeviceManager,
-                MediaSourceManager);
-            serviceCollection.AddSingleton(SessionManager);
+            serviceCollection.AddSingleton<ISessionManager, SessionManager>();
 
-            serviceCollection.AddSingleton<IDlnaManager>(
-                new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this));
+            serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
 
-            CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager);
-            serviceCollection.AddSingleton(CollectionManager);
+            serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
 
-            serviceCollection.AddSingleton(typeof(IPlaylistManager), typeof(PlaylistManager));
+            serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
 
-            LiveTvManager = new LiveTvManager(this, ServerConfigurationManager, LoggerFactory, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager, TaskManager, LocalizationManager, JsonSerializer, FileSystemManager, () => ChannelManager);
-            serviceCollection.AddSingleton(LiveTvManager);
+            serviceCollection.AddSingleton<LiveTvDtoService>();
+            serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
 
-            UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, UserManager, ChannelManager, LiveTvManager, ServerConfigurationManager);
-            serviceCollection.AddSingleton(UserViewManager);
+            serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
 
-            NotificationManager = new NotificationManager(
-                LoggerFactory.CreateLogger<NotificationManager>(),
-                UserManager,
-                ServerConfigurationManager);
-            serviceCollection.AddSingleton(NotificationManager);
+            serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
 
-            serviceCollection.AddSingleton<IDeviceDiscovery>(new DeviceDiscovery(ServerConfigurationManager));
+            serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
 
-            ChapterManager = new ChapterManager(ItemRepository);
-            serviceCollection.AddSingleton(ChapterManager);
+            serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
 
-            EncodingManager = new MediaEncoder.EncodingManager(
-                LoggerFactory.CreateLogger<MediaEncoder.EncodingManager>(),
-                FileSystemManager,
-                MediaEncoder,
-                ChapterManager,
-                LibraryManager);
-            serviceCollection.AddSingleton(EncodingManager);
+            serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
 
-            var activityLogRepo = GetActivityLogRepository();
-            serviceCollection.AddSingleton(activityLogRepo);
-            serviceCollection.AddSingleton<IActivityManager>(new ActivityManager(LoggerFactory, activityLogRepo, UserManager));
+            serviceCollection.AddSingleton<IActivityRepository, ActivityRepository>();
+            serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
 
-            var authContext = new AuthorizationContext(AuthenticationRepository, UserManager);
-            serviceCollection.AddSingleton<IAuthorizationContext>(authContext);
-            serviceCollection.AddSingleton<ISessionContext>(new SessionContext(UserManager, authContext, SessionManager));
+            serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
+            serviceCollection.AddSingleton<ISessionContext, SessionContext>();
 
-            AuthService = new AuthService(LoggerFactory.CreateLogger<AuthService>(), authContext, ServerConfigurationManager, SessionManager, NetworkManager);
-            serviceCollection.AddSingleton(AuthService);
+            serviceCollection.AddSingleton<IAuthService, AuthService>();
 
-            SubtitleEncoder = new MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder(
-                LibraryManager,
-                LoggerFactory.CreateLogger<MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(),
-                ApplicationPaths,
-                FileSystemManager,
-                MediaEncoder,
-                HttpClient,
-                MediaSourceManager,
-                ProcessFactory);
-            serviceCollection.AddSingleton(SubtitleEncoder);
+            serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
 
-            serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager));
+            serviceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
             serviceCollection.AddSingleton<EncodingHelper>();
 
-            serviceCollection.AddSingleton(typeof(IAttachmentExtractor), typeof(MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor));
+            serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
+        }
+
+        /// <summary>
+        /// Create services registered with the service container that need to be initialized at application startup.
+        /// </summary>
+        /// <returns>A task representing the service initialization operation.</returns>
+        public async Task InitializeServices()
+        {
+            var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
+            await localizationManager.LoadAll().ConfigureAwait(false);
 
-            _displayPreferencesRepository.Initialize();
+            _mediaEncoder = Resolve<IMediaEncoder>();
+            _sessionManager = Resolve<ISessionManager>();
+            _httpServer = Resolve<IHttpServer>();
+            _httpClient = Resolve<IHttpClient>();
 
-            var userDataRepo = new SqliteUserDataRepository(LoggerFactory.CreateLogger<SqliteUserDataRepository>(), ApplicationPaths);
+            ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
+            ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
+            ((SqliteUserRepository)Resolve<IUserRepository>()).Initialize();
+            ((ActivityRepository)Resolve<IActivityRepository>()).Initialize();
 
             SetStaticProperties();
 
-            ((UserManager)UserManager).Initialize();
+            var userManager = (UserManager)Resolve<IUserManager>();
+            userManager.Initialize();
 
-            ((UserDataManager)UserDataManager).Repository = userDataRepo;
-            ItemRepository.Initialize(userDataRepo, UserManager);
-            ((LibraryManager)LibraryManager).ItemRepository = ItemRepository;
-        }
+            var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
+            ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, userManager);
 
-        /// <summary>
-        /// Create services registered with the service container that need to be initialized at application startup.
-        /// </summary>
-        public void InitializeServices()
-        {
-            HttpServer = Resolve<IHttpServer>();
+            FindParts();
         }
 
         public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
@@ -953,111 +768,38 @@ namespace Emby.Server.Implementations
             }
         }
 
-        /// <summary>
-        /// Gets the user repository.
-        /// </summary>
-        /// <returns><see cref="Task{SqliteUserRepository}" />.</returns>
-        private SqliteUserRepository GetUserRepository()
-        {
-            var repo = new SqliteUserRepository(
-                LoggerFactory.CreateLogger<SqliteUserRepository>(),
-                ApplicationPaths);
-
-            repo.Initialize();
-
-            return repo;
-        }
-
-        private IAuthenticationRepository GetAuthenticationRepository()
-        {
-            var repo = new AuthenticationRepository(LoggerFactory, ServerConfigurationManager);
-
-            repo.Initialize();
-
-            return repo;
-        }
-
-        private IActivityRepository GetActivityLogRepository()
-        {
-            var repo = new ActivityRepository(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager);
-
-            repo.Initialize();
-
-            return repo;
-        }
-
         /// <summary>
         /// Dirty hacks.
         /// </summary>
         private void SetStaticProperties()
         {
-            ItemRepository.ImageProcessor = ImageProcessor;
-
             // For now there's no real way to inject these properly
-            BaseItem.Logger = LoggerFactory.CreateLogger("BaseItem");
+            BaseItem.Logger = Resolve<ILogger<BaseItem>>();
             BaseItem.ConfigurationManager = ServerConfigurationManager;
-            BaseItem.LibraryManager = LibraryManager;
-            BaseItem.ProviderManager = ProviderManager;
-            BaseItem.LocalizationManager = LocalizationManager;
-            BaseItem.ItemRepository = ItemRepository;
-            User.UserManager = UserManager;
-            BaseItem.FileSystem = FileSystemManager;
-            BaseItem.UserDataManager = UserDataManager;
-            BaseItem.ChannelManager = ChannelManager;
-            Video.LiveTvManager = LiveTvManager;
-            Folder.UserViewManager = UserViewManager;
-            UserView.TVSeriesManager = TVSeriesManager;
-            UserView.CollectionManager = CollectionManager;
-            BaseItem.MediaSourceManager = MediaSourceManager;
-            CollectionFolder.XmlSerializer = XmlSerializer;
-            CollectionFolder.JsonSerializer = JsonSerializer;
+            BaseItem.LibraryManager = Resolve<ILibraryManager>();
+            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>();
+            Video.LiveTvManager = Resolve<ILiveTvManager>();
+            Folder.UserViewManager = Resolve<IUserViewManager>();
+            UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
+            UserView.CollectionManager = Resolve<ICollectionManager>();
+            BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
+            CollectionFolder.XmlSerializer = _xmlSerializer;
+            CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
             CollectionFolder.ApplicationHost = this;
-            AuthenticatedAttribute.AuthService = AuthService;
-        }
-
-        private async void PluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> args)
-        {
-            string dir = Path.Combine(ApplicationPaths.PluginsPath, args.Argument.name);
-            var types = Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories)
-                        .Select(Assembly.LoadFrom)
-                        .SelectMany(x => x.ExportedTypes)
-                        .Where(x => x.IsClass && !x.IsAbstract && !x.IsInterface && !x.IsGenericType)
-                        .ToArray();
-
-            int oldLen = _allConcreteTypes.Length;
-            Array.Resize(ref _allConcreteTypes, oldLen + types.Length);
-            types.CopyTo(_allConcreteTypes, oldLen);
-
-            var plugins = types.Where(x => x.IsAssignableFrom(typeof(IPlugin)))
-                    .Select(CreateInstanceSafe)
-                    .Where(x => x != null)
-                    .Cast<IPlugin>()
-                    .Select(LoadPlugin)
-                    .Where(x => x != null)
-                    .ToArray();
-
-            oldLen = _plugins.Length;
-            Array.Resize(ref _plugins, oldLen + plugins.Length);
-            plugins.CopyTo(_plugins, oldLen);
-
-            var entries = types.Where(x => x.IsAssignableFrom(typeof(IServerEntryPoint)))
-                .Select(CreateInstanceSafe)
-                .Where(x => x != null)
-                .Cast<IServerEntryPoint>()
-                .ToList();
-
-            await Task.WhenAll(StartEntryPoints(entries, true)).ConfigureAwait(false);
-            await Task.WhenAll(StartEntryPoints(entries, false)).ConfigureAwait(false);
+            AuthenticatedAttribute.AuthService = Resolve<IAuthService>();
         }
 
         /// <summary>
-        /// Finds the parts.
+        /// Finds plugin components and register them with the appropriate services.
         /// </summary>
-        public void FindParts()
+        private void FindParts()
         {
-            InstallationManager = ServiceProvider.GetService<IInstallationManager>();
-            InstallationManager.PluginInstalled += PluginInstalled;
-
             if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
             {
                 ServerConfigurationManager.Configuration.IsPortAuthorized = true;
@@ -1070,34 +812,34 @@ namespace Emby.Server.Implementations
                         .Where(i => i != null)
                         .ToArray();
 
-            HttpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes());
+            _httpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes());
 
-            LibraryManager.AddParts(
+            Resolve<ILibraryManager>().AddParts(
                 GetExports<IResolverIgnoreRule>(),
                 GetExports<IItemResolver>(),
                 GetExports<IIntroProvider>(),
                 GetExports<IBaseItemComparer>(),
                 GetExports<ILibraryPostScanTask>());
 
-            ProviderManager.AddParts(
+            Resolve<IProviderManager>().AddParts(
                 GetExports<IImageProvider>(),
                 GetExports<IMetadataService>(),
                 GetExports<IMetadataProvider>(),
                 GetExports<IMetadataSaver>(),
                 GetExports<IExternalId>());
 
-            LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
+            Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
 
-            SubtitleManager.AddParts(GetExports<ISubtitleProvider>());
+            Resolve<ISubtitleManager>().AddParts(GetExports<ISubtitleProvider>());
 
-            ChannelManager.AddParts(GetExports<IChannel>());
+            Resolve<IChannelManager>().AddParts(GetExports<IChannel>());
 
-            MediaSourceManager.AddParts(GetExports<IMediaSourceProvider>());
+            Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
 
-            NotificationManager.AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
-            UserManager.AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
+            Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
+            Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
 
-            IsoManager.AddParts(GetExports<IIsoMounter>());
+            Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
         }
 
         private IPlugin LoadPlugin(IPlugin plugin)
@@ -1204,16 +946,6 @@ namespace Emby.Server.Implementations
             });
         }
 
-        private CertificateInfo GetCertificateInfo(bool generateCertificate)
-        {
-            // Custom cert
-            return new CertificateInfo
-            {
-                Path = ServerConfigurationManager.Configuration.CertificatePath,
-                Password = ServerConfigurationManager.Configuration.CertificatePassword
-            };
-        }
-
         /// <summary>
         /// Called when [configuration updated].
         /// </summary>
@@ -1240,14 +972,13 @@ namespace Emby.Server.Implementations
                 }
             }
 
-            if (!HttpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
+            if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
             {
                 requiresRestart = true;
             }
 
             var currentCertPath = CertificateInfo?.Path;
-            var newCertInfo = GetCertificateInfo(false);
-            var newCertPath = newCertInfo?.Path;
+            var newCertPath = ServerConfigurationManager.Configuration.CertificatePath;
 
             if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase))
             {
@@ -1300,7 +1031,7 @@ namespace Emby.Server.Implementations
             {
                 try
                 {
-                    await SessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false);
+                    await _sessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false);
                 }
                 catch (Exception ex)
                 {
@@ -1404,7 +1135,7 @@ namespace Emby.Server.Implementations
                 IsShuttingDown = IsShuttingDown,
                 Version = ApplicationVersionString,
                 WebSocketPortNumber = HttpPort,
-                CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(),
+                CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(),
                 Id = SystemId,
                 ProgramDataPath = ApplicationPaths.ProgramDataPath,
                 WebPath = ApplicationPaths.WebPath,
@@ -1424,15 +1155,14 @@ namespace Emby.Server.Implementations
                 ServerName = FriendlyName,
                 LocalAddress = localAddress,
                 SupportsLibraryMonitor = true,
-                EncoderLocation = MediaEncoder.EncoderLocation,
+                EncoderLocation = _mediaEncoder.EncoderLocation,
                 SystemArchitecture = RuntimeInformation.OSArchitecture,
-                SystemUpdateLevel = SystemUpdateLevel,
-                PackageName = StartupOptions.PackageName
+                PackageName = _startupOptions.PackageName
             };
         }
 
         public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo()
-            => NetworkManager.GetMacAddresses()
+            => _networkManager.GetMacAddresses()
                 .Select(i => new WakeOnLanInfo(i))
                 .ToList();
 
@@ -1544,7 +1274,7 @@ namespace Emby.Server.Implementations
 
             if (addresses.Count == 0)
             {
-                addresses.AddRange(NetworkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces));
+                addresses.AddRange(_networkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces));
             }
 
             var resultList = new List<IPAddress>();
@@ -1611,7 +1341,7 @@ namespace Emby.Server.Implementations
 
             try
             {
-                using (var response = await HttpClient.SendAsync(
+                using (var response = await _httpClient.SendAsync(
                     new HttpRequestOptions
                     {
                         Url = apiUrl,
@@ -1664,7 +1394,7 @@ namespace Emby.Server.Implementations
 
             try
             {
-                await SessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false);
+                await _sessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false);
             }
             catch (Exception ex)
             {
@@ -1714,15 +1444,17 @@ namespace Emby.Server.Implementations
                 throw new NotSupportedException();
             }
 
-            var process = ProcessFactory.Create(new ProcessOptions
+            var process = new Process
             {
-                FileName = url,
-                EnableRaisingEvents = true,
-                UseShellExecute = true,
-                ErrorDialog = false
-            });
-
-            process.Exited += ProcessExited;
+                StartInfo = new ProcessStartInfo
+                {
+                    FileName = url,
+                    UseShellExecute = true,
+                    ErrorDialog = false
+                },
+                EnableRaisingEvents = true
+            };
+            process.Exited += (sender, args) => ((Process)sender).Dispose();
 
             try
             {
@@ -1735,11 +1467,6 @@ namespace Emby.Server.Implementations
             }
         }
 
-        private static void ProcessExited(object sender, EventArgs e)
-        {
-            ((IProcess)sender).Dispose();
-        }
-
         public virtual void EnableLoopback(string appName)
         {
         }
@@ -1788,14 +1515,8 @@ namespace Emby.Server.Implementations
                         Logger.LogError(ex, "Error disposing {Type}", part.GetType().Name);
                     }
                 }
-
-                _userRepository?.Dispose();
-                _displayPreferencesRepository?.Dispose();
             }
 
-            _userRepository = null;
-            _displayPreferencesRepository = null;
-
             _disposed = true;
         }
     }

+ 3 - 0
Emby.Server.Implementations/Archiving/ZipClient.cs

@@ -50,6 +50,7 @@ namespace Emby.Server.Implementations.Archiving
             }
         }
 
+        /// <inheritdoc />
         public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles)
         {
             using (var reader = ZipReader.Open(source))
@@ -66,6 +67,7 @@ namespace Emby.Server.Implementations.Archiving
             }
         }
 
+        /// <inheritdoc />
         public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
         {
             using (var reader = GZipReader.Open(source))
@@ -82,6 +84,7 @@ namespace Emby.Server.Implementations.Archiving
             }
         }
 
+        /// <inheritdoc />
         public void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName)
         {
             using (var reader = GZipReader.Open(source))

+ 2 - 2
Emby.Server.Implementations/Channels/ChannelImageProvider.cs

@@ -20,6 +20,8 @@ namespace Emby.Server.Implementations.Channels
             _channelManager = channelManager;
         }
 
+        public string Name => "Channel Image Provider";
+
         public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
         {
             return GetChannel(item).GetSupportedChannelImages();
@@ -32,8 +34,6 @@ namespace Emby.Server.Implementations.Channels
             return channel.GetChannelImage(type, cancellationToken);
         }
 
-        public string Name => "Channel Image Provider";
-
         public bool Supports(BaseItem item)
         {
             return item is Channel;

+ 85 - 83
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -31,8 +31,6 @@ namespace Emby.Server.Implementations.Channels
 {
     public class ChannelManager : IChannelManager
     {
-        internal IChannel[] Channels { get; private set; }
-
         private readonly IUserManager _userManager;
         private readonly IUserDataManager _userDataManager;
         private readonly IDtoService _dtoService;
@@ -43,11 +41,16 @@ namespace Emby.Server.Implementations.Channels
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IProviderManager _providerManager;
 
+        private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
+            new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
+
+        private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
+
         public ChannelManager(
             IUserManager userManager,
             IDtoService dtoService,
             ILibraryManager libraryManager,
-            ILoggerFactory loggerFactory,
+            ILogger<ChannelManager> logger,
             IServerConfigurationManager config,
             IFileSystem fileSystem,
             IUserDataManager userDataManager,
@@ -57,7 +60,7 @@ namespace Emby.Server.Implementations.Channels
             _userManager = userManager;
             _dtoService = dtoService;
             _libraryManager = libraryManager;
-            _logger = loggerFactory.CreateLogger(nameof(ChannelManager));
+            _logger = logger;
             _config = config;
             _fileSystem = fileSystem;
             _userDataManager = userDataManager;
@@ -65,6 +68,8 @@ namespace Emby.Server.Implementations.Channels
             _providerManager = providerManager;
         }
 
+        internal IChannel[] Channels { get; private set; }
+
         private static TimeSpan CacheLength => TimeSpan.FromHours(3);
 
         public void AddParts(IEnumerable<IChannel> channels)
@@ -85,8 +90,7 @@ namespace Emby.Server.Implementations.Channels
             var internalChannel = _libraryManager.GetItemById(item.ChannelId);
             var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
 
-            var supportsDelete = channel as ISupportsDelete;
-            return supportsDelete != null && supportsDelete.CanDelete(item);
+            return channel is ISupportsDelete supportsDelete && supportsDelete.CanDelete(item);
         }
 
         public bool EnableMediaProbe(BaseItem item)
@@ -146,15 +150,13 @@ namespace Emby.Server.Implementations.Channels
                 {
                     try
                     {
-                        var hasAttributes = GetChannelProvider(i) as IHasFolderAttributes;
-
-                        return (hasAttributes != null && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
+                        return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
+                            && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
                     }
                     catch
                     {
                         return false;
                     }
-
                 }).ToList();
             }
 
@@ -171,7 +173,6 @@ namespace Emby.Server.Implementations.Channels
                     {
                         return false;
                     }
-
                 }).ToList();
             }
 
@@ -188,9 +189,9 @@ namespace Emby.Server.Implementations.Channels
                     {
                         return false;
                     }
-
                 }).ToList();
             }
+
             if (query.IsFavorite.HasValue)
             {
                 var val = query.IsFavorite.Value;
@@ -215,7 +216,6 @@ namespace Emby.Server.Implementations.Channels
                     {
                         return false;
                     }
-
                 }).ToList();
             }
 
@@ -226,6 +226,7 @@ namespace Emby.Server.Implementations.Channels
             {
                 all = all.Skip(query.StartIndex.Value).ToList();
             }
+
             if (query.Limit.HasValue)
             {
                 all = all.Take(query.Limit.Value).ToList();
@@ -256,11 +257,9 @@ namespace Emby.Server.Implementations.Channels
 
             var internalResult = GetChannelsInternal(query);
 
-            var dtoOptions = new DtoOptions()
-            {
-            };
+            var dtoOptions = new DtoOptions();
 
-            //TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can generate runtime issues.
+            // TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can generate runtime issues.
             var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user);
 
             var result = new QueryResult<BaseItemDto>
@@ -341,8 +340,8 @@ namespace Emby.Server.Implementations.Channels
                 }
                 catch
                 {
-
                 }
+
                 return;
             }
 
@@ -365,11 +364,9 @@ namespace Emby.Server.Implementations.Channels
             var channel = GetChannel(item.ChannelId);
             var channelPlugin = GetChannelProvider(channel);
 
-            var requiresCallback = channelPlugin as IRequiresMediaInfoCallback;
-
             IEnumerable<MediaSourceInfo> results;
 
-            if (requiresCallback != null)
+            if (channelPlugin is IRequiresMediaInfoCallback requiresCallback)
             {
                 results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken)
                     .ConfigureAwait(false);
@@ -384,9 +381,6 @@ namespace Emby.Server.Implementations.Channels
                 .ToList();
         }
 
-        private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
-            new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
-
         private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
         {
             if (_channelItemMediaInfo.TryGetValue(id, out Tuple<DateTime, List<MediaSourceInfo>> cachedInfo))
@@ -444,18 +438,21 @@ namespace Emby.Server.Implementations.Channels
             {
                 isNew = true;
             }
+
             item.Path = path;
 
             if (!item.ChannelId.Equals(id))
             {
                 forceUpdate = true;
             }
+
             item.ChannelId = id;
 
             if (item.ParentId != parentFolderId)
             {
                 forceUpdate = true;
             }
+
             item.ParentId = parentFolderId;
 
             item.OfficialRating = GetOfficialRating(channelInfo.ParentalRating);
@@ -472,10 +469,12 @@ namespace Emby.Server.Implementations.Channels
                 _libraryManager.CreateItem(item, null);
             }
 
-            await item.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem))
-            {
-                ForceSave = !isNew && forceUpdate
-            }, cancellationToken).ConfigureAwait(false);
+            await item.RefreshMetadata(
+                new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+                {
+                    ForceSave = !isNew && forceUpdate
+                },
+                cancellationToken).ConfigureAwait(false);
 
             return item;
         }
@@ -509,12 +508,12 @@ namespace Emby.Server.Implementations.Channels
 
         public ChannelFeatures[] GetAllChannelFeatures()
         {
-            return _libraryManager.GetItemIds(new InternalItemsQuery
-            {
-                IncludeItemTypes = new[] { typeof(Channel).Name },
-                OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
-
-            }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
+            return _libraryManager.GetItemIds(
+                new InternalItemsQuery
+                {
+                    IncludeItemTypes = new[] { typeof(Channel).Name },
+                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
+                }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
         }
 
         public ChannelFeatures GetChannelFeatures(string id)
@@ -532,13 +531,13 @@ namespace Emby.Server.Implementations.Channels
 
         public bool SupportsExternalTransfer(Guid channelId)
         {
-            //var channel = GetChannel(channelId);
             var channelProvider = GetChannelProvider(channelId);
 
             return channelProvider.GetChannelFeatures().SupportsContentDownloading;
         }
 
-        public ChannelFeatures GetChannelFeaturesDto(Channel channel,
+        public ChannelFeatures GetChannelFeaturesDto(
+            Channel channel,
             IChannel provider,
             InternalChannelFeatures features)
         {
@@ -567,6 +566,7 @@ namespace Emby.Server.Implementations.Channels
             {
                 throw new ArgumentNullException(nameof(name));
             }
+
             return _libraryManager.GetNewItemId("Channel " + name, typeof(Channel));
         }
 
@@ -614,7 +614,7 @@ namespace Emby.Server.Implementations.Channels
             query.IsFolder = false;
 
             // hack for trailers, figure out a better way later
-            var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.IndexOf("Trailer") != -1;
+            var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.Contains("Trailer", StringComparison.Ordinal);
 
             if (sortByPremiereDate)
             {
@@ -640,10 +640,12 @@ namespace Emby.Server.Implementations.Channels
         {
             var internalChannel = await GetChannel(channel, cancellationToken).ConfigureAwait(false);
 
-            var query = new InternalItemsQuery();
-            query.Parent = internalChannel;
-            query.EnableTotalRecordCount = false;
-            query.ChannelIds = new Guid[] { internalChannel.Id };
+            var query = new InternalItemsQuery
+            {
+                Parent = internalChannel,
+                EnableTotalRecordCount = false,
+                ChannelIds = new Guid[] { internalChannel.Id }
+            };
 
             var result = await GetChannelItemsInternal(query, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
 
@@ -651,13 +653,15 @@ namespace Emby.Server.Implementations.Channels
             {
                 if (item is Folder folder)
                 {
-                    await GetChannelItemsInternal(new InternalItemsQuery
-                    {
-                        Parent = folder,
-                        EnableTotalRecordCount = false,
-                        ChannelIds = new Guid[] { internalChannel.Id }
-
-                    }, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
+                    await GetChannelItemsInternal(
+                        new InternalItemsQuery
+                        {
+                            Parent = folder,
+                            EnableTotalRecordCount = false,
+                            ChannelIds = new Guid[] { internalChannel.Id }
+                        },
+                        new SimpleProgress<double>(),
+                        cancellationToken).ConfigureAwait(false);
                 }
             }
         }
@@ -672,7 +676,8 @@ namespace Emby.Server.Implementations.Channels
 
             var parentItem = query.ParentId == Guid.Empty ? channel : _libraryManager.GetItemById(query.ParentId);
 
-            var itemsResult = await GetChannelItems(channelProvider,
+            var itemsResult = await GetChannelItems(
+                channelProvider,
                 query.User,
                 parentItem is Channel ? null : parentItem.ExternalId,
                 null,
@@ -684,13 +689,12 @@ namespace Emby.Server.Implementations.Channels
             {
                 query.Parent = channel;
             }
+
             query.ChannelIds = Array.Empty<Guid>();
 
             // Not yet sure why this is causing a problem
             query.GroupByPresentationUniqueKey = false;
 
-            //_logger.LogDebug("GetChannelItemsInternal");
-
             // null if came from cache
             if (itemsResult != null)
             {
@@ -707,12 +711,15 @@ namespace Emby.Server.Implementations.Channels
                     var deadItem = _libraryManager.GetItemById(deadId);
                     if (deadItem != null)
                     {
-                        _libraryManager.DeleteItem(deadItem, new DeleteOptions
-                        {
-                            DeleteFileLocation = false,
-                            DeleteFromExternalProvider = false
-
-                        }, parentItem, false);
+                        _libraryManager.DeleteItem(
+                            deadItem,
+                            new DeleteOptions
+                            {
+                                DeleteFileLocation = false,
+                                DeleteFromExternalProvider = false
+                            },
+                            parentItem,
+                            false);
                     }
                 }
             }
@@ -735,7 +742,6 @@ namespace Emby.Server.Implementations.Channels
             return result;
         }
 
-        private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
         private async Task<ChannelItemResult> GetChannelItems(IChannel channel,
             User user,
             string externalFolderId,
@@ -743,7 +749,7 @@ namespace Emby.Server.Implementations.Channels
             bool sortDescending,
             CancellationToken cancellationToken)
         {
-            var userId = user == null ? null : user.Id.ToString("N", CultureInfo.InvariantCulture);
+            var userId = user?.Id.ToString("N", CultureInfo.InvariantCulture);
 
             var cacheLength = CacheLength;
             var cachePath = GetChannelDataCachePath(channel, userId, externalFolderId, sortField, sortDescending);
@@ -761,11 +767,9 @@ namespace Emby.Server.Implementations.Channels
             }
             catch (FileNotFoundException)
             {
-
             }
             catch (IOException)
             {
-
             }
 
             await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -785,11 +789,9 @@ namespace Emby.Server.Implementations.Channels
                 }
                 catch (FileNotFoundException)
                 {
-
                 }
                 catch (IOException)
                 {
-
                 }
 
                 var query = new InternalChannelItemQuery
@@ -833,7 +835,8 @@ namespace Emby.Server.Implementations.Channels
             }
         }
 
-        private string GetChannelDataCachePath(IChannel channel,
+        private string GetChannelDataCachePath(
+            IChannel channel,
             string userId,
             string externalFolderId,
             ChannelItemSortField? sortField,
@@ -843,8 +846,7 @@ namespace Emby.Server.Implementations.Channels
 
             var userCacheKey = string.Empty;
 
-            var hasCacheKey = channel as IHasCacheKey;
-            if (hasCacheKey != null)
+            if (channel is IHasCacheKey hasCacheKey)
             {
                 userCacheKey = hasCacheKey.GetCacheKey(userId) ?? string.Empty;
             }
@@ -858,6 +860,7 @@ namespace Emby.Server.Implementations.Channels
             {
                 filename += "-sortField-" + sortField.Value;
             }
+
             if (sortDescending)
             {
                 filename += "-sortDescending";
@@ -865,7 +868,8 @@ namespace Emby.Server.Implementations.Channels
 
             filename = filename.GetMD5().ToString("N", CultureInfo.InvariantCulture);
 
-            return Path.Combine(_config.ApplicationPaths.CachePath,
+            return Path.Combine(
+                _config.ApplicationPaths.CachePath,
                 "channels",
                 channelId,
                 version,
@@ -981,7 +985,6 @@ namespace Emby.Server.Implementations.Channels
             {
                 item.RunTimeTicks = null;
             }
-
             else if (isNew || !enableMediaProbe)
             {
                 item.RunTimeTicks = info.RunTimeTicks;
@@ -1014,26 +1017,24 @@ namespace Emby.Server.Implementations.Channels
                 }
             }
 
-            var hasArtists = item as IHasArtist;
-            if (hasArtists != null)
+            if (item is IHasArtist hasArtists)
             {
                 hasArtists.Artists = info.Artists.ToArray();
             }
 
-            var hasAlbumArtists = item as IHasAlbumArtist;
-            if (hasAlbumArtists != null)
+            if (item is IHasAlbumArtist hasAlbumArtists)
             {
                 hasAlbumArtists.AlbumArtists = info.AlbumArtists.ToArray();
             }
 
-            var trailer = item as Trailer;
-            if (trailer != null)
+            if (item is Trailer trailer)
             {
                 if (!info.TrailerTypes.SequenceEqual(trailer.TrailerTypes))
                 {
                     _logger.LogDebug("Forcing update due to TrailerTypes {0}", item.Name);
                     forceUpdate = true;
                 }
+
                 trailer.TrailerTypes = info.TrailerTypes.ToArray();
             }
 
@@ -1057,6 +1058,7 @@ namespace Emby.Server.Implementations.Channels
                 forceUpdate = true;
                 _logger.LogDebug("Forcing update due to ChannelId {0}", item.Name);
             }
+
             item.ChannelId = internalChannelId;
 
             if (!item.ParentId.Equals(parentFolderId))
@@ -1064,16 +1066,17 @@ namespace Emby.Server.Implementations.Channels
                 forceUpdate = true;
                 _logger.LogDebug("Forcing update due to parent folder Id {0}", item.Name);
             }
+
             item.ParentId = parentFolderId;
 
-            var hasSeries = item as IHasSeries;
-            if (hasSeries != null)
+            if (item is IHasSeries hasSeries)
             {
                 if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase))
                 {
                     forceUpdate = true;
                     _logger.LogDebug("Forcing update due to SeriesName {0}", item.Name);
                 }
+
                 hasSeries.SeriesName = info.SeriesName;
             }
 
@@ -1082,24 +1085,23 @@ namespace Emby.Server.Implementations.Channels
                 forceUpdate = true;
                 _logger.LogDebug("Forcing update due to ExternalId {0}", item.Name);
             }
+
             item.ExternalId = info.Id;
 
-            var channelAudioItem = item as Audio;
-            if (channelAudioItem != null)
+            if (item is Audio channelAudioItem)
             {
                 channelAudioItem.ExtraType = info.ExtraType;
 
                 var mediaSource = info.MediaSources.FirstOrDefault();
-                item.Path = mediaSource == null ? null : mediaSource.Path;
+                item.Path = mediaSource?.Path;
             }
 
-            var channelVideoItem = item as Video;
-            if (channelVideoItem != null)
+            if (item is Video channelVideoItem)
             {
                 channelVideoItem.ExtraType = info.ExtraType;
 
                 var mediaSource = info.MediaSources.FirstOrDefault();
-                item.Path = mediaSource == null ? null : mediaSource.Path;
+                item.Path = mediaSource?.Path;
             }
 
             if (!string.IsNullOrEmpty(info.ImageUrl) && !item.HasImage(ImageType.Primary))
@@ -1156,7 +1158,7 @@ namespace Emby.Server.Implementations.Channels
                 }
             }
 
-            if (isNew || forceUpdate || item.DateLastRefreshed == default(DateTime))
+            if (isNew || forceUpdate || item.DateLastRefreshed == default)
             {
                 _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
             }

+ 1 - 3
Emby.Server.Implementations/Channels/ChannelPostScanTask.cs

@@ -14,14 +14,12 @@ namespace Emby.Server.Implementations.Channels
     public class ChannelPostScanTask
     {
         private readonly IChannelManager _channelManager;
-        private readonly IUserManager _userManager;
         private readonly ILogger _logger;
         private readonly ILibraryManager _libraryManager;
 
-        public ChannelPostScanTask(IChannelManager channelManager, IUserManager userManager, ILogger logger, ILibraryManager libraryManager)
+        public ChannelPostScanTask(IChannelManager channelManager, ILogger logger, ILibraryManager libraryManager)
         {
             _channelManager = channelManager;
-            _userManager = userManager;
             _logger = logger;
             _libraryManager = libraryManager;
         }

+ 2 - 6
Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs

@@ -7,29 +7,26 @@ using System.Threading.Tasks;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
-using MediaBrowser.Model.Globalization;
 
 namespace Emby.Server.Implementations.Channels
 {
     public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask
     {
         private readonly IChannelManager _channelManager;
-        private readonly IUserManager _userManager;
         private readonly ILogger _logger;
         private readonly ILibraryManager _libraryManager;
         private readonly ILocalizationManager _localization;
 
         public RefreshChannelsScheduledTask(
             IChannelManager channelManager,
-            IUserManager userManager,
             ILogger<RefreshChannelsScheduledTask> logger,
             ILibraryManager libraryManager,
             ILocalizationManager localization)
         {
             _channelManager = channelManager;
-            _userManager = userManager;
             _logger = logger;
             _libraryManager = libraryManager;
             _localization = localization;
@@ -63,7 +60,7 @@ namespace Emby.Server.Implementations.Channels
 
             await manager.RefreshChannels(new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
 
-            await new ChannelPostScanTask(_channelManager, _userManager, _logger, _libraryManager).Run(progress, cancellationToken)
+            await new ChannelPostScanTask(_channelManager, _logger, _libraryManager).Run(progress, cancellationToken)
                     .ConfigureAwait(false);
         }
 
@@ -72,7 +69,6 @@ namespace Emby.Server.Implementations.Channels
         {
             return new[]
             {
-
                 // Every so often
                 new TaskTriggerInfo
                 {

+ 1 - 3
Emby.Server.Implementations/Collections/CollectionImageProvider.cs

@@ -46,9 +46,7 @@ namespace Emby.Server.Implementations.Collections
                 {
                     var subItem = i;
 
-                    var episode = subItem as Episode;
-
-                    if (episode != null)
+                    if (subItem is Episode episode)
                     {
                         var series = episode.Series;
                         if (series != null && series.HasImage(ImageType.Primary))

+ 12 - 8
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -52,7 +52,9 @@ namespace Emby.Server.Implementations.Collections
         }
 
         public event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
+
         public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
+
         public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
 
         private IEnumerable<Folder> FindFolders(string path)
@@ -109,9 +111,9 @@ namespace Emby.Server.Implementations.Collections
         {
             var folder = GetCollectionsFolder(false).Result;
 
-            return folder == null ?
-                new List<BoxSet>() :
-                folder.GetChildren(user, true).OfType<BoxSet>();
+            return folder == null
+                ? Enumerable.Empty<BoxSet>()
+                : folder.GetChildren(user, true).OfType<BoxSet>();
         }
 
         public BoxSet CreateCollection(CollectionCreationOptions options)
@@ -191,7 +193,6 @@ namespace Emby.Server.Implementations.Collections
         private void AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
         {
             var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
-
             if (collection == null)
             {
                 throw new ArgumentException("No collection exists with the supplied Id");
@@ -289,10 +290,13 @@ namespace Emby.Server.Implementations.Collections
             }
 
             collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
-            _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem))
-            {
-                ForceSave = true
-            }, RefreshPriority.High);
+            _providerManager.QueueRefresh(
+                collection.Id,
+                new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+                {
+                    ForceSave = true
+                },
+                RefreshPriority.High);
 
             ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs
             {

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

@@ -1,5 +1,6 @@
 using System.Collections.Generic;
 using Emby.Server.Implementations.HttpServer;
+using Emby.Server.Implementations.Updates;
 using MediaBrowser.Providers.Music;
 using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
 
@@ -17,6 +18,7 @@ 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 }

+ 4 - 25
Emby.Server.Implementations/Data/SqliteExtensions.cs

@@ -3,8 +3,6 @@
 using System;
 using System.Collections.Generic;
 using System.Globalization;
-using System.IO;
-using MediaBrowser.Model.Serialization;
 using SQLitePCL.pretty;
 
 namespace Emby.Server.Implementations.Data
@@ -109,25 +107,6 @@ namespace Emby.Server.Implementations.Data
             return null;
         }
 
-        /// <summary>
-        /// Serializes to bytes.
-        /// </summary>
-        /// <returns>System.Byte[][].</returns>
-        /// <exception cref="ArgumentNullException">obj</exception>
-        public static byte[] SerializeToBytes(this IJsonSerializer json, object obj)
-        {
-            if (obj == null)
-            {
-                throw new ArgumentNullException(nameof(obj));
-            }
-
-            using (var stream = new MemoryStream())
-            {
-                json.SerializeToStream(obj, stream);
-                return stream.ToArray();
-            }
-        }
-
         public static void Attach(SQLiteDatabaseConnection db, string path, string alias)
         {
             var commandText = string.Format(
@@ -287,7 +266,7 @@ namespace Emby.Server.Implementations.Data
             }
         }
 
-        public static void TryBind(this IStatement statement, string name, byte[] value)
+        public static void TryBind(this IStatement statement, string name, ReadOnlySpan<byte> value)
         {
             if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
             {
@@ -383,11 +362,11 @@ namespace Emby.Server.Implementations.Data
             }
         }
 
-        public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement This)
+        public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement statement)
         {
-            while (This.MoveNext())
+            while (statement.MoveNext())
             {
-                yield return This.Current;
+                yield return statement.Current;
             }
         }
     }

+ 142 - 273
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -39,12 +39,11 @@ namespace Emby.Server.Implementations.Data
     {
         private const string ChaptersTableName = "Chapters2";
 
-        /// <summary>
-        /// The _app paths
-        /// </summary>
         private readonly IServerConfigurationManager _config;
         private readonly IServerApplicationHost _appHost;
         private readonly ILocalizationManager _localization;
+        // TODO: Remove this dependency. GetImageCacheTag() is the only method used and it can be converted to a static helper method
+        private readonly IImageProcessor _imageProcessor;
 
         private readonly TypeMapper _typeMapper;
         private readonly JsonSerializerOptions _jsonOptions;
@@ -71,7 +70,8 @@ namespace Emby.Server.Implementations.Data
             IServerConfigurationManager config,
             IServerApplicationHost appHost,
             ILogger<SqliteItemRepository> logger,
-            ILocalizationManager localization)
+            ILocalizationManager localization,
+            IImageProcessor imageProcessor)
             : base(logger)
         {
             if (config == null)
@@ -82,6 +82,7 @@ namespace Emby.Server.Implementations.Data
             _config = config;
             _appHost = appHost;
             _localization = localization;
+            _imageProcessor = imageProcessor;
 
             _typeMapper = new TypeMapper();
             _jsonOptions = JsonDefaults.GetOptions();
@@ -98,8 +99,6 @@ namespace Emby.Server.Implementations.Data
         /// <inheritdoc />
         protected override TempStoreMode TempStore => TempStoreMode.Memory;
 
-        public IImageProcessor ImageProcessor { get; set; }
-
         /// <summary>
         /// Opens the connection to the database
         /// </summary>
@@ -1993,7 +1992,7 @@ namespace Emby.Server.Implementations.Data
                 {
                     try
                     {
-                        chapter.ImageTag = ImageProcessor.GetImageCacheTag(item, chapter);
+                        chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter);
                     }
                     catch (Exception ex)
                     {
@@ -3322,7 +3321,7 @@ namespace Emby.Server.Implementations.Data
 
             for (int i = 0; i < str.Length; i++)
             {
-                if (!(char.IsLetter(str[i])) && (!(char.IsNumber(str[i]))))
+                if (!char.IsLetter(str[i]) && !char.IsNumber(str[i]))
                 {
                     return false;
                 }
@@ -3346,7 +3345,7 @@ namespace Emby.Server.Implementations.Data
             return IsAlphaNumeric(value);
         }
 
-        private List<string> GetWhereClauses(InternalItemsQuery query, IStatement statement, string paramSuffix = "")
+        private List<string> GetWhereClauses(InternalItemsQuery query, IStatement statement)
         {
             if (query.IsResumable ?? false)
             {
@@ -3358,27 +3357,27 @@ namespace Emby.Server.Implementations.Data
 
             if (query.IsHD.HasValue)
             {
-                var threshold = 1200;
+                const int Threshold = 1200;
                 if (query.IsHD.Value)
                 {
-                    minWidth = threshold;
+                    minWidth = Threshold;
                 }
                 else
                 {
-                    maxWidth = threshold - 1;
+                    maxWidth = Threshold - 1;
                 }
             }
 
             if (query.Is4K.HasValue)
             {
-                var threshold = 3800;
+                const int Threshold = 3800;
                 if (query.Is4K.Value)
                 {
-                    minWidth = threshold;
+                    minWidth = Threshold;
                 }
                 else
                 {
-                    maxWidth = threshold - 1;
+                    maxWidth = Threshold - 1;
                 }
             }
 
@@ -3387,93 +3386,61 @@ namespace Emby.Server.Implementations.Data
             if (minWidth.HasValue)
             {
                 whereClauses.Add("Width>=@MinWidth");
-                if (statement != null)
-                {
-                    statement.TryBind("@MinWidth", minWidth);
-                }
+                statement?.TryBind("@MinWidth", minWidth);
             }
+
             if (query.MinHeight.HasValue)
             {
                 whereClauses.Add("Height>=@MinHeight");
-                if (statement != null)
-                {
-                    statement.TryBind("@MinHeight", query.MinHeight);
-                }
+                statement?.TryBind("@MinHeight", query.MinHeight);
             }
+
             if (maxWidth.HasValue)
             {
                 whereClauses.Add("Width<=@MaxWidth");
-                if (statement != null)
-                {
-                    statement.TryBind("@MaxWidth", maxWidth);
-                }
+                statement?.TryBind("@MaxWidth", maxWidth);
             }
+
             if (query.MaxHeight.HasValue)
             {
                 whereClauses.Add("Height<=@MaxHeight");
-                if (statement != null)
-                {
-                    statement.TryBind("@MaxHeight", query.MaxHeight);
-                }
+                statement?.TryBind("@MaxHeight", query.MaxHeight);
             }
 
             if (query.IsLocked.HasValue)
             {
                 whereClauses.Add("IsLocked=@IsLocked");
-                if (statement != null)
-                {
-                    statement.TryBind("@IsLocked", query.IsLocked);
-                }
+                statement?.TryBind("@IsLocked", query.IsLocked);
             }
 
             var tags = query.Tags.ToList();
             var excludeTags = query.ExcludeTags.ToList();
 
-            if (query.IsMovie ?? false)
+            if (query.IsMovie == true)
             {
-                var alternateTypes = new List<string>();
-                if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Movie).Name))
-                {
-                    alternateTypes.Add(typeof(Movie).FullName);
-                }
-                if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Trailer).Name))
-                {
-                    alternateTypes.Add(typeof(Trailer).FullName);
-                }
-
-                var programAttribtues = new List<string>();
-                if (alternateTypes.Count == 0)
+                if (query.IncludeItemTypes.Length == 0
+                    || query.IncludeItemTypes.Contains(nameof(Movie))
+                    || query.IncludeItemTypes.Contains(nameof(Trailer)))
                 {
-                    programAttribtues.Add("IsMovie=@IsMovie");
+                    whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)");
                 }
                 else
                 {
-                    programAttribtues.Add("(IsMovie is null OR IsMovie=@IsMovie)");
+                    whereClauses.Add("IsMovie=@IsMovie");
                 }
 
-                if (statement != null)
-                {
-                    statement.TryBind("@IsMovie", true);
-                }
-
-                whereClauses.Add("(" + string.Join(" OR ", programAttribtues) + ")");
+                statement?.TryBind("@IsMovie", true);
             }
             else if (query.IsMovie.HasValue)
             {
                 whereClauses.Add("IsMovie=@IsMovie");
-                if (statement != null)
-                {
-                    statement.TryBind("@IsMovie", query.IsMovie);
-                }
+                statement?.TryBind("@IsMovie", query.IsMovie);
             }
 
             if (query.IsSeries.HasValue)
             {
                 whereClauses.Add("IsSeries=@IsSeries");
-                if (statement != null)
-                {
-                    statement.TryBind("@IsSeries", query.IsSeries);
-                }
+                statement?.TryBind("@IsSeries", query.IsSeries);
             }
 
             if (query.IsSports.HasValue)
@@ -3525,10 +3492,7 @@ namespace Emby.Server.Implementations.Data
             if (query.IsFolder.HasValue)
             {
                 whereClauses.Add("IsFolder=@IsFolder");
-                if (statement != null)
-                {
-                    statement.TryBind("@IsFolder", query.IsFolder);
-                }
+                statement?.TryBind("@IsFolder", query.IsFolder);
             }
 
             var includeTypes = query.IncludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray();
@@ -3539,10 +3503,7 @@ namespace Emby.Server.Implementations.Data
                 if (excludeTypes.Length == 1)
                 {
                     whereClauses.Add("type<>@type");
-                    if (statement != null)
-                    {
-                        statement.TryBind("@type", excludeTypes[0]);
-                    }
+                    statement?.TryBind("@type", excludeTypes[0]);
                 }
                 else if (excludeTypes.Length > 1)
                 {
@@ -3553,10 +3514,7 @@ namespace Emby.Server.Implementations.Data
             else if (includeTypes.Length == 1)
             {
                 whereClauses.Add("type=@type");
-                if (statement != null)
-                {
-                    statement.TryBind("@type", includeTypes[0]);
-                }
+                statement?.TryBind("@type", includeTypes[0]);
             }
             else if (includeTypes.Length > 1)
             {
@@ -3567,10 +3525,7 @@ namespace Emby.Server.Implementations.Data
             if (query.ChannelIds.Length == 1)
             {
                 whereClauses.Add("ChannelId=@ChannelId");
-                if (statement != null)
-                {
-                    statement.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture));
-                }
+                statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture));
             }
             else if (query.ChannelIds.Length > 1)
             {
@@ -3581,98 +3536,65 @@ namespace Emby.Server.Implementations.Data
             if (!query.ParentId.Equals(Guid.Empty))
             {
                 whereClauses.Add("ParentId=@ParentId");
-                if (statement != null)
-                {
-                    statement.TryBind("@ParentId", query.ParentId);
-                }
+                statement?.TryBind("@ParentId", query.ParentId);
             }
 
             if (!string.IsNullOrWhiteSpace(query.Path))
             {
                 whereClauses.Add("Path=@Path");
-                if (statement != null)
-                {
-                    statement.TryBind("@Path", GetPathToSave(query.Path));
-                }
+                statement?.TryBind("@Path", GetPathToSave(query.Path));
             }
 
             if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
             {
                 whereClauses.Add("PresentationUniqueKey=@PresentationUniqueKey");
-                if (statement != null)
-                {
-                    statement.TryBind("@PresentationUniqueKey", query.PresentationUniqueKey);
-                }
+                statement?.TryBind("@PresentationUniqueKey", query.PresentationUniqueKey);
             }
 
             if (query.MinCommunityRating.HasValue)
             {
                 whereClauses.Add("CommunityRating>=@MinCommunityRating");
-                if (statement != null)
-                {
-                    statement.TryBind("@MinCommunityRating", query.MinCommunityRating.Value);
-                }
+                statement?.TryBind("@MinCommunityRating", query.MinCommunityRating.Value);
             }
 
             if (query.MinIndexNumber.HasValue)
             {
                 whereClauses.Add("IndexNumber>=@MinIndexNumber");
-                if (statement != null)
-                {
-                    statement.TryBind("@MinIndexNumber", query.MinIndexNumber.Value);
-                }
+                statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value);
             }
 
             if (query.MinDateCreated.HasValue)
             {
                 whereClauses.Add("DateCreated>=@MinDateCreated");
-                if (statement != null)
-                {
-                    statement.TryBind("@MinDateCreated", query.MinDateCreated.Value);
-                }
+                statement?.TryBind("@MinDateCreated", query.MinDateCreated.Value);
             }
 
             if (query.MinDateLastSaved.HasValue)
             {
                 whereClauses.Add("(DateLastSaved not null and DateLastSaved>=@MinDateLastSavedForUser)");
-                if (statement != null)
-                {
-                    statement.TryBind("@MinDateLastSaved", query.MinDateLastSaved.Value);
-                }
+                statement?.TryBind("@MinDateLastSaved", query.MinDateLastSaved.Value);
             }
 
             if (query.MinDateLastSavedForUser.HasValue)
             {
                 whereClauses.Add("(DateLastSaved not null and DateLastSaved>=@MinDateLastSavedForUser)");
-                if (statement != null)
-                {
-                    statement.TryBind("@MinDateLastSavedForUser", query.MinDateLastSavedForUser.Value);
-                }
+                statement?.TryBind("@MinDateLastSavedForUser", query.MinDateLastSavedForUser.Value);
             }
 
             if (query.IndexNumber.HasValue)
             {
                 whereClauses.Add("IndexNumber=@IndexNumber");
-                if (statement != null)
-                {
-                    statement.TryBind("@IndexNumber", query.IndexNumber.Value);
-                }
+                statement?.TryBind("@IndexNumber", query.IndexNumber.Value);
             }
             if (query.ParentIndexNumber.HasValue)
             {
                 whereClauses.Add("ParentIndexNumber=@ParentIndexNumber");
-                if (statement != null)
-                {
-                    statement.TryBind("@ParentIndexNumber", query.ParentIndexNumber.Value);
-                }
+                statement?.TryBind("@ParentIndexNumber", query.ParentIndexNumber.Value);
             }
             if (query.ParentIndexNumberNotEquals.HasValue)
             {
                 whereClauses.Add("(ParentIndexNumber<>@ParentIndexNumberNotEquals or ParentIndexNumber is null)");
-                if (statement != null)
-                {
-                    statement.TryBind("@ParentIndexNumberNotEquals", query.ParentIndexNumberNotEquals.Value);
-                }
+                statement?.TryBind("@ParentIndexNumberNotEquals", query.ParentIndexNumberNotEquals.Value);
             }
 
             var minEndDate = query.MinEndDate;
@@ -3693,73 +3615,59 @@ namespace Emby.Server.Implementations.Data
             if (minEndDate.HasValue)
             {
                 whereClauses.Add("EndDate>=@MinEndDate");
-                if (statement != null)
-                {
-                    statement.TryBind("@MinEndDate", minEndDate.Value);
-                }
+                statement?.TryBind("@MinEndDate", minEndDate.Value);
             }
 
             if (maxEndDate.HasValue)
             {
                 whereClauses.Add("EndDate<=@MaxEndDate");
-                if (statement != null)
-                {
-                    statement.TryBind("@MaxEndDate", maxEndDate.Value);
-                }
+                statement?.TryBind("@MaxEndDate", maxEndDate.Value);
             }
 
             if (query.MinStartDate.HasValue)
             {
                 whereClauses.Add("StartDate>=@MinStartDate");
-                if (statement != null)
-                {
-                    statement.TryBind("@MinStartDate", query.MinStartDate.Value);
-                }
+                statement?.TryBind("@MinStartDate", query.MinStartDate.Value);
             }
 
             if (query.MaxStartDate.HasValue)
             {
                 whereClauses.Add("StartDate<=@MaxStartDate");
-                if (statement != null)
-                {
-                    statement.TryBind("@MaxStartDate", query.MaxStartDate.Value);
-                }
+                statement?.TryBind("@MaxStartDate", query.MaxStartDate.Value);
             }
 
             if (query.MinPremiereDate.HasValue)
             {
                 whereClauses.Add("PremiereDate>=@MinPremiereDate");
-                if (statement != null)
-                {
-                    statement.TryBind("@MinPremiereDate", query.MinPremiereDate.Value);
-                }
+                statement?.TryBind("@MinPremiereDate", query.MinPremiereDate.Value);
             }
+
             if (query.MaxPremiereDate.HasValue)
             {
                 whereClauses.Add("PremiereDate<=@MaxPremiereDate");
-                if (statement != null)
-                {
-                    statement.TryBind("@MaxPremiereDate", query.MaxPremiereDate.Value);
-                }
+                statement?.TryBind("@MaxPremiereDate", query.MaxPremiereDate.Value);
             }
 
-            if (query.TrailerTypes.Length > 0)
+            var trailerTypes = query.TrailerTypes;
+            int trailerTypesLen = trailerTypes.Length;
+            if (trailerTypesLen > 0)
             {
-                var clauses = new List<string>();
-                var index = 0;
-                foreach (var type in query.TrailerTypes)
+                const string Or = " OR ";
+                StringBuilder clause = new StringBuilder("(", trailerTypesLen * 32);
+                for (int i = 0; i < trailerTypesLen; i++)
                 {
-                    var paramName = "@TrailerTypes" + index;
-
-                    clauses.Add("TrailerTypes like " + paramName);
-                    if (statement != null)
-                    {
-                        statement.TryBind(paramName, "%" + type + "%");
-                    }
-                    index++;
+                    var paramName = "@TrailerTypes" + i;
+                    clause.Append("TrailerTypes like ")
+                        .Append(paramName)
+                        .Append(Or);
+                    statement?.TryBind(paramName, "%" + trailerTypes[i] + "%");
                 }
-                var clause = "(" + string.Join(" OR ", clauses) + ")";
-                whereClauses.Add(clause);
+
+                // Remove last " OR "
+                clause.Length -= Or.Length;
+                clause.Append(')');
+
+                whereClauses.Add(clause.ToString());
             }
 
             if (query.IsAiring.HasValue)
@@ -3767,24 +3675,15 @@ namespace Emby.Server.Implementations.Data
                 if (query.IsAiring.Value)
                 {
                     whereClauses.Add("StartDate<=@MaxStartDate");
-                    if (statement != null)
-                    {
-                        statement.TryBind("@MaxStartDate", DateTime.UtcNow);
-                    }
+                    statement?.TryBind("@MaxStartDate", DateTime.UtcNow);
 
                     whereClauses.Add("EndDate>=@MinEndDate");
-                    if (statement != null)
-                    {
-                        statement.TryBind("@MinEndDate", DateTime.UtcNow);
-                    }
+                    statement?.TryBind("@MinEndDate", DateTime.UtcNow);
                 }
                 else
                 {
                     whereClauses.Add("(StartDate>@IsAiringDate OR EndDate < @IsAiringDate)");
-                    if (statement != null)
-                    {
-                        statement.TryBind("@IsAiringDate", DateTime.UtcNow);
-                    }
+                    statement?.TryBind("@IsAiringDate", DateTime.UtcNow);
                 }
             }
 
@@ -3799,13 +3698,10 @@ namespace Emby.Server.Implementations.Data
                     var paramName = "@PersonId" + index;
 
                     clauses.Add("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=" + paramName + ")))");
-
-                    if (statement != null)
-                    {
-                        statement.TryBind(paramName, personId.ToByteArray());
-                    }
+                    statement?.TryBind(paramName, personId.ToByteArray());
                     index++;
                 }
+
                 var clause = "(" + string.Join(" OR ", clauses) + ")";
                 whereClauses.Add(clause);
             }
@@ -3813,47 +3709,31 @@ namespace Emby.Server.Implementations.Data
             if (!string.IsNullOrWhiteSpace(query.Person))
             {
                 whereClauses.Add("Guid in (select ItemId from People where Name=@PersonName)");
-                if (statement != null)
-                {
-                    statement.TryBind("@PersonName", query.Person);
-                }
+                statement?.TryBind("@PersonName", query.Person);
             }
 
             if (!string.IsNullOrWhiteSpace(query.MinSortName))
             {
                 whereClauses.Add("SortName>=@MinSortName");
-                if (statement != null)
-                {
-                    statement.TryBind("@MinSortName", query.MinSortName);
-                }
+                statement?.TryBind("@MinSortName", query.MinSortName);
             }
 
             if (!string.IsNullOrWhiteSpace(query.ExternalSeriesId))
             {
                 whereClauses.Add("ExternalSeriesId=@ExternalSeriesId");
-                if (statement != null)
-                {
-                    statement.TryBind("@ExternalSeriesId", query.ExternalSeriesId);
-                }
+                statement?.TryBind("@ExternalSeriesId", query.ExternalSeriesId);
             }
 
             if (!string.IsNullOrWhiteSpace(query.ExternalId))
             {
                 whereClauses.Add("ExternalId=@ExternalId");
-                if (statement != null)
-                {
-                    statement.TryBind("@ExternalId", query.ExternalId);
-                }
+                statement?.TryBind("@ExternalId", query.ExternalId);
             }
 
             if (!string.IsNullOrWhiteSpace(query.Name))
             {
                 whereClauses.Add("CleanName=@Name");
-
-                if (statement != null)
-                {
-                    statement.TryBind("@Name", GetCleanValue(query.Name));
-                }
+                statement?.TryBind("@Name", GetCleanValue(query.Name));
             }
 
             // These are the same, for now
@@ -3872,28 +3752,21 @@ namespace Emby.Server.Implementations.Data
             if (!string.IsNullOrWhiteSpace(query.NameStartsWith))
             {
                 whereClauses.Add("SortName like @NameStartsWith");
-                if (statement != null)
-                {
-                    statement.TryBind("@NameStartsWith", query.NameStartsWith + "%");
-                }
+                statement?.TryBind("@NameStartsWith", query.NameStartsWith + "%");
             }
+
             if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater))
             {
                 whereClauses.Add("SortName >= @NameStartsWithOrGreater");
                 // lowercase this because SortName is stored as lowercase
-                if (statement != null)
-                {
-                    statement.TryBind("@NameStartsWithOrGreater", query.NameStartsWithOrGreater.ToLowerInvariant());
-                }
+                statement?.TryBind("@NameStartsWithOrGreater", query.NameStartsWithOrGreater.ToLowerInvariant());
             }
+
             if (!string.IsNullOrWhiteSpace(query.NameLessThan))
             {
                 whereClauses.Add("SortName < @NameLessThan");
                 // lowercase this because SortName is stored as lowercase
-                if (statement != null)
-                {
-                    statement.TryBind("@NameLessThan", query.NameLessThan.ToLowerInvariant());
-                }
+                statement?.TryBind("@NameLessThan", query.NameLessThan.ToLowerInvariant());
             }
 
             if (query.ImageTypes.Length > 0)
@@ -3909,18 +3782,12 @@ namespace Emby.Server.Implementations.Data
                 if (query.IsLiked.Value)
                 {
                     whereClauses.Add("rating>=@UserRating");
-                    if (statement != null)
-                    {
-                        statement.TryBind("@UserRating", UserItemData.MinLikeValue);
-                    }
+                    statement?.TryBind("@UserRating", UserItemData.MinLikeValue);
                 }
                 else
                 {
                     whereClauses.Add("(rating is null or rating<@UserRating)");
-                    if (statement != null)
-                    {
-                        statement.TryBind("@UserRating", UserItemData.MinLikeValue);
-                    }
+                    statement?.TryBind("@UserRating", UserItemData.MinLikeValue);
                 }
             }
 
@@ -3934,10 +3801,8 @@ namespace Emby.Server.Implementations.Data
                 {
                     whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavoriteOrLiked)");
                 }
-                if (statement != null)
-                {
-                    statement.TryBind("@IsFavoriteOrLiked", query.IsFavoriteOrLiked.Value);
-                }
+
+                statement?.TryBind("@IsFavoriteOrLiked", query.IsFavoriteOrLiked.Value);
             }
 
             if (query.IsFavorite.HasValue)
@@ -3950,10 +3815,8 @@ namespace Emby.Server.Implementations.Data
                 {
                     whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavorite)");
                 }
-                if (statement != null)
-                {
-                    statement.TryBind("@IsFavorite", query.IsFavorite.Value);
-                }
+
+                statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
             }
 
             if (EnableJoinUserData(query))
@@ -3982,10 +3845,8 @@ namespace Emby.Server.Implementations.Data
                         {
                             whereClauses.Add("(played is null or played=@IsPlayed)");
                         }
-                        if (statement != null)
-                        {
-                            statement.TryBind("@IsPlayed", query.IsPlayed.Value);
-                        }
+
+                        statement?.TryBind("@IsPlayed", query.IsPlayed.Value);
                     }
                 }
             }
@@ -4017,6 +3878,7 @@ namespace Emby.Server.Implementations.Data
                     }
                     index++;
                 }
+
                 var clause = "(" + string.Join(" OR ", clauses) + ")";
                 whereClauses.Add(clause);
             }
@@ -4036,6 +3898,7 @@ namespace Emby.Server.Implementations.Data
                     }
                     index++;
                 }
+
                 var clause = "(" + string.Join(" OR ", clauses) + ")";
                 whereClauses.Add(clause);
             }
@@ -4769,18 +4632,22 @@ namespace Emby.Server.Implementations.Data
             {
                 list.Add(typeof(Person).Name);
             }
+
             if (IsTypeInQuery(typeof(Genre).Name, query))
             {
                 list.Add(typeof(Genre).Name);
             }
+
             if (IsTypeInQuery(typeof(MusicGenre).Name, query))
             {
                 list.Add(typeof(MusicGenre).Name);
             }
+
             if (IsTypeInQuery(typeof(MusicArtist).Name, query))
             {
                 list.Add(typeof(MusicArtist).Name);
             }
+
             if (IsTypeInQuery(typeof(Studio).Name, query))
             {
                 list.Add(typeof(Studio).Name);
@@ -4854,7 +4721,7 @@ namespace Emby.Server.Implementations.Data
             return false;
         }
 
-        private static readonly Type[] KnownTypes =
+        private static readonly Type[] _knownTypes =
         {
             typeof(LiveTvProgram),
             typeof(LiveTvChannel),
@@ -4923,7 +4790,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
         {
             var dict = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase);
 
-            foreach (var t in KnownTypes)
+            foreach (var t in _knownTypes)
             {
                 dict[t.Name] = new[] { t.FullName };
             }
@@ -4935,7 +4802,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
         }
 
         // Not crazy about having this all the way down here, but at least it's in one place
-        readonly Dictionary<string, string[]> _types = GetTypeMapDictionary();
+        private readonly Dictionary<string, string[]> _types = GetTypeMapDictionary();
 
         private string[] MapIncludeItemTypes(string value)
         {
@@ -4952,7 +4819,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             return Array.Empty<string>();
         }
 
-        public void DeleteItem(Guid id, CancellationToken cancellationToken)
+        public void DeleteItem(Guid id)
         {
             if (id == Guid.Empty)
             {
@@ -4988,7 +4855,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             }
         }
 
-        private void ExecuteWithSingleParam(IDatabaseConnection db, string query, byte[] value)
+        private void ExecuteWithSingleParam(IDatabaseConnection db, string query, ReadOnlySpan<byte> value)
         {
             using (var statement = PrepareStatement(db, query))
             {
@@ -5548,6 +5415,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                                 {
                                     GetWhereClauses(typeSubQuery, null);
                                 }
+
                                 BindSimilarParams(query, statement);
                                 BindSearchParams(query, statement);
                                 GetWhereClauses(innerQuery, statement);
@@ -5589,7 +5457,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             }
 
             var allTypes = typeString.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
-                .ToLookup(i => i);
+                .ToLookup(x => x);
 
             foreach (var type in allTypes)
             {
@@ -5680,30 +5548,26 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
         private void InsertItemValues(byte[] idBlob, List<(int, string)> values, IDatabaseConnection db)
         {
+            const int Limit = 100;
             var startIndex = 0;
-            var limit = 100;
 
             while (startIndex < values.Count)
             {
                 var insertText = new StringBuilder("insert into ItemValues (ItemId, Type, Value, CleanValue) values ");
 
-                var endIndex = Math.Min(values.Count, startIndex + limit);
-                var isSubsequentRow = false;
+                var endIndex = Math.Min(values.Count, startIndex + Limit);
 
                 for (var i = startIndex; i < endIndex; i++)
                 {
-                    if (isSubsequentRow)
-                    {
-                        insertText.Append(',');
-                    }
-
                     insertText.AppendFormat(
                         CultureInfo.InvariantCulture,
-                        "(@ItemId, @Type{0}, @Value{0}, @CleanValue{0})",
+                        "(@ItemId, @Type{0}, @Value{0}, @CleanValue{0}),",
                         i);
-                    isSubsequentRow = true;
                 }
 
+                // Remove last comma
+                insertText.Length--;
+
                 using (var statement = PrepareStatement(db, insertText.ToString()))
                 {
                     statement.TryBind("@ItemId", idBlob);
@@ -5731,7 +5595,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                     statement.MoveNext();
                 }
 
-                startIndex += limit;
+                startIndex += Limit;
             }
         }
 
@@ -5766,28 +5630,23 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
         private void InsertPeople(byte[] idBlob, List<PersonInfo> people, IDatabaseConnection db)
         {
+            const int Limit = 100;
             var startIndex = 0;
-            var limit = 100;
             var listIndex = 0;
 
             while (startIndex < people.Count)
             {
                 var insertText = new StringBuilder("insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ");
 
-                var endIndex = Math.Min(people.Count, startIndex + limit);
-                var isSubsequentRow = false;
-
+                var endIndex = Math.Min(people.Count, startIndex + Limit);
                 for (var i = startIndex; i < endIndex; i++)
                 {
-                    if (isSubsequentRow)
-                    {
-                        insertText.Append(',');
-                    }
-
-                    insertText.AppendFormat("(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0})", i.ToString(CultureInfo.InvariantCulture));
-                    isSubsequentRow = true;
+                    insertText.AppendFormat("(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0}),", i.ToString(CultureInfo.InvariantCulture));
                 }
 
+                // Remove last comma
+                insertText.Length--;
+
                 using (var statement = PrepareStatement(db, insertText.ToString()))
                 {
                     statement.TryBind("@ItemId", idBlob);
@@ -5811,16 +5670,17 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                     statement.MoveNext();
                 }
 
-                startIndex += limit;
+                startIndex += Limit;
             }
         }
 
         private PersonInfo GetPerson(IReadOnlyList<IResultSetValue> reader)
         {
-            var item = new PersonInfo();
-
-            item.ItemId = reader.GetGuid(0);
-            item.Name = reader.GetString(1);
+            var item = new PersonInfo
+            {
+                ItemId = reader.GetGuid(0),
+                Name = reader.GetString(1)
+            };
 
             if (!reader.IsDBNull(2))
             {
@@ -5927,20 +5787,28 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
         private void InsertMediaStreams(byte[] idBlob, List<MediaStream> streams, IDatabaseConnection db)
         {
+            const int Limit = 10;
             var startIndex = 0;
-            var limit = 10;
 
             while (startIndex < streams.Count)
             {
-                var insertText = new StringBuilder(string.Format("insert into mediastreams ({0}) values ", string.Join(",", _mediaStreamSaveColumns)));
+                var insertText = new StringBuilder("insert into mediastreams (");
+                foreach (var column in _mediaStreamSaveColumns)
+                {
+                    insertText.Append(column).Append(',');
+                }
 
-                var endIndex = Math.Min(streams.Count, startIndex + limit);
+                // Remove last comma
+                insertText.Length--;
+                insertText.Append(") values ");
+
+                var endIndex = Math.Min(streams.Count, startIndex + Limit);
 
                 for (var i = startIndex; i < endIndex; i++)
                 {
                     if (i != startIndex)
                     {
-                        insertText.Append(",");
+                        insertText.Append(',');
                     }
 
                     var index = i.ToString(CultureInfo.InvariantCulture);
@@ -5948,11 +5816,12 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
                     foreach (var column in _mediaStreamSaveColumns.Skip(1))
                     {
-                        insertText.Append("@" + column + index + ",");
+                        insertText.Append('@').Append(column).Append(index).Append(',');
                     }
+
                     insertText.Length -= 1; // Remove the last comma
 
-                    insertText.Append(")");
+                    insertText.Append(')');
                 }
 
                 using (var statement = PrepareStatement(db, insertText.ToString()))
@@ -6014,7 +5883,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                     statement.MoveNext();
                 }
 
-                startIndex += limit;
+                startIndex += Limit;
             }
         }
 
@@ -6031,7 +5900,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 Index = reader[1].ToInt()
             };
 
-            item.Type = (MediaStreamType)Enum.Parse(typeof(MediaStreamType), reader[2].ToString(), true);
+            item.Type = Enum.Parse<MediaStreamType>(reader[2].ToString(), true);
 
             if (reader[3].SQLiteType != SQLiteType.Null)
             {

+ 3 - 3
Emby.Server.Implementations/Devices/DeviceManager.cs

@@ -38,10 +38,11 @@ namespace Emby.Server.Implementations.Devices
         private readonly IServerConfigurationManager _config;
         private readonly ILibraryManager _libraryManager;
         private readonly ILocalizationManager _localizationManager;
-
         private readonly IAuthenticationRepository _authRepo;
+        private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
 
         public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
+
         public event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
 
         private readonly object _cameraUploadSyncLock = new object();
@@ -65,10 +66,9 @@ namespace Emby.Server.Implementations.Devices
             _libraryManager = libraryManager;
             _localizationManager = localizationManager;
             _authRepo = authRepo;
+            _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
         }
 
-
-        private Dictionary<string, ClientCapabilities> _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
         public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
         {
             var path = Path.Combine(GetDevicePath(deviceId), "capabilities.json");

+ 0 - 152
Emby.Server.Implementations/Diagnostics/CommonProcess.cs

@@ -1,152 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Diagnostics;
-
-namespace Emby.Server.Implementations.Diagnostics
-{
-    public class CommonProcess : IProcess
-    {
-        private readonly Process _process;
-
-        private bool _disposed = false;
-        private bool _hasExited;
-
-        public CommonProcess(ProcessOptions options)
-        {
-            StartInfo = options;
-
-            var startInfo = new ProcessStartInfo
-            {
-                Arguments = options.Arguments,
-                FileName = options.FileName,
-                WorkingDirectory = options.WorkingDirectory,
-                UseShellExecute = options.UseShellExecute,
-                CreateNoWindow = options.CreateNoWindow,
-                RedirectStandardError = options.RedirectStandardError,
-                RedirectStandardInput = options.RedirectStandardInput,
-                RedirectStandardOutput = options.RedirectStandardOutput,
-                ErrorDialog = options.ErrorDialog
-            };
-
-
-            if (options.IsHidden)
-            {
-                startInfo.WindowStyle = ProcessWindowStyle.Hidden;
-            }
-
-            _process = new Process
-            {
-                StartInfo = startInfo
-            };
-
-            if (options.EnableRaisingEvents)
-            {
-                _process.EnableRaisingEvents = true;
-                _process.Exited += OnProcessExited;
-            }
-        }
-
-        public event EventHandler Exited;
-
-        public ProcessOptions StartInfo { get; }
-
-        public StreamWriter StandardInput => _process.StandardInput;
-
-        public StreamReader StandardError => _process.StandardError;
-
-        public StreamReader StandardOutput => _process.StandardOutput;
-
-        public int ExitCode => _process.ExitCode;
-
-        private bool HasExited
-        {
-            get
-            {
-                if (_hasExited)
-                {
-                    return true;
-                }
-
-                try
-                {
-                    _hasExited = _process.HasExited;
-                }
-                catch (InvalidOperationException)
-                {
-                    _hasExited = true;
-                }
-
-                return _hasExited;
-            }
-        }
-
-        public void Start()
-        {
-            _process.Start();
-        }
-
-        public void Kill()
-        {
-            _process.Kill();
-        }
-
-        public bool WaitForExit(int timeMs)
-        {
-            return _process.WaitForExit(timeMs);
-        }
-
-        public Task<bool> WaitForExitAsync(int timeMs)
-        {
-            // Note: For this function to work correctly, the option EnableRisingEvents needs to be set to true.
-
-            if (HasExited)
-            {
-                return Task.FromResult(true);
-            }
-
-            timeMs = Math.Max(0, timeMs);
-
-            var tcs = new TaskCompletionSource<bool>();
-
-            var cancellationToken = new CancellationTokenSource(timeMs).Token;
-
-            _process.Exited += (sender, args) => tcs.TrySetResult(true);
-
-            cancellationToken.Register(() => tcs.TrySetResult(HasExited));
-
-            return tcs.Task;
-        }
-
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        protected virtual void Dispose(bool disposing)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            if (disposing)
-            {
-                _process?.Dispose();
-            }
-
-            _disposed = true;
-        }
-
-        private void OnProcessExited(object sender, EventArgs e)
-        {
-            _hasExited = true;
-            Exited?.Invoke(this, e);
-        }
-    }
-}

+ 0 - 14
Emby.Server.Implementations/Diagnostics/ProcessFactory.cs

@@ -1,14 +0,0 @@
-#pragma warning disable CS1591
-
-using MediaBrowser.Model.Diagnostics;
-
-namespace Emby.Server.Implementations.Diagnostics
-{
-    public class ProcessFactory : IProcessFactory
-    {
-        public IProcess Create(ProcessOptions options)
-        {
-            return new CommonProcess(options);
-        }
-    }
-}

+ 20 - 29
Emby.Server.Implementations/Dto/DtoService.cs

@@ -38,21 +38,23 @@ namespace Emby.Server.Implementations.Dto
         private readonly IProviderManager _providerManager;
 
         private readonly IApplicationHost _appHost;
-        private readonly Func<IMediaSourceManager> _mediaSourceManager;
-        private readonly Func<ILiveTvManager> _livetvManager;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
+
+        private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
 
         public DtoService(
-            ILoggerFactory loggerFactory,
+            ILogger<DtoService> logger,
             ILibraryManager libraryManager,
             IUserDataManager userDataRepository,
             IItemRepository itemRepo,
             IImageProcessor imageProcessor,
             IProviderManager providerManager,
             IApplicationHost appHost,
-            Func<IMediaSourceManager> mediaSourceManager,
-            Func<ILiveTvManager> livetvManager)
+            IMediaSourceManager mediaSourceManager,
+            Lazy<ILiveTvManager> livetvManagerFactory)
         {
-            _logger = loggerFactory.CreateLogger(nameof(DtoService));
+            _logger = logger;
             _libraryManager = libraryManager;
             _userDataRepository = userDataRepository;
             _itemRepo = itemRepo;
@@ -60,7 +62,7 @@ namespace Emby.Server.Implementations.Dto
             _providerManager = providerManager;
             _appHost = appHost;
             _mediaSourceManager = mediaSourceManager;
-            _livetvManager = livetvManager;
+            _livetvManagerFactory = livetvManagerFactory;
         }
 
         /// <summary>
@@ -125,12 +127,12 @@ namespace Emby.Server.Implementations.Dto
 
             if (programTuples.Count > 0)
             {
-                _livetvManager().AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
+                LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
             }
 
             if (channelTuples.Count > 0)
             {
-                _livetvManager().AddChannelInfo(channelTuples, options, user);
+                LivetvManager.AddChannelInfo(channelTuples, options, user);
             }
 
             return returnItems;
@@ -142,12 +144,12 @@ namespace Emby.Server.Implementations.Dto
             if (item is LiveTvChannel tvChannel)
             {
                 var list = new List<(BaseItemDto, LiveTvChannel)>(1) { (dto, tvChannel) };
-                _livetvManager().AddChannelInfo(list, options, user);
+                LivetvManager.AddChannelInfo(list, options, user);
             }
             else if (item is LiveTvProgram)
             {
                 var list = new List<(BaseItem, BaseItemDto)>(1) { (item, dto) };
-                var task = _livetvManager().AddInfoToProgramDto(list, options.Fields, user);
+                var task = LivetvManager.AddInfoToProgramDto(list, options.Fields, user);
                 Task.WaitAll(task);
             }
 
@@ -223,7 +225,7 @@ namespace Emby.Server.Implementations.Dto
             if (item is IHasMediaSources
                 && options.ContainsField(ItemFields.MediaSources))
             {
-                dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(item, true, user).ToArray();
+                dto.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray();
 
                 NormalizeMediaSourceContainers(dto);
             }
@@ -254,7 +256,7 @@ namespace Emby.Server.Implementations.Dto
                 dto.Etag = item.GetEtag(user);
             }
 
-            var liveTvManager = _livetvManager();
+            var liveTvManager = LivetvManager;
             var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path);
             if (activeRecording != null)
             {
@@ -1045,7 +1047,7 @@ namespace Emby.Server.Implementations.Dto
                     }
                     else
                     {
-                        mediaStreams = _mediaSourceManager().GetStaticMediaSources(item, true)[0].MediaStreams.ToArray();
+                        mediaStreams = _mediaSourceManager.GetStaticMediaSources(item, true)[0].MediaStreams.ToArray();
                     }
 
                     dto.MediaStreams = mediaStreams;
@@ -1056,30 +1058,19 @@ namespace Emby.Server.Implementations.Dto
 
             if (options.ContainsField(ItemFields.SpecialFeatureCount))
             {
-                if (allExtras == null)
-                {
-                    allExtras = item.GetExtras().ToArray();
-                }
-
+                allExtras = item.GetExtras().ToArray();
                 dto.SpecialFeatureCount = allExtras.Count(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value));
             }
 
             if (options.ContainsField(ItemFields.LocalTrailerCount))
             {
-                int trailerCount = 0;
-                if (allExtras == null)
-                {
-                    allExtras = item.GetExtras().ToArray();
-                }
-
-                trailerCount += allExtras.Count(i => i.ExtraType.HasValue && i.ExtraType.Value == ExtraType.Trailer);
+                allExtras ??= item.GetExtras().ToArray();
+                dto.LocalTrailerCount = allExtras.Count(i => i.ExtraType == ExtraType.Trailer);
 
                 if (item is IHasTrailers hasTrailers)
                 {
-                    trailerCount += hasTrailers.GetTrailerCount();
+                    dto.LocalTrailerCount += hasTrailers.GetTrailerCount();
                 }
-
-                dto.LocalTrailerCount = trailerCount;
             }
 
             // Add EpisodeInfo

+ 8 - 3
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -1,5 +1,10 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
+  <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
+  <PropertyGroup>
+    <ProjectGuid>{E383961B-9356-4D5D-8233-9A1079D03055}</ProjectGuid>
+  </PropertyGroup>
+
   <ItemGroup>
     <ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
     <ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
@@ -32,11 +37,11 @@
     <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="Mono.Nat" Version="2.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.3" />
+    <PackageReference Include="Mono.Nat" Version="2.0.1" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.8.0" />
-    <PackageReference Include="sharpcompress" Version="0.24.0" />
+    <PackageReference Include="sharpcompress" Version="0.25.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
-    <PackageReference Include="System.Interactive.Async" Version="4.0.0" />
   </ItemGroup>
 
   <ItemGroup>

+ 41 - 52
Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Net;
 using System.Text;
@@ -26,10 +27,10 @@ namespace Emby.Server.Implementations.EntryPoints
         private readonly IServerConfigurationManager _config;
         private readonly IDeviceDiscovery _deviceDiscovery;
 
-        private readonly object _createdRulesLock = new object();
-        private List<IPEndPoint> _createdRules = new List<IPEndPoint>();
+        private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
+
         private Timer _timer;
-        private string _lastConfigIdentifier;
+        private string _configIdentifier;
 
         private bool _disposed = false;
 
@@ -60,6 +61,7 @@ namespace Emby.Server.Implementations.EntryPoints
             return new StringBuilder(32)
                 .Append(config.EnableUPnP).Append(Separator)
                 .Append(config.PublicPort).Append(Separator)
+                .Append(config.PublicHttpsPort).Append(Separator)
                 .Append(_appHost.HttpPort).Append(Separator)
                 .Append(_appHost.HttpsPort).Append(Separator)
                 .Append(_appHost.EnableHttps).Append(Separator)
@@ -69,7 +71,10 @@ namespace Emby.Server.Implementations.EntryPoints
 
         private void OnConfigurationUpdated(object sender, EventArgs e)
         {
-            if (!string.Equals(_lastConfigIdentifier, GetConfigIdentifier(), StringComparison.OrdinalIgnoreCase))
+            var oldConfigIdentifier = _configIdentifier;
+            _configIdentifier = GetConfigIdentifier();
+
+            if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase))
             {
                 Stop();
                 Start();
@@ -93,21 +98,19 @@ namespace Emby.Server.Implementations.EntryPoints
                 return;
             }
 
-            _logger.LogDebug("Starting NAT discovery");
+            _logger.LogInformation("Starting NAT discovery");
 
             NatUtility.DeviceFound += OnNatUtilityDeviceFound;
             NatUtility.StartDiscovery();
 
-            _timer = new Timer(ClearCreatedRules, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
+            _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
 
             _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
-
-            _lastConfigIdentifier = GetConfigIdentifier();
         }
 
         private void Stop()
         {
-            _logger.LogDebug("Stopping NAT discovery");
+            _logger.LogInformation("Stopping NAT discovery");
 
             NatUtility.StopDiscovery();
             NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
@@ -117,26 +120,16 @@ namespace Emby.Server.Implementations.EntryPoints
             _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
         }
 
-        private void ClearCreatedRules(object state)
-        {
-            lock (_createdRulesLock)
-            {
-                _createdRules.Clear();
-            }
-        }
-
         private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
         {
             NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp);
         }
 
-        private void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
+        private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
         {
             try
             {
-                var device = e.Device;
-
-                CreateRules(device);
+                await CreateRules(e.Device).ConfigureAwait(false);
             }
             catch (Exception ex)
             {
@@ -144,7 +137,7 @@ namespace Emby.Server.Implementations.EntryPoints
             }
         }
 
-        private async void CreateRules(INatDevice device)
+        private Task CreateRules(INatDevice device)
         {
             if (_disposed)
             {
@@ -153,50 +146,46 @@ namespace Emby.Server.Implementations.EntryPoints
 
             // On some systems the device discovered event seems to fire repeatedly
             // This check will help ensure we're not trying to port map the same device over and over
-            var address = device.DeviceEndpoint;
-
-            lock (_createdRulesLock)
+            if (!_createdRules.TryAdd(device.DeviceEndpoint, 0))
             {
-                if (!_createdRules.Contains(address))
-                {
-                    _createdRules.Add(address);
-                }
-                else
-                {
-                    return;
-                }
+                return Task.CompletedTask;
             }
 
-            try
-            {
-                await CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort).ConfigureAwait(false);
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error creating http port map");
-                return;
-            }
+            return Task.WhenAll(CreatePortMaps(device));
+        }
 
-            try
-            {
-                await CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort).ConfigureAwait(false);
-            }
-            catch (Exception ex)
+        private IEnumerable<Task> CreatePortMaps(INatDevice device)
+        {
+            yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort);
+
+            if (_appHost.EnableHttps)
             {
-                _logger.LogError(ex, "Error creating https port map");
+                yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort);
             }
         }
 
-        private Task<Mapping> CreatePortMap(INatDevice device, int privatePort, int publicPort)
+        private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort)
         {
             _logger.LogDebug(
-                "Creating port map on local port {0} to public port {1} with device {2}",
+                "Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}",
                 privatePort,
                 publicPort,
                 device.DeviceEndpoint);
 
-            return device.CreatePortMapAsync(
-                new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name));
+            try
+            {
+                var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name);
+                await device.CreatePortMapAsync(mapping).ConfigureAwait(false);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(
+                    ex,
+                    "Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.",
+                    privatePort,
+                    publicPort,
+                    device.DeviceEndpoint);
+            }
         }
 
         /// <inheritdoc />

+ 31 - 14
Emby.Server.Implementations/EntryPoints/StartupWizard.cs

@@ -16,46 +16,63 @@ namespace Emby.Server.Implementations.EntryPoints
         private readonly IServerApplicationHost _appHost;
         private readonly IConfiguration _appConfig;
         private readonly IServerConfigurationManager _config;
+        private readonly IStartupOptions _startupOptions;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="StartupWizard"/> class.
         /// </summary>
         /// <param name="appHost">The application host.</param>
+        /// <param name="appConfig">The application configuration.</param>
         /// <param name="config">The configuration manager.</param>
-        public StartupWizard(IServerApplicationHost appHost, IConfiguration appConfig, IServerConfigurationManager config)
+        /// <param name="startupOptions">The application startup options.</param>
+        public StartupWizard(
+            IServerApplicationHost appHost,
+            IConfiguration appConfig,
+            IServerConfigurationManager config,
+            IStartupOptions startupOptions)
         {
             _appHost = appHost;
             _appConfig = appConfig;
             _config = config;
+            _startupOptions = startupOptions;
         }
 
         /// <inheritdoc />
         public Task RunAsync()
+        {
+            Run();
+            return Task.CompletedTask;
+        }
+
+        private void Run()
         {
             if (!_appHost.CanLaunchWebBrowser)
             {
-                return Task.CompletedTask;
+                return;
             }
 
-            if (!_appConfig.HostWebClient())
+            // Always launch the startup wizard if possible when it has not been completed
+            if (!_config.Configuration.IsStartupWizardCompleted && _appConfig.HostWebClient())
             {
-                BrowserLauncher.OpenSwaggerPage(_appHost);
+                BrowserLauncher.OpenWebApp(_appHost);
+                return;
+            }
+
+            // Do nothing if the web app is configured to not run automatically
+            if (!_config.Configuration.AutoRunWebApp || _startupOptions.NoAutoRunWebApp)
+            {
+                return;
             }
-            else if (!_config.Configuration.IsStartupWizardCompleted)
+
+            // Launch the swagger page if the web client is not hosted, otherwise open the web client
+            if (_appConfig.HostWebClient())
             {
                 BrowserLauncher.OpenWebApp(_appHost);
             }
-            else if (_config.Configuration.AutoRunWebApp)
+            else
             {
-                var options = ((ApplicationHost)_appHost).StartupOptions;
-
-                if (!options.NoAutoRunWebApp)
-                {
-                    BrowserLauncher.OpenWebApp(_appHost);
-                }
+                BrowserLauncher.OpenSwaggerPage(_appHost);
             }
-
-            return Task.CompletedTask;
         }
 
         /// <inheritdoc />

+ 5 - 4
Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Net;
 using System.Net.Http;
 using System.Threading.Tasks;
+using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
@@ -24,7 +25,7 @@ namespace Emby.Server.Implementations.HttpClientManager
         private readonly ILogger _logger;
         private readonly IApplicationPaths _appPaths;
         private readonly IFileSystem _fileSystem;
-        private readonly Func<string> _defaultUserAgentFn;
+        private readonly IApplicationHost _appHost;
 
         /// <summary>
         /// Holds a dictionary of http clients by host.  Use GetHttpClient(host) to retrieve or create a client for web requests.
@@ -40,12 +41,12 @@ namespace Emby.Server.Implementations.HttpClientManager
             IApplicationPaths appPaths,
             ILogger<HttpClientManager> logger,
             IFileSystem fileSystem,
-            Func<string> defaultUserAgentFn)
+            IApplicationHost appHost)
         {
             _logger = logger ?? throw new ArgumentNullException(nameof(logger));
             _fileSystem = fileSystem;
             _appPaths = appPaths ?? throw new ArgumentNullException(nameof(appPaths));
-            _defaultUserAgentFn = defaultUserAgentFn;
+            _appHost = appHost;
         }
 
         /// <summary>
@@ -91,7 +92,7 @@ namespace Emby.Server.Implementations.HttpClientManager
             if (options.EnableDefaultUserAgent
                 && !request.Headers.TryGetValues(HeaderNames.UserAgent, out _))
             {
-                request.Headers.Add(HeaderNames.UserAgent, _defaultUserAgentFn());
+                request.Headers.Add(HeaderNames.UserAgent, _appHost.ApplicationUserAgent);
             }
 
             switch (options.DecompressionMethod)

+ 68 - 53
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -14,6 +14,7 @@ using Emby.Server.Implementations.Services;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
+using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Events;
@@ -23,6 +24,7 @@ using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.WebUtilities;
 using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 using ServiceStack.Text.Jsv;
 
@@ -48,6 +50,8 @@ namespace Emby.Server.Implementations.HttpServer
         private readonly string _baseUrlPrefix;
         private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
         private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>();
+        private readonly IHostEnvironment _hostEnvironment;
+
         private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
         private bool _disposed = false;
 
@@ -61,7 +65,8 @@ namespace Emby.Server.Implementations.HttpServer
             IXmlSerializer xmlSerializer,
             IHttpListener socketListener,
             ILocalizationManager localizationManager,
-            ServiceController serviceController)
+            ServiceController serviceController,
+            IHostEnvironment hostEnvironment)
         {
             _appHost = applicationHost;
             _logger = logger;
@@ -75,6 +80,7 @@ namespace Emby.Server.Implementations.HttpServer
             ServiceController = serviceController;
 
             _socketListener.WebSocketConnected = OnWebSocketConnected;
+            _hostEnvironment = hostEnvironment;
 
             _funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
 
@@ -225,7 +231,8 @@ namespace Emby.Server.Implementations.HttpServer
             switch (ex)
             {
                 case ArgumentException _: return 400;
-                case SecurityException _: return 401;
+                case AuthenticationException _: return 401;
+                case SecurityException _: return 403;
                 case DirectoryNotFoundException _:
                 case FileNotFoundException _:
                 case ResourceNotFoundException _: return 404;
@@ -234,55 +241,52 @@ namespace Emby.Server.Implementations.HttpServer
             }
         }
 
-        private async Task ErrorHandler(Exception ex, IRequest httpReq, bool logExceptionStackTrace)
+        private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog)
         {
-            try
-            {
-                ex = GetActualException(ex);
+            bool ignoreStackTrace =
+                ex is SocketException
+                || ex is IOException
+                || ex is OperationCanceledException
+                || ex is SecurityException
+                || ex is AuthenticationException
+                || ex is FileNotFoundException;
 
-                if (logExceptionStackTrace)
-                {
-                    _logger.LogError(ex, "Error processing request");
-                }
-                else
-                {
-                    _logger.LogError("Error processing request: {Message}", ex.Message);
-                }
-
-                var httpRes = httpReq.Response;
-
-                if (httpRes.HasStarted)
-                {
-                    return;
-                }
+            if (ignoreStackTrace)
+            {
+                _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
+            }
+            else
+            {
+                _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
+            }
 
-                var statusCode = GetStatusCode(ex);
-                httpRes.StatusCode = statusCode;
+            var httpRes = httpReq.Response;
 
-                var errContent = NormalizeExceptionMessage(ex.Message);
-                httpRes.ContentType = "text/plain";
-                httpRes.ContentLength = errContent.Length;
-                await httpRes.WriteAsync(errContent).ConfigureAwait(false);
-            }
-            catch (Exception errorEx)
+            if (httpRes.HasStarted)
             {
-                _logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response)");
+                return;
             }
+
+            httpRes.StatusCode = statusCode;
+
+            var errContent = NormalizeExceptionMessage(ex) ?? string.Empty;
+            httpRes.ContentType = "text/plain";
+            httpRes.ContentLength = errContent.Length;
+            await httpRes.WriteAsync(errContent).ConfigureAwait(false);
         }
 
-        private string NormalizeExceptionMessage(string msg)
+        private string NormalizeExceptionMessage(Exception ex)
         {
-            if (msg == null)
+            // Do not expose the exception message for AuthenticationException
+            if (ex is AuthenticationException)
             {
-                return string.Empty;
+                return null;
             }
 
             // Strip any information we don't want to reveal
-
-            msg = msg.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase);
-            msg = msg.Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
-
-            return msg;
+            return ex.Message
+                ?.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase)
+                .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
         }
 
         /// <summary>
@@ -451,7 +455,7 @@ namespace Emby.Server.Implementations.HttpServer
             var stopWatch = new Stopwatch();
             stopWatch.Start();
             var httpRes = httpReq.Response;
-            string urlToLog = null;
+            string urlToLog = GetUrlToLog(urlString);
             string remoteIp = httpReq.RemoteIp;
 
             try
@@ -497,8 +501,6 @@ namespace Emby.Server.Implementations.HttpServer
                     return;
                 }
 
-                urlToLog = GetUrlToLog(urlString);
-
                 if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
                     || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
                     || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
@@ -530,22 +532,35 @@ namespace Emby.Server.Implementations.HttpServer
                 }
                 else
                 {
-                    await ErrorHandler(new FileNotFoundException(), httpReq, false).ConfigureAwait(false);
+                    throw new FileNotFoundException();
                 }
             }
-            catch (Exception ex) when (ex is SocketException || ex is IOException || ex is OperationCanceledException)
+            catch (Exception requestEx)
             {
-                await ErrorHandler(ex, httpReq, false).ConfigureAwait(false);
-            }
-            catch (SecurityException ex)
-            {
-                await ErrorHandler(ex, httpReq, false).ConfigureAwait(false);
-            }
-            catch (Exception ex)
-            {
-                var logException = !string.Equals(ex.GetType().Name, "SocketException", StringComparison.OrdinalIgnoreCase);
+                try
+                {
+                    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())
+                    {
+                        throw;
+                    }
+
+                    await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog).ConfigureAwait(false);
+                }
+                catch (Exception handlerException)
+                {
+                    var aggregateEx = new AggregateException("Error while handling request exception", requestEx, handlerException);
+                    _logger.LogError(aggregateEx, "Error while handling exception in response to {Url}", urlToLog);
 
-                await ErrorHandler(ex, httpReq, logException).ConfigureAwait(false);
+                    if (_hostEnvironment.IsDevelopment())
+                    {
+                        throw aggregateEx;
+                    }
+                }
             }
             finally
             {

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

@@ -28,6 +28,12 @@ namespace Emby.Server.Implementations.HttpServer
     /// </summary>
     public class HttpResultFactory : IHttpResultFactory
     {
+        // Last-Modified and If-Modified-Since must follow strict date format,
+        // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
+        private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\"";
+        // We specifically use en-US culture because both day of week and month names require it
+        private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false);
+
         /// <summary>
         /// The logger.
         /// </summary>
@@ -420,7 +426,11 @@ namespace Emby.Server.Implementations.HttpServer
 
             if (!noCache)
             {
-                DateTime.TryParse(requestContext.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader);
+                if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal, out var ifModifiedSinceHeader))
+                {
+                    _logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]);
+                    return null;
+                }
 
                 if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified))
                 {
@@ -629,7 +639,7 @@ namespace Emby.Server.Implementations.HttpServer
 
             if (lastModifiedDate.HasValue)
             {
-                responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToString(CultureInfo.InvariantCulture);
+                responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture);
             }
         }
 

+ 10 - 27
Emby.Server.Implementations/HttpServer/Security/AuthService.cs

@@ -2,6 +2,7 @@
 
 using System;
 using System.Linq;
+using System.Security.Authentication;
 using Emby.Server.Implementations.SocketSharp;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
@@ -68,7 +69,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
             if (user == null && auth.UserId != Guid.Empty)
             {
-                throw new SecurityException("User with Id " + auth.UserId + " not found");
+                throw new AuthenticationException("User with Id " + auth.UserId + " not found");
             }
 
             if (user != null)
@@ -108,18 +109,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
         {
             if (user.Policy.IsDisabled)
             {
-                throw new SecurityException("User account has been disabled.")
-                {
-                    SecurityExceptionType = SecurityExceptionType.Unauthenticated
-                };
+                throw new SecurityException("User account has been disabled.");
             }
 
             if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(request.RemoteIp))
             {
-                throw new SecurityException("User account has been disabled.")
-                {
-                    SecurityExceptionType = SecurityExceptionType.Unauthenticated
-                };
+                throw new SecurityException("User account has been disabled.");
             }
 
             if (!user.Policy.IsAdministrator
@@ -128,10 +123,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
             {
                 request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl");
 
-                throw new SecurityException("This user account is not allowed access at this time.")
-                {
-                    SecurityExceptionType = SecurityExceptionType.ParentalControl
-                };
+                throw new SecurityException("This user account is not allowed access at this time.");
             }
         }
 
@@ -190,10 +182,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
             {
                 if (user == null || !user.Policy.IsAdministrator)
                 {
-                    throw new SecurityException("User does not have admin access.")
-                    {
-                        SecurityExceptionType = SecurityExceptionType.Unauthenticated
-                    };
+                    throw new SecurityException("User does not have admin access.");
                 }
             }
 
@@ -201,10 +190,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
             {
                 if (user == null || !user.Policy.EnableContentDeletion)
                 {
-                    throw new SecurityException("User does not have delete access.")
-                    {
-                        SecurityExceptionType = SecurityExceptionType.Unauthenticated
-                    };
+                    throw new SecurityException("User does not have delete access.");
                 }
             }
 
@@ -212,10 +198,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
             {
                 if (user == null || !user.Policy.EnableContentDownloading)
                 {
-                    throw new SecurityException("User does not have download access.")
-                    {
-                        SecurityExceptionType = SecurityExceptionType.Unauthenticated
-                    };
+                    throw new SecurityException("User does not have download access.");
                 }
             }
         }
@@ -230,14 +213,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
         {
             if (string.IsNullOrEmpty(token))
             {
-                throw new SecurityException("Access token is required.");
+                throw new AuthenticationException("Access token is required.");
             }
 
             var info = GetTokenInfo(request);
 
             if (info == null)
             {
-                throw new SecurityException("Access token is invalid or expired.");
+                throw new AuthenticationException("Access token is invalid or expired.");
             }
 
             //if (!string.IsNullOrEmpty(info.UserId))

+ 26 - 32
Emby.Server.Implementations/IO/LibraryMonitor.cs

@@ -17,6 +17,11 @@ namespace Emby.Server.Implementations.IO
 {
     public class LibraryMonitor : ILibraryMonitor
     {
+        private readonly ILogger _logger;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IServerConfigurationManager _configurationManager;
+        private readonly IFileSystem _fileSystem;
+
         /// <summary>
         /// The file system watchers.
         /// </summary>
@@ -113,34 +118,23 @@ namespace Emby.Server.Implementations.IO
                 }
                 catch (Exception ex)
                 {
-                    Logger.LogError(ex, "Error in ReportFileSystemChanged for {path}", path);
+                    _logger.LogError(ex, "Error in ReportFileSystemChanged for {path}", path);
                 }
             }
         }
 
-        /// <summary>
-        /// Gets or sets the logger.
-        /// </summary>
-        /// <value>The logger.</value>
-        private ILogger Logger { get; set; }
-
-        private ILibraryManager LibraryManager { get; set; }
-        private IServerConfigurationManager ConfigurationManager { get; set; }
-
-        private readonly IFileSystem _fileSystem;
-
         /// <summary>
         /// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
         /// </summary>
         public LibraryMonitor(
-            ILoggerFactory loggerFactory,
+            ILogger<LibraryMonitor> logger,
             ILibraryManager libraryManager,
             IServerConfigurationManager configurationManager,
             IFileSystem fileSystem)
         {
-            LibraryManager = libraryManager;
-            Logger = loggerFactory.CreateLogger(GetType().Name);
-            ConfigurationManager = configurationManager;
+            _libraryManager = libraryManager;
+            _logger = logger;
+            _configurationManager = configurationManager;
             _fileSystem = fileSystem;
         }
 
@@ -151,7 +145,7 @@ namespace Emby.Server.Implementations.IO
                 return false;
             }
 
-            var options = LibraryManager.GetLibraryOptions(item);
+            var options = _libraryManager.GetLibraryOptions(item);
 
             if (options != null)
             {
@@ -163,12 +157,12 @@ namespace Emby.Server.Implementations.IO
 
         public void Start()
         {
-            LibraryManager.ItemAdded += OnLibraryManagerItemAdded;
-            LibraryManager.ItemRemoved += OnLibraryManagerItemRemoved;
+            _libraryManager.ItemAdded += OnLibraryManagerItemAdded;
+            _libraryManager.ItemRemoved += OnLibraryManagerItemRemoved;
 
             var pathsToWatch = new List<string>();
 
-            var paths = LibraryManager
+            var paths = _libraryManager
                 .RootFolder
                 .Children
                 .Where(IsLibraryMonitorEnabled)
@@ -261,7 +255,7 @@ namespace Emby.Server.Implementations.IO
             if (!Directory.Exists(path))
             {
                 // Seeing a crash in the mono runtime due to an exception being thrown on a different thread
-                Logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path);
+                _logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path);
                 return;
             }
 
@@ -297,7 +291,7 @@ namespace Emby.Server.Implementations.IO
                     if (_fileSystemWatchers.TryAdd(path, newWatcher))
                     {
                         newWatcher.EnableRaisingEvents = true;
-                        Logger.LogInformation("Watching directory " + path);
+                        _logger.LogInformation("Watching directory " + path);
                     }
                     else
                     {
@@ -307,7 +301,7 @@ namespace Emby.Server.Implementations.IO
                 }
                 catch (Exception ex)
                 {
-                    Logger.LogError(ex, "Error watching path: {path}", path);
+                    _logger.LogError(ex, "Error watching path: {path}", path);
                 }
             });
         }
@@ -333,7 +327,7 @@ namespace Emby.Server.Implementations.IO
             {
                 using (watcher)
                 {
-                    Logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path);
+                    _logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path);
 
                     watcher.Created -= OnWatcherChanged;
                     watcher.Deleted -= OnWatcherChanged;
@@ -372,7 +366,7 @@ namespace Emby.Server.Implementations.IO
             var ex = e.GetException();
             var dw = (FileSystemWatcher)sender;
 
-            Logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path);
+            _logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path);
 
             DisposeWatcher(dw, true);
         }
@@ -390,7 +384,7 @@ namespace Emby.Server.Implementations.IO
             }
             catch (Exception ex)
             {
-                Logger.LogError(ex, "Exception in ReportFileSystemChanged. Path: {FullPath}", e.FullPath);
+                _logger.LogError(ex, "Exception in ReportFileSystemChanged. Path: {FullPath}", e.FullPath);
             }
         }
 
@@ -416,13 +410,13 @@ namespace Emby.Server.Implementations.IO
             {
                 if (_fileSystem.AreEqual(i, path))
                 {
-                    Logger.LogDebug("Ignoring change to {Path}", path);
+                    _logger.LogDebug("Ignoring change to {Path}", path);
                     return true;
                 }
 
                 if (_fileSystem.ContainsSubPath(i, path))
                 {
-                    Logger.LogDebug("Ignoring change to {Path}", path);
+                    _logger.LogDebug("Ignoring change to {Path}", path);
                     return true;
                 }
 
@@ -430,7 +424,7 @@ namespace Emby.Server.Implementations.IO
                 var parent = Path.GetDirectoryName(i);
                 if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
                 {
-                    Logger.LogDebug("Ignoring change to {Path}", path);
+                    _logger.LogDebug("Ignoring change to {Path}", path);
                     return true;
                 }
 
@@ -485,7 +479,7 @@ namespace Emby.Server.Implementations.IO
                     }
                 }
 
-                var newRefresher = new FileRefresher(path, ConfigurationManager, LibraryManager, Logger);
+                var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
                 newRefresher.Completed += NewRefresher_Completed;
                 _activeRefreshers.Add(newRefresher);
             }
@@ -502,8 +496,8 @@ namespace Emby.Server.Implementations.IO
         /// </summary>
         public void Stop()
         {
-            LibraryManager.ItemAdded -= OnLibraryManagerItemAdded;
-            LibraryManager.ItemRemoved -= OnLibraryManagerItemRemoved;
+            _libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
+            _libraryManager.ItemRemoved -= OnLibraryManagerItemRemoved;
 
             foreach (var watcher in _fileSystemWatchers.Values.ToList())
             {

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

@@ -3,33 +3,38 @@ namespace Emby.Server.Implementations
     public interface IStartupOptions
     {
         /// <summary>
-        /// --ffmpeg
+        /// Gets the value of the --ffmpeg command line option.
         /// </summary>
         string FFmpegPath { get; }
 
         /// <summary>
-        /// --service
+        /// Gets the value of the --service command line option.
         /// </summary>
         bool IsService { get; }
 
         /// <summary>
-        /// --noautorunwebapp
+        /// Gets the value of the --noautorunwebapp command line option.
         /// </summary>
         bool NoAutoRunWebApp { get; }
 
         /// <summary>
-        /// --package-name
+        /// Gets the value of the --package-name command line option.
         /// </summary>
         string PackageName { get; }
 
         /// <summary>
-        /// --restartpath
+        /// Gets the value of the --restartpath command line option.
         /// </summary>
         string RestartPath { get; }
 
         /// <summary>
-        /// --restartargs
+        /// Gets the value of the --restartargs command line option.
         /// </summary>
         string RestartArgs { get; }
+
+        /// <summary>
+        /// Gets the value of the --plugin-manifest-url command line option.
+        /// </summary>
+        string PluginManifestUrl { get; }
     }
 }

+ 1 - 1
Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs

@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library
         {
             if (resolvedUser == null)
             {
-                throw new ArgumentNullException(nameof(resolvedUser));
+                throw new AuthenticationException($"Specified user does not exist.");
             }
 
             bool success = false;

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

@@ -54,9 +54,29 @@ namespace Emby.Server.Implementations.Library
     /// </summary>
     public class LibraryManager : ILibraryManager
     {
+        private readonly ILogger _logger;
+        private readonly ITaskManager _taskManager;
+        private readonly IUserManager _userManager;
+        private readonly IUserDataManager _userDataRepository;
+        private readonly IServerConfigurationManager _configurationManager;
+        private readonly Lazy<ILibraryMonitor> _libraryMonitorFactory;
+        private readonly Lazy<IProviderManager> _providerManagerFactory;
+        private readonly Lazy<IUserViewManager> _userviewManagerFactory;
+        private readonly IServerApplicationHost _appHost;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IFileSystem _fileSystem;
+        private readonly IItemRepository _itemRepository;
+        private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
+
         private NamingOptions _namingOptions;
         private string[] _videoFileExtensions;
 
+        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
+
+        private IProviderManager ProviderManager => _providerManagerFactory.Value;
+
+        private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
+
         /// <summary>
         /// Gets or sets the postscan tasks.
         /// </summary>
@@ -89,12 +109,6 @@ namespace Emby.Server.Implementations.Library
         /// <value>The comparers.</value>
         private IBaseItemComparer[] Comparers { get; set; }
 
-        /// <summary>
-        /// Gets or sets the active item repository
-        /// </summary>
-        /// <value>The item repository.</value>
-        public IItemRepository ItemRepository { get; set; }
-
         /// <summary>
         /// Occurs when [item added].
         /// </summary>
@@ -110,90 +124,47 @@ namespace Emby.Server.Implementations.Library
         /// </summary>
         public event EventHandler<ItemChangeEventArgs> ItemRemoved;
 
-        /// <summary>
-        /// The _logger
-        /// </summary>
-        private readonly ILogger _logger;
-
-        /// <summary>
-        /// The _task manager
-        /// </summary>
-        private readonly ITaskManager _taskManager;
-
-        /// <summary>
-        /// The _user manager
-        /// </summary>
-        private readonly IUserManager _userManager;
-
-        /// <summary>
-        /// The _user data repository
-        /// </summary>
-        private readonly IUserDataManager _userDataRepository;
-
-        /// <summary>
-        /// Gets or sets the configuration manager.
-        /// </summary>
-        /// <value>The configuration manager.</value>
-        private IServerConfigurationManager ConfigurationManager { get; set; }
-
-        private readonly Func<ILibraryMonitor> _libraryMonitorFactory;
-        private readonly Func<IProviderManager> _providerManagerFactory;
-        private readonly Func<IUserViewManager> _userviewManager;
         public bool IsScanRunning { get; private set; }
 
-        private IServerApplicationHost _appHost;
-        private readonly IMediaEncoder _mediaEncoder;
-
-        /// <summary>
-        /// The _library items cache
-        /// </summary>
-        private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
-
-        /// <summary>
-        /// Gets the library items cache.
-        /// </summary>
-        /// <value>The library items cache.</value>
-        private ConcurrentDictionary<Guid, BaseItem> LibraryItemsCache => _libraryItemsCache;
-
-        private readonly IFileSystem _fileSystem;
-
         /// <summary>
         /// Initializes a new instance of the <see cref="LibraryManager" /> class.
         /// </summary>
         /// <param name="appHost">The application host</param>
-        /// <param name="loggerFactory">The logger factory.</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>
         public LibraryManager(
             IServerApplicationHost appHost,
-            ILoggerFactory loggerFactory,
+            ILogger<LibraryManager> logger,
             ITaskManager taskManager,
             IUserManager userManager,
             IServerConfigurationManager configurationManager,
             IUserDataManager userDataRepository,
-            Func<ILibraryMonitor> libraryMonitorFactory,
+            Lazy<ILibraryMonitor> libraryMonitorFactory,
             IFileSystem fileSystem,
-            Func<IProviderManager> providerManagerFactory,
-            Func<IUserViewManager> userviewManager,
-            IMediaEncoder mediaEncoder)
+            Lazy<IProviderManager> providerManagerFactory,
+            Lazy<IUserViewManager> userviewManagerFactory,
+            IMediaEncoder mediaEncoder,
+            IItemRepository itemRepository)
         {
             _appHost = appHost;
-            _logger = loggerFactory.CreateLogger(nameof(LibraryManager));
+            _logger = logger;
             _taskManager = taskManager;
             _userManager = userManager;
-            ConfigurationManager = configurationManager;
+            _configurationManager = configurationManager;
             _userDataRepository = userDataRepository;
             _libraryMonitorFactory = libraryMonitorFactory;
             _fileSystem = fileSystem;
             _providerManagerFactory = providerManagerFactory;
-            _userviewManager = userviewManager;
+            _userviewManagerFactory = userviewManagerFactory;
             _mediaEncoder = mediaEncoder;
+            _itemRepository = itemRepository;
 
             _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
 
-            ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated;
+            _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
 
             RecordConfigurationValues(configurationManager.Configuration);
         }
@@ -272,7 +243,7 @@ namespace Emby.Server.Implementations.Library
         /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
         private void ConfigurationUpdated(object sender, EventArgs e)
         {
-            var config = ConfigurationManager.Configuration;
+            var config = _configurationManager.Configuration;
 
             var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted;
 
@@ -306,7 +277,7 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            LibraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; });
+            _libraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; });
         }
 
         public void DeleteItem(BaseItem item, DeleteOptions options)
@@ -437,10 +408,10 @@ namespace Emby.Server.Implementations.Library
 
             item.SetParent(null);
 
-            ItemRepository.DeleteItem(item.Id, CancellationToken.None);
+            _itemRepository.DeleteItem(item.Id);
             foreach (var child in children)
             {
-                ItemRepository.DeleteItem(child.Id, CancellationToken.None);
+                _itemRepository.DeleteItem(child.Id);
             }
 
             _libraryItemsCache.TryRemove(item.Id, out BaseItem removed);
@@ -509,15 +480,15 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentNullException(nameof(type));
             }
 
-            if (key.StartsWith(ConfigurationManager.ApplicationPaths.ProgramDataPath, StringComparison.Ordinal))
+            if (key.StartsWith(_configurationManager.ApplicationPaths.ProgramDataPath, StringComparison.Ordinal))
             {
                 // Try to normalize paths located underneath program-data in an attempt to make them more portable
-                key = key.Substring(ConfigurationManager.ApplicationPaths.ProgramDataPath.Length)
+                key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
                     .TrimStart(new[] { '/', '\\' })
                     .Replace("/", "\\");
             }
 
-            if (forceCaseInsensitive || !ConfigurationManager.Configuration.EnableCaseSensitiveItemIds)
+            if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
             {
                 key = key.ToLowerInvariant();
             }
@@ -550,7 +521,7 @@ namespace Emby.Server.Implementations.Library
                 collectionType = GetContentTypeOverride(fullPath, true);
             }
 
-            var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
+            var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService)
             {
                 Parent = parent,
                 Path = fullPath,
@@ -720,7 +691,7 @@ namespace Emby.Server.Implementations.Library
         /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</exception>
         public AggregateFolder CreateRootFolder()
         {
-            var rootFolderPath = ConfigurationManager.ApplicationPaths.RootFolderPath;
+            var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath;
 
             Directory.CreateDirectory(rootFolderPath);
 
@@ -734,7 +705,7 @@ namespace Emby.Server.Implementations.Library
             }
 
             // Add in the plug-in folders
-            var path = Path.Combine(ConfigurationManager.ApplicationPaths.DataPath, "playlists");
+            var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
 
             Directory.CreateDirectory(path);
 
@@ -786,7 +757,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     if (_userRootFolder == null)
                     {
-                        var userRootPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+                        var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 
                         _logger.LogDebug("Creating userRootPath at {path}", userRootPath);
                         Directory.CreateDirectory(userRootPath);
@@ -980,7 +951,7 @@ namespace Emby.Server.Implementations.Library
               where T : BaseItem, new()
         {
             var path = getPathFn(name);
-            var forceCaseInsensitiveId = ConfigurationManager.Configuration.EnableNormalizedItemByNameIds;
+            var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
             return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
         }
 
@@ -994,7 +965,7 @@ namespace Emby.Server.Implementations.Library
         public Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
         {
             // Ensure the location is available.
-            Directory.CreateDirectory(ConfigurationManager.ApplicationPaths.PeoplePath);
+            Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
 
             return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
         }
@@ -1031,7 +1002,7 @@ namespace Emby.Server.Implementations.Library
         public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
         {
             IsScanRunning = true;
-            _libraryMonitorFactory().Stop();
+            LibraryMonitor.Stop();
 
             try
             {
@@ -1039,7 +1010,7 @@ namespace Emby.Server.Implementations.Library
             }
             finally
             {
-                _libraryMonitorFactory().Start();
+                LibraryMonitor.Start();
                 IsScanRunning = false;
             }
         }
@@ -1148,7 +1119,7 @@ namespace Emby.Server.Implementations.Library
                 progress.Report(percent * 100);
             }
 
-            ItemRepository.UpdateInheritedValues(cancellationToken);
+            _itemRepository.UpdateInheritedValues(cancellationToken);
 
             progress.Report(100);
         }
@@ -1168,9 +1139,9 @@ namespace Emby.Server.Implementations.Library
             var topLibraryFolders = GetUserRootFolder().Children.ToList();
 
             _logger.LogDebug("Getting refreshQueue");
-            var refreshQueue = includeRefreshState ? _providerManagerFactory().GetRefreshQueue() : null;
+            var refreshQueue = includeRefreshState ? ProviderManager.GetRefreshQueue() : null;
 
-            return _fileSystem.GetDirectoryPaths(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath)
+            return _fileSystem.GetDirectoryPaths(_configurationManager.ApplicationPaths.DefaultUserViewsPath)
                 .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
                 .ToList();
         }
@@ -1245,7 +1216,7 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentException("Guid can't be empty", nameof(id));
             }
 
-            if (LibraryItemsCache.TryGetValue(id, out BaseItem item))
+            if (_libraryItemsCache.TryGetValue(id, out BaseItem item))
             {
                 return item;
             }
@@ -1276,7 +1247,7 @@ namespace Emby.Server.Implementations.Library
                 AddUserToQuery(query, query.User, allowExternalContent);
             }
 
-            return ItemRepository.GetItemList(query);
+            return _itemRepository.GetItemList(query);
         }
 
         public List<BaseItem> GetItemList(InternalItemsQuery query)
@@ -1300,7 +1271,7 @@ namespace Emby.Server.Implementations.Library
                 AddUserToQuery(query, query.User);
             }
 
-            return ItemRepository.GetCount(query);
+            return _itemRepository.GetCount(query);
         }
 
         public List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
@@ -1315,7 +1286,7 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            return ItemRepository.GetItemList(query);
+            return _itemRepository.GetItemList(query);
         }
 
         public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
@@ -1327,12 +1298,12 @@ namespace Emby.Server.Implementations.Library
 
             if (query.EnableTotalRecordCount)
             {
-                return ItemRepository.GetItems(query);
+                return _itemRepository.GetItems(query);
             }
 
             return new QueryResult<BaseItem>
             {
-                Items = ItemRepository.GetItemList(query).ToArray()
+                Items = _itemRepository.GetItemList(query).ToArray()
             };
         }
 
@@ -1343,7 +1314,7 @@ namespace Emby.Server.Implementations.Library
                 AddUserToQuery(query, query.User);
             }
 
-            return ItemRepository.GetItemIdsList(query);
+            return _itemRepository.GetItemIdsList(query);
         }
 
         public QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query)
@@ -1354,7 +1325,7 @@ namespace Emby.Server.Implementations.Library
             }
 
             SetTopParentOrAncestorIds(query);
-            return ItemRepository.GetStudios(query);
+            return _itemRepository.GetStudios(query);
         }
 
         public QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query)
@@ -1365,7 +1336,7 @@ namespace Emby.Server.Implementations.Library
             }
 
             SetTopParentOrAncestorIds(query);
-            return ItemRepository.GetGenres(query);
+            return _itemRepository.GetGenres(query);
         }
 
         public QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query)
@@ -1376,7 +1347,7 @@ namespace Emby.Server.Implementations.Library
             }
 
             SetTopParentOrAncestorIds(query);
-            return ItemRepository.GetMusicGenres(query);
+            return _itemRepository.GetMusicGenres(query);
         }
 
         public QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query)
@@ -1387,7 +1358,7 @@ namespace Emby.Server.Implementations.Library
             }
 
             SetTopParentOrAncestorIds(query);
-            return ItemRepository.GetAllArtists(query);
+            return _itemRepository.GetAllArtists(query);
         }
 
         public QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query)
@@ -1398,7 +1369,7 @@ namespace Emby.Server.Implementations.Library
             }
 
             SetTopParentOrAncestorIds(query);
-            return ItemRepository.GetArtists(query);
+            return _itemRepository.GetArtists(query);
         }
 
         private void SetTopParentOrAncestorIds(InternalItemsQuery query)
@@ -1439,7 +1410,7 @@ namespace Emby.Server.Implementations.Library
             }
 
             SetTopParentOrAncestorIds(query);
-            return ItemRepository.GetAlbumArtists(query);
+            return _itemRepository.GetAlbumArtists(query);
         }
 
         public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
@@ -1460,10 +1431,10 @@ namespace Emby.Server.Implementations.Library
 
             if (query.EnableTotalRecordCount)
             {
-                return ItemRepository.GetItems(query);
+                return _itemRepository.GetItems(query);
             }
 
-            var list = ItemRepository.GetItemList(query);
+            var list = _itemRepository.GetItemList(query);
 
             return new QueryResult<BaseItem>
             {
@@ -1509,7 +1480,7 @@ namespace Emby.Server.Implementations.Library
                 string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
                 query.ItemIds.Length == 0)
             {
-                var userViews = _userviewManager().GetUserViews(new UserViewQuery
+                var userViews = UserViewManager.GetUserViews(new UserViewQuery
                 {
                     UserId = user.Id,
                     IncludeHidden = true,
@@ -1809,7 +1780,7 @@ namespace Emby.Server.Implementations.Library
             // Don't iterate multiple times
             var itemsList = items.ToList();
 
-            ItemRepository.SaveItems(itemsList, cancellationToken);
+            _itemRepository.SaveItems(itemsList, cancellationToken);
 
             foreach (var item in itemsList)
             {
@@ -1846,7 +1817,7 @@ namespace Emby.Server.Implementations.Library
 
         public void UpdateImages(BaseItem item)
         {
-            ItemRepository.SaveImages(item);
+            _itemRepository.SaveImages(item);
 
             RegisterItem(item);
         }
@@ -1863,7 +1834,7 @@ namespace Emby.Server.Implementations.Library
             {
                 if (item.IsFileProtocol)
                 {
-                    _providerManagerFactory().SaveMetadata(item, updateReason);
+                    ProviderManager.SaveMetadata(item, updateReason);
                 }
 
                 item.DateLastSaved = DateTime.UtcNow;
@@ -1871,7 +1842,7 @@ namespace Emby.Server.Implementations.Library
                 RegisterItem(item);
             }
 
-            ItemRepository.SaveItems(itemsList, cancellationToken);
+            _itemRepository.SaveItems(itemsList, cancellationToken);
 
             if (ItemUpdated != null)
             {
@@ -1947,7 +1918,7 @@ namespace Emby.Server.Implementations.Library
         /// <returns>BaseItem.</returns>
         public BaseItem RetrieveItem(Guid id)
         {
-            return ItemRepository.RetrieveItem(id);
+            return _itemRepository.RetrieveItem(id);
         }
 
         public List<Folder> GetCollectionFolders(BaseItem item)
@@ -2066,7 +2037,7 @@ namespace Emby.Server.Implementations.Library
 
         private string GetContentTypeOverride(string path, bool inherit)
         {
-            var nameValuePair = ConfigurationManager.Configuration.ContentTypes
+            var nameValuePair = _configurationManager.Configuration.ContentTypes
                                     .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
                                                          || (inherit && !string.IsNullOrEmpty(i.Name)
                                                                      && _fileSystem.ContainsSubPath(i.Name, path)));
@@ -2115,7 +2086,7 @@ namespace Emby.Server.Implementations.Library
             string sortName)
         {
             var path = Path.Combine(
-                ConfigurationManager.ApplicationPaths.InternalMetadataPath,
+                _configurationManager.ApplicationPaths.InternalMetadataPath,
                 "views",
                 _fileSystem.GetValidFilename(viewType));
 
@@ -2147,7 +2118,7 @@ namespace Emby.Server.Implementations.Library
             if (refresh)
             {
                 item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None);
-                _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
+                ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
             }
 
             return item;
@@ -2165,7 +2136,7 @@ namespace Emby.Server.Implementations.Library
 
             var id = GetNewItemId(idValues, typeof(UserView));
 
-            var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture));
+            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture));
 
             var item = GetItemById(id) as UserView;
 
@@ -2202,7 +2173,7 @@ namespace Emby.Server.Implementations.Library
 
             if (refresh)
             {
-                _providerManagerFactory().QueueRefresh(
+                ProviderManager.QueueRefresh(
                     item.Id,
                     new MetadataRefreshOptions(new DirectoryService(_fileSystem))
                     {
@@ -2269,7 +2240,7 @@ namespace Emby.Server.Implementations.Library
 
             if (refresh)
             {
-                _providerManagerFactory().QueueRefresh(
+                ProviderManager.QueueRefresh(
                     item.Id,
                     new MetadataRefreshOptions(new DirectoryService(_fileSystem))
                     {
@@ -2303,7 +2274,7 @@ namespace Emby.Server.Implementations.Library
 
             var id = GetNewItemId(idValues, typeof(UserView));
 
-            var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture));
+            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture));
 
             var item = GetItemById(id) as UserView;
 
@@ -2346,7 +2317,7 @@ namespace Emby.Server.Implementations.Library
 
             if (refresh)
             {
-                _providerManagerFactory().QueueRefresh(
+                ProviderManager.QueueRefresh(
                     item.Id,
                     new MetadataRefreshOptions(new DirectoryService(_fileSystem))
                     {
@@ -2364,7 +2335,7 @@ namespace Emby.Server.Implementations.Library
             string videoPath,
             string[] files)
         {
-            new SubtitleResolver(BaseItem.LocalizationManager, _fileSystem).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
+            new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
         }
 
         /// <inheritdoc />
@@ -2609,14 +2580,12 @@ namespace Emby.Server.Implementations.Library
                 }).OrderBy(i => i.Path);
         }
 
-        private static readonly string[] ExtrasSubfolderNames = new[] { "extras", "specials", "shorts", "scenes", "featurettes", "behind the scenes", "deleted scenes", "interviews" };
-
         public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
         {
             var namingOptions = GetNamingOptions();
 
             var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
-                .Where(i => ExtrasSubfolderNames.Contains(i.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+                .Where(i => BaseItem.AllExtrasTypesFolderNames.Contains(i.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase))
                 .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
                 .ToList();
 
@@ -2677,8 +2646,8 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            var metadataPath = ConfigurationManager.Configuration.MetadataPath;
-            var metadataNetworkPath = ConfigurationManager.Configuration.MetadataNetworkPath;
+            var metadataPath = _configurationManager.Configuration.MetadataPath;
+            var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath;
 
             if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath))
             {
@@ -2689,7 +2658,7 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            foreach (var map in ConfigurationManager.Configuration.PathSubstitutions)
+            foreach (var map in _configurationManager.Configuration.PathSubstitutions)
             {
                 if (!string.IsNullOrWhiteSpace(map.From))
                 {
@@ -2758,7 +2727,7 @@ namespace Emby.Server.Implementations.Library
 
         public List<PersonInfo> GetPeople(InternalPeopleQuery query)
         {
-            return ItemRepository.GetPeople(query);
+            return _itemRepository.GetPeople(query);
         }
 
         public List<PersonInfo> GetPeople(BaseItem item)
@@ -2781,7 +2750,7 @@ namespace Emby.Server.Implementations.Library
 
         public List<Person> GetPeopleItems(InternalPeopleQuery query)
         {
-            return ItemRepository.GetPeopleNames(query).Select(i =>
+            return _itemRepository.GetPeopleNames(query).Select(i =>
             {
                 try
                 {
@@ -2798,7 +2767,7 @@ namespace Emby.Server.Implementations.Library
 
         public List<string> GetPeopleNames(InternalPeopleQuery query)
         {
-            return ItemRepository.GetPeopleNames(query);
+            return _itemRepository.GetPeopleNames(query);
         }
 
         public void UpdatePeople(BaseItem item, List<PersonInfo> people)
@@ -2808,7 +2777,7 @@ namespace Emby.Server.Implementations.Library
                 return;
             }
 
-            ItemRepository.UpdatePeople(item.Id, people);
+            _itemRepository.UpdatePeople(item.Id, people);
         }
 
         public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex)
@@ -2819,7 +2788,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     _logger.LogDebug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url);
 
-                    await _providerManagerFactory().SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false);
+                    await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false);
 
                     item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
 
@@ -2852,7 +2821,7 @@ namespace Emby.Server.Implementations.Library
 
             name = _fileSystem.GetValidFilename(name);
 
-            var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 
             var virtualFolderPath = Path.Combine(rootFolderPath, name);
             while (Directory.Exists(virtualFolderPath))
@@ -2871,7 +2840,7 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            _libraryMonitorFactory().Stop();
+            LibraryMonitor.Stop();
 
             try
             {
@@ -2906,7 +2875,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     // Need to add a delay here or directory watchers may still pick up the changes
                     await Task.Delay(1000).ConfigureAwait(false);
-                    _libraryMonitorFactory().Start();
+                    LibraryMonitor.Start();
                 }
             }
         }
@@ -2966,7 +2935,7 @@ namespace Emby.Server.Implementations.Library
                 throw new FileNotFoundException("The network path does not exist.");
             }
 
-            var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
             var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 
             var shortcutFilename = Path.GetFileNameWithoutExtension(path);
@@ -3009,7 +2978,7 @@ namespace Emby.Server.Implementations.Library
                 throw new FileNotFoundException("The network path does not exist.");
             }
 
-            var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
             var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 
             var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
@@ -3062,7 +3031,7 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentNullException(nameof(name));
             }
 
-            var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 
             var path = Path.Combine(rootFolderPath, name);
 
@@ -3071,7 +3040,7 @@ namespace Emby.Server.Implementations.Library
                 throw new FileNotFoundException("The media folder does not exist");
             }
 
-            _libraryMonitorFactory().Stop();
+            LibraryMonitor.Stop();
 
             try
             {
@@ -3091,7 +3060,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     // Need to add a delay here or directory watchers may still pick up the changes
                     await Task.Delay(1000).ConfigureAwait(false);
-                    _libraryMonitorFactory().Start();
+                    LibraryMonitor.Start();
                 }
             }
         }
@@ -3105,7 +3074,7 @@ namespace Emby.Server.Implementations.Library
 
             var removeList = new List<NameValuePair>();
 
-            foreach (var contentType in ConfigurationManager.Configuration.ContentTypes)
+            foreach (var contentType in _configurationManager.Configuration.ContentTypes)
             {
                 if (string.IsNullOrWhiteSpace(contentType.Name))
                 {
@@ -3120,11 +3089,11 @@ namespace Emby.Server.Implementations.Library
 
             if (removeList.Count > 0)
             {
-                ConfigurationManager.Configuration.ContentTypes = ConfigurationManager.Configuration.ContentTypes
+                _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
                     .Except(removeList)
-                        .ToArray();
+                    .ToArray();
 
-                ConfigurationManager.SaveConfiguration();
+                _configurationManager.SaveConfiguration();
             }
         }
 
@@ -3135,7 +3104,7 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentNullException(nameof(mediaPath));
             }
 
-            var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
             var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 
             if (!Directory.Exists(virtualFolderPath))

+ 11 - 11
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -33,13 +33,13 @@ namespace Emby.Server.Implementations.Library
         private readonly ILibraryManager _libraryManager;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IFileSystem _fileSystem;
-
-        private IMediaSourceProvider[] _providers;
         private readonly ILogger _logger;
         private readonly IUserDataManager _userDataManager;
-        private readonly Func<IMediaEncoder> _mediaEncoder;
-        private ILocalizationManager _localizationManager;
-        private IApplicationPaths _appPaths;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IApplicationPaths _appPaths;
+
+        private IMediaSourceProvider[] _providers;
 
         public MediaSourceManager(
             IItemRepository itemRepo,
@@ -47,16 +47,16 @@ namespace Emby.Server.Implementations.Library
             ILocalizationManager localizationManager,
             IUserManager userManager,
             ILibraryManager libraryManager,
-            ILoggerFactory loggerFactory,
+            ILogger<MediaSourceManager> logger,
             IJsonSerializer jsonSerializer,
             IFileSystem fileSystem,
             IUserDataManager userDataManager,
-            Func<IMediaEncoder> mediaEncoder)
+            IMediaEncoder mediaEncoder)
         {
             _itemRepo = itemRepo;
             _userManager = userManager;
             _libraryManager = libraryManager;
-            _logger = loggerFactory.CreateLogger(nameof(MediaSourceManager));
+            _logger = logger;
             _jsonSerializer = jsonSerializer;
             _fileSystem = fileSystem;
             _userDataManager = userDataManager;
@@ -496,7 +496,7 @@ namespace Emby.Server.Implementations.Library
                     // hack - these two values were taken from LiveTVMediaSourceProvider
                     string cacheKey = request.OpenToken;
 
-                    await new LiveStreamHelper(_mediaEncoder(), _logger, _jsonSerializer, _appPaths)
+                    await new LiveStreamHelper(_mediaEncoder, _logger, _jsonSerializer, _appPaths)
                         .AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken)
                         .ConfigureAwait(false);
                 }
@@ -621,7 +621,7 @@ namespace Emby.Server.Implementations.Library
 
             if (liveStreamInfo is IDirectStreamProvider)
             {
-                var info = await _mediaEncoder().GetMediaInfo(new MediaInfoRequest
+                var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
                 {
                     MediaSource = mediaSource,
                     ExtractChapters = false,
@@ -674,7 +674,7 @@ 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,

+ 1 - 1
Emby.Server.Implementations/Library/PathExtensions.cs

@@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.Library
             // for imdbid we also accept pattern matching
             if (string.Equals(attrib, "imdbid", StringComparison.OrdinalIgnoreCase))
             {
-                var m = Regex.Match(str, "tt\\d{7}", RegexOptions.IgnoreCase);
+                var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase);
                 return m.Success ? m.Value : null;
             }
 

+ 3 - 4
Emby.Server.Implementations/Library/SearchEngine.cs

@@ -17,16 +17,15 @@ namespace Emby.Server.Implementations.Library
 {
     public class SearchEngine : ISearchEngine
     {
+        private readonly ILogger _logger;
         private readonly ILibraryManager _libraryManager;
         private readonly IUserManager _userManager;
-        private readonly ILogger _logger;
 
-        public SearchEngine(ILoggerFactory loggerFactory, ILibraryManager libraryManager, IUserManager userManager)
+        public SearchEngine(ILogger<SearchEngine> logger, ILibraryManager libraryManager, IUserManager userManager)
         {
+            _logger = logger;
             _libraryManager = libraryManager;
             _userManager = userManager;
-
-            _logger = loggerFactory.CreateLogger("SearchEngine");
         }
 
         public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)

+ 18 - 19
Emby.Server.Implementations/Library/UserDataManager.cs

@@ -28,25 +28,24 @@ namespace Emby.Server.Implementations.Library
 
         private readonly ILogger _logger;
         private readonly IServerConfigurationManager _config;
-
-        private Func<IUserManager> _userManager;
-
-        public UserDataManager(ILoggerFactory loggerFactory, IServerConfigurationManager config, Func<IUserManager> userManager)
+        private readonly IUserManager _userManager;
+        private readonly IUserDataRepository _repository;
+
+        public UserDataManager(
+            ILogger<UserDataManager> logger,
+            IServerConfigurationManager config,
+            IUserManager userManager,
+            IUserDataRepository repository)
         {
+            _logger = logger;
             _config = config;
-            _logger = loggerFactory.CreateLogger(GetType().Name);
             _userManager = userManager;
+            _repository = repository;
         }
 
-        /// <summary>
-        /// Gets or sets the repository.
-        /// </summary>
-        /// <value>The repository.</value>
-        public IUserDataRepository Repository { get; set; }
-
         public void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken)
         {
-            var user = _userManager().GetUserById(userId);
+            var user = _userManager.GetUserById(userId);
 
             SaveUserData(user, item, userData, reason, cancellationToken);
         }
@@ -71,7 +70,7 @@ namespace Emby.Server.Implementations.Library
 
             foreach (var key in keys)
             {
-                Repository.SaveUserData(userId, key, userData, cancellationToken);
+                _repository.SaveUserData(userId, key, userData, cancellationToken);
             }
 
             var cacheKey = GetCacheKey(userId, item.Id);
@@ -96,9 +95,9 @@ namespace Emby.Server.Implementations.Library
         /// <returns></returns>
         public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken)
         {
-            var user = _userManager().GetUserById(userId);
+            var user = _userManager.GetUserById(userId);
 
-            Repository.SaveAllUserData(user.InternalId, userData, cancellationToken);
+            _repository.SaveAllUserData(user.InternalId, userData, cancellationToken);
         }
 
         /// <summary>
@@ -108,14 +107,14 @@ namespace Emby.Server.Implementations.Library
         /// <returns></returns>
         public List<UserItemData> GetAllUserData(Guid userId)
         {
-            var user = _userManager().GetUserById(userId);
+            var user = _userManager.GetUserById(userId);
 
-            return Repository.GetAllUserData(user.InternalId);
+            return _repository.GetAllUserData(user.InternalId);
         }
 
         public UserItemData GetUserData(Guid userId, Guid itemId, List<string> keys)
         {
-            var user = _userManager().GetUserById(userId);
+            var user = _userManager.GetUserById(userId);
 
             return GetUserData(user, itemId, keys);
         }
@@ -131,7 +130,7 @@ namespace Emby.Server.Implementations.Library
 
         private UserItemData GetUserDataInternal(long internalUserId, List<string> keys)
         {
-            var userData = Repository.GetUserData(internalUserId, keys);
+            var userData = _repository.GetUserData(internalUserId, keys);
 
             if (userData != null)
             {

+ 21 - 25
Emby.Server.Implementations/Library/UserManager.cs

@@ -20,6 +20,7 @@ using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Providers;
@@ -44,22 +45,14 @@ namespace Emby.Server.Implementations.Library
     {
         private readonly object _policySyncLock = new object();
         private readonly object _configSyncLock = new object();
-        /// <summary>
-        /// The logger.
-        /// </summary>
-        private readonly ILogger _logger;
 
-        /// <summary>
-        /// Gets the active user repository.
-        /// </summary>
-        /// <value>The user repository.</value>
+        private readonly ILogger _logger;
         private readonly IUserRepository _userRepository;
         private readonly IXmlSerializer _xmlSerializer;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly INetworkManager _networkManager;
-
-        private readonly Func<IImageProcessor> _imageProcessorFactory;
-        private readonly Func<IDtoService> _dtoServiceFactory;
+        private readonly IImageProcessor _imageProcessor;
+        private readonly Lazy<IDtoService> _dtoServiceFactory;
         private readonly IServerApplicationHost _appHost;
         private readonly IFileSystem _fileSystem;
         private readonly ICryptoProvider _cryptoProvider;
@@ -74,13 +67,15 @@ namespace Emby.Server.Implementations.Library
         private IPasswordResetProvider[] _passwordResetProviders;
         private DefaultPasswordResetProvider _defaultPasswordResetProvider;
 
+        private IDtoService DtoService => _dtoServiceFactory.Value;
+
         public UserManager(
             ILogger<UserManager> logger,
             IUserRepository userRepository,
             IXmlSerializer xmlSerializer,
             INetworkManager networkManager,
-            Func<IImageProcessor> imageProcessorFactory,
-            Func<IDtoService> dtoServiceFactory,
+            IImageProcessor imageProcessor,
+            Lazy<IDtoService> dtoServiceFactory,
             IServerApplicationHost appHost,
             IJsonSerializer jsonSerializer,
             IFileSystem fileSystem,
@@ -90,7 +85,7 @@ namespace Emby.Server.Implementations.Library
             _userRepository = userRepository;
             _xmlSerializer = xmlSerializer;
             _networkManager = networkManager;
-            _imageProcessorFactory = imageProcessorFactory;
+            _imageProcessor = imageProcessor;
             _dtoServiceFactory = dtoServiceFactory;
             _appHost = appHost;
             _jsonSerializer = jsonSerializer;
@@ -264,6 +259,7 @@ namespace Emby.Server.Implementations.Library
         {
             if (string.IsNullOrWhiteSpace(username))
             {
+                _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint);
                 throw new ArgumentNullException(nameof(username));
             }
 
@@ -319,26 +315,26 @@ namespace Emby.Server.Implementations.Library
 
             if (user == null)
             {
+                _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", username, remoteEndPoint);
                 throw new AuthenticationException("Invalid username or password entered.");
             }
 
             if (user.Policy.IsDisabled)
             {
-                throw new AuthenticationException(
-                    string.Format(
-                        CultureInfo.InvariantCulture,
-                        "The {0} account is currently disabled. Please consult with your administrator.",
-                        user.Name));
+                _logger.LogInformation("Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", username, remoteEndPoint);
+                throw new SecurityException($"The {user.Name} account is currently disabled. Please consult with your administrator.");
             }
 
             if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint))
             {
-                throw new AuthenticationException("Forbidden.");
+                _logger.LogInformation("Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", username, remoteEndPoint);
+                throw new SecurityException("Forbidden.");
             }
 
             if (!user.IsParentalScheduleAllowed())
             {
-                throw new AuthenticationException("User is not allowed access at this time.");
+                _logger.LogInformation("Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", username, remoteEndPoint);
+                throw new SecurityException("User is not allowed access at this time.");
             }
 
             // Update LastActivityDate and LastLoginDate, then save
@@ -351,14 +347,14 @@ namespace Emby.Server.Implementations.Library
                 }
 
                 ResetInvalidLoginAttemptCount(user);
+                _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Name);
             }
             else
             {
                 IncrementInvalidLoginAttemptCount(user);
+                _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", user.Name, remoteEndPoint);
             }
 
-            _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied");
-
             return success ? user : null;
         }
 
@@ -600,7 +596,7 @@ namespace Emby.Server.Implementations.Library
 
                 try
                 {
-                    _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user);
+                    DtoService.AttachPrimaryImageAspectRatio(dto, user);
                 }
                 catch (Exception ex)
                 {
@@ -625,7 +621,7 @@ namespace Emby.Server.Implementations.Library
         {
             try
             {
-                return _imageProcessorFactory().GetImageCacheTag(item, image);
+                return _imageProcessor.GetImageCacheTag(item, image);
             }
             catch (Exception ex)
             {

+ 16 - 18
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -3,6 +3,7 @@
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Globalization;
 using System.IO;
 using System.Linq;
@@ -25,7 +26,6 @@ using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Diagnostics;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Events;
@@ -61,7 +61,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         private readonly ILibraryManager _libraryManager;
         private readonly IProviderManager _providerManager;
         private readonly IMediaEncoder _mediaEncoder;
-        private readonly IProcessFactory _processFactory;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IStreamHelper _streamHelper;
 
@@ -88,8 +87,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             ILibraryManager libraryManager,
             ILibraryMonitor libraryMonitor,
             IProviderManager providerManager,
-            IMediaEncoder mediaEncoder,
-            IProcessFactory processFactory)
+            IMediaEncoder mediaEncoder)
         {
             Current = this;
 
@@ -102,7 +100,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             _libraryMonitor = libraryMonitor;
             _providerManager = providerManager;
             _mediaEncoder = mediaEncoder;
-            _processFactory = processFactory;
             _liveTvManager = (LiveTvManager)liveTvManager;
             _jsonSerializer = jsonSerializer;
             _mediaSourceManager = mediaSourceManager;
@@ -1662,7 +1659,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
             if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
             {
-                return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _processFactory, _config);
+                return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config);
             }
 
             return new DirectRecorder(_logger, _httpClient, _streamHelper);
@@ -1683,16 +1680,19 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
             try
             {
-                var process = _processFactory.Create(new ProcessOptions
+                var process = new Process
                 {
-                    Arguments = GetPostProcessArguments(path, options.RecordingPostProcessorArguments),
-                    CreateNoWindow = true,
-                    EnableRaisingEvents = true,
-                    ErrorDialog = false,
-                    FileName = options.RecordingPostProcessor,
-                    IsHidden = true,
-                    UseShellExecute = false
-                });
+                    StartInfo = new ProcessStartInfo
+                    {
+                        Arguments = GetPostProcessArguments(path, options.RecordingPostProcessorArguments),
+                        CreateNoWindow = true,
+                        ErrorDialog = false,
+                        FileName = options.RecordingPostProcessor,
+                        WindowStyle = ProcessWindowStyle.Hidden,
+                        UseShellExecute = false
+                    },
+                    EnableRaisingEvents = true
+                };
 
                 _logger.LogInformation("Running recording post processor {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
 
@@ -1712,11 +1712,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
         private void Process_Exited(object sender, EventArgs e)
         {
-            using (var process = (IProcess)sender)
+            using (var process = (Process)sender)
             {
                 _logger.LogInformation("Recording post-processing script completed with exit code {ExitCode}", process.ExitCode);
-
-                process.Dispose();
             }
         }
 

+ 37 - 35
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -2,6 +2,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Globalization;
 using System.IO;
 using System.Text;
@@ -13,7 +14,6 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Diagnostics;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
@@ -29,8 +29,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         private bool _hasExited;
         private Stream _logFileStream;
         private string _targetPath;
-        private IProcess _process;
-        private readonly IProcessFactory _processFactory;
+        private Process _process;
         private readonly IJsonSerializer _json;
         private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
         private readonly IServerConfigurationManager _config;
@@ -40,14 +39,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             IMediaEncoder mediaEncoder,
             IServerApplicationPaths appPaths,
             IJsonSerializer json,
-            IProcessFactory processFactory,
             IServerConfigurationManager config)
         {
             _logger = logger;
             _mediaEncoder = mediaEncoder;
             _appPaths = appPaths;
             _json = json;
-            _processFactory = processFactory;
             _config = config;
         }
 
@@ -79,7 +76,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             _targetPath = targetFile;
             Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
 
-            var process = _processFactory.Create(new ProcessOptions
+            var processStartInfo = new ProcessStartInfo
             {
                 CreateNoWindow = true,
                 UseShellExecute = false,
@@ -90,14 +87,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 FileName = _mediaEncoder.EncoderPath,
                 Arguments = GetCommandLineArgs(mediaSource, inputFile, targetFile, duration),
 
-                IsHidden = true,
-                ErrorDialog = false,
-                EnableRaisingEvents = true
-            });
-
-            _process = process;
+                WindowStyle = ProcessWindowStyle.Hidden,
+                ErrorDialog = false
+            };
 
-            var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
+            var commandLineLogMessage = processStartInfo.FileName + " " + processStartInfo.Arguments;
             _logger.LogInformation(commandLineLogMessage);
 
             var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt");
@@ -109,16 +103,21 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
             _logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length);
 
-            process.Exited += (sender, args) => OnFfMpegProcessExited(process, inputFile);
+            _process = new Process
+            {
+                StartInfo = processStartInfo,
+                EnableRaisingEvents = true
+            };
+            _process.Exited += (sender, args) => OnFfMpegProcessExited(_process, inputFile);
 
-            process.Start();
+            _process.Start();
 
             cancellationToken.Register(Stop);
 
             onStarted();
 
             // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
-            StartStreamingLog(process.StandardError.BaseStream, _logFileStream);
+            StartStreamingLog(_process.StandardError.BaseStream, _logFileStream);
 
             _logger.LogInformation("ffmpeg recording process started for {0}", _targetPath);
 
@@ -292,30 +291,33 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         /// <summary>
         /// Processes the exited.
         /// </summary>
-        private void OnFfMpegProcessExited(IProcess process, string inputFile)
+        private void OnFfMpegProcessExited(Process process, string inputFile)
         {
-            _hasExited = true;
+            using (process)
+            {
+                _hasExited = true;
 
-            _logFileStream?.Dispose();
-            _logFileStream = null;
+                _logFileStream?.Dispose();
+                _logFileStream = null;
 
-            var exitCode = process.ExitCode;
+                var exitCode = process.ExitCode;
 
-            _logger.LogInformation("FFMpeg recording exited with code {ExitCode} for {Path}", exitCode, _targetPath);
+                _logger.LogInformation("FFMpeg recording exited with code {ExitCode} for {Path}", exitCode, _targetPath);
 
-            if (exitCode == 0)
-            {
-                _taskCompletionSource.TrySetResult(true);
-            }
-            else
-            {
-                _taskCompletionSource.TrySetException(
-                    new Exception(
-                        string.Format(
-                            CultureInfo.InvariantCulture,
-                            "Recording for {0} failed. Exit code {1}",
-                            _targetPath,
-                            exitCode)));
+                if (exitCode == 0)
+                {
+                    _taskCompletionSource.TrySetResult(true);
+                }
+                else
+                {
+                    _taskCompletionSource.TrySetException(
+                        new Exception(
+                            string.Format(
+                                CultureInfo.InvariantCulture,
+                                "Recording for {0} failed. Exit code {1}",
+                                _targetPath,
+                                exitCode)));
+                }
             }
         }
 

+ 12 - 14
Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs

@@ -22,9 +22,12 @@ namespace Emby.Server.Implementations.LiveTv
 {
     public class LiveTvDtoService
     {
+        private const string InternalVersionNumber = "4";
+
+        private const string ServiceName = "Emby";
+
         private readonly ILogger _logger;
         private readonly IImageProcessor _imageProcessor;
-
         private readonly IDtoService _dtoService;
         private readonly IApplicationHost _appHost;
         private readonly ILibraryManager _libraryManager;
@@ -32,13 +35,13 @@ namespace Emby.Server.Implementations.LiveTv
         public LiveTvDtoService(
             IDtoService dtoService,
             IImageProcessor imageProcessor,
-            ILoggerFactory loggerFactory,
+            ILogger<LiveTvDtoService> logger,
             IApplicationHost appHost,
             ILibraryManager libraryManager)
         {
             _dtoService = dtoService;
             _imageProcessor = imageProcessor;
-            _logger = loggerFactory.CreateLogger(nameof(LiveTvDtoService));
+            _logger = logger;
             _appHost = appHost;
             _libraryManager = libraryManager;
         }
@@ -161,7 +164,6 @@ namespace Emby.Server.Implementations.LiveTv
                 Limit = 1,
                 ImageTypes = new ImageType[] { ImageType.Thumb },
                 DtoOptions = new DtoOptions(false)
-
             }).FirstOrDefault();
 
             if (librarySeries != null)
@@ -179,6 +181,7 @@ namespace Emby.Server.Implementations.LiveTv
                         _logger.LogError(ex, "Error");
                     }
                 }
+
                 image = librarySeries.GetImageInfo(ImageType.Backdrop, 0);
                 if (image != null)
                 {
@@ -199,13 +202,12 @@ namespace Emby.Server.Implementations.LiveTv
 
             var program = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+                IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
                 ExternalSeriesId = programSeriesId,
                 Limit = 1,
                 ImageTypes = new ImageType[] { ImageType.Primary },
                 DtoOptions = new DtoOptions(false),
                 Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null
-
             }).FirstOrDefault();
 
             if (program != null)
@@ -232,9 +234,10 @@ namespace Emby.Server.Implementations.LiveTv
                         try
                         {
                             dto.ParentBackdropImageTags = new string[]
-                        {
+                            {
                                 _imageProcessor.GetImageCacheTag(program, image)
-                        };
+                            };
+
                             dto.ParentBackdropItemId = program.Id.ToString("N", CultureInfo.InvariantCulture);
                         }
                         catch (Exception ex)
@@ -255,7 +258,6 @@ namespace Emby.Server.Implementations.LiveTv
                 Limit = 1,
                 ImageTypes = new ImageType[] { ImageType.Thumb },
                 DtoOptions = new DtoOptions(false)
-
             }).FirstOrDefault();
 
             if (librarySeries != null)
@@ -273,6 +275,7 @@ namespace Emby.Server.Implementations.LiveTv
                         _logger.LogError(ex, "Error");
                     }
                 }
+
                 image = librarySeries.GetImageInfo(ImageType.Backdrop, 0);
                 if (image != null)
                 {
@@ -298,7 +301,6 @@ namespace Emby.Server.Implementations.LiveTv
                 Limit = 1,
                 ImageTypes = new ImageType[] { ImageType.Primary },
                 DtoOptions = new DtoOptions(false)
-
             }).FirstOrDefault();
 
             if (program == null)
@@ -311,7 +313,6 @@ namespace Emby.Server.Implementations.LiveTv
                     ImageTypes = new ImageType[] { ImageType.Primary },
                     DtoOptions = new DtoOptions(false),
                     Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null
-
                 }).FirstOrDefault();
             }
 
@@ -396,8 +397,6 @@ namespace Emby.Server.Implementations.LiveTv
             return null;
         }
 
-        private const string InternalVersionNumber = "4";
-
         public Guid GetInternalChannelId(string serviceName, string externalId)
         {
             var name = serviceName + externalId + InternalVersionNumber;
@@ -405,7 +404,6 @@ namespace Emby.Server.Implementations.LiveTv
             return _libraryManager.GetNewItemId(name.ToLowerInvariant(), typeof(LiveTvChannel));
         }
 
-        private const string ServiceName = "Emby";
         public string GetInternalTimerId(string externalId)
         {
             var name = ServiceName + externalId + InternalVersionNumber;

+ 28 - 19
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -41,33 +41,32 @@ namespace Emby.Server.Implementations.LiveTv
     /// </summary>
     public class LiveTvManager : ILiveTvManager, IDisposable
     {
+        private const string ExternalServiceTag = "ExternalServiceId";
+
+        private const string EtagKey = "ProgramEtag";
+
         private readonly IServerConfigurationManager _config;
         private readonly ILogger _logger;
         private readonly IItemRepository _itemRepo;
         private readonly IUserManager _userManager;
+        private readonly IDtoService _dtoService;
         private readonly IUserDataManager _userDataManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ITaskManager _taskManager;
-        private readonly IJsonSerializer _jsonSerializer;
-        private readonly Func<IChannelManager> _channelManager;
-
-        private readonly IDtoService _dtoService;
         private readonly ILocalizationManager _localization;
-
+        private readonly IJsonSerializer _jsonSerializer;
+        private readonly IFileSystem _fileSystem;
+        private readonly IChannelManager _channelManager;
         private readonly LiveTvDtoService _tvDtoService;
 
         private ILiveTvService[] _services = Array.Empty<ILiveTvService>();
-
         private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>();
         private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>();
-        private readonly IFileSystem _fileSystem;
 
         public LiveTvManager(
-            IServerApplicationHost appHost,
             IServerConfigurationManager config,
-            ILoggerFactory loggerFactory,
+            ILogger<LiveTvManager> logger,
             IItemRepository itemRepo,
-            IImageProcessor imageProcessor,
             IUserDataManager userDataManager,
             IDtoService dtoService,
             IUserManager userManager,
@@ -76,10 +75,11 @@ namespace Emby.Server.Implementations.LiveTv
             ILocalizationManager localization,
             IJsonSerializer jsonSerializer,
             IFileSystem fileSystem,
-            Func<IChannelManager> channelManager)
+            IChannelManager channelManager,
+            LiveTvDtoService liveTvDtoService)
         {
             _config = config;
-            _logger = loggerFactory.CreateLogger(nameof(LiveTvManager));
+            _logger = logger;
             _itemRepo = itemRepo;
             _userManager = userManager;
             _libraryManager = libraryManager;
@@ -90,8 +90,7 @@ namespace Emby.Server.Implementations.LiveTv
             _dtoService = dtoService;
             _userDataManager = userDataManager;
             _channelManager = channelManager;
-
-            _tvDtoService = new LiveTvDtoService(dtoService, imageProcessor, loggerFactory, appHost, _libraryManager);
+            _tvDtoService = liveTvDtoService;
         }
 
         public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
@@ -178,7 +177,6 @@ namespace Emby.Server.Implementations.LiveTv
             {
                 Name = i.Name,
                 Id = i.Type
-
             }).ToList();
         }
 
@@ -261,6 +259,7 @@ namespace Emby.Server.Implementations.LiveTv
                 var endTime = DateTime.UtcNow;
                 _logger.LogInformation("Live stream opened after {0}ms", (endTime - startTime).TotalMilliseconds);
             }
+
             info.RequiresClosing = true;
 
             var idPrefix = service.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_";
@@ -362,30 +361,37 @@ namespace Emby.Server.Implementations.LiveTv
                 {
                     stream.BitRate = null;
                 }
+
                 if (stream.Channels.HasValue && stream.Channels <= 0)
                 {
                     stream.Channels = null;
                 }
+
                 if (stream.AverageFrameRate.HasValue && stream.AverageFrameRate <= 0)
                 {
                     stream.AverageFrameRate = null;
                 }
+
                 if (stream.RealFrameRate.HasValue && stream.RealFrameRate <= 0)
                 {
                     stream.RealFrameRate = null;
                 }
+
                 if (stream.Width.HasValue && stream.Width <= 0)
                 {
                     stream.Width = null;
                 }
+
                 if (stream.Height.HasValue && stream.Height <= 0)
                 {
                     stream.Height = null;
                 }
+
                 if (stream.SampleRate.HasValue && stream.SampleRate <= 0)
                 {
                     stream.SampleRate = null;
                 }
+
                 if (stream.Level.HasValue && stream.Level <= 0)
                 {
                     stream.Level = null;
@@ -427,7 +433,6 @@ namespace Emby.Server.Implementations.LiveTv
             }
         }
 
-        private const string ExternalServiceTag = "ExternalServiceId";
         private LiveTvChannel GetChannel(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken)
         {
             var parentFolderId = parentFolder.Id;
@@ -456,6 +461,7 @@ namespace Emby.Server.Implementations.LiveTv
                 {
                     isNew = true;
                 }
+
                 item.Tags = channelInfo.Tags;
             }
 
@@ -463,6 +469,7 @@ namespace Emby.Server.Implementations.LiveTv
             {
                 isNew = true;
             }
+
             item.ParentId = parentFolderId;
 
             item.ChannelType = channelInfo.ChannelType;
@@ -472,24 +479,28 @@ namespace Emby.Server.Implementations.LiveTv
             {
                 forceUpdate = true;
             }
+
             item.SetProviderId(ExternalServiceTag, serviceName);
 
             if (!string.Equals(channelInfo.Id, item.ExternalId, StringComparison.Ordinal))
             {
                 forceUpdate = true;
             }
+
             item.ExternalId = channelInfo.Id;
 
             if (!string.Equals(channelInfo.Number, item.Number, StringComparison.Ordinal))
             {
                 forceUpdate = true;
             }
+
             item.Number = channelInfo.Number;
 
             if (!string.Equals(channelInfo.Name, item.Name, StringComparison.Ordinal))
             {
                 forceUpdate = true;
             }
+
             item.Name = channelInfo.Name;
 
             if (!item.HasImage(ImageType.Primary))
@@ -518,8 +529,6 @@ namespace Emby.Server.Implementations.LiveTv
             return item;
         }
 
-        private const string EtagKey = "ProgramEtag";
-
         private Tuple<LiveTvProgram, bool, bool> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken)
         {
             var id = _tvDtoService.GetInternalProgramId(info.Id);
@@ -2482,7 +2491,7 @@ namespace Emby.Server.Implementations.LiveTv
                 .OrderBy(i => i.SortName)
                 .ToList();
 
-            folders.AddRange(_channelManager().GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery
+            folders.AddRange(_channelManager.GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery
             {
                 UserId = user.Id,
                 IsRecordingsFolder = true,

+ 13 - 1
Emby.Server.Implementations/Localization/Core/ar.json

@@ -102,5 +102,17 @@
     "TaskRefreshLibrary": "افحص مكتبة الوسائط",
     "TaskRefreshChapterImagesDescription": "إنشاء صور مصغرة لمقاطع الفيديو ذات فصول.",
     "TaskRefreshChapterImages": "استخراج صور الفصل",
-    "TasksApplicationCategory": "تطبيق"
+    "TasksApplicationCategory": "تطبيق",
+    "TaskDownloadMissingSubtitlesDescription": "ابحث في الإنترنت على الترجمات المفقودة إستنادا على الميتاداتا.",
+    "TaskDownloadMissingSubtitles": "تحميل الترجمات المفقودة",
+    "TaskRefreshChannelsDescription": "تحديث معلومات قنوات الإنترنت.",
+    "TaskRefreshChannels": "إعادة تحديث القنوات",
+    "TaskCleanTranscodeDescription": "حذف ملفات الترميز الأقدم من يوم واحد.",
+    "TaskCleanTranscode": "حذف سجلات الترميز",
+    "TaskUpdatePluginsDescription": "تحميل وتثبيت الإضافات التي تم تفعيل التحديث التلقائي لها.",
+    "TaskUpdatePlugins": "تحديث الإضافات",
+    "TaskRefreshPeopleDescription": "تحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.",
+    "TaskRefreshPeople": "إعادة تحميل الأشخاص",
+    "TaskCleanLogsDescription": "حذف السجلات الأقدم من {0} يوم.",
+    "TaskCleanLogs": "حذف دليل السجل"
 }

+ 5 - 5
Emby.Server.Implementations/Localization/Core/ca.json

@@ -3,19 +3,19 @@
     "AppDeviceValues": "Aplicació: {0}, Dispositiu: {1}",
     "Application": "Aplicació",
     "Artists": "Artistes",
-    "AuthenticationSucceededWithUserName": "{0} s'ha autentificat correctament",
+    "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
     "Books": "Llibres",
-    "CameraImageUploadedFrom": "Una nova imatge de la càmera ha sigut pujada des de {0}",
+    "CameraImageUploadedFrom": "Una nova imatge de la càmera ha estat pujada des de {0}",
     "Channels": "Canals",
-    "ChapterNameValue": "Episodi {0}",
+    "ChapterNameValue": "Capítol {0}",
     "Collections": "Col·leccions",
     "DeviceOfflineWithName": "{0} s'ha desconnectat",
     "DeviceOnlineWithName": "{0} està connectat",
     "FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}",
     "Favorites": "Preferits",
-    "Folders": "Directoris",
+    "Folders": "Carpetes",
     "Genres": "Gèneres",
-    "HeaderAlbumArtists": "Artistes dels Àlbums",
+    "HeaderAlbumArtists": "Artistes del Àlbum",
     "HeaderCameraUploads": "Pujades de Càmera",
     "HeaderContinueWatching": "Continua Veient",
     "HeaderFavoriteAlbums": "Àlbums Preferits",

+ 26 - 4
Emby.Server.Implementations/Localization/Core/da.json

@@ -1,5 +1,5 @@
 {
-    "Albums": "Album",
+    "Albums": "Albums",
     "AppDeviceValues": "App: {0}, Enhed: {1}",
     "Application": "Applikation",
     "Artists": "Kunstnere",
@@ -35,8 +35,8 @@
     "Latest": "Seneste",
     "MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
     "MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfigurationsafsnit {0} er blevet opdateret",
-    "MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret",
+    "MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret",
     "MixedContent": "Blandet indhold",
     "Movies": "Film",
     "Music": "Musik",
@@ -92,5 +92,27 @@
     "UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
     "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
     "ValueSpecialEpisodeName": "Special - {0}",
-    "VersionNumber": "Version {0}"
+    "VersionNumber": "Version {0}",
+    "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfiguration.",
+    "TaskDownloadMissingSubtitles": "Download manglende undertekster",
+    "TaskUpdatePluginsDescription": "Downloader og installere opdateringer for plugins som er konfigureret til at opdatere automatisk.",
+    "TaskUpdatePlugins": "Opdater Plugins",
+    "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gammle.",
+    "TaskCleanLogs": "Ryd Log Mappe",
+    "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdaterer metadata.",
+    "TaskRefreshLibrary": "Scan Medie Bibliotek",
+    "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke har brug for længere.",
+    "TaskCleanCache": "Ryd Cache Mappe",
+    "TasksChannelsCategory": "Internet Kanaler",
+    "TasksApplicationCategory": "Applikation",
+    "TasksLibraryCategory": "Bibliotek",
+    "TasksMaintenanceCategory": "Vedligeholdelse",
+    "TaskRefreshChapterImages": "Udtræk Kapitel billeder",
+    "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
+    "TaskRefreshChannelsDescription": "Genopfrisker internet kanal information.",
+    "TaskRefreshChannels": "Genopfrisk Kanaler",
+    "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end en dag gammel.",
+    "TaskCleanTranscode": "Rengør Transcode Mappen",
+    "TaskRefreshPeople": "Genopfrisk Personer",
+    "TaskRefreshPeopleDescription": "Opdatere metadata for skuespillere og instruktører i dit bibliotek."
 }

+ 3 - 3
Emby.Server.Implementations/Localization/Core/de.json

@@ -3,7 +3,7 @@
     "AppDeviceValues": "App: {0}, Gerät: {1}",
     "Application": "Anwendung",
     "Artists": "Interpreten",
-    "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifziert",
+    "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert",
     "Books": "Bücher",
     "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
     "Channels": "Kanäle",
@@ -99,11 +99,11 @@
     "TaskRefreshChannels": "Erneuere Kanäle",
     "TaskCleanTranscodeDescription": "Löscht Transkodierdateien welche älter als ein Tag sind.",
     "TaskCleanTranscode": "Lösche Transkodier Pfad",
-    "TaskUpdatePluginsDescription": "Läd Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
+    "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
     "TaskUpdatePlugins": "Update Plugins",
     "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.",
     "TaskRefreshPeople": "Erneuere Schausteller",
-    "TaskCleanLogsDescription": "Lösche Log Datein die älter als {0} Tage sind.",
+    "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
     "TaskCleanLogs": "Lösche Log Pfad",
     "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
     "TaskRefreshLibrary": "Scanne alle Bibliotheken",

+ 23 - 1
Emby.Server.Implementations/Localization/Core/en-GB.json

@@ -92,5 +92,27 @@
     "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
     "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
     "ValueSpecialEpisodeName": "Special - {0}",
-    "VersionNumber": "Version {0}"
+    "VersionNumber": "Version {0}",
+    "TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration.",
+    "TaskDownloadMissingSubtitles": "Download missing subtitles",
+    "TaskRefreshChannelsDescription": "Refreshes internet channel information.",
+    "TaskRefreshChannels": "Refresh Channels",
+    "TaskCleanTranscodeDescription": "Deletes transcode files more than one day old.",
+    "TaskCleanTranscode": "Clean Transcode Directory",
+    "TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.",
+    "TaskUpdatePlugins": "Update Plugins",
+    "TaskRefreshPeopleDescription": "Updates metadata for actors and directors in your media library.",
+    "TaskRefreshPeople": "Refresh People",
+    "TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.",
+    "TaskCleanLogs": "Clean Log Directory",
+    "TaskRefreshLibraryDescription": "Scans your media library for new files and refreshes metadata.",
+    "TaskRefreshLibrary": "Scan Media Library",
+    "TaskRefreshChapterImagesDescription": "Creates thumbnails for videos that have chapters.",
+    "TaskRefreshChapterImages": "Extract Chapter Images",
+    "TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
+    "TaskCleanCache": "Clean Cache Directory",
+    "TasksChannelsCategory": "Internet Channels",
+    "TasksApplicationCategory": "Application",
+    "TasksLibraryCategory": "Library",
+    "TasksMaintenanceCategory": "Maintenance"
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/es-AR.json

@@ -17,7 +17,7 @@
     "Genres": "Géneros",
     "HeaderAlbumArtists": "Artistas de álbum",
     "HeaderCameraUploads": "Subidas de cámara",
-    "HeaderContinueWatching": "Continuar viendo",
+    "HeaderContinueWatching": "Seguir viendo",
     "HeaderFavoriteAlbums": "Álbumes favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",
     "HeaderFavoriteEpisodes": "Episodios favoritos",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/es_DO.json

@@ -5,7 +5,7 @@
     "Collections": "Colecciones",
     "Artists": "Artistas",
     "DeviceOnlineWithName": "{0} está conectado",
-    "DeviceOfflineWithName": "{0} ha desconectado",
+    "DeviceOfflineWithName": "{0} se ha desconectado",
     "ChapterNameValue": "Capítulo {0}",
     "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
     "AuthenticationSucceededWithUserName": "{0} autenticado con éxito",

+ 24 - 2
Emby.Server.Implementations/Localization/Core/fa.json

@@ -23,7 +23,7 @@
     "HeaderFavoriteEpisodes": "قسمت‌های مورد علاقه",
     "HeaderFavoriteShows": "سریال‌های مورد علاقه",
     "HeaderFavoriteSongs": "آهنگ‌های مورد علاقه",
-    "HeaderLiveTV": "پخش زنده تلویزیون",
+    "HeaderLiveTV": "پخش زنده",
     "HeaderNextUp": "قسمت بعدی",
     "HeaderRecordingGroups": "گروه‌های ضبط",
     "HomeVideos": "ویدیوهای خانگی",
@@ -92,5 +92,27 @@
     "UserStoppedPlayingItemWithValues": "{0} پخش {1} را بر روی {2} به پایان رساند",
     "ValueHasBeenAddedToLibrary": "{0} به کتابخانه‌ی رسانه‌ی شما افزوده شد",
     "ValueSpecialEpisodeName": "ویژه - {0}",
-    "VersionNumber": "نسخه {0}"
+    "VersionNumber": "نسخه {0}",
+    "TaskCleanTranscodeDescription": "فایل‌های کدگذاری که قدیمی‌تر از یک روز هستند را حذف می‌کند.",
+    "TaskCleanTranscode": "پاکسازی مسیر کد گذاری",
+    "TaskUpdatePluginsDescription": "دانلود و نصب به روز رسانی افزونه‌هایی که برای به روز رسانی خودکار پیکربندی شده‌اند.",
+    "TaskDownloadMissingSubtitlesDescription": "جستجوی زیرنویس‌های ناموجود در اینترنت بر اساس پیکربندی ابرداده‌ها.",
+    "TaskDownloadMissingSubtitles": "دانلود زیرنویس‌های ناموجود",
+    "TaskRefreshChannelsDescription": "اطلاعات کانال اینترنتی را تازه سازی می‌کند.",
+    "TaskRefreshChannels": "تازه سازی کانال‌ها",
+    "TaskUpdatePlugins": "به روز رسانی افزونه‌ها",
+    "TaskRefreshPeopleDescription": "ابرداده‌ها برای بازیگران و کارگردانان در کتابخانه رسانه شما به روزرسانی می شوند.",
+    "TaskRefreshPeople": "تازه سازی افراد",
+    "TaskCleanLogsDescription": "واقعه نگارهایی را که قدیمی تر {0} روز هستند را حذف می کند.",
+    "TaskCleanLogs": "پاکسازی مسیر واقعه نگار",
+    "TaskRefreshLibraryDescription": "کتابخانه رسانه شما را اسکن می‌کند و ابرداده‌ها را تازه سازی می‌کند.",
+    "TaskRefreshLibrary": "اسکن کتابخانه رسانه",
+    "TaskRefreshChapterImagesDescription": "عکس‌های کوچک برای ویدیوهایی که سکانس دارند ایجاد می‌کند.",
+    "TaskRefreshChapterImages": "استخراج عکس‌های سکانس",
+    "TaskCleanCacheDescription": "فایل‌های حافظه موقت که توسط سیستم دیگر مورد نیاز نیستند حذف می‌شوند.",
+    "TaskCleanCache": "پاکسازی مسیر حافظه موقت",
+    "TasksChannelsCategory": "کانال‌های داخلی",
+    "TasksApplicationCategory": "برنامه",
+    "TasksLibraryCategory": "کتابخانه",
+    "TasksMaintenanceCategory": "تعمیر"
 }

+ 40 - 18
Emby.Server.Implementations/Localization/Core/fi.json

@@ -1,5 +1,5 @@
 {
-    "HeaderLiveTV": "TV-lähetykset",
+    "HeaderLiveTV": "Suorat lähetykset",
     "NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.",
     "NameSeasonUnknown": "Tuntematon Kausi",
     "NameSeasonNumber": "Kausi {0}",
@@ -19,12 +19,12 @@
     "ItemAddedWithName": "{0} lisättiin kirjastoon",
     "Inherit": "Periytyä",
     "HomeVideos": "Kotivideot",
-    "HeaderRecordingGroups": "Nauhoitusryhmät",
+    "HeaderRecordingGroups": "Nauhoiteryhmät",
     "HeaderNextUp": "Seuraavaksi",
     "HeaderFavoriteSongs": "Lempikappaleet",
     "HeaderFavoriteShows": "Lempisarjat",
     "HeaderFavoriteEpisodes": "Lempijaksot",
-    "HeaderCameraUploads": "Kameralataukset",
+    "HeaderCameraUploads": "Kamerasta Lähetetyt",
     "HeaderFavoriteArtists": "Lempiartistit",
     "HeaderFavoriteAlbums": "Lempialbumit",
     "HeaderContinueWatching": "Jatka katsomista",
@@ -63,10 +63,10 @@
     "UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}",
     "UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}",
     "UserOfflineFromDevice": "{0} yhteys katkaistu {1}",
-    "UserLockedOutWithName": "Käyttäjä {0} kirjautui ulos",
-    "UserDownloadingItemWithValues": "{0} latautumassa {1}",
-    "UserDeletedWithName": "Poistettiin käyttäjä {0}",
-    "UserCreatedWithName": "Luotiin käyttäjä {0}",
+    "UserLockedOutWithName": "Käyttäjä {0} lukittu",
+    "UserDownloadingItemWithValues": "{0} lataa {1}",
+    "UserDeletedWithName": "Käyttäjä {0} poistettu",
+    "UserCreatedWithName": "Käyttäjä {0} luotu",
     "TvShows": "TV-Ohjelmat",
     "Sync": "Synkronoi",
     "SubtitleDownloadFailureFromForItem": "Tekstityksen lataaminen epäonnistui {0} - {1}",
@@ -74,22 +74,44 @@
     "Songs": "Kappaleet",
     "Shows": "Ohjelmat",
     "ServerNameNeedsToBeRestarted": "{0} vaatii uudelleenkäynnistyksen",
-    "ProviderValue": "Palveluntarjoaja: {0}",
+    "ProviderValue": "Tarjoaja: {0}",
     "Plugin": "Liitännäinen",
-    "NotificationOptionVideoPlaybackStopped": "Videon toistaminen pysäytetty",
-    "NotificationOptionVideoPlayback": "Videon toistaminen aloitettu",
-    "NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos",
-    "NotificationOptionTaskFailed": "Ajastetun tehtävän ongelma",
+    "NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty",
+    "NotificationOptionVideoPlayback": "Videon toisto aloitettu",
+    "NotificationOptionUserLockedOut": "Käyttäjä lukittu",
+    "NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui",
     "NotificationOptionServerRestartRequired": "Palvelimen uudelleenkäynnistys vaaditaan",
-    "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty",
+    "NotificationOptionPluginUpdateInstalled": "Lisäosan päivitys asennettu",
     "NotificationOptionPluginUninstalled": "Liitännäinen poistettu",
     "NotificationOptionPluginInstalled": "Liitännäinen asennettu",
     "NotificationOptionPluginError": "Ongelma liitännäisessä",
     "NotificationOptionNewLibraryContent": "Uutta sisältöä lisätty",
     "NotificationOptionInstallationFailed": "Asennus epäonnistui",
-    "NotificationOptionCameraImageUploaded": "Kuva ladattu kamerasta",
-    "NotificationOptionAudioPlaybackStopped": "Audion toisto pysäytetty",
-    "NotificationOptionAudioPlayback": "Audion toisto aloitettu",
-    "NotificationOptionApplicationUpdateInstalled": "Ohjelmistopäivitys asennettu",
-    "NotificationOptionApplicationUpdateAvailable": "Ohjelmistopäivitys saatavilla"
+    "NotificationOptionCameraImageUploaded": "Kameran kuva ladattu",
+    "NotificationOptionAudioPlaybackStopped": "Äänen toisto lopetettu",
+    "NotificationOptionAudioPlayback": "Toistetaan ääntä",
+    "NotificationOptionApplicationUpdateInstalled": "Uusi sovellusversio asennettu",
+    "NotificationOptionApplicationUpdateAvailable": "Sovelluksesta on uusi versio saatavilla",
+    "TasksMaintenanceCategory": "Ylläpito",
+    "TaskDownloadMissingSubtitlesDescription": "Etsii puuttuvia tekstityksiä videon metadatatietojen pohjalta.",
+    "TaskDownloadMissingSubtitles": "Lataa puuttuvat tekstitykset",
+    "TaskRefreshChannelsDescription": "Päivittää internet-kanavien tiedot.",
+    "TaskRefreshChannels": "Päivitä kanavat",
+    "TaskCleanTranscodeDescription": "Poistaa transkoodatut tiedostot jotka ovat yli päivän vanhoja.",
+    "TaskCleanTranscode": "Puhdista transkoodaushakemisto",
+    "TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset liitännäisille jotka on asetettu päivittymään automaattisesti.",
+    "TaskUpdatePlugins": "Päivitä liitännäiset",
+    "TaskRefreshPeopleDescription": "Päivittää näyttelijöiden ja ohjaajien mediatiedot kirjastossasi.",
+    "TaskRefreshPeople": "Päivitä henkilöt",
+    "TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.",
+    "TaskCleanLogs": "Puhdista lokihakemisto",
+    "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uusien tiedostojen varalle, sekä virkistää metatiedot.",
+    "TaskRefreshLibrary": "Skannaa mediakirjasto",
+    "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on lukuja.",
+    "TaskRefreshChapterImages": "Eristä lukujen kuvat",
+    "TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.",
+    "TaskCleanCache": "Tyhjennä välimuisti-hakemisto",
+    "TasksChannelsCategory": "Internet kanavat",
+    "TasksApplicationCategory": "Sovellus",
+    "TasksLibraryCategory": "Kirjasto"
 }

+ 9 - 1
Emby.Server.Implementations/Localization/Core/fil.json

@@ -90,5 +90,13 @@
     "Artists": "Artista",
     "Application": "Aplikasyon",
     "AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}",
-    "Albums": "Albums"
+    "Albums": "Albums",
+    "TaskRefreshLibrary": "Suriin ang nasa librerya",
+    "TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata",
+    "TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata",
+    "TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng systema.",
+    "TasksChannelsCategory": "Palabas sa internet",
+    "TasksLibraryCategory": "Librerya",
+    "TasksMaintenanceCategory": "Pagpapanatili",
+    "HomeVideos": "Sariling pelikula"
 }

+ 14 - 14
Emby.Server.Implementations/Localization/Core/fr.json

@@ -5,17 +5,17 @@
     "Artists": "Artistes",
     "AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
     "Books": "Livres",
-    "CameraImageUploadedFrom": "Une nouvelle photo a été chargée depuis {0}",
+    "CameraImageUploadedFrom": "Une nouvelle photographie a été chargée depuis {0}",
     "Channels": "Chaînes",
     "ChapterNameValue": "Chapitre {0}",
     "Collections": "Collections",
     "DeviceOfflineWithName": "{0} s'est déconnecté",
     "DeviceOnlineWithName": "{0} est connecté",
-    "FailedLoginAttemptWithUserName": "Échec de connexion de {0}",
+    "FailedLoginAttemptWithUserName": "Échec de connexion depuis {0}",
     "Favorites": "Favoris",
     "Folders": "Dossiers",
     "Genres": "Genres",
-    "HeaderAlbumArtists": "Artistes d'album",
+    "HeaderAlbumArtists": "Artistes de l'album",
     "HeaderCameraUploads": "Photos transférées",
     "HeaderContinueWatching": "Continuer à regarder",
     "HeaderFavoriteAlbums": "Albums favoris",
@@ -69,7 +69,7 @@
     "PluginUpdatedWithName": "{0} a été mis à jour",
     "ProviderValue": "Fournisseur : {0}",
     "ScheduledTaskFailedWithName": "{0} a échoué",
-    "ScheduledTaskStartedWithName": "{0} a commencé",
+    "ScheduledTaskStartedWithName": "{0} a démarré",
     "ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
     "Shows": "Émissions",
     "Songs": "Chansons",
@@ -95,21 +95,21 @@
     "VersionNumber": "Version {0}",
     "TasksChannelsCategory": "Chaines en ligne",
     "TaskDownloadMissingSubtitlesDescription": "Cherche les sous-titres manquant sur internet en se basant sur la configuration des métadonnées.",
-    "TaskDownloadMissingSubtitles": "Télécharge les sous-titres manquant",
+    "TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquant",
     "TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines en ligne.",
-    "TaskRefreshChannels": "Rafraîchit les chaines",
+    "TaskRefreshChannels": "Rafraîchir les chaines",
     "TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.",
-    "TaskCleanTranscode": "Nettoie les dossier des transcodages",
-    "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des plugins configurés pour être mis à jour automatiquement.",
-    "TaskUpdatePlugins": "Mettre à jour les plugins",
-    "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et directeurs dans votre bibliothèque.",
-    "TaskRefreshPeople": "Rafraîchit les acteurs",
+    "TaskCleanTranscode": "Nettoyer les dossier des transcodages",
+    "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurés pour être mises à jour automatiquement.",
+    "TaskUpdatePlugins": "Mettre à jour les extensions",
+    "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.",
+    "TaskRefreshPeople": "Rafraîchir les acteurs",
     "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
-    "TaskCleanLogs": "Nettoie le répertoire des journaux",
+    "TaskCleanLogs": "Nettoyer le répertoire des journaux",
     "TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
-    "TaskRefreshLibrary": "Scanne toute les Bibliothèques",
+    "TaskRefreshLibrary": "Scanner toute les Bibliothèques",
     "TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.",
-    "TaskRefreshChapterImages": "Extrait les images de chapitre",
+    "TaskRefreshChapterImages": "Extraire les images de chapitre",
     "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
     "TaskCleanCache": "Vider le répertoire cache",
     "TasksApplicationCategory": "Application",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/hu.json

@@ -71,7 +71,7 @@
     "ScheduledTaskFailedWithName": "{0} sikertelen",
     "ScheduledTaskStartedWithName": "{0} elkezdve",
     "ServerNameNeedsToBeRestarted": "{0}-t újra kell indítani",
-    "Shows": "Műsorok",
+    "Shows": "Sorozatok",
     "Songs": "Dalok",
     "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek, próbáld újra hamarosan.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",

+ 8 - 4
Emby.Server.Implementations/Localization/Core/ja.json

@@ -104,10 +104,14 @@
     "TasksMaintenanceCategory": "メンテナンス",
     "TaskRefreshChannelsDescription": "ネットチャンネルの情報をリフレッシュします。",
     "TaskRefreshChannels": "チャンネルのリフレッシュ",
-    "TaskCleanTranscodeDescription": "一日以上前のトランスコードを消去します。",
-    "TaskCleanTranscode": "トランスコード用のディレクトリの掃除",
+    "TaskCleanTranscodeDescription": "1日以上経過したトランスコードファイルを削除します。",
+    "TaskCleanTranscode": "トランスコードディレクトリの削除",
     "TaskUpdatePluginsDescription": "自動更新可能なプラグインのアップデートをダウンロードしてインストールします。",
     "TaskUpdatePlugins": "プラグインの更新",
-    "TaskRefreshPeopleDescription": "メディアライブラリで俳優や監督のメタデータをリフレッシュします。",
-    "TaskRefreshPeople": "俳優や監督のデータのリフレッシュ"
+    "TaskRefreshPeopleDescription": "メディアライブラリで俳優や監督のメタデータを更新します。",
+    "TaskRefreshPeople": "俳優や監督のデータの更新",
+    "TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。",
+    "TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
+    "TaskRefreshChapterImages": "チャプター画像を抽出する",
+    "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする"
 }

+ 61 - 0
Emby.Server.Implementations/Localization/Core/mr.json

@@ -0,0 +1,61 @@
+{
+    "Books": "पुस्तकं",
+    "Artists": "संगीतकार",
+    "Albums": "अल्बम",
+    "Playlists": "प्लेलिस्ट",
+    "HeaderAlbumArtists": "अल्बम संगीतकार",
+    "Folders": "फोल्डर",
+    "HeaderFavoriteEpisodes": "आवडते भाग",
+    "HeaderFavoriteSongs": "आवडती गाणी",
+    "Movies": "चित्रपट",
+    "HeaderFavoriteArtists": "आवडते संगीतकार",
+    "Shows": "कार्यक्रम",
+    "HeaderFavoriteAlbums": "आवडते अल्बम",
+    "Channels": "वाहिन्या",
+    "ValueSpecialEpisodeName": "विशेष - {0}",
+    "HeaderFavoriteShows": "आवडते कार्यक्रम",
+    "Favorites": "आवडीचे",
+    "HeaderNextUp": "यानंतर",
+    "Songs": "गाणी",
+    "HeaderLiveTV": "लाइव्ह टीव्ही",
+    "Genres": "जाँनरे",
+    "Photos": "चित्र",
+    "TaskDownloadMissingSubtitles": "नसलेले सबटायटल डाउनलोड करा",
+    "TaskCleanTranscodeDescription": "एक दिवसापेक्षा जुन्या ट्रान्सकोड फायली काढून टाका.",
+    "TaskCleanTranscode": "ट्रान्सकोड डिरेक्टरी साफ करून टाका",
+    "TaskUpdatePlugins": "प्लगइन अपडेट करा",
+    "TaskCleanLogs": "लॉग डिरेक्टरी साफ करून टाका",
+    "TaskCleanCache": "कॅश डिरेक्टरी साफ करून टाका",
+    "TasksChannelsCategory": "इंटरनेट वाहिन्या",
+    "TasksApplicationCategory": "अ‍ॅप्लिकेशन",
+    "TasksLibraryCategory": "संग्रहालय",
+    "VersionNumber": "आवृत्ती {0}",
+    "UserPasswordChangedWithName": "{0} या प्रयोक्त्याचे पासवर्ड बदलण्यात आले आहे",
+    "UserOnlineFromDevice": "{0} हे {1} येथून ऑनलाइन आहेत",
+    "UserDeletedWithName": "प्रयोक्ता {0} काढून टाकण्यात आले आहे",
+    "UserCreatedWithName": "प्रयोक्ता {0} बनवण्यात आले आहे",
+    "User": "प्रयोक्ता",
+    "TvShows": "टीव्ही कार्यक्रम",
+    "StartupEmbyServerIsLoading": "जेलिफिन सर्व्हर लोड होत आहे. कृपया थोड्या वेळात पुन्हा प्रयत्न करा.",
+    "Plugin": "प्लगइन",
+    "NotificationOptionCameraImageUploaded": "कॅमेरा चित्र अपलोड केले आहे",
+    "NotificationOptionApplicationUpdateInstalled": "अ‍ॅप्लिकेशन अपडेट इन्स्टॉल केले आहे",
+    "NotificationOptionApplicationUpdateAvailable": "अ‍ॅप्लिकेशन अपडेट उपलब्ध आहे",
+    "NewVersionIsAvailable": "जेलिफिन सर्व्हरची एक नवीन आवृत्ती डाउनलोड करण्यास उपलब्ध आहे.",
+    "NameSeasonUnknown": "अज्ञात सीझन",
+    "NameSeasonNumber": "सीझन {0}",
+    "MusicVideos": "संगीत व्हिडीयो",
+    "Music": "संगीत",
+    "MessageApplicationUpdatedTo": "जेलिफिन सर्व्हर अपडेट होऊन {0} आवृत्तीवर पोहोचला आहे",
+    "MessageApplicationUpdated": "जेलिफिन सर्व्हर अपडेट केला गेला आहे",
+    "Latest": "नवीनतम",
+    "LabelIpAddressValue": "आयपी पत्ता: {0}",
+    "ItemRemovedWithName": "{0} हे संग्रहालयातून काढून टाकण्यात आले",
+    "ItemAddedWithName": "{0} हे संग्रहालयात जोडले गेले",
+    "HomeVideos": "घरचे व्हिडीयो",
+    "HeaderRecordingGroups": "रेकॉर्डिंग गट",
+    "HeaderCameraUploads": "कॅमेरा अपलोड",
+    "CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
+    "Application": "अ‍ॅप्लिकेशन",
+    "AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}"
+}

+ 26 - 4
Emby.Server.Implementations/Localization/Core/nl.json

@@ -1,11 +1,11 @@
 {
     "Albums": "Albums",
     "AppDeviceValues": "App: {0}, Apparaat: {1}",
-    "Application": "Applicatie",
+    "Application": "Programma",
     "Artists": "Artiesten",
-    "AuthenticationSucceededWithUserName": "{0} succesvol geauthenticeerd",
+    "AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd",
     "Books": "Boeken",
-    "CameraImageUploadedFrom": "Er is een nieuwe foto toegevoegd van {0}",
+    "CameraImageUploadedFrom": "Er is een nieuwe afbeelding toegevoegd via {0}",
     "Channels": "Kanalen",
     "ChapterNameValue": "Hoofdstuk {0}",
     "Collections": "Verzamelingen",
@@ -92,5 +92,27 @@
     "UserStoppedPlayingItemWithValues": "{0} heeft afspelen van {1} gestopt op {2}",
     "ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek",
     "ValueSpecialEpisodeName": "Speciaal - {0}",
-    "VersionNumber": "Versie {0}"
+    "VersionNumber": "Versie {0}",
+    "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar missende ondertitels gebaseerd op metadata configuratie.",
+    "TaskDownloadMissingSubtitles": "Download missende ondertitels",
+    "TaskRefreshChannelsDescription": "Vernieuwt informatie van internet kanalen.",
+    "TaskRefreshChannels": "Vernieuw Kanalen",
+    "TaskCleanTranscodeDescription": "Verwijder transcode bestanden ouder dan 1 dag.",
+    "TaskCleanLogs": "Log Folder Opschonen",
+    "TaskCleanTranscode": "Transcode Folder Opschonen",
+    "TaskUpdatePluginsDescription": "Download en installeert updates voor plugins waar automatisch updaten aan staat.",
+    "TaskUpdatePlugins": "Update Plugins",
+    "TaskRefreshPeopleDescription": "Update metadata for acteurs en regisseurs in de media bibliotheek.",
+    "TaskRefreshPeople": "Vernieuw Personen",
+    "TaskCleanLogsDescription": "Verwijdert log bestanden ouder dan {0} dagen.",
+    "TaskRefreshLibraryDescription": "Scant de media bibliotheek voor nieuwe bestanden en vernieuwt de metadata.",
+    "TaskRefreshLibrary": "Scan Media Bibliotheek",
+    "TaskRefreshChapterImagesDescription": "Maakt thumbnails aan voor videos met hoofdstukken.",
+    "TaskRefreshChapterImages": "Hoofdstukafbeeldingen Uitpakken",
+    "TaskCleanCacheDescription": "Verwijder gecachte bestanden die het systeem niet langer nodig heeft.",
+    "TaskCleanCache": "Cache Folder Opschonen",
+    "TasksChannelsCategory": "Internet Kanalen",
+    "TasksApplicationCategory": "Applicatie",
+    "TasksLibraryCategory": "Bibliotheek",
+    "TasksMaintenanceCategory": "Onderhoud"
 }

+ 24 - 2
Emby.Server.Implementations/Localization/Core/pt-PT.json

@@ -26,7 +26,7 @@
     "HeaderLiveTV": "TV em Direto",
     "HeaderNextUp": "A Seguir",
     "HeaderRecordingGroups": "Grupos de Gravação",
-    "HomeVideos": "Home videos",
+    "HomeVideos": "Videos caseiros",
     "Inherit": "Herdar",
     "ItemAddedWithName": "{0} foi adicionado à biblioteca",
     "ItemRemovedWithName": "{0} foi removido da biblioteca",
@@ -92,5 +92,27 @@
     "UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
     "ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua biblioteca multimédia",
     "ValueSpecialEpisodeName": "Especial - {0}",
-    "VersionNumber": "Versão {0}"
+    "VersionNumber": "Versão {0}",
+    "TaskDownloadMissingSubtitlesDescription": "Procurar na internet por legendas em falta baseado na configuração de metadados.",
+    "TaskDownloadMissingSubtitles": "Fazer download de legendas em falta",
+    "TaskRefreshChannelsDescription": "Atualizar informação sobre canais da Internet.",
+    "TaskRefreshChannels": "Atualizar Canais",
+    "TaskCleanTranscodeDescription": "Apagar ficheiros de transcode com mais de um dia.",
+    "TaskCleanTranscode": "Limpar a Diretoria de Transcode",
+    "TaskUpdatePluginsDescription": "Faz o download e instala updates para os plugins que estão configurados para atualizar automaticamente.",
+    "TaskUpdatePlugins": "Atualizar Plugins",
+    "TaskRefreshPeopleDescription": "Atualizar metadados para atores e diretores na biblioteca.",
+    "TaskRefreshPeople": "Atualizar Pessoas",
+    "TaskCleanLogsDescription": "Apagar ficheiros de log que têm mais de {0} dias.",
+    "TaskCleanLogs": "Limpar a Diretoria de Logs",
+    "TaskRefreshLibraryDescription": "Scannear a biblioteca de música para novos ficheiros e atualizar os metadados.",
+    "TaskRefreshLibrary": "Scannear Biblioteca de Música",
+    "TaskRefreshChapterImagesDescription": "Criar thumbnails para os vídeos que têm capítulos.",
+    "TaskRefreshChapterImages": "Extrair Imagens dos Capítulos",
+    "TaskCleanCacheDescription": "Apagar ficheiros em cache que já não são necessários.",
+    "TaskCleanCache": "Limpar Cache",
+    "TasksChannelsCategory": "Canais da Internet",
+    "TasksApplicationCategory": "Aplicação",
+    "TasksLibraryCategory": "Biblioteca",
+    "TasksMaintenanceCategory": "Manutenção"
 }

+ 13 - 1
Emby.Server.Implementations/Localization/Core/pt.json

@@ -91,5 +91,17 @@
     "CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}",
     "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
     "Application": "Aplicação",
-    "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}"
+    "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}",
+    "TaskCleanCache": "Limpar Diretório de Cache",
+    "TasksApplicationCategory": "Aplicação",
+    "TasksLibraryCategory": "Biblioteca",
+    "TasksMaintenanceCategory": "Manutenção",
+    "TaskRefreshChannels": "Atualizar Canais",
+    "TaskUpdatePlugins": "Atualizar Plugins",
+    "TaskCleanLogsDescription": "Deletar arquivos de log que existe a mais de {0} dias.",
+    "TaskCleanLogs": "Limpar diretório de log",
+    "TaskRefreshLibrary": "Escanear biblioteca de mídias",
+    "TaskRefreshChapterImagesDescription": "Criar miniaturas para videos que tem capítulos.",
+    "TaskCleanCacheDescription": "Deletar arquivos de cache que não são mais usados pelo sistema.",
+    "TasksChannelsCategory": "Canais de Internet"
 }

+ 44 - 22
Emby.Server.Implementations/Localization/Core/ru.json

@@ -9,8 +9,8 @@
     "Channels": "Каналы",
     "ChapterNameValue": "Сцена {0}",
     "Collections": "Коллекции",
-    "DeviceOfflineWithName": "{0} - подкл. разъ-но",
-    "DeviceOnlineWithName": "{0} - подкл. уст-но",
+    "DeviceOfflineWithName": "{0} - отключено",
+    "DeviceOnlineWithName": "{0} - подключено",
     "FailedLoginAttemptWithUserName": "{0} - попытка входа неудачна",
     "Favorites": "Избранное",
     "Folders": "Папки",
@@ -26,30 +26,30 @@
     "HeaderLiveTV": "Эфир",
     "HeaderNextUp": "Очередное",
     "HeaderRecordingGroups": "Группы записей",
-    "HomeVideos": "Дом. видео",
+    "HomeVideos": "Домашнее видео",
     "Inherit": "Наследуемое",
     "ItemAddedWithName": "{0} - добавлено в медиатеку",
     "ItemRemovedWithName": "{0} - изъято из медиатеки",
     "LabelIpAddressValue": "IP-адрес: {0}",
     "LabelRunningTimeValue": "Длительность: {0}",
-    "Latest": "Новейшее",
+    "Latest": "Последнее",
     "MessageApplicationUpdated": "Jellyfin Server был обновлён",
     "MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Конфиг-ия сервера (раздел {0}) была обновлена",
-    "MessageServerConfigurationUpdated": "Конфиг-ия сервера была обновлена",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
+    "MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена",
     "MixedContent": "Смешанное содержимое",
     "Movies": "Кино",
     "Music": "Музыка",
-    "MusicVideos": "Муз. видео",
+    "MusicVideos": "Музыкальные клипы",
     "NameInstallFailed": "Установка {0} неудачна",
     "NameSeasonNumber": "Сезон {0}",
     "NameSeasonUnknown": "Сезон неопознан",
     "NewVersionIsAvailable": "Новая версия Jellyfin Server доступна для загрузки.",
     "NotificationOptionApplicationUpdateAvailable": "Имеется обновление приложения",
     "NotificationOptionApplicationUpdateInstalled": "Обновление приложения установлено",
-    "NotificationOptionAudioPlayback": "Воспр-ие аудио зап-но",
-    "NotificationOptionAudioPlaybackStopped": "Восп-ие аудио ост-но",
-    "NotificationOptionCameraImageUploaded": "Произведена выкладка отснятого с камеры",
+    "NotificationOptionAudioPlayback": "Воспроизведение аудио запущено",
+    "NotificationOptionAudioPlaybackStopped": "Воспроизведение аудио остановлено",
+    "NotificationOptionCameraImageUploaded": "Изображения с камеры загружены",
     "NotificationOptionInstallationFailed": "Сбой установки",
     "NotificationOptionNewLibraryContent": "Новое содержание добавлено",
     "NotificationOptionPluginError": "Сбой плагина",
@@ -59,8 +59,8 @@
     "NotificationOptionServerRestartRequired": "Требуется перезапуск сервера",
     "NotificationOptionTaskFailed": "Сбой назначенной задачи",
     "NotificationOptionUserLockedOut": "Пользователь заблокирован",
-    "NotificationOptionVideoPlayback": "Воспр-ие видео зап-но",
-    "NotificationOptionVideoPlaybackStopped": "Восп-ие видео ост-но",
+    "NotificationOptionVideoPlayback": "Воспроизведение видео запущено",
+    "NotificationOptionVideoPlaybackStopped": "Воспроизведение видео остановлено",
     "Photos": "Фото",
     "Playlists": "Плей-листы",
     "Plugin": "Плагин",
@@ -76,21 +76,43 @@
     "StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
     "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",
     "SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
-    "Sync": "Синхро",
+    "Sync": "Синхронизация",
     "System": "Система",
     "TvShows": "ТВ",
-    "User": "Польз-ль",
+    "User": "Пользователь",
     "UserCreatedWithName": "Пользователь {0} был создан",
     "UserDeletedWithName": "Пользователь {0} был удалён",
     "UserDownloadingItemWithValues": "{0} загружает {1}",
     "UserLockedOutWithName": "Пользователь {0} был заблокирован",
-    "UserOfflineFromDevice": "{0} - подкл. с {1} разъ-но",
-    "UserOnlineFromDevice": "{0} - подкл. с {1} уст-но",
-    "UserPasswordChangedWithName": "Пароль польз-ля {0} был изменён",
-    "UserPolicyUpdatedWithName": "Польз-ие политики {0} были обновлены",
-    "UserStartedPlayingItemWithValues": "{0} - воспр. «{1}» на {2}",
-    "UserStoppedPlayingItemWithValues": "{0} - воспр. «{1}» ост-но на {2}",
+    "UserOfflineFromDevice": "{0} отключился с {1}",
+    "UserOnlineFromDevice": "{0} подключился с {1}",
+    "UserPasswordChangedWithName": "Пароль пользователя {0} был изменён",
+    "UserPolicyUpdatedWithName": "Политики пользователя {0} были обновлены",
+    "UserStartedPlayingItemWithValues": "{0} - воспроизведение «{1}» на {2}",
+    "UserStoppedPlayingItemWithValues": "{0} - воспроизведение остановлено «{1}» на {2}",
     "ValueHasBeenAddedToLibrary": "{0} (добавлено в медиатеку)",
-    "ValueSpecialEpisodeName": "Спецэпизод - {0}",
-    "VersionNumber": "Версия {0}"
+    "ValueSpecialEpisodeName": "Специальный эпизод - {0}",
+    "VersionNumber": "Версия {0}",
+    "TaskDownloadMissingSubtitles": "Загрузка отсутствующих субтитров",
+    "TaskRefreshChannels": "Обновление каналов",
+    "TaskCleanTranscode": "Очистка каталога перекодировки",
+    "TaskUpdatePlugins": "Обновление плагинов",
+    "TaskRefreshPeople": "Обновление метаданных людей",
+    "TaskCleanLogs": "Очистка каталога журналов",
+    "TaskRefreshLibrary": "Сканирование медиатеки",
+    "TaskRefreshChapterImages": "Извлечение изображений сцен",
+    "TaskCleanCache": "Очистка каталога кеша",
+    "TasksChannelsCategory": "Интернет-каналы",
+    "TasksApplicationCategory": "Приложение",
+    "TasksLibraryCategory": "Медиатека",
+    "TasksMaintenanceCategory": "Обслуживание",
+    "TaskDownloadMissingSubtitlesDescription": "Выполняется поиск в Интернете отсутствующих субтитров на основе конфигурации метаданных.",
+    "TaskRefreshChannelsDescription": "Обновляются данные интернет-каналов.",
+    "TaskCleanTranscodeDescription": "Удаляются файлы перекодировки старше одного дня.",
+    "TaskUpdatePluginsDescription": "Загружаются и устанавливаются обновления для плагинов, у которых включено автоматическое обновление.",
+    "TaskRefreshPeopleDescription": "Обновляются метаданные актеров и режиссёров в медиатеке.",
+    "TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).",
+    "TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.",
+    "TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.",
+    "TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе."
 }

+ 22 - 1
Emby.Server.Implementations/Localization/Core/sv.json

@@ -92,5 +92,26 @@
     "UserStoppedPlayingItemWithValues": "{0} har avslutat uppspelningen av {1} på {2}",
     "ValueHasBeenAddedToLibrary": "{0} har lagts till i ditt mediebibliotek",
     "ValueSpecialEpisodeName": "Specialavsnitt - {0}",
-    "VersionNumber": "Version {0}"
+    "VersionNumber": "Version {0}",
+    "TaskDownloadMissingSubtitlesDescription": "Söker på internet efter saknade undertexter baserad på metadatas konfiguration.",
+    "TaskDownloadMissingSubtitles": "Ladda ned saknade undertexter",
+    "TaskRefreshChannelsDescription": "Uppdaterar information för internetkanaler.",
+    "TaskRefreshChannels": "Uppdatera kanaler",
+    "TaskCleanTranscodeDescription": "Raderar transkodningsfiler som är mer än en dag gamla.",
+    "TaskCleanTranscode": "Töm transkodningskatalog",
+    "TaskUpdatePluginsDescription": "Laddar ned och installerar uppdateringar till insticksprogram som är konfigurerade att uppdateras automatiskt.",
+    "TaskUpdatePlugins": "Uppdatera insticksprogram",
+    "TaskRefreshPeopleDescription": "Uppdaterar metadata för skådespelare och regissörer i ditt mediabibliotek.",
+    "TaskCleanLogsDescription": "Raderar loggfiler som är mer än {0} dagar gamla.",
+    "TaskCleanLogs": "Töm loggkatalog",
+    "TaskRefreshLibraryDescription": "Söker igenom ditt mediabibliotek efter nya filer och förnyar metadata.",
+    "TaskRefreshLibrary": "Genomsök mediabibliotek",
+    "TaskRefreshChapterImagesDescription": "Skapa miniatyrbilder för videor med kapitel.",
+    "TaskRefreshChapterImages": "Extrahera kapitelbilder",
+    "TaskCleanCacheDescription": "Radera cachade filer som systemet inte längre behöver.",
+    "TaskCleanCache": "Rensa cachekatalog",
+    "TasksChannelsCategory": "Internetkanaler",
+    "TasksApplicationCategory": "Applikation",
+    "TasksLibraryCategory": "Bibliotek",
+    "TasksMaintenanceCategory": "Underhåll"
 }

+ 24 - 2
Emby.Server.Implementations/Localization/Core/tr.json

@@ -50,7 +50,7 @@
     "NotificationOptionAudioPlayback": "Ses çalma başladı",
     "NotificationOptionAudioPlaybackStopped": "Ses çalma durduruldu",
     "NotificationOptionCameraImageUploaded": "Kamera fotoğrafı yüklendi",
-    "NotificationOptionInstallationFailed": "Yükleme başarısız oldu",
+    "NotificationOptionInstallationFailed": "Kurulum hatası",
     "NotificationOptionNewLibraryContent": "Yeni içerik eklendi",
     "NotificationOptionPluginError": "Eklenti hatası",
     "NotificationOptionPluginInstalled": "Eklenti yüklendi",
@@ -92,5 +92,27 @@
     "UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi",
     "ValueHasBeenAddedToLibrary": "Medya kitaplığınıza {0} eklendi",
     "ValueSpecialEpisodeName": "Özel - {0}",
-    "VersionNumber": "Versiyon {0}"
+    "VersionNumber": "Versiyon {0}",
+    "TaskCleanCache": "Geçici dosya klasörünü temizle",
+    "TasksChannelsCategory": "İnternet kanalları",
+    "TasksApplicationCategory": "Uygulama",
+    "TasksLibraryCategory": "Kütüphane",
+    "TasksMaintenanceCategory": "Onarım",
+    "TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.",
+    "TaskDownloadMissingSubtitlesDescription": "Metadata ayarlarını baz alarak eksik altyazıları internette arar.",
+    "TaskDownloadMissingSubtitles": "Eksik altyazıları indir",
+    "TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.",
+    "TaskRefreshChannels": "Kanalları Yenile",
+    "TaskCleanTranscodeDescription": "Bir günü dolmuş dönüştürme bilgisi içeren dosyaları siler.",
+    "TaskCleanTranscode": "Dönüşüm Dizinini Temizle",
+    "TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.",
+    "TaskUpdatePlugins": "Eklentileri Güncelle",
+    "TaskRefreshPeople": "Kullanıcıları Yenile",
+    "TaskCleanLogsDescription": "{0} günden eski log dosyalarını siler.",
+    "TaskCleanLogs": "Log Dizinini Temizle",
+    "TaskRefreshLibraryDescription": "Medya kütüphanenize eklenen yeni dosyaları arar ve bilgileri yeniler.",
+    "TaskRefreshLibrary": "Medya Kütüphanesini Tara",
+    "TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.",
+    "TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar",
+    "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler."
 }

+ 117 - 0
Emby.Server.Implementations/Localization/Core/ur_PK.json

@@ -0,0 +1,117 @@
+{
+    "HeaderFavoriteAlbums": "پسندیدہ البمز",
+    "HeaderNextUp": "اگلا",
+    "HeaderFavoriteArtists": "پسندیدہ فنکار",
+    "HeaderAlbumArtists": "البم کے فنکار",
+    "Movies": "فلمیں",
+    "HeaderFavoriteEpisodes": "پسندیدہ اقساط",
+    "Collections": "مجموعہ",
+    "Folders": "فولڈرز",
+    "HeaderLiveTV": "براہ راست ٹی وی",
+    "Channels": "چینل",
+    "HeaderContinueWatching": "دیکھنا جاری رکھیں",
+    "Playlists": "پلے لسٹس",
+    "ValueSpecialEpisodeName": "خاص - {0}",
+    "Shows": "شوز",
+    "Genres": "انواع",
+    "Artists": "فنکار",
+    "Sync": "مطابقت",
+    "Photos": "تصوریں",
+    "Albums": "البم",
+    "Favorites": "پسندیدہ",
+    "Songs": "گانے",
+    "Books": "کتابیں",
+    "HeaderFavoriteSongs": "پسندیدہ گانے",
+    "HeaderFavoriteShows": "پسندیدہ شوز",
+    "TaskDownloadMissingSubtitlesDescription": "میٹا ڈیٹا کی تشکیل پر مبنی ذیلی عنوانات کے غائب عنوانات انٹرنیٹ پے تلاش کرتا ہے۔",
+    "TaskDownloadMissingSubtitles": "غائب سب ٹائٹلز ڈاؤن لوڈ کریں",
+    "TaskRefreshChannelsDescription": "انٹرنیٹ چینل کی معلومات کو تازہ دم کرتا ہے۔",
+    "TaskRefreshChannels": "چینلز ریفریش کریں",
+    "TaskCleanTranscodeDescription": "ایک دن سے زیادہ پرانی ٹرانسکوڈ فائلوں کو حذف کرتا ہے۔",
+    "TaskCleanTranscode": "ٹرانس کوڈ ڈائرکٹری صاف کریں",
+    "TaskUpdatePluginsDescription": "پلگ انز کے لئے اپ ڈیٹس ڈاؤن لوڈ اور انسٹال کرتے ہیں جو خود بخود اپ ڈیٹ کرنے کیلئے تشکیل شدہ ہیں۔",
+    "TaskUpdatePlugins": "پلگ انز کو اپ ڈیٹ کریں",
+    "TaskRefreshPeopleDescription": "آپ کی میڈیا لائبریری میں اداکاروں اور ہدایت کاروں کے لئے میٹا ڈیٹا کی تازہ کاری۔",
+    "TaskRefreshPeople": "لوگوں کو تروتازہ کریں",
+    "TaskCleanLogsDescription": "لاگ فائلوں کو حذف کریں جو {0} دن سے زیادہ پرانی ہیں۔",
+    "TaskCleanLogs": "لاگ ڈائرکٹری کو صاف کریں",
+    "TaskRefreshLibraryDescription": "میڈیا لائبریری کو اسکین کرتا ھے ہر میٹا دیٹا کہ تازہ دم کرتا ھے.",
+    "TaskRefreshLibrary": "اسکین میڈیا لائبریری",
+    "TaskRefreshChapterImagesDescription": "بابوں والی ویڈیوز کے لئے تمبنیل بنایں۔",
+    "TaskRefreshChapterImages": "باب کی تصاویر نکالیں",
+    "TaskCleanCacheDescription": "فائلوں کو حذف کریں جنکی ضرورت نھیں ھے۔",
+    "TaskCleanCache": "کیش ڈائرکٹری کلیر کریں",
+    "TasksChannelsCategory": "انٹرنیٹ چینلز",
+    "TasksApplicationCategory": "پروگرام",
+    "TasksLibraryCategory": "لآیبریری",
+    "TasksMaintenanceCategory": "مرمت",
+    "VersionNumber": "ورژن {0}",
+    "ValueHasBeenAddedToLibrary": "{0} آپ کی میڈیا لائبریری میں شامل کر دیا گیا ہے",
+    "UserStoppedPlayingItemWithValues": "{0} نے {1} چلانا ختم کر دیا ھے {2} پے",
+    "UserStartedPlayingItemWithValues": "{0} چلا رہا ہے {1} {2} پے",
+    "UserPolicyUpdatedWithName": "صارف {0} کی پالیسی کیلئے تازہ کاری کی گئی ہے",
+    "UserPasswordChangedWithName": "صارف {0} کے لئے پاس ورڈ تبدیل کر دیا گیا ہے",
+    "UserOnlineFromDevice": "{0} آن لائن ہے {1} سے",
+    "UserOfflineFromDevice": "{0} سے منقطع ہوگیا ہے {1}",
+    "UserLockedOutWithName": "صارف {0} کو لاک آؤٹ کردیا گیا ہے",
+    "UserDownloadingItemWithValues": "{0} ڈاؤن لوڈ کر رھا ھے {1}",
+    "UserDeletedWithName": "صارف {0} کو ہٹا دیا گیا ہے",
+    "UserCreatedWithName": "صارف {0} تشکیل دیا گیا ہے",
+    "User": "صارف",
+    "TvShows": "ٹی وی کے پروگرام",
+    "System": "نظام",
+    "SubtitleDownloadFailureFromForItem": "ذیلی عنوانات {0} سے ڈاؤن لوڈ کرنے میں ناکام {1} کے لیے",
+    "StartupEmbyServerIsLoading": "جیلیفن سرور لوڈ ہورہا ہے۔ براہ کرم جلد ہی دوبارہ کوشش کریں۔",
+    "ServerNameNeedsToBeRestarted": "{0} دوبارہ چلانے کرنے کی ضرورت ہے",
+    "ScheduledTaskStartedWithName": "{0} شروع",
+    "ScheduledTaskFailedWithName": "{0} ناکام",
+    "ProviderValue": "فراہم کرنے والا: {0}",
+    "PluginUpdatedWithName": "{0} تازہ کاری کی گئی تھی",
+    "PluginUninstalledWithName": "[0} ہٹا دیا گیا تھا",
+    "PluginInstalledWithName": "{0} انسٹال کیا گیا تھا",
+    "Plugin": "پلگن",
+    "NotificationOptionVideoPlaybackStopped": "ویڈیو پلے بیک رک گیا",
+    "NotificationOptionVideoPlayback": "ویڈیو پلے بیک شروع ہوا",
+    "NotificationOptionUserLockedOut": "صارف کو لاک آؤٹ کیا گیا",
+    "NotificationOptionTaskFailed": "طے شدہ کام کی ناکامی",
+    "NotificationOptionServerRestartRequired": "سرور دوبارہ چلانے کرنے کی ضرورت ہے",
+    "NotificationOptionPluginUpdateInstalled": "پلگ ان اپ ڈیٹ انسٹال",
+    "NotificationOptionPluginUninstalled": "پلگ ان ہٹا دیا گیا",
+    "NotificationOptionPluginInstalled": "پلگ ان انسٹال ہوا",
+    "NotificationOptionPluginError": "پلگ ان کی ناکامی",
+    "NotificationOptionNewLibraryContent": "نیا مواد شامل کیا گیا",
+    "NotificationOptionInstallationFailed": "تنصیب کی ناکامی",
+    "NotificationOptionCameraImageUploaded": "کیمرے کی تصویر اپ لوڈ ہوگئی",
+    "NotificationOptionAudioPlaybackStopped": "آڈیو پلے بیک رک گیا",
+    "NotificationOptionAudioPlayback": "آڈیو پلے بیک شروع ہوا",
+    "NotificationOptionApplicationUpdateInstalled": "پروگرام اپ ڈیٹ انسٹال ہوچکا ھے",
+    "NotificationOptionApplicationUpdateAvailable": "پروگرام کی تازہ کاری دستیاب ہے",
+    "NewVersionIsAvailable": "جیلیفن سرور کا ایک نیا ورژن ڈاؤن لوڈ کے لئے دستیاب ہے۔",
+    "NameSeasonUnknown": "نامعلوم باب",
+    "NameSeasonNumber": "باب {0}",
+    "NameInstallFailed": "{0} تنصیب ناکام ہوگئی",
+    "MusicVideos": "موسیقی ویڈیو",
+    "Music": "موسیقی",
+    "MixedContent": "مخلوط مواد",
+    "MessageServerConfigurationUpdated": "سرور کو اپ ڈیٹ کر دیا گیا ہے",
+    "MessageNamedServerConfigurationUpdatedWithValue": "سرور ضمن {0} کو ترتیب دے دیا گیا ھے",
+    "MessageApplicationUpdatedTo": "جیلیفن سرور کو اپ ڈیٹ کیا ہے {0}",
+    "MessageApplicationUpdated": "جیلیفن سرور کو اپ ڈیٹ کر دیا گیا ہے",
+    "Latest": "تازہ ترین",
+    "LabelRunningTimeValue": "چلانے کی مدت",
+    "LabelIpAddressValue": "ای پی پتے {0}",
+    "ItemRemovedWithName": "لائبریری سے ہٹا دیا گیا ھے",
+    "ItemAddedWithName": "[0} لائبریری میں شامل کیا گیا ھے",
+    "Inherit": "وراثت میں",
+    "HomeVideos": "ہوم ویڈیو",
+    "HeaderRecordingGroups": "ریکارڈنگ گروپس",
+    "HeaderCameraUploads": "کیمرہ اپلوڈز",
+    "FailedLoginAttemptWithUserName": "لاگن کئ کوشش ناکام {0}",
+    "DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
+    "DeviceOfflineWithName": "{0} منقطع ھو چکا ھے",
+    "ChapterNameValue": "باب",
+    "AuthenticationSucceededWithUserName": "{0} کامیابی کے ساتھ تصدیق ھوچکی ھے",
+    "CameraImageUploadedFrom": "ایک نئی کیمرہ تصویر اپ لوڈ کی گئی ہے {0}",
+    "Application": "پروگرام",
+    "AppDeviceValues": "پروگرام:{0}, آلہ:{1}"
+}

+ 29 - 7
Emby.Server.Implementations/Localization/Core/zh-TW.json

@@ -20,7 +20,7 @@
     "HeaderContinueWatching": "繼續觀賞",
     "HeaderFavoriteAlbums": "最愛專輯",
     "HeaderFavoriteArtists": "最愛演出者",
-    "HeaderFavoriteEpisodes": "最愛級數",
+    "HeaderFavoriteEpisodes": "最愛影集",
     "HeaderFavoriteShows": "最愛節目",
     "HeaderFavoriteSongs": "最愛歌曲",
     "HeaderLiveTV": "電視直播",
@@ -50,10 +50,10 @@
     "NotificationOptionCameraImageUploaded": "相機相片已上傳",
     "NotificationOptionInstallationFailed": "安裝失敗",
     "NotificationOptionNewLibraryContent": "已新增新內容",
-    "NotificationOptionPluginError": "擴充元件錯誤",
-    "NotificationOptionPluginInstalled": "擴充元件已安裝",
-    "NotificationOptionPluginUninstalled": "擴充元件已移除",
-    "NotificationOptionPluginUpdateInstalled": "已更新擴充元件",
+    "NotificationOptionPluginError": "插件安裝錯誤",
+    "NotificationOptionPluginInstalled": "件已安裝",
+    "NotificationOptionPluginUninstalled": "件已移除",
+    "NotificationOptionPluginUpdateInstalled": "插件已更新",
     "NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
     "NotificationOptionTaskFailed": "排程任務失敗",
     "NotificationOptionUserLockedOut": "使用者已鎖定",
@@ -61,7 +61,7 @@
     "NotificationOptionVideoPlaybackStopped": "影片停止播放",
     "Photos": "相片",
     "Playlists": "播放清單",
-    "Plugin": "外掛",
+    "Plugin": "插件",
     "PluginInstalledWithName": "{0} 已安裝",
     "PluginUninstalledWithName": "{0} 已移除",
     "PluginUpdatedWithName": "{0} 已更新",
@@ -91,5 +91,27 @@
     "VersionNumber": "版本 {0}",
     "HeaderRecordingGroups": "錄製組",
     "Inherit": "繼承",
-    "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕"
+    "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
+    "TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。",
+    "TaskDownloadMissingSubtitles": "下載遺失的字幕",
+    "TaskRefreshChannels": "重新整理頻道",
+    "TaskUpdatePlugins": "更新插件",
+    "TaskRefreshPeople": "重新整理人員",
+    "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔案。",
+    "TaskCleanLogs": "清空紀錄資料夾",
+    "TaskRefreshLibraryDescription": "掃描媒體庫內新的檔案並重新整理描述資料。",
+    "TaskRefreshLibrary": "掃描媒體庫",
+    "TaskRefreshChapterImages": "擷取章節圖片",
+    "TaskCleanCacheDescription": "刪除系統長時間不需要的快取。",
+    "TaskCleanCache": "清除快取資料夾",
+    "TasksLibraryCategory": "媒體庫",
+    "TaskRefreshChannelsDescription": "重新整理網絡頻道資料。",
+    "TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。",
+    "TaskCleanTranscode": "清除轉碼資料夾",
+    "TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。",
+    "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的中繼資料。",
+    "TaskRefreshChapterImagesDescription": "為有章節的視頻創建縮圖。",
+    "TasksChannelsCategory": "網絡頻道",
+    "TasksApplicationCategory": "應用程式",
+    "TasksMaintenanceCategory": "維修"
 }

+ 0 - 3
Emby.Server.Implementations/Localization/LocalizationManager.cs

@@ -23,9 +23,6 @@ namespace Emby.Server.Implementations.Localization
         private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly;
         private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
 
-        /// <summary>
-        /// The _configuration manager.
-        /// </summary>
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly ILogger _logger;

+ 3 - 17
Emby.Server.Implementations/ScheduledTasks/TaskManager.cs

@@ -32,22 +32,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
         private readonly ConcurrentQueue<Tuple<Type, TaskOptions>> _taskQueue =
             new ConcurrentQueue<Tuple<Type, TaskOptions>>();
 
-        /// <summary>
-        /// Gets or sets the json serializer.
-        /// </summary>
-        /// <value>The json serializer.</value>
         private readonly IJsonSerializer _jsonSerializer;
-
-        /// <summary>
-        /// Gets or sets the application paths.
-        /// </summary>
-        /// <value>The application paths.</value>
         private readonly IApplicationPaths _applicationPaths;
-
-        /// <summary>
-        /// Gets the logger.
-        /// </summary>
-        /// <value>The logger.</value>
         private readonly ILogger _logger;
         private readonly IFileSystem _fileSystem;
 
@@ -56,17 +42,17 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// </summary>
         /// <param name="applicationPaths">The application paths.</param>
         /// <param name="jsonSerializer">The json serializer.</param>
-        /// <param name="loggerFactory">The logger factory.</param>
+        /// <param name="logger">The logger.</param>
         /// <param name="fileSystem">The filesystem manager.</param>
         public TaskManager(
             IApplicationPaths applicationPaths,
             IJsonSerializer jsonSerializer,
-            ILoggerFactory loggerFactory,
+            ILogger<TaskManager> logger,
             IFileSystem fileSystem)
         {
             _applicationPaths = applicationPaths;
             _jsonSerializer = jsonSerializer;
-            _logger = loggerFactory.CreateLogger(nameof(TaskManager));
+            _logger = logger;
             _fileSystem = fileSystem;
 
             ScheduledTasks = Array.Empty<IScheduledTaskWorker>();

+ 2 - 3
Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs

@@ -55,9 +55,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
         {
             progress.Report(0);
 
-            var packagesToInstall = await _installationManager.GetAvailablePluginUpdates(cancellationToken)
-                .ToListAsync(cancellationToken)
-                .ConfigureAwait(false);
+            var packageFetchTask = _installationManager.GetAvailablePluginUpdates(cancellationToken);
+            var packagesToInstall = (await packageFetchTask.ConfigureAwait(false)).ToList();
 
             progress.Report(10);
 

+ 2 - 2
Emby.Server.Implementations/Security/AuthenticationRepository.cs

@@ -15,8 +15,8 @@ namespace Emby.Server.Implementations.Security
 {
     public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository
     {
-        public AuthenticationRepository(ILoggerFactory loggerFactory, IServerConfigurationManager config)
-            : base(loggerFactory.CreateLogger(nameof(AuthenticationRepository)))
+        public AuthenticationRepository(ILogger<AuthenticationRepository> logger, IServerConfigurationManager config)
+            : base(logger)
         {
             DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "authentication.db");
         }

+ 1 - 1
Emby.Server.Implementations/Session/SessionManager.cs

@@ -1414,7 +1414,7 @@ namespace Emby.Server.Implementations.Session
             if (user == null)
             {
                 AuthenticationFailed?.Invoke(this, new GenericEventArgs<AuthenticationRequest>(request));
-                throw new SecurityException("Invalid username or password entered.");
+                throw new AuthenticationException("Invalid username or password entered.");
             }
 
             if (!string.IsNullOrEmpty(request.DeviceId)

+ 85 - 56
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -3,8 +3,10 @@ using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using System.Net;
 using System.Net.Http;
 using System.Runtime.CompilerServices;
+using System.Runtime.Serialization;
 using System.Security.Cryptography;
 using System.Threading;
 using System.Threading.Tasks;
@@ -18,17 +20,23 @@ using MediaBrowser.Model.Events;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Updates;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Updates
 {
     /// <summary>
-    /// Manages all install, uninstall and update operations (both plugins and system).
+    /// Manages all install, uninstall, and update operations for the system and individual plugins.
     /// </summary>
     public class InstallationManager : IInstallationManager
     {
         /// <summary>
-        /// The _logger.
+        /// The key for a setting that specifies a URL for the plugin repository JSON manifest.
+        /// </summary>
+        public const string PluginManifestUrlKey = "InstallationManager:PluginManifestUrl";
+
+        /// <summary>
+        /// The logger.
         /// </summary>
         private readonly ILogger _logger;
         private readonly IApplicationPaths _appPaths;
@@ -44,6 +52,7 @@ namespace Emby.Server.Implementations.Updates
         private readonly IApplicationHost _applicationHost;
 
         private readonly IZipClient _zipClient;
+        private readonly IConfiguration _appConfig;
 
         private readonly object _currentInstallationsLock = new object();
 
@@ -65,7 +74,8 @@ namespace Emby.Server.Implementations.Updates
             IJsonSerializer jsonSerializer,
             IServerConfigurationManager config,
             IFileSystem fileSystem,
-            IZipClient zipClient)
+            IZipClient zipClient,
+            IConfiguration appConfig)
         {
             if (logger == null)
             {
@@ -83,6 +93,7 @@ namespace Emby.Server.Implementations.Updates
             _config = config;
             _fileSystem = fileSystem;
             _zipClient = zipClient;
+            _appConfig = appConfig;
         }
 
         /// <inheritdoc />
@@ -101,10 +112,10 @@ namespace Emby.Server.Implementations.Updates
         public event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled;
 
         /// <inheritdoc />
-        public event EventHandler<GenericEventArgs<(IPlugin, PackageVersionInfo)>> PluginUpdated;
+        public event EventHandler<GenericEventArgs<(IPlugin, VersionInfo)>> PluginUpdated;
 
         /// <inheritdoc />
-        public event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled;
+        public event EventHandler<GenericEventArgs<VersionInfo>> PluginInstalled;
 
         /// <inheritdoc />
         public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
@@ -112,19 +123,43 @@ namespace Emby.Server.Implementations.Updates
         /// <inheritdoc />
         public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
         {
-            using (var response = await _httpClient.SendAsync(
-                new HttpRequestOptions
+            var manifestUrl = _appConfig.GetValue<string>(PluginManifestUrlKey);
+
+            try
+            {
+                using (var response = await _httpClient.SendAsync(
+                    new HttpRequestOptions
+                    {
+                        Url = manifestUrl,
+                        CancellationToken = cancellationToken,
+                        CacheMode = CacheMode.Unconditional,
+                        CacheLength = TimeSpan.FromMinutes(3)
+                    },
+                    HttpMethod.Get).ConfigureAwait(false))
+                using (Stream stream = response.Content)
                 {
-                    Url = "https://repo.jellyfin.org/releases/plugin/manifest.json",
-                    CancellationToken = cancellationToken,
-                    CacheMode = CacheMode.Unconditional,
-                    CacheLength = TimeSpan.FromMinutes(3)
-                },
-                HttpMethod.Get).ConfigureAwait(false))
-            using (Stream stream = response.Content)
+                    try
+                    {
+                        return await _jsonSerializer.DeserializeFromStreamAsync<IReadOnlyList<PackageInfo>>(stream).ConfigureAwait(false);
+                    }
+                    catch (SerializationException ex)
+                    {
+                        const string LogTemplate =
+                            "Failed to deserialize the plugin manifest retrieved from {PluginManifestUrl}. If you " +
+                            "have specified a custom plugin repository manifest URL with --plugin-manifest-url or " +
+                            PluginManifestUrlKey + ", please ensure that it is correct.";
+                        _logger.LogError(ex, LogTemplate, manifestUrl);
+                        throw;
+                    }
+                }
+            }
+            catch (UriFormatException ex)
             {
-                return await _jsonSerializer.DeserializeFromStreamAsync<IReadOnlyList<PackageInfo>>(
-                    stream).ConfigureAwait(false);
+                const string LogTemplate =
+                    "The URL configured for the plugin repository manifest URL is not valid: {PluginManifestUrl}. " +
+                    "Please check the URL configured by --plugin-manifest-url or " + PluginManifestUrlKey;
+                _logger.LogError(ex, LogTemplate, manifestUrl);
+                throw;
             }
         }
 
@@ -148,60 +183,56 @@ namespace Emby.Server.Implementations.Updates
         }
 
         /// <inheritdoc />
-        public IEnumerable<PackageVersionInfo> GetCompatibleVersions(
-            IEnumerable<PackageVersionInfo> availableVersions,
-            Version minVersion = null,
-            PackageVersionClass classification = PackageVersionClass.Release)
+        public IEnumerable<VersionInfo> GetCompatibleVersions(
+            IEnumerable<VersionInfo> availableVersions,
+            Version minVersion = null)
         {
             var appVer = _applicationHost.ApplicationVersion;
             availableVersions = availableVersions
-                .Where(x => x.classification == classification
-                    && Version.Parse(x.requiredVersionStr) <= appVer);
+                .Where(x => Version.Parse(x.targetAbi) <= appVer);
 
             if (minVersion != null)
             {
-                availableVersions = availableVersions.Where(x => x.Version >= minVersion);
+                availableVersions = availableVersions.Where(x => x.version >= minVersion);
             }
 
-            return availableVersions.OrderByDescending(x => x.Version);
+            return availableVersions.OrderByDescending(x => x.version);
         }
 
         /// <inheritdoc />
-        public IEnumerable<PackageVersionInfo> GetCompatibleVersions(
+        public IEnumerable<VersionInfo> GetCompatibleVersions(
             IEnumerable<PackageInfo> availablePackages,
             string name = null,
             Guid guid = default,
-            Version minVersion = null,
-            PackageVersionClass classification = PackageVersionClass.Release)
+            Version minVersion = null)
         {
             var package = FilterPackages(availablePackages, name, guid).FirstOrDefault();
 
-            // Package not found.
+            // Package not found in repository
             if (package == null)
             {
-                return Enumerable.Empty<PackageVersionInfo>();
+                return Enumerable.Empty<VersionInfo>();
             }
 
             return GetCompatibleVersions(
                 package.versions,
-                minVersion,
-                classification);
+                minVersion);
         }
 
         /// <inheritdoc />
-        public async IAsyncEnumerable<PackageVersionInfo> GetAvailablePluginUpdates([EnumeratorCancellation] CancellationToken cancellationToken = default)
+        public async Task<IEnumerable<VersionInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken = default)
         {
             var catalog = await GetAvailablePackages(cancellationToken).ConfigureAwait(false);
+            return GetAvailablePluginUpdates(catalog);
+        }
 
-            var systemUpdateLevel = _applicationHost.SystemUpdateLevel;
-
-            // Figure out what needs to be installed
+        private IEnumerable<VersionInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
+        {
             foreach (var plugin in _applicationHost.Plugins)
             {
-                var compatibleversions = GetCompatibleVersions(catalog, plugin.Name, plugin.Id, plugin.Version, systemUpdateLevel);
-                var version = compatibleversions.FirstOrDefault(y => y.Version > plugin.Version);
-                if (version != null
-                    && !CompletedInstallations.Any(x => string.Equals(x.AssemblyGuid, version.guid, StringComparison.OrdinalIgnoreCase)))
+                var compatibleversions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, plugin.Version);
+                var version = compatibleversions.FirstOrDefault(y => y.version > plugin.Version);
+                if (version != null && !CompletedInstallations.Any(x => string.Equals(x.Guid, version.guid, StringComparison.OrdinalIgnoreCase)))
                 {
                     yield return version;
                 }
@@ -209,7 +240,7 @@ namespace Emby.Server.Implementations.Updates
         }
 
         /// <inheritdoc />
-        public async Task InstallPackage(PackageVersionInfo package, CancellationToken cancellationToken)
+        public async Task InstallPackage(VersionInfo package, CancellationToken cancellationToken)
         {
             if (package == null)
             {
@@ -218,11 +249,9 @@ namespace Emby.Server.Implementations.Updates
 
             var installationInfo = new InstallationInfo
             {
-                Id = Guid.NewGuid(),
+                Guid = package.guid,
                 Name = package.name,
-                AssemblyGuid = package.guid,
-                UpdateClass = package.classification,
-                Version = package.versionStr
+                Version = package.version.ToString()
             };
 
             var innerCancellationTokenSource = new CancellationTokenSource();
@@ -240,7 +269,7 @@ namespace Emby.Server.Implementations.Updates
             var installationEventArgs = new InstallationEventArgs
             {
                 InstallationInfo = installationInfo,
-                PackageVersionInfo = package
+                VersionInfo = package
             };
 
             PackageInstalling?.Invoke(this, installationEventArgs);
@@ -265,7 +294,7 @@ namespace Emby.Server.Implementations.Updates
                     _currentInstallations.Remove(tuple);
                 }
 
-                _logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.versionStr);
+                _logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.version);
 
                 PackageInstallationCancelled?.Invoke(this, installationEventArgs);
 
@@ -301,7 +330,7 @@ namespace Emby.Server.Implementations.Updates
         /// <param name="package">The package.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns><see cref="Task" />.</returns>
-        private async Task InstallPackageInternal(PackageVersionInfo package, CancellationToken cancellationToken)
+        private async Task InstallPackageInternal(VersionInfo package, CancellationToken cancellationToken)
         {
             // Set last update time if we were installed before
             IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => string.Equals(p.Id.ToString(), package.guid, StringComparison.OrdinalIgnoreCase))
@@ -313,26 +342,26 @@ namespace Emby.Server.Implementations.Updates
             // Do plugin-specific processing
             if (plugin == null)
             {
-                _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification);
+                _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.version);
 
-                PluginInstalled?.Invoke(this, new GenericEventArgs<PackageVersionInfo>(package));
+                PluginInstalled?.Invoke(this, new GenericEventArgs<VersionInfo>(package));
             }
             else
             {
-                _logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification);
+                _logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.version);
 
-                PluginUpdated?.Invoke(this, new GenericEventArgs<(IPlugin, PackageVersionInfo)>((plugin, package)));
+                PluginUpdated?.Invoke(this, new GenericEventArgs<(IPlugin, VersionInfo)>((plugin, package)));
             }
 
             _applicationHost.NotifyPendingRestart();
         }
 
-        private async Task PerformPackageInstallation(PackageVersionInfo package, CancellationToken cancellationToken)
+        private async Task PerformPackageInstallation(VersionInfo package, CancellationToken cancellationToken)
         {
-            var extension = Path.GetExtension(package.targetFilename);
+            var extension = Path.GetExtension(package.filename);
             if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
             {
-                _logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.targetFilename);
+                _logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.filename);
                 return;
             }
 
@@ -379,7 +408,7 @@ namespace Emby.Server.Implementations.Updates
         }
 
         /// <summary>
-        /// Uninstalls a plugin
+        /// Uninstalls a plugin.
         /// </summary>
         /// <param name="plugin">The plugin.</param>
         public void UninstallPlugin(IPlugin plugin)
@@ -437,7 +466,7 @@ namespace Emby.Server.Implementations.Updates
         {
             lock (_currentInstallationsLock)
             {
-                var install = _currentInstallations.Find(x => x.info.Id == id);
+                var install = _currentInstallations.Find(x => x.info.Guid == id.ToString());
                 if (install == default((InstallationInfo, CancellationTokenSource)))
                 {
                     return false;

+ 5 - 0
Jellyfin.Api/Jellyfin.Api.csproj

@@ -1,5 +1,10 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
+  <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
+  <PropertyGroup>
+    <ProjectGuid>{DFBEFB4C-DA19-4143-98B7-27320C7F7163}</ProjectGuid>
+  </PropertyGroup>
+
   <PropertyGroup>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>

+ 5 - 0
Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj

@@ -1,5 +1,10 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
+  <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
+  <PropertyGroup>
+    <ProjectGuid>{154872D9-6C12-4007-96E3-8F70A58386CE}</ProjectGuid>
+  </PropertyGroup>
+
   <PropertyGroup>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>

+ 13 - 4
Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -78,12 +78,21 @@ namespace Jellyfin.Drawing.Skia
             => new HashSet<ImageFormat>() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
 
         /// <summary>
-        /// Test to determine if the native lib is available.
+        /// Check if the native lib is available.
         /// </summary>
-        public static void TestSkia()
+        /// <returns>True if the native lib is available, otherwise false.</returns>
+        public static bool IsNativeLibAvailable()
         {
-            // test an operation that requires the native library
-            SKPMColor.PreMultiply(SKColors.Black);
+            try
+            {
+                // test an operation that requires the native library
+                SKPMColor.PreMultiply(SKColors.Black);
+                return true;
+            }
+            catch (Exception)
+            {
+                return false;
+            }
         }
 
         private static bool IsTransparent(SKColor color)

+ 22 - 5
Jellyfin.Server/CoreAppHost.cs

@@ -1,9 +1,13 @@
+using System;
 using System.Collections.Generic;
 using System.Reflection;
+using Emby.Drawing;
 using Emby.Server.Implementations;
+using Jellyfin.Drawing.Skia;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Model.IO;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Server
@@ -20,27 +24,40 @@ namespace Jellyfin.Server
         /// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
-        /// <param name="imageEncoder">The <see cref="IImageEncoder" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
         public CoreAppHost(
             ServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
             StartupOptions options,
             IFileSystem fileSystem,
-            IImageEncoder imageEncoder,
             INetworkManager networkManager)
             : base(
                 applicationPaths,
                 loggerFactory,
                 options,
                 fileSystem,
-                imageEncoder,
                 networkManager)
         {
         }
 
-        /// <inheritdoc />
-        public override bool CanSelfRestart => StartupOptions.RestartPath != null;
+        /// <inheritdoc/>
+        protected override void RegisterServices(IServiceCollection serviceCollection)
+        {
+            // Register an image encoder
+            bool useSkiaEncoder = SkiaEncoder.IsNativeLibAvailable();
+            Type imageEncoderType = useSkiaEncoder
+                ? typeof(SkiaEncoder)
+                : typeof(NullImageEncoder);
+            serviceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType);
+
+            // Log a warning if the Skia encoder could not be used
+            if (!useSkiaEncoder)
+            {
+                Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
+            }
+
+            base.RegisterServices(serviceCollection);
+        }
 
         /// <inheritdoc />
         protected override void RestartInternal() => Program.Restart();

+ 5 - 0
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -71,6 +71,11 @@ namespace Jellyfin.Server.Extensions
                 // Clear app parts to avoid other assemblies being picked up
                 .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear())
                 .AddApplicationPart(typeof(StartupController).Assembly)
+                .AddJsonOptions(options =>
+                {
+                    // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON.
+                    options.JsonSerializerOptions.PropertyNamingPolicy = null;
+                })
                 .AddControllersAsServices();
         }
 

+ 5 - 0
Jellyfin.Server/Jellyfin.Server.csproj

@@ -1,5 +1,10 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
+  <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
+  <PropertyGroup>
+    <ProjectGuid>{07E39F42-A2C6-4B32-AF8C-725F957A73FF}</ProjectGuid>
+  </PropertyGroup>
+
   <PropertyGroup>
     <AssemblyName>jellyfin</AssemblyName>
     <OutputType>Exe</OutputType>

+ 2 - 23
Jellyfin.Server/Program.cs

@@ -184,7 +184,6 @@ namespace Jellyfin.Server
                 _loggerFactory,
                 options,
                 new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
-                GetImageEncoder(appPaths),
                 new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()));
 
             try
@@ -204,14 +203,13 @@ namespace Jellyfin.Server
                 }
 
                 ServiceCollection serviceCollection = new ServiceCollection();
-                await appHost.InitAsync(serviceCollection, startupConfig).ConfigureAwait(false);
+                appHost.Init(serviceCollection);
 
                 var webHost = CreateWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build();
 
                 // Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection.
                 appHost.ServiceProvider = webHost.Services;
-                appHost.InitializeServices();
-                appHost.FindParts();
+                await appHost.InitializeServices().ConfigureAwait(false);
                 Migrations.MigrationRunner.Run(appHost, _loggerFactory);
 
                 try
@@ -571,25 +569,6 @@ namespace Jellyfin.Server
             }
         }
 
-        private static IImageEncoder GetImageEncoder(IApplicationPaths appPaths)
-        {
-            try
-            {
-                // Test if the native lib is available
-                SkiaEncoder.TestSkia();
-
-                return new SkiaEncoder(
-                    _loggerFactory.CreateLogger<SkiaEncoder>(),
-                    appPaths);
-            }
-            catch (Exception ex)
-            {
-                _logger.LogWarning(ex, $"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
-            }
-
-            return new NullImageEncoder();
-        }
-
         private static void StartNewInstance(StartupOptions options)
         {
             _logger.LogInformation("Starting new instance");

+ 2 - 2
Jellyfin.Server/Properties/launchSettings.json

@@ -8,10 +8,10 @@
     },
     "Jellyfin.Server (nowebclient)": {
       "commandName": "Project",
-      "commandLineArgs": "--nowebclient",
       "environmentVariables": {
         "ASPNETCORE_ENVIRONMENT": "Development"
-      }
+      },
+      "commandLineArgs": "--nowebclient"
     }
   }
 }

+ 10 - 1
Jellyfin.Server/StartupOptions.cs

@@ -1,7 +1,7 @@
 using System.Collections.Generic;
-using System.Globalization;
 using CommandLine;
 using Emby.Server.Implementations;
+using Emby.Server.Implementations.Updates;
 using MediaBrowser.Controller.Extensions;
 
 namespace Jellyfin.Server
@@ -76,6 +76,10 @@ namespace Jellyfin.Server
         [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")]
         public string? RestartArgs { get; set; }
 
+        /// <inheritdoc />
+        [Option("plugin-manifest-url", Required = false, HelpText = "A custom URL for the plugin repository JSON manifest")]
+        public string? PluginManifestUrl { get; set; }
+
         /// <summary>
         /// Gets the command line options as a dictionary that can be used in the .NET configuration system.
         /// </summary>
@@ -84,6 +88,11 @@ namespace Jellyfin.Server
         {
             var config = new Dictionary<string, string>();
 
+            if (PluginManifestUrl != null)
+            {
+                config.Add(InstallationManager.PluginManifestUrlKey, PluginManifestUrl);
+            }
+
             if (NoWebClient)
             {
                 config.Add(ConfigurationExtensions.HostWebClientKey, bool.FalseString);

Some files were not shown because too many files changed in this diff