浏览代码

Merge branch 'master' into feature/ffmpeg-version-check

Bond-009 4 年之前
父节点
当前提交
5160e627f1
共有 100 个文件被更改,包括 1269 次插入2122 次删除
  1. 16 12
      .ci/azure-pipelines-abi.yml
  2. 43 8
      .ci/azure-pipelines-package.yml
  3. 4 0
      .ci/azure-pipelines.yml
  4. 12 2
      .vscode/launch.json
  5. 0 383
      Emby.Dlna/Api/DlnaServerService.cs
  6. 0 88
      Emby.Dlna/Api/DlnaService.cs
  7. 1 0
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  8. 6 0
      Emby.Dlna/ControlResponse.cs
  9. 2 1
      Emby.Dlna/Didl/DidlBuilder.cs
  10. 18 18
      Emby.Dlna/DlnaManager.cs
  11. 9 5
      Emby.Dlna/Eventing/EventManager.cs
  12. 3 3
      Emby.Dlna/Main/DlnaEntryPoint.cs
  13. 19 21
      Emby.Dlna/PlayTo/Device.cs
  14. 9 8
      Emby.Dlna/PlayTo/TransportCommands.cs
  15. 1 1
      Emby.Dlna/Profiles/DefaultProfile.cs
  16. 1 1
      Emby.Dlna/Profiles/DenonAvrProfile.cs
  17. 1 1
      Emby.Dlna/Profiles/DirectTvProfile.cs
  18. 1 1
      Emby.Dlna/Profiles/Foobar2000Profile.cs
  19. 1 1
      Emby.Dlna/Profiles/MarantzProfile.cs
  20. 2 1
      Emby.Dlna/Profiles/MediaMonkeyProfile.cs
  21. 2 1
      Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs
  22. 2 1
      Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs
  23. 2 1
      Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs
  24. 2 1
      Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs
  25. 75 107
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  26. 25 9
      Emby.Dlna/Service/ServiceXmlBuilder.cs
  27. 7 7
      Emby.Drawing/ImageProcessor.cs
  28. 30 30
      Emby.Naming/Common/NamingOptions.cs
  29. 1 1
      Emby.Naming/TV/SeasonPathParser.cs
  30. 0 191
      Emby.Notifications/Api/NotificationsService.cs
  31. 11 5
      Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
  32. 24 30
      Emby.Server.Implementations/ApplicationHost.cs
  33. 3 1
      Emby.Server.Implementations/Browser/BrowserLauncher.cs
  34. 10 14
      Emby.Server.Implementations/Channels/ChannelManager.cs
  35. 2 2
      Emby.Server.Implementations/ConfigurationOptions.cs
  36. 0 225
      Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
  37. 85 164
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  38. 12 11
      Emby.Server.Implementations/Devices/DeviceManager.cs
  39. 5 43
      Emby.Server.Implementations/Dto/DtoService.cs
  40. 9 11
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  41. 41 52
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  42. 11 2
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  43. 0 2
      Emby.Server.Implementations/HttpServer/FileWriter.cs
  44. 3 3
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  45. 3 2
      Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
  46. 14 24
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  47. 7 6
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  48. 2 2
      Emby.Server.Implementations/HttpServer/StreamWriter.cs
  49. 4 4
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  50. 2 1
      Emby.Server.Implementations/IO/FileRefresher.cs
  51. 10 0
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  52. 2 8
      Emby.Server.Implementations/Images/ArtistImageProvider.cs
  53. 1 1
      Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
  54. 1 1
      Emby.Server.Implementations/Images/FolderImageProvider.cs
  55. 1 0
      Emby.Server.Implementations/Images/GenreImageProvider.cs
  56. 1 1
      Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
  57. 12 12
      Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
  58. 20 3
      Emby.Server.Implementations/Library/IgnorePatterns.cs
  59. 129 153
      Emby.Server.Implementations/Library/LibraryManager.cs
  60. 12 12
      Emby.Server.Implementations/Library/LiveStreamHelper.cs
  61. 23 25
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  62. 1 1
      Emby.Server.Implementations/Library/MediaStreamSelector.cs
  63. 1 1
      Emby.Server.Implementations/Library/MusicManager.cs
  64. 0 5
      Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
  65. 7 13
      Emby.Server.Implementations/Library/SearchEngine.cs
  66. 0 4
      Emby.Server.Implementations/Library/UserDataManager.cs
  67. 1 0
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  68. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  69. 2 2
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  70. 2 3
      Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
  71. 57 58
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  72. 4 5
      Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
  73. 14 22
      Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
  74. 48 40
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  75. 6 5
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
  76. 4 4
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  77. 12 13
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  78. 7 3
      Emby.Server.Implementations/Localization/Core/af.json
  79. 19 19
      Emby.Server.Implementations/Localization/Core/bn.json
  80. 5 5
      Emby.Server.Implementations/Localization/Core/de.json
  81. 28 28
      Emby.Server.Implementations/Localization/Core/he.json
  82. 36 14
      Emby.Server.Implementations/Localization/Core/id.json
  83. 4 4
      Emby.Server.Implementations/Localization/Core/it.json
  84. 3 1
      Emby.Server.Implementations/Localization/Core/mr.json
  85. 32 32
      Emby.Server.Implementations/Localization/Core/ms.json
  86. 10 1
      Emby.Server.Implementations/Localization/Core/pt.json
  87. 1 1
      Emby.Server.Implementations/Localization/Core/ru.json
  88. 21 3
      Emby.Server.Implementations/Localization/Core/ta.json
  89. 3 1
      Emby.Server.Implementations/Localization/Core/th.json
  90. 94 13
      Emby.Server.Implementations/Localization/Core/uk.json
  91. 1 1
      Emby.Server.Implementations/Localization/Core/zh-TW.json
  92. 4 4
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  93. 23 20
      Emby.Server.Implementations/Net/UdpSocket.cs
  94. 14 3
      Emby.Server.Implementations/Networking/NetworkManager.cs
  95. 20 44
      Emby.Server.Implementations/Playlists/PlaylistManager.cs
  96. 1 6
      Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
  97. 0 8
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  98. 6 4
      Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
  99. 14 5
      Emby.Server.Implementations/Services/ServiceController.cs
  100. 8 1
      Emby.Server.Implementations/Services/ServiceExec.cs

+ 16 - 12
.ci/azure-pipelines-abi.yml

@@ -12,10 +12,12 @@ parameters:
 jobs:
 jobs:
   - job: CompatibilityCheck
   - job: CompatibilityCheck
     displayName: Compatibility Check
     displayName: Compatibility Check
+    dependsOn: Build
+    condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
+
     pool:
     pool:
       vmImage: "${{ parameters.LinuxImage }}"
       vmImage: "${{ parameters.LinuxImage }}"
-    # only execute for pull requests
-    condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
+
     strategy:
     strategy:
       matrix:
       matrix:
         ${{ each Package in parameters.Packages }}:
         ${{ each Package in parameters.Packages }}:
@@ -23,7 +25,7 @@ jobs:
             NugetPackageName: ${{ Package.value.NugetPackageName }}
             NugetPackageName: ${{ Package.value.NugetPackageName }}
             AssemblyFileName: ${{ Package.value.AssemblyFileName }}
             AssemblyFileName: ${{ Package.value.AssemblyFileName }}
       maxParallel: 2
       maxParallel: 2
-    dependsOn: Build
+
     steps:
     steps:
       - checkout: none
       - checkout: none
 
 
@@ -34,32 +36,33 @@ jobs:
           version: ${{ parameters.DotNetSdkVersion }}
           version: ${{ parameters.DotNetSdkVersion }}
 
 
       - task: DotNetCoreCLI@2
       - task: DotNetCoreCLI@2
-        displayName: 'Install ABI CompatibilityChecker tool'
+        displayName: 'Install ABI CompatibilityChecker Tool'
         inputs:
         inputs:
           command: custom
           command: custom
           custom: tool
           custom: tool
           arguments: 'update compatibilitychecker -g'
           arguments: 'update compatibilitychecker -g'
 
 
       - task: DownloadPipelineArtifact@2
       - task: DownloadPipelineArtifact@2
-        displayName: "Download New Assembly Build Artifact"
+        displayName: 'Download New Assembly Build Artifact'
         inputs:
         inputs:
-          source: "current"
+          source: 'current'
           artifact: "$(NugetPackageName)"
           artifact: "$(NugetPackageName)"
           path: "$(System.ArtifactsDirectory)/new-artifacts"
           path: "$(System.ArtifactsDirectory)/new-artifacts"
           runVersion: "latest"
           runVersion: "latest"
 
 
       - task: CopyFiles@2
       - task: CopyFiles@2
-        displayName: "Copy New Assembly Build Artifact"
+        displayName: 'Copy New Assembly Build Artifact'
         inputs:
         inputs:
           sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
           sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
-          contents: "**/*.dll"
+          contents: '**/*.dll'
           targetFolder: $(System.ArtifactsDirectory)/new-release
           targetFolder: $(System.ArtifactsDirectory)/new-release
           cleanTargetFolder: true
           cleanTargetFolder: true
           overWrite: true
           overWrite: true
           flattenFolders: true
           flattenFolders: true
 
 
       - task: DownloadPipelineArtifact@2
       - task: DownloadPipelineArtifact@2
-        displayName: "Download Reference Assembly Build Artifact"
+        displayName: 'Download Reference Assembly Build Artifact'
+        enabled: false
         inputs:
         inputs:
           source: "specific"
           source: "specific"
           artifact: "$(NugetPackageName)"
           artifact: "$(NugetPackageName)"
@@ -70,18 +73,19 @@ jobs:
           runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
           runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
 
 
       - task: CopyFiles@2
       - task: CopyFiles@2
-        displayName: "Copy Reference Assembly Build Artifact"
+        displayName: 'Copy Reference Assembly Build Artifact'
+        enabled: false
         inputs:
         inputs:
           sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
           sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
-          contents: "**/*.dll"
+          contents: '**/*.dll'
           targetFolder: $(System.ArtifactsDirectory)/current-release
           targetFolder: $(System.ArtifactsDirectory)/current-release
           cleanTargetFolder: true
           cleanTargetFolder: true
           overWrite: true
           overWrite: true
           flattenFolders: true
           flattenFolders: true
 
 
-      # The `--warnings-only` switch will swallow the return code and not emit any errors.
       - task: DotNetCoreCLI@2
       - task: DotNetCoreCLI@2
         displayName: 'Execute ABI Compatibility Check Tool'
         displayName: 'Execute ABI Compatibility Check Tool'
+        enabled: false
         inputs:
         inputs:
           command: custom
           command: custom
           custom: compat
           custom: compat

+ 43 - 8
.ci/azure-pipelines-package.yml

@@ -35,7 +35,6 @@ jobs:
   steps:
   steps:
   - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
   - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
     displayName: 'Build Dockerfile'
     displayName: 'Build Dockerfile'
-    condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
 
 
   - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
   - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
     displayName: 'Run Dockerfile (unstable)'
     displayName: 'Run Dockerfile (unstable)'
@@ -47,14 +46,19 @@ jobs:
 
 
   - task: PublishPipelineArtifact@1
   - task: PublishPipelineArtifact@1
     displayName: 'Publish Release'
     displayName: 'Publish Release'
-    condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
     inputs:
     inputs:
       targetPath: '$(Build.SourcesDirectory)/deployment/dist'
       targetPath: '$(Build.SourcesDirectory)/deployment/dist'
       artifactName: 'jellyfin-server-$(BuildConfiguration)'
       artifactName: 'jellyfin-server-$(BuildConfiguration)'
 
 
+  - task: SSH@0
+    displayName: 'Create target directory on repository server'
+    inputs:
+      sshEndpoint: repository
+      runOptions: 'inline'
+      inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
+
   - task: CopyFilesOverSSH@0
   - task: CopyFilesOverSSH@0
     displayName: 'Upload artifacts to repository server'
     displayName: 'Upload artifacts to repository server'
-    condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
     inputs:
     inputs:
       sshEndpoint: repository
       sshEndpoint: repository
       sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
       sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
@@ -76,7 +80,15 @@ jobs:
   pool:
   pool:
     vmImage: 'ubuntu-latest'
     vmImage: 'ubuntu-latest'
 
 
+  variables:
+  - name: JellyfinVersion
+    value: 0.0.0
+
   steps:
   steps:
+  - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
+    displayName: Set release version (stable)
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+
   - task: Docker@2
   - task: Docker@2
     displayName: 'Push Unstable Image'
     displayName: 'Push Unstable Image'
     condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
     condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
@@ -101,9 +113,10 @@ jobs:
       containerRegistry: Docker Hub
       containerRegistry: Docker Hub
       tags: |
       tags: |
         stable-$(Build.BuildNumber)-$(BuildConfiguration)
         stable-$(Build.BuildNumber)-$(BuildConfiguration)
-        stable-$(BuildConfiguration)
+        $(JellyfinVersion)-$(BuildConfiguration)
 
 
 - job: CollectArtifacts
 - job: CollectArtifacts
+  timeoutInMinutes: 10
   displayName: 'Collect Artifacts'
   displayName: 'Collect Artifacts'
   dependsOn:
   dependsOn:
   - BuildPackage
   - BuildPackage
@@ -119,13 +132,35 @@ jobs:
     condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
     condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
     inputs:
     inputs:
       sshEndpoint: repository
       sshEndpoint: repository
-      runOptions: 'inline'
-      inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable'
+      runOptions: 'commands'
+      commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
 
 
   - task: SSH@0
   - task: SSH@0
     displayName: 'Update Stable Repository'
     displayName: 'Update Stable Repository'
     condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
     condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
     inputs:
     inputs:
       sshEndpoint: repository
       sshEndpoint: repository
-      runOptions: 'inline'
-      inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)'
+      runOptions: 'commands'
+      commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
+      
+- job: PublishNuget
+  displayName: 'Publish NuGet packages'
+  dependsOn:
+  - BuildPackage
+  condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags'))
+
+  pool:
+    vmImage: 'ubuntu-latest'
+
+  steps:
+  - task: NuGetCommand@2
+    inputs:
+      command: 'pack'
+      packagesToPack: Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj
+      packDestination: '$(Build.ArtifactStagingDirectory)'
+
+  - task: NuGetCommand@2
+    inputs:
+      command: 'push'
+      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
+      includeNugetOrg: 'true'

+ 4 - 0
.ci/azure-pipelines.yml

@@ -15,11 +15,13 @@ trigger:
   batch: true
   batch: true
 
 
 jobs:
 jobs:
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
   - template: azure-pipelines-main.yml
   - template: azure-pipelines-main.yml
     parameters:
     parameters:
       LinuxImage: 'ubuntu-latest'
       LinuxImage: 'ubuntu-latest'
       RestoreBuildProjects: $(RestoreBuildProjects)
       RestoreBuildProjects: $(RestoreBuildProjects)
 
 
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
   - template: azure-pipelines-test.yml
   - template: azure-pipelines-test.yml
     parameters:
     parameters:
       ImageNames:
       ImageNames:
@@ -27,6 +29,7 @@ jobs:
         Windows: 'windows-latest'
         Windows: 'windows-latest'
         macOS: 'macos-latest'
         macOS: 'macos-latest'
 
 
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
   - template: azure-pipelines-abi.yml
   - template: azure-pipelines-abi.yml
     parameters:
     parameters:
       Packages:
       Packages:
@@ -44,4 +47,5 @@ jobs:
           AssemblyFileName: MediaBrowser.Common.dll
           AssemblyFileName: MediaBrowser.Common.dll
       LinuxImage: 'ubuntu-latest'
       LinuxImage: 'ubuntu-latest'
 
 
+- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
   - template: azure-pipelines-package.yml
   - template: azure-pipelines-package.yml

+ 12 - 2
.vscode/launch.json

@@ -6,11 +6,21 @@
             "type": "coreclr",
             "type": "coreclr",
             "request": "launch",
             "request": "launch",
             "preLaunchTask": "build",
             "preLaunchTask": "build",
-            // If you have changed target frameworks, make sure to update the program path.
             "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
             "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
             "args": [],
             "args": [],
             "cwd": "${workspaceFolder}/Jellyfin.Server",
             "cwd": "${workspaceFolder}/Jellyfin.Server",
-            // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
+            "console": "internalConsole",
+            "stopAtEntry": false,
+            "internalConsoleOptions": "openOnSessionStart"
+        },
+        {
+            "name": ".NET Core Launch (nowebclient)",
+            "type": "coreclr",
+            "request": "launch",
+            "preLaunchTask": "build",
+            "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
+            "args": ["--nowebclient"],
+            "cwd": "${workspaceFolder}/Jellyfin.Server",
             "console": "internalConsole",
             "console": "internalConsole",
             "stopAtEntry": false,
             "stopAtEntry": false,
             "internalConsoleOptions": "openOnSessionStart"
             "internalConsoleOptions": "openOnSessionStart"

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

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

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

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

+ 1 - 0
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -11,6 +11,7 @@ using System.Xml;
 using Emby.Dlna.Didl;
 using Emby.Dlna.Didl;
 using Emby.Dlna.Service;
 using Emby.Dlna.Service;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;

+ 6 - 0
Emby.Dlna/ControlResponse.cs

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

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

@@ -364,7 +364,8 @@ namespace Emby.Dlna.Didl
                 writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
                 writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
             }
             }
 
 
-            var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container,
+            var mediaProfile = _profile.GetVideoMediaProfile(
+                streamInfo.Container,
                 streamInfo.TargetAudioCodec.FirstOrDefault(),
                 streamInfo.TargetAudioCodec.FirstOrDefault(),
                 streamInfo.TargetVideoCodec.FirstOrDefault(),
                 streamInfo.TargetVideoCodec.FirstOrDefault(),
                 streamInfo.TargetAudioBitrate,
                 streamInfo.TargetAudioBitrate,

+ 18 - 18
Emby.Dlna/DlnaManager.cs

@@ -54,11 +54,15 @@ namespace Emby.Dlna
             _appHost = appHost;
             _appHost = appHost;
         }
         }
 
 
+        private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
+
+        private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
+
         public async Task InitProfilesAsync()
         public async Task InitProfilesAsync()
         {
         {
             try
             try
             {
             {
-                await ExtractSystemProfilesAsync();
+                await ExtractSystemProfilesAsync().ConfigureAwait(false);
                 LoadProfiles();
                 LoadProfiles();
             }
             }
             catch (Exception ex)
             catch (Exception ex)
@@ -122,15 +126,15 @@ namespace Emby.Dlna
             var builder = new StringBuilder();
             var builder = new StringBuilder();
 
 
             builder.AppendLine("No matching device profile found. The default will need to be used.");
             builder.AppendLine("No matching device profile found. The default will need to be used.");
-            builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty));
-            builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
-            builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
-            builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
-            builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty));
-            builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty));
-            builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty));
-            builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty));
-            builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty));
+            builder.AppendFormat(CultureInfo.InvariantCulture, "DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
 
 
             _logger.LogInformation(builder.ToString());
             _logger.LogInformation(builder.ToString());
         }
         }
@@ -240,7 +244,7 @@ namespace Emby.Dlna
             }
             }
             else
             else
             {
             {
-                var headerString = string.Join(", ", headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray());
+                var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
                 _logger.LogDebug("No matching device profile found. {0}", headerString);
                 _logger.LogDebug("No matching device profile found. {0}", headerString);
             }
             }
 
 
@@ -280,10 +284,6 @@ namespace Emby.Dlna
             return false;
             return false;
         }
         }
 
 
-        private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
-
-        private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
-
         private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
         private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
         {
         {
             try
             try
@@ -387,7 +387,7 @@ namespace Emby.Dlna
 
 
             foreach (var name in _assembly.GetManifestResourceNames())
             foreach (var name in _assembly.GetManifestResourceNames())
             {
             {
-                if (!name.StartsWith(namespaceName))
+                if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
                 {
                 {
                     continue;
                     continue;
                 }
                 }
@@ -406,7 +406,7 @@ namespace Emby.Dlna
 
 
                         using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
                         using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
                         {
                         {
-                            await stream.CopyToAsync(fileStream);
+                            await stream.CopyToAsync(fileStream).ConfigureAwait(false);
                         }
                         }
                     }
                     }
                 }
                 }
@@ -509,7 +509,7 @@ namespace Emby.Dlna
             return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
             return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
         }
         }
 
 
-        class InternalProfileInfo
+        private class InternalProfileInfo
         {
         {
             internal DeviceProfileInfo Info { get; set; }
             internal DeviceProfileInfo Info { get; set; }
 
 

+ 9 - 5
Emby.Dlna/Eventing/EventManager.cs

@@ -152,11 +152,15 @@ namespace Emby.Dlna.Eventing
             builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
             builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
             foreach (var key in stateVariables.Keys)
             foreach (var key in stateVariables.Keys)
             {
             {
-                builder.Append("<e:property>");
-                builder.Append("<" + key + ">");
-                builder.Append(stateVariables[key]);
-                builder.Append("</" + key + ">");
-                builder.Append("</e:property>");
+                builder.Append("<e:property>")
+                    .Append('<')
+                    .Append(key)
+                    .Append('>')
+                    .Append(stateVariables[key])
+                    .Append("</")
+                    .Append(key)
+                    .Append('>')
+                    .Append("</e:property>");
             }
             }
 
 
             builder.Append("</e:propertyset>");
             builder.Append("</e:propertyset>");

+ 3 - 3
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -54,11 +54,11 @@ namespace Emby.Dlna.Main
         private SsdpDevicePublisher _publisher;
         private SsdpDevicePublisher _publisher;
         private ISsdpCommunicationsServer _communicationsServer;
         private ISsdpCommunicationsServer _communicationsServer;
 
 
-        internal IContentDirectory ContentDirectory { get; private set; }
+        public IContentDirectory ContentDirectory { get; private set; }
 
 
-        internal IConnectionManager ConnectionManager { get; private set; }
+        public IConnectionManager ConnectionManager { get; private set; }
 
 
-        internal IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
+        public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
 
 
         public static DlnaEntryPoint Current;
         public static DlnaEntryPoint Current;
 
 

+ 19 - 21
Emby.Dlna/PlayTo/Device.cs

@@ -4,12 +4,12 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
+using System.Security;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using System.Xml;
 using System.Xml;
 using System.Xml.Linq;
 using System.Xml.Linq;
 using Emby.Dlna.Common;
 using Emby.Dlna.Common;
-using Emby.Dlna.Server;
 using Emby.Dlna.Ssdp;
 using Emby.Dlna.Ssdp;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
@@ -19,6 +19,8 @@ namespace Emby.Dlna.PlayTo
 {
 {
     public class Device : IDisposable
     public class Device : IDisposable
     {
     {
+        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
         private Timer _timer;
         private Timer _timer;
 
 
         public DeviceInfo Properties { get; set; }
         public DeviceInfo Properties { get; set; }
@@ -55,16 +57,13 @@ namespace Emby.Dlna.PlayTo
 
 
         private readonly ILogger _logger;
         private readonly ILogger _logger;
 
 
-        private readonly IServerConfigurationManager _config;
-
         public Action OnDeviceUnavailable { get; set; }
         public Action OnDeviceUnavailable { get; set; }
 
 
-        public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config)
+        public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger)
         {
         {
             Properties = deviceProperties;
             Properties = deviceProperties;
             _httpClient = httpClient;
             _httpClient = httpClient;
             _logger = logger;
             _logger = logger;
-            _config = config;
         }
         }
 
 
         public void Start()
         public void Start()
@@ -275,7 +274,7 @@ namespace Emby.Dlna.PlayTo
                 throw new InvalidOperationException("Unable to find service");
                 throw new InvalidOperationException("Unable to find service");
             }
             }
 
 
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
+            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
 
 
             RestartTimer(true);
             RestartTimer(true);
@@ -285,7 +284,7 @@ namespace Emby.Dlna.PlayTo
         {
         {
             var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
             var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
 
 
-            url = url.Replace("&", "&amp;");
+            url = url.Replace("&", "&amp;", StringComparison.Ordinal);
 
 
             _logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
             _logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
 
 
@@ -297,8 +296,8 @@ namespace Emby.Dlna.PlayTo
 
 
             var dictionary = new Dictionary<string, string>
             var dictionary = new Dictionary<string, string>
             {
             {
-                {"CurrentURI", url},
-                {"CurrentURIMetaData", CreateDidlMeta(metaData)}
+                { "CurrentURI", url },
+                { "CurrentURIMetaData", CreateDidlMeta(metaData) }
             };
             };
 
 
             var service = GetAvTransportService();
             var service = GetAvTransportService();
@@ -334,7 +333,7 @@ namespace Emby.Dlna.PlayTo
                 return string.Empty;
                 return string.Empty;
             }
             }
 
 
-            return DescriptionXmlBuilder.Escape(value);
+            return SecurityElement.Escape(value);
         }
         }
 
 
         private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
         private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
@@ -732,10 +731,10 @@ namespace Emby.Dlna.PlayTo
             }
             }
 
 
             var trackUriElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
             var trackUriElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
-            var trackUri = trackUriElem == null ? null : trackUriElem.Value;
+            var trackUri = trackUriElem?.Value;
 
 
             var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
             var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
-            var duration = durationElem == null ? null : durationElem.Value;
+            var duration = durationElem?.Value;
 
 
             if (!string.IsNullOrWhiteSpace(duration)
             if (!string.IsNullOrWhiteSpace(duration)
                 && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
                 && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
@@ -748,7 +747,7 @@ namespace Emby.Dlna.PlayTo
             }
             }
 
 
             var positionElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
             var positionElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
-            var position = positionElem == null ? null : positionElem.Value;
+            var position = positionElem?.Value;
 
 
             if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
             if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
             {
             {
@@ -819,7 +818,7 @@ namespace Emby.Dlna.PlayTo
             // some devices send back invalid xml
             // some devices send back invalid xml
             try
             try
             {
             {
-                return XElement.Parse(xml.Replace("&", "&amp;"));
+                return XElement.Parse(xml.Replace("&", "&amp;", StringComparison.Ordinal));
             }
             }
             catch (XmlException)
             catch (XmlException)
             {
             {
@@ -848,7 +847,7 @@ namespace Emby.Dlna.PlayTo
                 ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
                 ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
                 Title = container.GetValue(uPnpNamespaces.title),
                 Title = container.GetValue(uPnpNamespaces.title),
                 IconUrl = container.GetValue(uPnpNamespaces.Artwork),
                 IconUrl = container.GetValue(uPnpNamespaces.Artwork),
-                SecondText = "",
+                SecondText = string.Empty,
                 Url = url,
                 Url = url,
                 ProtocolInfo = GetProtocolInfo(container),
                 ProtocolInfo = GetProtocolInfo(container),
                 MetaData = container.ToString()
                 MetaData = container.ToString()
@@ -941,12 +940,12 @@ namespace Emby.Dlna.PlayTo
                 return url;
                 return url;
             }
             }
 
 
-            if (!url.Contains("/"))
+            if (!url.Contains('/', StringComparison.Ordinal))
             {
             {
                 url = "/dmr/" + url;
                 url = "/dmr/" + url;
             }
             }
 
 
-            if (!url.StartsWith("/"))
+            if (!url.StartsWith("/", StringComparison.Ordinal))
             {
             {
                 url = "/" + url;
                 url = "/" + url;
             }
             }
@@ -981,7 +980,7 @@ namespace Emby.Dlna.PlayTo
             var deviceProperties = new DeviceInfo()
             var deviceProperties = new DeviceInfo()
             {
             {
                 Name = string.Join(" ", friendlyNames),
                 Name = string.Join(" ", friendlyNames),
-                BaseUrl = string.Format("http://{0}:{1}", url.Host, url.Port)
+                BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
             };
             };
 
 
             var model = document.Descendants(uPnpNamespaces.ud.GetName("modelName")).FirstOrDefault();
             var model = document.Descendants(uPnpNamespaces.ud.GetName("modelName")).FirstOrDefault();
@@ -1068,10 +1067,9 @@ namespace Emby.Dlna.PlayTo
                 }
                 }
             }
             }
 
 
-            return new Device(deviceProperties, httpClient, logger, config);
+            return new Device(deviceProperties, httpClient, logger);
         }
         }
 
 
-        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
         private static DeviceIcon CreateIcon(XElement element)
         private static DeviceIcon CreateIcon(XElement element)
         {
         {
             if (element == null)
             if (element == null)
@@ -1222,7 +1220,7 @@ namespace Emby.Dlna.PlayTo
 
 
         public override string ToString()
         public override string ToString()
         {
         {
-            return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);
+            return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl);
         }
         }
     }
     }
 }
 }

+ 9 - 8
Emby.Dlna/PlayTo/TransportCommands.cs

@@ -2,6 +2,7 @@
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Xml.Linq;
 using System.Xml.Linq;
 using Emby.Dlna.Common;
 using Emby.Dlna.Common;
@@ -11,14 +12,16 @@ namespace Emby.Dlna.PlayTo
 {
 {
     public class TransportCommands
     public class TransportCommands
     {
     {
+        private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
         private List<StateVariable> _stateVariables = new List<StateVariable>();
         private List<StateVariable> _stateVariables = new List<StateVariable>();
+        private List<ServiceAction> _serviceActions = new List<ServiceAction>();
+
         public List<StateVariable> StateVariables
         public List<StateVariable> StateVariables
         {
         {
             get => _stateVariables;
             get => _stateVariables;
             set => _stateVariables = value;
             set => _stateVariables = value;
         }
         }
 
 
-        private List<ServiceAction> _serviceActions = new List<ServiceAction>();
         public List<ServiceAction> ServiceActions
         public List<ServiceAction> ServiceActions
         {
         {
             get => _serviceActions;
             get => _serviceActions;
@@ -123,7 +126,7 @@ namespace Emby.Dlna.PlayTo
                 }
                 }
             }
             }
 
 
-            return string.Format(CommandBase, action.Name, xmlNamespace, stateString);
+            return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
         }
         }
 
 
         public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "")
         public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "")
@@ -147,7 +150,7 @@ namespace Emby.Dlna.PlayTo
                 }
                 }
             }
             }
 
 
-            return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
+            return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
         }
         }
 
 
         public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary)
         public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary)
@@ -170,7 +173,7 @@ namespace Emby.Dlna.PlayTo
                 }
                 }
             }
             }
 
 
-            return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
+            return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
         }
         }
 
 
         private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
         private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
@@ -183,12 +186,10 @@ namespace Emby.Dlna.PlayTo
                                  state.AllowedValues.FirstOrDefault() ??
                                  state.AllowedValues.FirstOrDefault() ??
                                  value;
                                  value;
 
 
-                return string.Format("<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
+                return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
             }
             }
 
 
-            return string.Format("<{0}>{1}</{0}>", argument.Name, value);
+            return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value);
         }
         }
-
-        private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
     }
     }
 }
 }

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

@@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
 
 
         public void AddXmlRootAttribute(string name, string value)
         public void AddXmlRootAttribute(string name, string value)
         {
         {
-            var atts = XmlRootAttributes ?? new XmlAttribute[] { };
+            var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>();
             var list = atts.ToList();
             var list = atts.ToList();
 
 
             list.Add(new XmlAttribute
             list.Add(new XmlAttribute

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 7 - 7
Emby.Drawing/ImageProcessor.cs

@@ -448,21 +448,21 @@ namespace Emby.Drawing
         /// or
         /// or
         /// filename.
         /// filename.
         /// </exception>
         /// </exception>
-        public string GetCachePath(string path, string filename)
+        public string GetCachePath(ReadOnlySpan<char> path, ReadOnlySpan<char> filename)
         {
         {
-            if (string.IsNullOrEmpty(path))
+            if (path.IsEmpty)
             {
             {
-                throw new ArgumentNullException(nameof(path));
+                throw new ArgumentException("Path can't be empty.", nameof(path));
             }
             }
 
 
-            if (string.IsNullOrEmpty(filename))
+            if (path.IsEmpty)
             {
             {
-                throw new ArgumentNullException(nameof(filename));
+                throw new ArgumentException("Filename can't be empty.", nameof(filename));
             }
             }
 
 
-            var prefix = filename.Substring(0, 1);
+            var prefix = filename.Slice(0, 1);
 
 
-            return Path.Combine(path, prefix, filename);
+            return Path.Join(path, prefix, filename);
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />

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

@@ -136,8 +136,8 @@ namespace Emby.Naming.Common
 
 
             CleanDateTimes = new[]
             CleanDateTimes = new[]
             {
             {
-                @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*",
-                @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*"
+                @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
+                @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
             };
             };
 
 
             CleanStrings = new[]
             CleanStrings = new[]
@@ -277,7 +277,7 @@ namespace Emby.Naming.Common
                 // This isn't a Kodi naming rule, but the expression below causes false positives,
                 // This isn't a Kodi naming rule, but the expression below causes false positives,
                 // so we make sure this one gets tested first.
                 // so we make sure this one gets tested first.
                 // "Foo Bar 889"
                 // "Foo Bar 889"
-                new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/x]*$")
+                new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
                 },
                 },
@@ -300,32 +300,32 @@ namespace Emby.Naming.Common
                 // *** End Kodi Standard Naming
                 // *** End Kodi Standard Naming
 
 
                 // [bar] Foo - 1 [baz]
                 // [bar] Foo - 1 [baz]
-                new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$")
+                new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
                 },
                 },
-                new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d+)[xX](?<epnumber>\d+)[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
-                new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>\d+)[x,X]?[eE](?<epnumber>\d+)[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>[0-9]+)[x,X]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
-                new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d+))[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]+))[^\\\/]*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
-                new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d+)[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
                 // "01.avi"
                 // "01.avi"
-                new EpisodeExpression(@".*[\\\/](?<epnumber>\d+)(-(?<endingepnumber>\d+))*\.\w+$")
+                new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))*\.\w+$")
                 {
                 {
                     IsOptimistic = true,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
@@ -335,34 +335,34 @@ namespace Emby.Naming.Common
                 new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
                 new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
 
 
                 // "01 - blah.avi", "01-blah.avi"
                 // "01 - blah.avi", "01-blah.avi"
-                new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\s?-\s?[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
                 {
                 {
                     IsOptimistic = true,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
                 // "01.blah.avi"
                 // "01.blah.avi"
-                new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\.[^\\\/]+$")
+                new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\.[^\\\/]+$")
                 {
                 {
                     IsOptimistic = true,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
                 // "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah"
                 // "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah"
-                new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
+                new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
                 {
                 {
                     IsOptimistic = true,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
                 // "01 episode title.avi"
                 // "01 episode title.avi"
-                new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>\d{1,3})([^\\\/]*)$")
+                new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>[0-9]{1,3})([^\\\/]*)$")
                 {
                 {
                     IsOptimistic = true,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
                 },
                 },
                 // "Episode 16", "Episode 16 - Title"
                 // "Episode 16", "Episode 16 - Title"
-                new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
+                new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
                 {
                 {
                     IsOptimistic = true,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
@@ -625,17 +625,17 @@ namespace Emby.Naming.Common
             AudioBookPartsExpressions = new[]
             AudioBookPartsExpressions = new[]
             {
             {
                 // Detect specified chapters, like CH 01
                 // Detect specified chapters, like CH 01
-                @"ch(?:apter)?[\s_-]?(?<chapter>\d+)",
+                @"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
                 // Detect specified parts, like Part 02
                 // Detect specified parts, like Part 02
-                @"p(?:ar)?t[\s_-]?(?<part>\d+)",
+                @"p(?:ar)?t[\s_-]?(?<part>[0-9]+)",
                 // Chapter is often beginning of filename
                 // Chapter is often beginning of filename
-                @"^(?<chapter>\d+)",
+                "^(?<chapter>[0-9]+)",
                 // Part if often ending of filename
                 // Part if often ending of filename
-                @"(?<part>\d+)$",
+                "(?<part>[0-9]+)$",
                 // Sometimes named as 0001_005 (chapter_part)
                 // Sometimes named as 0001_005 (chapter_part)
-                @"(?<chapter>\d+)_(?<part>\d+)",
+                "(?<chapter>[0-9]+)_(?<part>[0-9]+)",
                 // Some audiobooks are ripped from cd's, and will be named by disk number.
                 // Some audiobooks are ripped from cd's, and will be named by disk number.
-                @"dis(?:c|k)[\s_-]?(?<chapter>\d+)"
+                @"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
             };
             };
 
 
             var extensions = VideoFileExtensions.ToList();
             var extensions = VideoFileExtensions.ToList();
@@ -675,16 +675,16 @@ namespace Emby.Naming.Common
 
 
             MultipleEpisodeExpressions = new string[]
             MultipleEpisodeExpressions = new string[]
             {
             {
-                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[eExX](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})(-[xE]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$"
+                @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})(-[xE]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$"
             }.Select(i => new EpisodeExpression(i)
             }.Select(i => new EpisodeExpression(i)
             {
             {
                 IsNamed = true
                 IsNamed = true

+ 1 - 1
Emby.Naming/TV/SeasonPathParser.cs

@@ -77,7 +77,7 @@ namespace Emby.Naming.TV
 
 
             if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase))
             if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase))
             {
             {
-                var testFilename = filename.Substring(1);
+                var testFilename = filename.AsSpan().Slice(1);
 
 
                 if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
                 if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
                 {
                 {

+ 0 - 191
Emby.Notifications/Api/NotificationsService.cs

@@ -1,191 +0,0 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1402
-#pragma warning disable SA1649
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Notifications;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Notifications;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Notifications.Api
-{
-    [Route("/Notifications/{UserId}", "GET", Summary = "Gets notifications")]
-    public class GetNotifications : IReturn<NotificationResult>
-    {
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UserId { get; set; } = string.Empty;
-
-        [ApiMember(Name = "IsRead", Description = "An optional filter by IsRead", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsRead { get; set; }
-
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-    }
-
-    public class Notification
-    {
-        public string Id { get; set; } = string.Empty;
-
-        public string UserId { get; set; } = string.Empty;
-
-        public DateTime Date { get; set; }
-
-        public bool IsRead { get; set; }
-
-        public string Name { get; set; } = string.Empty;
-
-        public string Description { get; set; } = string.Empty;
-
-        public string Url { get; set; } = string.Empty;
-
-        public NotificationLevel Level { get; set; }
-    }
-
-    public class NotificationResult
-    {
-        public IReadOnlyList<Notification> Notifications { get; set; } = Array.Empty<Notification>();
-
-        public int TotalRecordCount { get; set; }
-    }
-
-    public class NotificationsSummary
-    {
-        public int UnreadCount { get; set; }
-
-        public NotificationLevel MaxUnreadNotificationLevel { get; set; }
-    }
-
-    [Route("/Notifications/{UserId}/Summary", "GET", Summary = "Gets a notification summary for a user")]
-    public class GetNotificationsSummary : IReturn<NotificationsSummary>
-    {
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UserId { get; set; } = string.Empty;
-    }
-
-    [Route("/Notifications/Types", "GET", Summary = "Gets notification types")]
-    public class GetNotificationTypes : IReturn<List<NotificationTypeInfo>>
-    {
-    }
-
-    [Route("/Notifications/Services", "GET", Summary = "Gets notification types")]
-    public class GetNotificationServices : IReturn<List<NameIdPair>>
-    {
-    }
-
-    [Route("/Notifications/Admin", "POST", Summary = "Sends a notification to all admin users")]
-    public class AddAdminNotification : IReturnVoid
-    {
-        [ApiMember(Name = "Name", Description = "The notification's name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Name { get; set; } = string.Empty;
-
-        [ApiMember(Name = "Description", Description = "The notification's description", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Description { get; set; } = string.Empty;
-
-        [ApiMember(Name = "ImageUrl", Description = "The notification's image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string? ImageUrl { get; set; }
-
-        [ApiMember(Name = "Url", Description = "The notification's info url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string? Url { get; set; }
-
-        [ApiMember(Name = "Level", Description = "The notification level", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public NotificationLevel Level { get; set; }
-    }
-
-    [Route("/Notifications/{UserId}/Read", "POST", Summary = "Marks notifications as read")]
-    public class MarkRead : IReturnVoid
-    {
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; } = string.Empty;
-
-        [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
-        public string Ids { get; set; } = string.Empty;
-    }
-
-    [Route("/Notifications/{UserId}/Unread", "POST", Summary = "Marks notifications as unread")]
-    public class MarkUnread : IReturnVoid
-    {
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; } = string.Empty;
-
-        [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
-        public string Ids { get; set; } = string.Empty;
-    }
-
-    [Authenticated]
-    public class NotificationsService : IService
-    {
-        private readonly INotificationManager _notificationManager;
-        private readonly IUserManager _userManager;
-
-        public NotificationsService(INotificationManager notificationManager, IUserManager userManager)
-        {
-            _notificationManager = notificationManager;
-            _userManager = userManager;
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetNotificationTypes request)
-        {
-            return _notificationManager.GetNotificationTypes();
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetNotificationServices request)
-        {
-            return _notificationManager.GetNotificationServices().ToList();
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetNotificationsSummary request)
-        {
-            return new NotificationsSummary();
-        }
-
-        public Task Post(AddAdminNotification request)
-        {
-            // This endpoint really just exists as post of a real with sickbeard
-            var notification = new NotificationRequest
-            {
-                Date = DateTime.UtcNow,
-                Description = request.Description,
-                Level = request.Level,
-                Name = request.Name,
-                Url = request.Url,
-                UserIds = _userManager.Users
-                    .Where(user => user.HasPermission(PermissionKind.IsAdministrator))
-                    .Select(user => user.Id)
-                    .ToArray()
-            };
-
-            return _notificationManager.SendNotification(notification, CancellationToken.None);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public void Post(MarkRead request)
-        {
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public void Post(MarkUnread request)
-        {
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetNotifications request)
-        {
-            return new NotificationResult();
-        }
-    }
-}

+ 11 - 5
Emby.Server.Implementations/AppBase/ConfigurationHelper.cs

@@ -1,3 +1,5 @@
+#nullable enable
+
 using System;
 using System;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
@@ -22,7 +24,7 @@ namespace Emby.Server.Implementations.AppBase
         {
         {
             object configuration;
             object configuration;
 
 
-            byte[] buffer = null;
+            byte[]? buffer = null;
 
 
             // Use try/catch to avoid the extra file system lookup using File.Exists
             // Use try/catch to avoid the extra file system lookup using File.Exists
             try
             try
@@ -36,19 +38,23 @@ namespace Emby.Server.Implementations.AppBase
                 configuration = Activator.CreateInstance(type);
                 configuration = Activator.CreateInstance(type);
             }
             }
 
 
-            using var stream = new MemoryStream();
+            using var stream = new MemoryStream(buffer?.Length ?? 0);
             xmlSerializer.SerializeToStream(configuration, stream);
             xmlSerializer.SerializeToStream(configuration, stream);
 
 
             // Take the object we just got and serialize it back to bytes
             // Take the object we just got and serialize it back to bytes
-            var newBytes = stream.ToArray();
+            byte[] newBytes = stream.GetBuffer();
+            int newBytesLen = (int)stream.Length;
 
 
             // If the file didn't exist before, or if something has changed, re-save
             // If the file didn't exist before, or if something has changed, re-save
-            if (buffer == null || !buffer.SequenceEqual(newBytes))
+            if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
             {
             {
                 Directory.CreateDirectory(Path.GetDirectoryName(path));
                 Directory.CreateDirectory(Path.GetDirectoryName(path));
 
 
                 // Save it after load in case we got new items
                 // Save it after load in case we got new items
-                File.WriteAllBytes(path, newBytes);
+                using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
+                {
+                    fs.Write(newBytes, 0, newBytesLen);
+                }
             }
             }
 
 
             return configuration;
             return configuration;

+ 24 - 30
Emby.Server.Implementations/ApplicationHost.cs

@@ -4,7 +4,6 @@ using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics;
-using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Net;
 using System.Net;
@@ -43,10 +42,10 @@ using Emby.Server.Implementations.Security;
 using Emby.Server.Implementations.Serialization;
 using Emby.Server.Implementations.Serialization;
 using Emby.Server.Implementations.Services;
 using Emby.Server.Implementations.Services;
 using Emby.Server.Implementations.Session;
 using Emby.Server.Implementations.Session;
+using Emby.Server.Implementations.SyncPlay;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.Updates;
 using Emby.Server.Implementations.Updates;
-using Emby.Server.Implementations.SyncPlay;
-using MediaBrowser.Api;
+using Jellyfin.Api.Helpers;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Events;
@@ -78,8 +77,8 @@ using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Controller.Subtitles;
-using MediaBrowser.Controller.TV;
 using MediaBrowser.Controller.SyncPlay;
 using MediaBrowser.Controller.SyncPlay;
+using MediaBrowser.Controller.TV;
 using MediaBrowser.LocalMetadata.Savers;
 using MediaBrowser.LocalMetadata.Savers;
 using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
@@ -97,7 +96,6 @@ using MediaBrowser.Providers.Chapters;
 using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Plugins.TheTvdb;
 using MediaBrowser.Providers.Plugins.TheTvdb;
 using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.Providers.Subtitles;
-using MediaBrowser.WebDashboard.Api;
 using MediaBrowser.XbmcMetadata.Providers;
 using MediaBrowser.XbmcMetadata.Providers;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
@@ -192,7 +190,7 @@ namespace Emby.Server.Implementations
         /// Gets or sets the application paths.
         /// Gets or sets the application paths.
         /// </summary>
         /// </summary>
         /// <value>The application paths.</value>
         /// <value>The application paths.</value>
-        protected ServerApplicationPaths ApplicationPaths { get; set; }
+        protected IServerApplicationPaths ApplicationPaths { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets all concrete types.
         /// Gets or sets all concrete types.
@@ -236,7 +234,7 @@ namespace Emby.Server.Implementations
         /// Initializes a new instance of the <see cref="ApplicationHost" /> class.
         /// Initializes a new instance of the <see cref="ApplicationHost" /> class.
         /// </summary>
         /// </summary>
         public ApplicationHost(
         public ApplicationHost(
-            ServerApplicationPaths applicationPaths,
+            IServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
             ILoggerFactory loggerFactory,
             IStartupOptions options,
             IStartupOptions options,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
@@ -484,12 +482,10 @@ namespace Emby.Server.Implementations
 
 
                 foreach (var plugin in Plugins)
                 foreach (var plugin in Plugins)
                 {
                 {
-                    pluginBuilder.AppendLine(
-                        string.Format(
-                            CultureInfo.InvariantCulture,
-                            "{0} {1}",
-                            plugin.Name,
-                            plugin.Version));
+                    pluginBuilder.Append(plugin.Name)
+                        .Append(' ')
+                        .Append(plugin.Version)
+                        .AppendLine();
                 }
                 }
 
 
                 Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
                 Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
@@ -556,8 +552,6 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
             serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
             serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
             serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
 
 
-            serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
-
             serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
             serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
 
 
             serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
             serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
@@ -636,6 +630,11 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton<EncodingHelper>();
             serviceCollection.AddSingleton<EncodingHelper>();
 
 
             serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
             serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
+
+            serviceCollection.AddSingleton<TranscodingJobHelper>();
+            serviceCollection.AddScoped<MediaInfoHelper>();
+            serviceCollection.AddScoped<AudioHelper>();
+            serviceCollection.AddScoped<DynamicHlsHelper>();
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -652,7 +651,6 @@ namespace Emby.Server.Implementations
             _httpServer = Resolve<IHttpServer>();
             _httpServer = Resolve<IHttpServer>();
             _httpClient = Resolve<IHttpClient>();
             _httpClient = Resolve<IHttpClient>();
 
 
-            ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
 
 
             SetStaticProperties();
             SetStaticProperties();
@@ -797,7 +795,6 @@ namespace Emby.Server.Implementations
             Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
             Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
 
 
             Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
             Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
-            Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
 
 
             Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
             Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
         }
         }
@@ -871,6 +868,11 @@ namespace Emby.Server.Implementations
                     Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
                     Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
                     continue;
                     continue;
                 }
                 }
+                catch (TypeLoadException ex)
+                {
+                    Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
+                    continue;
+                }
 
 
                 foreach (Type type in exportedTypes)
                 foreach (Type type in exportedTypes)
                 {
                 {
@@ -1032,12 +1034,6 @@ namespace Emby.Server.Implementations
                 }
                 }
             }
             }
 
 
-            // Include composable parts in the Api assembly
-            yield return typeof(ApiEntryPoint).Assembly;
-
-            // Include composable parts in the Dashboard assembly
-            yield return typeof(DashboardService).Assembly;
-
             // Include composable parts in the Model assembly
             // Include composable parts in the Model assembly
             yield return typeof(SystemInfo).Assembly;
             yield return typeof(SystemInfo).Assembly;
 
 
@@ -1153,7 +1149,7 @@ namespace Emby.Server.Implementations
                     return null;
                     return null;
                 }
                 }
 
 
-                return GetLocalApiUrl(addresses.First());
+                return GetLocalApiUrl(addresses[0]);
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
@@ -1226,7 +1222,7 @@ namespace Emby.Server.Implementations
             var addresses = ServerConfigurationManager
             var addresses = ServerConfigurationManager
                 .Configuration
                 .Configuration
                 .LocalNetworkAddresses
                 .LocalNetworkAddresses
-                .Select(NormalizeConfiguredLocalAddress)
+                .Select(x => NormalizeConfiguredLocalAddress(x))
                 .Where(i => i != null)
                 .Where(i => i != null)
                 .ToList();
                 .ToList();
 
 
@@ -1247,8 +1243,7 @@ namespace Emby.Server.Implementations
                     }
                     }
                 }
                 }
 
 
-                var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
-                if (valid)
+                if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false))
                 {
                 {
                     resultList.Add(address);
                     resultList.Add(address);
 
 
@@ -1262,13 +1257,12 @@ namespace Emby.Server.Implementations
             return resultList;
             return resultList;
         }
         }
 
 
-        public IPAddress NormalizeConfiguredLocalAddress(string address)
+        public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan<char> address)
         {
         {
             var index = address.Trim('/').IndexOf('/');
             var index = address.Trim('/').IndexOf('/');
-
             if (index != -1)
             if (index != -1)
             {
             {
-                address = address.Substring(index + 1);
+                address = address.Slice(index + 1);
             }
             }
 
 
             if (IPAddress.TryParse(address.Trim('/'), out IPAddress result))
             if (IPAddress.TryParse(address.Trim('/'), out IPAddress result))

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

@@ -1,5 +1,7 @@
 using System;
 using System;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.Browser
 namespace Emby.Server.Implementations.Browser
@@ -24,7 +26,7 @@ namespace Emby.Server.Implementations.Browser
         /// <param name="appHost">The app host.</param>
         /// <param name="appHost">The app host.</param>
         public static void OpenSwaggerPage(IServerApplicationHost appHost)
         public static void OpenSwaggerPage(IServerApplicationHost appHost)
         {
         {
-            TryOpenUrl(appHost, "/swagger/index.html");
+            TryOpenUrl(appHost, "/api-docs/swagger");
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 10 - 14
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -1,5 +1,4 @@
 using System;
 using System;
-using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
@@ -7,6 +6,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
@@ -22,6 +22,7 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
 using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
@@ -45,10 +46,7 @@ namespace Emby.Server.Implementations.Channels
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IProviderManager _providerManager;
         private readonly IProviderManager _providerManager;
-
-        private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
-            new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
-
+        private readonly IMemoryCache _memoryCache;
         private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
         private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
 
 
         /// <summary>
         /// <summary>
@@ -63,6 +61,7 @@ namespace Emby.Server.Implementations.Channels
         /// <param name="userDataManager">The user data manager.</param>
         /// <param name="userDataManager">The user data manager.</param>
         /// <param name="jsonSerializer">The JSON serializer.</param>
         /// <param name="jsonSerializer">The JSON serializer.</param>
         /// <param name="providerManager">The provider manager.</param>
         /// <param name="providerManager">The provider manager.</param>
+        /// <param name="memoryCache">The memory cache.</param>
         public ChannelManager(
         public ChannelManager(
             IUserManager userManager,
             IUserManager userManager,
             IDtoService dtoService,
             IDtoService dtoService,
@@ -72,7 +71,8 @@ namespace Emby.Server.Implementations.Channels
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             IUserDataManager userDataManager,
             IUserDataManager userDataManager,
             IJsonSerializer jsonSerializer,
             IJsonSerializer jsonSerializer,
-            IProviderManager providerManager)
+            IProviderManager providerManager,
+            IMemoryCache memoryCache)
         {
         {
             _userManager = userManager;
             _userManager = userManager;
             _dtoService = dtoService;
             _dtoService = dtoService;
@@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Channels
             _userDataManager = userDataManager;
             _userDataManager = userDataManager;
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
             _providerManager = providerManager;
             _providerManager = providerManager;
+            _memoryCache = memoryCache;
         }
         }
 
 
         internal IChannel[] Channels { get; private set; }
         internal IChannel[] Channels { get; private set; }
@@ -417,20 +418,15 @@ namespace Emby.Server.Implementations.Channels
 
 
         private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
         private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
         {
         {
-            if (_channelItemMediaInfo.TryGetValue(id, out Tuple<DateTime, List<MediaSourceInfo>> cachedInfo))
+            if (_memoryCache.TryGetValue(id, out List<MediaSourceInfo> cachedInfo))
             {
             {
-                if ((DateTime.UtcNow - cachedInfo.Item1).TotalMinutes < 5)
-                {
-                    return cachedInfo.Item2;
-                }
+                return cachedInfo;
             }
             }
 
 
             var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
             var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
                    .ConfigureAwait(false);
                    .ConfigureAwait(false);
             var list = mediaInfo.ToList();
             var list = mediaInfo.ToList();
-
-            var item2 = new Tuple<DateTime, List<MediaSourceInfo>>(DateTime.UtcNow, list);
-            _channelItemMediaInfo.AddOrUpdate(id, item2, (key, oldValue) => item2);
+            _memoryCache.Set(id, list, DateTimeOffset.UtcNow.AddMinutes(5));
 
 
             return list;
             return list;
         }
         }

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

@@ -1,6 +1,5 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
 using Emby.Server.Implementations.HttpServer;
 using Emby.Server.Implementations.HttpServer;
-using Emby.Server.Implementations.Updates;
 using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
 using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
 
 
 namespace Emby.Server.Implementations
 namespace Emby.Server.Implementations
@@ -19,7 +18,8 @@ namespace Emby.Server.Implementations
             { HttpListenerHost.DefaultRedirectKey, "web/index.html" },
             { HttpListenerHost.DefaultRedirectKey, "web/index.html" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegAnalyzeDurationKey, "200M" },
             { FfmpegAnalyzeDurationKey, "200M" },
-            { PlaylistsAllowDuplicatesKey, bool.TrueString }
+            { PlaylistsAllowDuplicatesKey, bool.TrueString },
+            { BindToUnixSocketKey, bool.FalseString }
         };
         };
     }
     }
 }
 }

+ 0 - 225
Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs

@@ -1,225 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text.Json;
-using System.Threading;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Data
-{
-    /// <summary>
-    /// Class SQLiteDisplayPreferencesRepository.
-    /// </summary>
-    public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository
-    {
-        private readonly IFileSystem _fileSystem;
-
-        private readonly JsonSerializerOptions _jsonOptions;
-
-        public SqliteDisplayPreferencesRepository(ILogger<SqliteDisplayPreferencesRepository> logger, IApplicationPaths appPaths, IFileSystem fileSystem)
-            : base(logger)
-        {
-            _fileSystem = fileSystem;
-
-            _jsonOptions = JsonDefaults.GetOptions();
-
-            DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db");
-        }
-
-        /// <summary>
-        /// Gets the name of the repository.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name => "SQLite";
-
-        public void Initialize()
-        {
-            try
-            {
-                InitializeInternal();
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error loading database file. Will reset and retry.");
-
-                _fileSystem.DeleteFile(DbFilePath);
-
-                InitializeInternal();
-            }
-        }
-
-        /// <summary>
-        /// Opens the connection to the database.
-        /// </summary>
-        /// <returns>Task.</returns>
-        private void InitializeInternal()
-        {
-            string[] queries =
-            {
-                "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)",
-                "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)"
-            };
-
-            using (var connection = GetConnection())
-            {
-                connection.RunQueries(queries);
-            }
-        }
-
-        /// <summary>
-        /// Save the display preferences associated with an item in the repo.
-        /// </summary>
-        /// <param name="displayPreferences">The display preferences.</param>
-        /// <param name="userId">The user id.</param>
-        /// <param name="client">The client.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken)
-        {
-            if (displayPreferences == null)
-            {
-                throw new ArgumentNullException(nameof(displayPreferences));
-            }
-
-            if (string.IsNullOrEmpty(displayPreferences.Id))
-            {
-                throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
-            }
-
-            cancellationToken.ThrowIfCancellationRequested();
-
-            using (var connection = GetConnection())
-            {
-                connection.RunInTransaction(
-                    db => SaveDisplayPreferences(displayPreferences, userId, client, db),
-                    TransactionMode);
-            }
-        }
-
-        private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection)
-        {
-            var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions);
-
-            using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)"))
-            {
-                statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray());
-                statement.TryBind("@userId", userId.ToByteArray());
-                statement.TryBind("@client", client);
-                statement.TryBind("@data", serialized);
-
-                statement.MoveNext();
-            }
-        }
-
-        /// <summary>
-        /// Save all display preferences associated with a user in the repo.
-        /// </summary>
-        /// <param name="displayPreferences">The display preferences.</param>
-        /// <param name="userId">The user id.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, CancellationToken cancellationToken)
-        {
-            if (displayPreferences == null)
-            {
-                throw new ArgumentNullException(nameof(displayPreferences));
-            }
-
-            cancellationToken.ThrowIfCancellationRequested();
-
-            using (var connection = GetConnection())
-            {
-                connection.RunInTransaction(
-                    db =>
-                    {
-                        foreach (var displayPreference in displayPreferences)
-                        {
-                            SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
-                        }
-                    },
-                    TransactionMode);
-            }
-        }
-
-        /// <summary>
-        /// Gets the display preferences.
-        /// </summary>
-        /// <param name="displayPreferencesId">The display preferences id.</param>
-        /// <param name="userId">The user id.</param>
-        /// <param name="client">The client.</param>
-        /// <returns>Task{DisplayPreferences}.</returns>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client)
-        {
-            if (string.IsNullOrEmpty(displayPreferencesId))
-            {
-                throw new ArgumentNullException(nameof(displayPreferencesId));
-            }
-
-            var guidId = displayPreferencesId.GetMD5();
-
-            using (var connection = GetConnection(true))
-            {
-                using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
-                {
-                    statement.TryBind("@id", guidId.ToByteArray());
-                    statement.TryBind("@userId", userId.ToByteArray());
-                    statement.TryBind("@client", client);
-
-                    foreach (var row in statement.ExecuteQuery())
-                    {
-                        return Get(row);
-                    }
-                }
-            }
-
-            return new DisplayPreferences
-            {
-                Id = guidId.ToString("N", CultureInfo.InvariantCulture)
-            };
-        }
-
-        /// <summary>
-        /// Gets all display preferences for the given user.
-        /// </summary>
-        /// <param name="userId">The user id.</param>
-        /// <returns>Task{DisplayPreferences}.</returns>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId)
-        {
-            var list = new List<DisplayPreferences>();
-
-            using (var connection = GetConnection(true))
-            using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
-            {
-                statement.TryBind("@userId", userId.ToByteArray());
-
-                foreach (var row in statement.ExecuteQuery())
-                {
-                    list.Add(Get(row));
-                }
-            }
-
-            return list;
-        }
-
-        private DisplayPreferences Get(IReadOnlyList<IResultSetValue> row)
-            => JsonSerializer.Deserialize<DisplayPreferences>(row[0].ToBlob(), _jsonOptions);
-
-        public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken)
-            => SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken);
-
-        public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client)
-            => GetDisplayPreferences(displayPreferencesId, new Guid(userId), client);
-    }
-}

+ 85 - 164
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -9,6 +9,7 @@ using System.Text;
 using System.Text.Json;
 using System.Text.Json;
 using System.Threading;
 using System.Threading;
 using Emby.Server.Implementations.Playlists;
 using Emby.Server.Implementations.Playlists;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Json;
 using MediaBrowser.Common.Json;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
@@ -400,6 +401,8 @@ namespace Emby.Server.Implementations.Data
             "OwnerId"
             "OwnerId"
         };
         };
 
 
+        private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
+
         private static readonly string[] _mediaStreamSaveColumns =
         private static readonly string[] _mediaStreamSaveColumns =
         {
         {
             "ItemId",
             "ItemId",
@@ -439,6 +442,12 @@ namespace Emby.Server.Implementations.Data
             "ColorTransfer"
             "ColorTransfer"
         };
         };
 
 
+        private static readonly string _mediaStreamSaveColumnsInsertQuery =
+            $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
+
+        private static readonly string _mediaStreamSaveColumnsSelectQuery =
+            $"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
+
         private static readonly string[] _mediaAttachmentSaveColumns =
         private static readonly string[] _mediaAttachmentSaveColumns =
         {
         {
             "ItemId",
             "ItemId",
@@ -450,102 +459,15 @@ namespace Emby.Server.Implementations.Data
             "MIMEType"
             "MIMEType"
         };
         };
 
 
-        private static readonly string _mediaAttachmentInsertPrefix;
-
-        private static string GetSaveItemCommandText()
-        {
-            var saveColumns = new[]
-            {
-                "guid",
-                "type",
-                "data",
-                "Path",
-                "StartDate",
-                "EndDate",
-                "ChannelId",
-                "IsMovie",
-                "IsSeries",
-                "EpisodeTitle",
-                "IsRepeat",
-                "CommunityRating",
-                "CustomRating",
-                "IndexNumber",
-                "IsLocked",
-                "Name",
-                "OfficialRating",
-                "MediaType",
-                "Overview",
-                "ParentIndexNumber",
-                "PremiereDate",
-                "ProductionYear",
-                "ParentId",
-                "Genres",
-                "InheritedParentalRatingValue",
-                "SortName",
-                "ForcedSortName",
-                "RunTimeTicks",
-                "Size",
-                "DateCreated",
-                "DateModified",
-                "PreferredMetadataLanguage",
-                "PreferredMetadataCountryCode",
-                "Width",
-                "Height",
-                "DateLastRefreshed",
-                "DateLastSaved",
-                "IsInMixedFolder",
-                "LockedFields",
-                "Studios",
-                "Audio",
-                "ExternalServiceId",
-                "Tags",
-                "IsFolder",
-                "UnratedType",
-                "TopParentId",
-                "TrailerTypes",
-                "CriticRating",
-                "CleanName",
-                "PresentationUniqueKey",
-                "OriginalTitle",
-                "PrimaryVersionId",
-                "DateLastMediaAdded",
-                "Album",
-                "IsVirtualItem",
-                "SeriesName",
-                "UserDataKey",
-                "SeasonName",
-                "SeasonId",
-                "SeriesId",
-                "ExternalSeriesId",
-                "Tagline",
-                "ProviderIds",
-                "Images",
-                "ProductionLocations",
-                "ExtraIds",
-                "TotalBitrate",
-                "ExtraType",
-                "Artists",
-                "AlbumArtists",
-                "ExternalId",
-                "SeriesPresentationUniqueKey",
-                "ShowId",
-                "OwnerId"
-            };
-
-            var saveItemCommandCommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns) + ") values (";
+        private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
+            $"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
 
 
-            for (var i = 0; i < saveColumns.Length; i++)
-            {
-                if (i != 0)
-                {
-                    saveItemCommandCommandText += ",";
-                }
-
-                saveItemCommandCommandText += "@" + saveColumns[i];
-            }
+        private static readonly string _mediaAttachmentInsertPrefix;
 
 
-            return saveItemCommandCommandText + ")";
-        }
+        private const string SaveItemCommandText =
+            @"replace into TypedBaseItems
+            (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
+            values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
 
 
         /// <summary>
         /// <summary>
         /// Save a standard item in the repo.
         /// Save a standard item in the repo.
@@ -636,7 +558,7 @@ namespace Emby.Server.Implementations.Data
         {
         {
             var statements = PrepareAll(db, new string[]
             var statements = PrepareAll(db, new string[]
             {
             {
-                GetSaveItemCommandText(),
+                SaveItemCommandText,
                 "delete from AncestorIds where ItemId=@ItemId"
                 "delete from AncestorIds where ItemId=@ItemId"
             }).ToList();
             }).ToList();
 
 
@@ -1056,7 +978,10 @@ namespace Emby.Server.Implementations.Data
                     continue;
                     continue;
                 }
                 }
 
 
-                str.Append($"{i.Key}={i.Value}|");
+                str.Append(i.Key)
+                    .Append('=')
+                    .Append(i.Value)
+                    .Append('|');
             }
             }
 
 
             if (str.Length == 0)
             if (str.Length == 0)
@@ -1110,7 +1035,8 @@ namespace Emby.Server.Implementations.Data
                     continue;
                     continue;
                 }
                 }
 
 
-                str.Append(ToValueString(i) + "|");
+                AppendItemImageInfo(str, i);
+                str.Append('|');
             }
             }
 
 
             str.Length -= 1; // Remove last |
             str.Length -= 1; // Remove last |
@@ -1144,26 +1070,26 @@ namespace Emby.Server.Implementations.Data
             item.ImageInfos = list.ToArray();
             item.ImageInfos = list.ToArray();
         }
         }
 
 
-        public string ToValueString(ItemImageInfo image)
+        public void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
         {
         {
-            const string Delimeter = "*";
+            const char Delimiter = '*';
 
 
             var path = image.Path ?? string.Empty;
             var path = image.Path ?? string.Empty;
             var hash = image.BlurHash ?? string.Empty;
             var hash = image.BlurHash ?? string.Empty;
 
 
-            return GetPathToSave(path) +
-                   Delimeter +
-                   image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) +
-                   Delimeter +
-                   image.Type +
-                   Delimeter +
-                   image.Width.ToString(CultureInfo.InvariantCulture) +
-                   Delimeter +
-                   image.Height.ToString(CultureInfo.InvariantCulture) +
-                   Delimeter +
-                   // Replace delimiters with other characters.
-                   // This can be removed when we migrate to a proper DB.
-                   hash.Replace('*', '/').Replace('|', '\\');
+            bldr.Append(GetPathToSave(path))
+                .Append(Delimiter)
+                .Append(image.DateModified.Ticks)
+                .Append(Delimiter)
+                .Append(image.Type)
+                .Append(Delimiter)
+                .Append(image.Width)
+                .Append(Delimiter)
+                .Append(image.Height)
+                .Append(Delimiter)
+                // Replace delimiters with other characters.
+                // This can be removed when we migrate to a proper DB.
+                .Append(hash.Replace('*', '/').Replace('|', '\\'));
         }
         }
 
 
         public ItemImageInfo ItemImageInfoFromValueString(string value)
         public ItemImageInfo ItemImageInfoFromValueString(string value)
@@ -1225,7 +1151,7 @@ namespace Emby.Server.Implementations.Data
 
 
             using (var connection = GetConnection(true))
             using (var connection = GetConnection(true))
             {
             {
-                using (var statement = PrepareStatement(connection, "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where guid = @guid"))
+                using (var statement = PrepareStatement(connection, _retriveItemColumnsSelectQuery))
                 {
                 {
                     statement.TryBind("@guid", id);
                     statement.TryBind("@guid", id);
 
 
@@ -2471,7 +2397,7 @@ namespace Emby.Server.Implementations.Data
                 var item = query.SimilarTo;
                 var item = query.SimilarTo;
 
 
                 var builder = new StringBuilder();
                 var builder = new StringBuilder();
-                builder.Append("(");
+                builder.Append('(');
 
 
                 if (string.IsNullOrEmpty(item.OfficialRating))
                 if (string.IsNullOrEmpty(item.OfficialRating))
                 {
                 {
@@ -2509,7 +2435,7 @@ namespace Emby.Server.Implementations.Data
             if (!string.IsNullOrEmpty(query.SearchTerm))
             if (!string.IsNullOrEmpty(query.SearchTerm))
             {
             {
                 var builder = new StringBuilder();
                 var builder = new StringBuilder();
-                builder.Append("(");
+                builder.Append('(');
 
 
                 builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
                 builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
 
 
@@ -2775,82 +2701,82 @@ namespace Emby.Server.Implementations.Data
 
 
         private string FixUnicodeChars(string buffer)
         private string FixUnicodeChars(string buffer)
         {
         {
-            if (buffer.IndexOf('\u2013') > -1)
+            if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1)
             {
             {
                 buffer = buffer.Replace('\u2013', '-'); // en dash
                 buffer = buffer.Replace('\u2013', '-'); // en dash
             }
             }
 
 
-            if (buffer.IndexOf('\u2014') > -1)
+            if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1)
             {
             {
                 buffer = buffer.Replace('\u2014', '-'); // em dash
                 buffer = buffer.Replace('\u2014', '-'); // em dash
             }
             }
 
 
-            if (buffer.IndexOf('\u2015') > -1)
+            if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1)
             {
             {
                 buffer = buffer.Replace('\u2015', '-'); // horizontal bar
                 buffer = buffer.Replace('\u2015', '-'); // horizontal bar
             }
             }
 
 
-            if (buffer.IndexOf('\u2017') > -1)
+            if (buffer.IndexOf('\u2017', StringComparison.Ordinal) > -1)
             {
             {
                 buffer = buffer.Replace('\u2017', '_'); // double low line
                 buffer = buffer.Replace('\u2017', '_'); // double low line
             }
             }
 
 
-            if (buffer.IndexOf('\u2018') > -1)
+            if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1)
             {
             {
                 buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
                 buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
             }
             }
 
 
-            if (buffer.IndexOf('\u2019') > -1)
+            if (buffer.IndexOf('\u2019', StringComparison.Ordinal) > -1)
             {
             {
                 buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
                 buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
             }
             }
 
 
-            if (buffer.IndexOf('\u201a') > -1)
+            if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1)
             {
             {
                 buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
                 buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
             }
             }
 
 
-            if (buffer.IndexOf('\u201b') > -1)
+            if (buffer.IndexOf('\u201b', StringComparison.Ordinal) > -1)
             {
             {
                 buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
                 buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
             }
             }
 
 
-            if (buffer.IndexOf('\u201c') > -1)
+            if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1)
             {
             {
                 buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
                 buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
             }
             }
 
 
-            if (buffer.IndexOf('\u201d') > -1)
+            if (buffer.IndexOf('\u201d', StringComparison.Ordinal) > -1)
             {
             {
                 buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
                 buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
             }
             }
 
 
-            if (buffer.IndexOf('\u201e') > -1)
+            if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1)
             {
             {
                 buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
                 buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
             }
             }
 
 
-            if (buffer.IndexOf('\u2026') > -1)
+            if (buffer.IndexOf('\u2026', StringComparison.Ordinal) > -1)
             {
             {
-                buffer = buffer.Replace("\u2026", "..."); // horizontal ellipsis
+                buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
             }
             }
 
 
-            if (buffer.IndexOf('\u2032') > -1)
+            if (buffer.IndexOf('\u2032', StringComparison.Ordinal) > -1)
             {
             {
                 buffer = buffer.Replace('\u2032', '\''); // prime
                 buffer = buffer.Replace('\u2032', '\''); // prime
             }
             }
 
 
-            if (buffer.IndexOf('\u2033') > -1)
+            if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1)
             {
             {
                 buffer = buffer.Replace('\u2033', '\"'); // double prime
                 buffer = buffer.Replace('\u2033', '\"'); // double prime
             }
             }
 
 
-            if (buffer.IndexOf('\u0060') > -1)
+            if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1)
             {
             {
                 buffer = buffer.Replace('\u0060', '\''); // grave accent
                 buffer = buffer.Replace('\u0060', '\''); // grave accent
             }
             }
 
 
-            if (buffer.IndexOf('\u00B4') > -1)
+            if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1)
             {
             {
                 buffer = buffer.Replace('\u00B4', '\''); // acute accent
                 buffer = buffer.Replace('\u00B4', '\''); // acute accent
             }
             }
@@ -2999,7 +2925,6 @@ namespace Emby.Server.Implementations.Data
             {
             {
                 connection.RunInTransaction(db =>
                 connection.RunInTransaction(db =>
                 {
                 {
-
                     var statements = PrepareAll(db, statementTexts).ToList();
                     var statements = PrepareAll(db, statementTexts).ToList();
 
 
                     if (!isReturningZeroItems)
                     if (!isReturningZeroItems)
@@ -4635,13 +4560,13 @@ namespace Emby.Server.Implementations.Data
             if (query.AncestorIds.Length > 1)
             if (query.AncestorIds.Length > 1)
             {
             {
                 var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
                 var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
-                whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
+                whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
             }
             }
 
 
             if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey))
             if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey))
             {
             {
                 var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey";
                 var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey";
-                whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
+                whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
                 if (statement != null)
                 if (statement != null)
                 {
                 {
                     statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
                     statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
@@ -4669,8 +4594,12 @@ namespace Emby.Server.Implementations.Data
 
 
             if (query.BlockUnratedItems.Length > 1)
             if (query.BlockUnratedItems.Length > 1)
             {
             {
-                var inClause = string.Join(",", query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
-                whereClauses.Add(string.Format("(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", inClause));
+                var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
+                whereClauses.Add(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))",
+                        inClause));
             }
             }
 
 
             if (query.ExcludeInheritedTags.Length > 0)
             if (query.ExcludeInheritedTags.Length > 0)
@@ -4679,7 +4608,7 @@ namespace Emby.Server.Implementations.Data
                 if (statement == null)
                 if (statement == null)
                 {
                 {
                     int index = 0;
                     int index = 0;
-                    string excludedTags = string.Join(",", query.ExcludeInheritedTags.Select(t => paramName + index++));
+                    string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(t => paramName + index++));
                     whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
                     whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
                 }
                 }
                 else
                 else
@@ -5238,7 +5167,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             {
             {
                 if (i > 0)
                 if (i > 0)
                 {
                 {
-                    insertText.Append(",");
+                    insertText.Append(',');
                 }
                 }
 
 
                 insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
                 insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
@@ -5733,10 +5662,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             const int Limit = 100;
             const int Limit = 100;
             var startIndex = 0;
             var startIndex = 0;
 
 
+            const string StartInsertText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values ";
+            var insertText = new StringBuilder(StartInsertText);
             while (startIndex < values.Count)
             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 endIndex = Math.Min(values.Count, startIndex + Limit);
 
 
                 for (var i = startIndex; i < endIndex; i++)
                 for (var i = startIndex; i < endIndex; i++)
@@ -5778,6 +5707,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 }
                 }
 
 
                 startIndex += Limit;
                 startIndex += Limit;
+                insertText.Length = StartInsertText.Length;
             }
             }
         }
         }
 
 
@@ -5815,10 +5745,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             var startIndex = 0;
             var startIndex = 0;
             var listIndex = 0;
             var listIndex = 0;
 
 
+            const string StartInsertText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ";
+            var insertText = new StringBuilder(StartInsertText);
             while (startIndex < people.Count)
             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 endIndex = Math.Min(people.Count, startIndex + Limit);
                 for (var i = startIndex; i < endIndex; i++)
                 for (var i = startIndex; i < endIndex; i++)
                 {
                 {
@@ -5852,6 +5782,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 }
                 }
 
 
                 startIndex += Limit;
                 startIndex += Limit;
+                insertText.Length = StartInsertText.Length;
             }
             }
         }
         }
 
 
@@ -5890,10 +5821,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 throw new ArgumentNullException(nameof(query));
                 throw new ArgumentNullException(nameof(query));
             }
             }
 
 
-            var cmdText = "select "
-                        + string.Join(",", _mediaStreamSaveColumns)
-                        + " from mediastreams where"
-                        + " ItemId=@ItemId";
+            var cmdText = _mediaStreamSaveColumnsSelectQuery;
 
 
             if (query.Type.HasValue)
             if (query.Type.HasValue)
             {
             {
@@ -5970,18 +5898,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             const int Limit = 10;
             const int Limit = 10;
             var startIndex = 0;
             var startIndex = 0;
 
 
+            var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
             while (startIndex < streams.Count)
             while (startIndex < streams.Count)
             {
             {
-                var insertText = new StringBuilder("insert into mediastreams (");
-                foreach (var column in _mediaStreamSaveColumns)
-                {
-                    insertText.Append(column).Append(',');
-                }
-
-                // Remove last comma
-                insertText.Length--;
-                insertText.Append(") values ");
-
                 var endIndex = Math.Min(streams.Count, startIndex + Limit);
                 var endIndex = Math.Min(streams.Count, startIndex + Limit);
 
 
                 for (var i = startIndex; i < endIndex; i++)
                 for (var i = startIndex; i < endIndex; i++)
@@ -6064,6 +5983,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 }
                 }
 
 
                 startIndex += Limit;
                 startIndex += Limit;
+                insertText.Length = _mediaStreamSaveColumnsInsertQuery.Length;
             }
             }
         }
         }
 
 
@@ -6247,10 +6167,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 throw new ArgumentNullException(nameof(query));
                 throw new ArgumentNullException(nameof(query));
             }
             }
 
 
-            var cmdText = "select "
-                        + string.Join(",", _mediaAttachmentSaveColumns)
-                        + " from mediaattachments where"
-                        + " ItemId=@ItemId";
+            var cmdText = _mediaAttachmentSaveColumnsSelectQuery;
 
 
             if (query.Index.HasValue)
             if (query.Index.HasValue)
             {
             {
@@ -6318,10 +6235,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
         {
         {
             const int InsertAtOnce = 10;
             const int InsertAtOnce = 10;
 
 
+            var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
             for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce)
             for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce)
             {
             {
-                var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
-
                 var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce);
                 var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce);
 
 
                 for (var i = startIndex; i < endIndex; i++)
                 for (var i = startIndex; i < endIndex; i++)
@@ -6331,7 +6247,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
 
                     foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
                     foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
                     {
                     {
-                        insertText.Append("@" + column + index + ",");
+                        insertText.Append('@')
+                            .Append(column)
+                            .Append(index)
+                            .Append(',');
                     }
                     }
 
 
                     insertText.Length -= 1;
                     insertText.Length -= 1;
@@ -6364,6 +6283,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                     statement.Reset();
                     statement.Reset();
                     statement.MoveNext();
                     statement.MoveNext();
                 }
                 }
+
+                insertText.Length = _mediaAttachmentInsertPrefix.Length;
             }
             }
         }
         }
 
 

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

@@ -5,8 +5,8 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
-using Jellyfin.Data.Enums;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Devices;
@@ -17,16 +17,17 @@ using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Session;
+using Microsoft.Extensions.Caching.Memory;
 
 
 namespace Emby.Server.Implementations.Devices
 namespace Emby.Server.Implementations.Devices
 {
 {
     public class DeviceManager : IDeviceManager
     public class DeviceManager : IDeviceManager
     {
     {
+        private readonly IMemoryCache _memoryCache;
         private readonly IJsonSerializer _json;
         private readonly IJsonSerializer _json;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly IAuthenticationRepository _authRepo;
         private readonly IAuthenticationRepository _authRepo;
-        private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
         private readonly object _capabilitiesSyncLock = new object();
         private readonly object _capabilitiesSyncLock = new object();
 
 
         public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
         public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
@@ -35,13 +36,14 @@ namespace Emby.Server.Implementations.Devices
             IAuthenticationRepository authRepo,
             IAuthenticationRepository authRepo,
             IJsonSerializer json,
             IJsonSerializer json,
             IUserManager userManager,
             IUserManager userManager,
-            IServerConfigurationManager config)
+            IServerConfigurationManager config,
+            IMemoryCache memoryCache)
         {
         {
             _json = json;
             _json = json;
             _userManager = userManager;
             _userManager = userManager;
             _config = config;
             _config = config;
+            _memoryCache = memoryCache;
             _authRepo = authRepo;
             _authRepo = authRepo;
-            _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
         }
         }
 
 
         public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
         public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
@@ -51,8 +53,7 @@ namespace Emby.Server.Implementations.Devices
 
 
             lock (_capabilitiesSyncLock)
             lock (_capabilitiesSyncLock)
             {
             {
-                _capabilitiesCache[deviceId] = capabilities;
-
+                _memoryCache.Set(deviceId, capabilities);
                 _json.SerializeToFile(capabilities, path);
                 _json.SerializeToFile(capabilities, path);
             }
             }
         }
         }
@@ -71,13 +72,13 @@ namespace Emby.Server.Implementations.Devices
 
 
         public ClientCapabilities GetCapabilities(string id)
         public ClientCapabilities GetCapabilities(string id)
         {
         {
-            lock (_capabilitiesSyncLock)
+            if (_memoryCache.TryGetValue(id, out ClientCapabilities result))
             {
             {
-                if (_capabilitiesCache.TryGetValue(id, out var result))
-                {
-                    return result;
-                }
+                return result;
+            }
 
 
+            lock (_capabilitiesSyncLock)
+            {
                 var path = Path.Combine(GetDevicePath(id), "capabilities.json");
                 var path = Path.Combine(GetDevicePath(id), "capabilities.json");
                 try
                 try
                 {
                 {

+ 5 - 43
Emby.Server.Implementations/Dto/DtoService.cs

@@ -73,25 +73,6 @@ namespace Emby.Server.Implementations.Dto
             _livetvManagerFactory = livetvManagerFactory;
             _livetvManagerFactory = livetvManagerFactory;
         }
         }
 
 
-        /// <summary>
-        /// Converts a BaseItem to a DTOBaseItem.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="fields">The fields.</param>
-        /// <param name="user">The user.</param>
-        /// <param name="owner">The owner.</param>
-        /// <returns>Task{DtoBaseItem}.</returns>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, User user = null, BaseItem owner = null)
-        {
-            var options = new DtoOptions
-            {
-                Fields = fields
-            };
-
-            return GetBaseItemDto(item, options, user, owner);
-        }
-
         /// <inheritdoc />
         /// <inheritdoc />
         public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
         public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
         {
         {
@@ -443,17 +424,6 @@ namespace Emby.Server.Implementations.Dto
             return folder.GetChildCount(user);
             return folder.GetChildCount(user);
         }
         }
 
 
-        /// <summary>
-        /// Gets client-side Id of a server-side BaseItem.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns>System.String.</returns>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public string GetDtoId(BaseItem item)
-        {
-            return item.Id.ToString("N", CultureInfo.InvariantCulture);
-        }
-
         private static void SetBookProperties(BaseItemDto dto, Book item)
         private static void SetBookProperties(BaseItemDto dto, Book item)
         {
         {
             dto.SeriesName = item.SeriesName;
             dto.SeriesName = item.SeriesName;
@@ -484,6 +454,11 @@ namespace Emby.Server.Implementations.Dto
             }
             }
         }
         }
 
 
+        private string GetDtoId(BaseItem item)
+        {
+            return item.Id.ToString("N", CultureInfo.InvariantCulture);
+        }
+
         private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item)
         private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item)
         {
         {
             if (!string.IsNullOrEmpty(item.Album))
             if (!string.IsNullOrEmpty(item.Album))
@@ -513,19 +488,6 @@ namespace Emby.Server.Implementations.Dto
                 .ToArray();
                 .ToArray();
         }
         }
 
 
-        private string GetImageCacheTag(BaseItem item, ImageType type)
-        {
-            try
-            {
-                return _imageProcessor.GetImageCacheTag(item, type);
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error getting {type} image info", type);
-                return null;
-            }
-        }
-
         private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
         private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
         {
         {
             try
             try

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

@@ -13,10 +13,8 @@
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
     <ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
     <ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
-    <ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" />
     <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" />
     <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" />
     <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
     <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
-    <ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
     <ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" />
     <ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" />
     <ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
     <ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
     <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
     <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
@@ -25,7 +23,7 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="IPNetwork2" Version="2.5.211" />
     <PackageReference Include="IPNetwork2" Version="2.5.211" />
-    <PackageReference Include="Jellyfin.XmlTv" Version="10.4.3" />
+    <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
@@ -34,14 +32,14 @@
     <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.5" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.5" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" />
-    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.5" />
-    <PackageReference Include="Mono.Nat" Version="2.0.1" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
+    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
+    <PackageReference Include="Mono.Nat" Version="2.0.2" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
-    <PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />
-    <PackageReference Include="sharpcompress" Version="0.25.1" />
+    <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
+    <PackageReference Include="sharpcompress" Version="0.26.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
     <PackageReference Include="DotNet.Glob" Version="3.0.9" />
     <PackageReference Include="DotNet.Glob" Version="3.0.9" />
   </ItemGroup>
   </ItemGroup>
@@ -54,7 +52,7 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
+    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->

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

@@ -23,10 +23,12 @@ namespace Emby.Server.Implementations.EntryPoints
     public class LibraryChangedNotifier : IServerEntryPoint
     public class LibraryChangedNotifier : IServerEntryPoint
     {
     {
         /// <summary>
         /// <summary>
-        /// The library manager.
+        /// The library update duration.
         /// </summary>
         /// </summary>
-        private readonly ILibraryManager _libraryManager;
+        private const int LibraryUpdateDuration = 30000;
 
 
+        private readonly ILibraryManager _libraryManager;
+        private readonly IProviderManager _providerManager;
         private readonly ISessionManager _sessionManager;
         private readonly ISessionManager _sessionManager;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
         private readonly ILogger<LibraryChangedNotifier> _logger;
         private readonly ILogger<LibraryChangedNotifier> _logger;
@@ -38,23 +40,10 @@ namespace Emby.Server.Implementations.EntryPoints
 
 
         private readonly List<Folder> _foldersAddedTo = new List<Folder>();
         private readonly List<Folder> _foldersAddedTo = new List<Folder>();
         private readonly List<Folder> _foldersRemovedFrom = new List<Folder>();
         private readonly List<Folder> _foldersRemovedFrom = new List<Folder>();
-
         private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
         private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
         private readonly List<BaseItem> _itemsRemoved = new List<BaseItem>();
         private readonly List<BaseItem> _itemsRemoved = new List<BaseItem>();
         private readonly List<BaseItem> _itemsUpdated = new List<BaseItem>();
         private readonly List<BaseItem> _itemsUpdated = new List<BaseItem>();
-
-        /// <summary>
-        /// Gets or sets the library update timer.
-        /// </summary>
-        /// <value>The library update timer.</value>
-        private Timer LibraryUpdateTimer { get; set; }
-
-        /// <summary>
-        /// The library update duration.
-        /// </summary>
-        private const int LibraryUpdateDuration = 30000;
-
-        private readonly IProviderManager _providerManager;
+        private readonly Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
 
 
         public LibraryChangedNotifier(
         public LibraryChangedNotifier(
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
@@ -70,22 +59,26 @@ namespace Emby.Server.Implementations.EntryPoints
             _providerManager = providerManager;
             _providerManager = providerManager;
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets the library update timer.
+        /// </summary>
+        /// <value>The library update timer.</value>
+        private Timer LibraryUpdateTimer { get; set; }
+
         public Task RunAsync()
         public Task RunAsync()
         {
         {
-            _libraryManager.ItemAdded += libraryManager_ItemAdded;
-            _libraryManager.ItemUpdated += libraryManager_ItemUpdated;
-            _libraryManager.ItemRemoved += libraryManager_ItemRemoved;
+            _libraryManager.ItemAdded += OnLibraryItemAdded;
+            _libraryManager.ItemUpdated += OnLibraryItemUpdated;
+            _libraryManager.ItemRemoved += OnLibraryItemRemoved;
 
 
-            _providerManager.RefreshCompleted += _providerManager_RefreshCompleted;
-            _providerManager.RefreshStarted += _providerManager_RefreshStarted;
-            _providerManager.RefreshProgress += _providerManager_RefreshProgress;
+            _providerManager.RefreshCompleted += OnProviderRefreshCompleted;
+            _providerManager.RefreshStarted += OnProviderRefreshStarted;
+            _providerManager.RefreshProgress += OnProviderRefreshProgress;
 
 
             return Task.CompletedTask;
             return Task.CompletedTask;
         }
         }
 
 
-        private Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
-
-        private void _providerManager_RefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
+        private void OnProviderRefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
         {
         {
             var item = e.Argument.Item1;
             var item = e.Argument.Item1;
 
 
@@ -122,9 +115,11 @@ namespace Emby.Server.Implementations.EntryPoints
 
 
             foreach (var collectionFolder in collectionFolders)
             foreach (var collectionFolder in collectionFolders)
             {
             {
-                var collectionFolderDict = new Dictionary<string, string>();
-                collectionFolderDict["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture);
-                collectionFolderDict["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture);
+                var collectionFolderDict = new Dictionary<string, string>
+                {
+                    ["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
+                    ["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture)
+                };
 
 
                 try
                 try
                 {
                 {
@@ -136,21 +131,19 @@ namespace Emby.Server.Implementations.EntryPoints
             }
             }
         }
         }
 
 
-        private void _providerManager_RefreshStarted(object sender, GenericEventArgs<BaseItem> e)
+        private void OnProviderRefreshStarted(object sender, GenericEventArgs<BaseItem> e)
         {
         {
-            _providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
+            OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
         }
         }
 
 
-        private void _providerManager_RefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
+        private void OnProviderRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
         {
         {
-            _providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
+            OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
         }
         }
 
 
         private static bool EnableRefreshMessage(BaseItem item)
         private static bool EnableRefreshMessage(BaseItem item)
         {
         {
-            var folder = item as Folder;
-
-            if (folder == null)
+            if (!(item is Folder folder))
             {
             {
                 return false;
                 return false;
             }
             }
@@ -183,7 +176,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// </summary>
         /// <param name="sender">The source of the event.</param>
         /// <param name="sender">The source of the event.</param>
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
-        void libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
+        private void OnLibraryItemAdded(object sender, ItemChangeEventArgs e)
         {
         {
             if (!FilterItem(e.Item))
             if (!FilterItem(e.Item))
             {
             {
@@ -205,8 +198,7 @@ namespace Emby.Server.Implementations.EntryPoints
                     LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
                     LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
                 }
                 }
 
 
-                var parent = e.Item.GetParent() as Folder;
-                if (parent != null)
+                if (e.Item.GetParent() is Folder parent)
                 {
                 {
                     _foldersAddedTo.Add(parent);
                     _foldersAddedTo.Add(parent);
                 }
                 }
@@ -220,7 +212,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// </summary>
         /// <param name="sender">The source of the event.</param>
         /// <param name="sender">The source of the event.</param>
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
-        void libraryManager_ItemUpdated(object sender, ItemChangeEventArgs e)
+        private void OnLibraryItemUpdated(object sender, ItemChangeEventArgs e)
         {
         {
             if (!FilterItem(e.Item))
             if (!FilterItem(e.Item))
             {
             {
@@ -231,8 +223,7 @@ namespace Emby.Server.Implementations.EntryPoints
             {
             {
                 if (LibraryUpdateTimer == null)
                 if (LibraryUpdateTimer == null)
                 {
                 {
-                    LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
-                                                   Timeout.Infinite);
+                    LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
                 }
                 }
                 else
                 else
                 {
                 {
@@ -248,7 +239,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// </summary>
         /// <param name="sender">The source of the event.</param>
         /// <param name="sender">The source of the event.</param>
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
-        void libraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)
+        private void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e)
         {
         {
             if (!FilterItem(e.Item))
             if (!FilterItem(e.Item))
             {
             {
@@ -259,16 +250,14 @@ namespace Emby.Server.Implementations.EntryPoints
             {
             {
                 if (LibraryUpdateTimer == null)
                 if (LibraryUpdateTimer == null)
                 {
                 {
-                    LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
-                                                   Timeout.Infinite);
+                    LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
                 }
                 }
                 else
                 else
                 {
                 {
                     LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
                     LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
                 }
                 }
 
 
-                var parent = e.Parent as Folder;
-                if (parent != null)
+                if (e.Parent is Folder parent)
                 {
                 {
                     _foldersRemovedFrom.Add(parent);
                     _foldersRemovedFrom.Add(parent);
                 }
                 }
@@ -486,13 +475,13 @@ namespace Emby.Server.Implementations.EntryPoints
                     LibraryUpdateTimer = null;
                     LibraryUpdateTimer = null;
                 }
                 }
 
 
-                _libraryManager.ItemAdded -= libraryManager_ItemAdded;
-                _libraryManager.ItemUpdated -= libraryManager_ItemUpdated;
-                _libraryManager.ItemRemoved -= libraryManager_ItemRemoved;
+                _libraryManager.ItemAdded -= OnLibraryItemAdded;
+                _libraryManager.ItemUpdated -= OnLibraryItemUpdated;
+                _libraryManager.ItemRemoved -= OnLibraryItemRemoved;
 
 
-                _providerManager.RefreshCompleted -= _providerManager_RefreshCompleted;
-                _providerManager.RefreshStarted -= _providerManager_RefreshStarted;
-                _providerManager.RefreshProgress -= _providerManager_RefreshProgress;
+                _providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
+                _providerManager.RefreshStarted -= OnProviderRefreshStarted;
+                _providerManager.RefreshProgress -= OnProviderRefreshProgress;
             }
             }
         }
         }
     }
     }

+ 11 - 2
Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs

@@ -1,3 +1,4 @@
+using System.Net.Sockets;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.Udp;
 using Emby.Server.Implementations.Udp;
@@ -48,8 +49,16 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <inheritdoc />
         /// <inheritdoc />
         public Task RunAsync()
         public Task RunAsync()
         {
         {
-            _udpServer = new UdpServer(_logger, _appHost, _config);
-            _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+            try
+            {
+                _udpServer = new UdpServer(_logger, _appHost, _config);
+                _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+            }
+            catch (SocketException ex)
+            {
+                _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber);
+            }
+
             return Task.CompletedTask;
             return Task.CompletedTask;
         }
         }
 
 

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

@@ -29,7 +29,6 @@ namespace Emby.Server.Implementations.HttpServer
 
 
         private readonly IStreamHelper _streamHelper;
         private readonly IStreamHelper _streamHelper;
         private readonly ILogger _logger;
         private readonly ILogger _logger;
-        private readonly IFileSystem _fileSystem;
 
 
         /// <summary>
         /// <summary>
         /// The _options.
         /// The _options.
@@ -49,7 +48,6 @@ namespace Emby.Server.Implementations.HttpServer
             }
             }
 
 
             _streamHelper = streamHelper;
             _streamHelper = streamHelper;
-            _fileSystem = fileSystem;
 
 
             Path = path;
             Path = path;
             _logger = logger;
             _logger = logger;

+ 3 - 3
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -449,7 +449,7 @@ namespace Emby.Server.Implementations.HttpServer
                 if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
                 if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     httpRes.StatusCode = 200;
                     httpRes.StatusCode = 200;
-                    foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
+                    foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
                     {
                     {
                         httpRes.Headers.Add(key, value);
                         httpRes.Headers.Add(key, value);
                     }
                     }
@@ -486,7 +486,7 @@ namespace Emby.Server.Implementations.HttpServer
                 var handler = GetServiceHandler(httpReq);
                 var handler = GetServiceHandler(httpReq);
                 if (handler != null)
                 if (handler != null)
                 {
                 {
-                    await handler.ProcessRequestAsync(this, httpReq, httpRes, _logger, cancellationToken).ConfigureAwait(false);
+                    await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
                 }
                 }
                 else
                 else
                 {
                 {
@@ -567,7 +567,7 @@ namespace Emby.Server.Implementations.HttpServer
 
 
                 WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
                 WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
 
 
-                var connection = new WebSocketConnection(
+                using var connection = new WebSocketConnection(
                     _loggerFactory.CreateLogger<WebSocketConnection>(),
                     _loggerFactory.CreateLogger<WebSocketConnection>(),
                     webSocket,
                     webSocket,
                     context.Connection.RemoteIpAddress,
                     context.Connection.RemoteIpAddress,

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

@@ -105,7 +105,7 @@ namespace Emby.Server.Implementations.HttpServer
                 responseHeaders = new Dictionary<string, string>();
                 responseHeaders = new Dictionary<string, string>();
             }
             }
 
 
-            if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string expires))
+            if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out _))
             {
             {
                 responseHeaders[HeaderNames.Expires] = "0";
                 responseHeaders[HeaderNames.Expires] = "0";
             }
             }
@@ -326,7 +326,8 @@ namespace Emby.Server.Implementations.HttpServer
             return GetHttpResult(request, ms, contentType, true, responseHeaders);
             return GetHttpResult(request, ms, contentType, true, responseHeaders);
         }
         }
 
 
-        private IHasHeaders GetCompressedResult(byte[] content,
+        private IHasHeaders GetCompressedResult(
+            byte[] content,
             string requestedCompressionType,
             string requestedCompressionType,
             IDictionary<string, string> responseHeaders,
             IDictionary<string, string> responseHeaders,
             bool isHeadRequest,
             bool isHeadRequest,

+ 14 - 24
Emby.Server.Implementations/HttpServer/Security/AuthService.cs

@@ -13,35 +13,31 @@ using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Services;
 using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.HttpServer.Security
 namespace Emby.Server.Implementations.HttpServer.Security
 {
 {
     public class AuthService : IAuthService
     public class AuthService : IAuthService
     {
     {
-        private readonly ILogger<AuthService> _logger;
         private readonly IAuthorizationContext _authorizationContext;
         private readonly IAuthorizationContext _authorizationContext;
         private readonly ISessionManager _sessionManager;
         private readonly ISessionManager _sessionManager;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly INetworkManager _networkManager;
         private readonly INetworkManager _networkManager;
 
 
         public AuthService(
         public AuthService(
-            ILogger<AuthService> logger,
             IAuthorizationContext authorizationContext,
             IAuthorizationContext authorizationContext,
             IServerConfigurationManager config,
             IServerConfigurationManager config,
             ISessionManager sessionManager,
             ISessionManager sessionManager,
             INetworkManager networkManager)
             INetworkManager networkManager)
         {
         {
-            _logger = logger;
             _authorizationContext = authorizationContext;
             _authorizationContext = authorizationContext;
             _config = config;
             _config = config;
             _sessionManager = sessionManager;
             _sessionManager = sessionManager;
             _networkManager = networkManager;
             _networkManager = networkManager;
         }
         }
 
 
-        public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues)
+        public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
         {
         {
-            ValidateUser(request, authAttribtues);
+            ValidateUser(request, authAttributes);
         }
         }
 
 
         public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
         public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
@@ -67,17 +63,17 @@ namespace Emby.Server.Implementations.HttpServer.Security
             return auth;
             return auth;
         }
         }
 
 
-        private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
+        private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
         {
         {
             // This code is executed before the service
             // This code is executed before the service
             var auth = _authorizationContext.GetAuthorizationInfo(request);
             var auth = _authorizationContext.GetAuthorizationInfo(request);
 
 
-            if (!IsExemptFromAuthenticationToken(authAttribtues, request))
+            if (!IsExemptFromAuthenticationToken(authAttributes, request))
             {
             {
                 ValidateSecurityToken(request, auth.Token);
                 ValidateSecurityToken(request, auth.Token);
             }
             }
 
 
-            if (authAttribtues.AllowLocalOnly && !request.IsLocal)
+            if (authAttributes.AllowLocalOnly && !request.IsLocal)
             {
             {
                 throw new SecurityException("Operation not found.");
                 throw new SecurityException("Operation not found.");
             }
             }
@@ -91,14 +87,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
 
             if (user != null)
             if (user != null)
             {
             {
-                ValidateUserAccess(user, request, authAttribtues, auth);
+                ValidateUserAccess(user, request, authAttributes);
             }
             }
 
 
             var info = GetTokenInfo(request);
             var info = GetTokenInfo(request);
 
 
-            if (!IsExemptFromRoles(auth, authAttribtues, request, info))
+            if (!IsExemptFromRoles(auth, authAttributes, request, info))
             {
             {
-                var roles = authAttribtues.GetRoles();
+                var roles = authAttributes.GetRoles();
 
 
                 ValidateRoles(roles, user);
                 ValidateRoles(roles, user);
             }
             }
@@ -122,8 +118,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
         private void ValidateUserAccess(
         private void ValidateUserAccess(
             User user,
             User user,
             IRequest request,
             IRequest request,
-            IAuthenticationAttributes authAttributes,
-            AuthorizationInfo auth)
+            IAuthenticationAttributes authAttributes)
         {
         {
             if (user.HasPermission(PermissionKind.IsDisabled))
             if (user.HasPermission(PermissionKind.IsDisabled))
             {
             {
@@ -162,6 +157,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 return true;
                 return true;
             }
             }
 
 
+            if (authAttribtues.IgnoreLegacyAuth)
+            {
+                return true;
+            }
+
             return false;
             return false;
         }
         }
 
 
@@ -241,16 +241,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
             {
             {
                 throw new AuthenticationException("Access token is invalid or expired.");
                 throw new AuthenticationException("Access token is invalid or expired.");
             }
             }
-
-            // if (!string.IsNullOrEmpty(info.UserId))
-            //{
-            //    var user = _userManager.GetUserById(info.UserId);
-
-            //    if (user == null || user.Configuration.IsDisabled)
-            //    {
-            //        throw new SecurityException("User account has been disabled.");
-            //    }
-            //}
         }
         }
     }
     }
 }
 }

+ 7 - 6
Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs

@@ -97,6 +97,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 token = headers["X-MediaBrowser-Token"];
                 token = headers["X-MediaBrowser-Token"];
             }
             }
 
 
+            if (string.IsNullOrEmpty(token))
+            {
+                token = queryString["ApiKey"];
+            }
+
+            // TODO deprecate this query parameter.
             if (string.IsNullOrEmpty(token))
             if (string.IsNullOrEmpty(token))
             {
             {
                 token = queryString["api_key"];
                 token = queryString["api_key"];
@@ -276,12 +282,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
 
         private static string NormalizeValue(string value)
         private static string NormalizeValue(string value)
         {
         {
-            if (string.IsNullOrEmpty(value))
-            {
-                return value;
-            }
-
-            return WebUtility.HtmlEncode(value);
+            return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
         }
         }
     }
     }
 }
 }

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

@@ -95,13 +95,13 @@ namespace Emby.Server.Implementations.HttpServer
 
 
                 if (bytes != null)
                 if (bytes != null)
                 {
                 {
-                    await responseStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+                    await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
                 }
                 }
                 else
                 else
                 {
                 {
                     using (var src = SourceStream)
                     using (var src = SourceStream)
                     {
                     {
-                        await src.CopyToAsync(responseStream).ConfigureAwait(false);
+                        await src.CopyToAsync(responseStream, cancellationToken).ConfigureAwait(false);
                     }
                     }
                 }
                 }
             }
             }

+ 4 - 4
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.HttpServer
     /// <summary>
     /// <summary>
     /// Class WebSocketConnection.
     /// Class WebSocketConnection.
     /// </summary>
     /// </summary>
-    public class WebSocketConnection : IWebSocketConnection
+    public class WebSocketConnection : IWebSocketConnection, IDisposable
     {
     {
         /// <summary>
         /// <summary>
         /// The logger.
         /// The logger.
@@ -119,7 +119,7 @@ namespace Emby.Server.Implementations.HttpServer
                 Memory<byte> memory = writer.GetMemory(512);
                 Memory<byte> memory = writer.GetMemory(512);
                 try
                 try
                 {
                 {
-                    receiveresult = await _socket.ReceiveAsync(memory, cancellationToken);
+                    receiveresult = await _socket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false);
                 }
                 }
                 catch (WebSocketException ex)
                 catch (WebSocketException ex)
                 {
                 {
@@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.HttpServer
                 writer.Advance(bytesRead);
                 writer.Advance(bytesRead);
 
 
                 // Make the data available to the PipeReader
                 // Make the data available to the PipeReader
-                FlushResult flushResult = await writer.FlushAsync();
+                FlushResult flushResult = await writer.FlushAsync().ConfigureAwait(false);
                 if (flushResult.IsCompleted)
                 if (flushResult.IsCompleted)
                 {
                 {
                     // The PipeReader stopped reading
                     // The PipeReader stopped reading
@@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.HttpServer
 
 
             if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
             if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
             {
             {
-                await SendKeepAliveResponse();
+                await SendKeepAliveResponse().ConfigureAwait(false);
             }
             }
             else
             else
             {
             {

+ 2 - 1
Emby.Server.Implementations/IO/FileRefresher.cs

@@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.IO
         private readonly List<string> _affectedPaths = new List<string>();
         private readonly List<string> _affectedPaths = new List<string>();
         private readonly object _timerLock = new object();
         private readonly object _timerLock = new object();
         private Timer _timer;
         private Timer _timer;
+        private bool _disposed;
 
 
         public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
         public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
         {
         {
@@ -213,11 +214,11 @@ namespace Emby.Server.Implementations.IO
             }
             }
         }
         }
 
 
-        private bool _disposed;
         public void Dispose()
         public void Dispose()
         {
         {
             _disposed = true;
             _disposed = true;
             DisposeTimer();
             DisposeTimer();
+            GC.SuppressFinalize(this);
         }
         }
     }
     }
 }
 }

+ 10 - 0
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -245,6 +245,16 @@ namespace Emby.Server.Implementations.IO
                 if (info is FileInfo fileInfo)
                 if (info is FileInfo fileInfo)
                 {
                 {
                     result.Length = fileInfo.Length;
                     result.Length = fileInfo.Length;
+
+                    // Issue #2354 get the size of files behind symbolic links
+                    if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
+                    {
+                        using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
+                        {
+                            result.Length = thisFileStream.Length;
+                        }
+                    }
+
                     result.DirectoryName = fileInfo.DirectoryName;
                     result.DirectoryName = fileInfo.DirectoryName;
                 }
                 }
 
 

+ 2 - 8
Emby.Server.Implementations/Images/ArtistImageProvider.cs

@@ -11,7 +11,6 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
@@ -25,14 +24,9 @@ namespace Emby.Server.Implementations.Images
     /// </summary>
     /// </summary>
     public class ArtistImageProvider : BaseDynamicImageProvider<MusicArtist>
     public class ArtistImageProvider : BaseDynamicImageProvider<MusicArtist>
     {
     {
-        /// <summary>
-        /// The library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-
-        public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+        public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor)
+            : base(fileSystem, providerManager, applicationPaths, imageProcessor)
         {
         {
-            _libraryManager = libraryManager;
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 1 - 1
Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs

@@ -3,7 +3,7 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
-using Emby.Server.Implementations.Images;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;

+ 1 - 1
Emby.Server.Implementations/Images/FolderImageProvider.cs

@@ -1,7 +1,7 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
-using Emby.Server.Implementations.Images;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;

+ 1 - 0
Emby.Server.Implementations/Images/GenreImageProvider.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;

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

@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
                 if (parent != null)
                 if (parent != null)
                 {
                 {
                     // Don't resolve these into audio files
                     // Don't resolve these into audio files
-                    if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename)
+                    if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal)
                         && _libraryManager.IsAudioFile(filename))
                         && _libraryManager.IsAudioFile(filename))
                     {
                     {
                         return true;
                         return true;

+ 12 - 12
Emby.Server.Implementations/Library/ExclusiveLiveStream.cs

@@ -11,6 +11,17 @@ namespace Emby.Server.Implementations.Library
 {
 {
     public class ExclusiveLiveStream : ILiveStream
     public class ExclusiveLiveStream : ILiveStream
     {
     {
+        private readonly Func<Task> _closeFn;
+
+        public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn)
+        {
+            MediaSource = mediaSource;
+            EnableStreamSharing = false;
+            _closeFn = closeFn;
+            ConsumerCount = 1;
+            UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+        }
+
         public int ConsumerCount { get; set; }
         public int ConsumerCount { get; set; }
 
 
         public string OriginalStreamId { get; set; }
         public string OriginalStreamId { get; set; }
@@ -21,18 +32,7 @@ namespace Emby.Server.Implementations.Library
 
 
         public MediaSourceInfo MediaSource { get; set; }
         public MediaSourceInfo MediaSource { get; set; }
 
 
-        public string UniqueId { get; private set; }
-
-        private Func<Task> _closeFn;
-
-        public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn)
-        {
-            MediaSource = mediaSource;
-            EnableStreamSharing = false;
-            _closeFn = closeFn;
-            ConsumerCount = 1;
-            UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
-        }
+        public string UniqueId { get; }
 
 
         public Task Close()
         public Task Close()
         {
         {

+ 20 - 3
Emby.Server.Implementations/Library/IgnorePatterns.cs

@@ -18,7 +18,21 @@ namespace Emby.Server.Implementations.Library
         {
         {
             "**/small.jpg",
             "**/small.jpg",
             "**/albumart.jpg",
             "**/albumart.jpg",
-            "**/*sample*",
+
+            // We have neither non-greedy matching or character group repetitions, working around that here.
+            // https://github.com/dazinator/DotNet.Glob#patterns
+            // .*/sample\..{1,5}
+            "**/sample.?",
+            "**/sample.??",
+            "**/sample.???", // Matches sample.mkv
+            "**/sample.????", // Matches sample.webm
+            "**/sample.?????",
+            "**/*.sample.?",
+            "**/*.sample.??",
+            "**/*.sample.???",
+            "**/*.sample.????",
+            "**/*.sample.?????",
+            "**/sample/*",
 
 
             // Directories
             // Directories
             "**/metadata/**",
             "**/metadata/**",
@@ -64,10 +78,13 @@ namespace Emby.Server.Implementations.Library
             "**/.grab/**",
             "**/.grab/**",
             "**/.grab",
             "**/.grab",
 
 
-            // Unix hidden files and directories
-            "**/.*/**",
+            // Unix hidden files
             "**/.*",
             "**/.*",
 
 
+            // Mac - if you ever remove the above.
+            // "**/._*",
+            // "**/.DS_Store",
+
             // thumbs.db
             // thumbs.db
             "**/thumbs.db",
             "**/thumbs.db",
 
 

+ 129 - 153
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -1,7 +1,6 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
-using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
@@ -46,11 +45,11 @@ using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Providers.MediaInfo;
 using MediaBrowser.Providers.MediaInfo;
+using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Genre = MediaBrowser.Controller.Entities.Genre;
 using Genre = MediaBrowser.Controller.Entities.Genre;
 using Person = MediaBrowser.Controller.Entities.Person;
 using Person = MediaBrowser.Controller.Entities.Person;
-using SortOrder = MediaBrowser.Model.Entities.SortOrder;
 using VideoResolver = Emby.Naming.Video.VideoResolver;
 using VideoResolver = Emby.Naming.Video.VideoResolver;
 
 
 namespace Emby.Server.Implementations.Library
 namespace Emby.Server.Implementations.Library
@@ -60,7 +59,10 @@ namespace Emby.Server.Implementations.Library
     /// </summary>
     /// </summary>
     public class LibraryManager : ILibraryManager
     public class LibraryManager : ILibraryManager
     {
     {
+        private const string ShortcutFileExtension = ".mblink";
+
         private readonly ILogger<LibraryManager> _logger;
         private readonly ILogger<LibraryManager> _logger;
+        private readonly IMemoryCache _memoryCache;
         private readonly ITaskManager _taskManager;
         private readonly ITaskManager _taskManager;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
         private readonly IUserDataManager _userDataRepository;
         private readonly IUserDataManager _userDataRepository;
@@ -72,66 +74,26 @@ namespace Emby.Server.Implementations.Library
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IItemRepository _itemRepository;
         private readonly IItemRepository _itemRepository;
-        private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
         private readonly IImageProcessor _imageProcessor;
         private readonly IImageProcessor _imageProcessor;
 
 
-        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>
-        /// <value>The postscan tasks.</value>
-        private ILibraryPostScanTask[] PostscanTasks { get; set; }
-
-        /// <summary>
-        /// Gets or sets the intro providers.
-        /// </summary>
-        /// <value>The intro providers.</value>
-        private IIntroProvider[] IntroProviders { get; set; }
-
-        /// <summary>
-        /// Gets or sets the list of entity resolution ignore rules.
-        /// </summary>
-        /// <value>The entity resolution ignore rules.</value>
-        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
-
-        /// <summary>
-        /// Gets or sets the list of currently registered entity resolvers.
-        /// </summary>
-        /// <value>The entity resolvers enumerable.</value>
-        private IItemResolver[] EntityResolvers { get; set; }
-
-        private IMultiItemResolver[] MultiItemResolvers { get; set; }
-
         /// <summary>
         /// <summary>
-        /// Gets or sets the comparers.
+        /// The _root folder sync lock.
         /// </summary>
         /// </summary>
-        /// <value>The comparers.</value>
-        private IBaseItemComparer[] Comparers { get; set; }
+        private readonly object _rootFolderSyncLock = new object();
+        private readonly object _userRootFolderSyncLock = new object();
 
 
-        /// <summary>
-        /// Occurs when [item added].
-        /// </summary>
-        public event EventHandler<ItemChangeEventArgs> ItemAdded;
+        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
 
 
-        /// <summary>
-        /// Occurs when [item updated].
-        /// </summary>
-        public event EventHandler<ItemChangeEventArgs> ItemUpdated;
+        private NamingOptions _namingOptions;
+        private string[] _videoFileExtensions;
 
 
         /// <summary>
         /// <summary>
-        /// Occurs when [item removed].
+        /// The _root folder.
         /// </summary>
         /// </summary>
-        public event EventHandler<ItemChangeEventArgs> ItemRemoved;
+        private volatile AggregateFolder _rootFolder;
+        private volatile UserRootFolder _userRootFolder;
 
 
-        public bool IsScanRunning { get; private set; }
+        private bool _wizardCompleted;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="LibraryManager" /> class.
         /// Initializes a new instance of the <see cref="LibraryManager" /> class.
@@ -149,6 +111,7 @@ namespace Emby.Server.Implementations.Library
         /// <param name="mediaEncoder">The media encoder.</param>
         /// <param name="mediaEncoder">The media encoder.</param>
         /// <param name="itemRepository">The item repository.</param>
         /// <param name="itemRepository">The item repository.</param>
         /// <param name="imageProcessor">The image processor.</param>
         /// <param name="imageProcessor">The image processor.</param>
+        /// <param name="memoryCache">The memory cache.</param>
         public LibraryManager(
         public LibraryManager(
             IServerApplicationHost appHost,
             IServerApplicationHost appHost,
             ILogger<LibraryManager> logger,
             ILogger<LibraryManager> logger,
@@ -162,7 +125,8 @@ namespace Emby.Server.Implementations.Library
             Lazy<IUserViewManager> userviewManagerFactory,
             Lazy<IUserViewManager> userviewManagerFactory,
             IMediaEncoder mediaEncoder,
             IMediaEncoder mediaEncoder,
             IItemRepository itemRepository,
             IItemRepository itemRepository,
-            IImageProcessor imageProcessor)
+            IImageProcessor imageProcessor,
+            IMemoryCache memoryCache)
         {
         {
             _appHost = appHost;
             _appHost = appHost;
             _logger = logger;
             _logger = logger;
@@ -177,8 +141,7 @@ namespace Emby.Server.Implementations.Library
             _mediaEncoder = mediaEncoder;
             _mediaEncoder = mediaEncoder;
             _itemRepository = itemRepository;
             _itemRepository = itemRepository;
             _imageProcessor = imageProcessor;
             _imageProcessor = imageProcessor;
-
-            _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
+            _memoryCache = memoryCache;
 
 
             _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
             _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
 
 
@@ -186,37 +149,19 @@ namespace Emby.Server.Implementations.Library
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Adds the parts.
+        /// Occurs when [item added].
         /// </summary>
         /// </summary>
-        /// <param name="rules">The rules.</param>
-        /// <param name="resolvers">The resolvers.</param>
-        /// <param name="introProviders">The intro providers.</param>
-        /// <param name="itemComparers">The item comparers.</param>
-        /// <param name="postscanTasks">The post scan tasks.</param>
-        public void AddParts(
-            IEnumerable<IResolverIgnoreRule> rules,
-            IEnumerable<IItemResolver> resolvers,
-            IEnumerable<IIntroProvider> introProviders,
-            IEnumerable<IBaseItemComparer> itemComparers,
-            IEnumerable<ILibraryPostScanTask> postscanTasks)
-        {
-            EntityResolutionIgnoreRules = rules.ToArray();
-            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
-            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
-            IntroProviders = introProviders.ToArray();
-            Comparers = itemComparers.ToArray();
-            PostscanTasks = postscanTasks.ToArray();
-        }
+        public event EventHandler<ItemChangeEventArgs> ItemAdded;
 
 
         /// <summary>
         /// <summary>
-        /// The _root folder.
+        /// Occurs when [item updated].
         /// </summary>
         /// </summary>
-        private volatile AggregateFolder _rootFolder;
+        public event EventHandler<ItemChangeEventArgs> ItemUpdated;
 
 
         /// <summary>
         /// <summary>
-        /// The _root folder sync lock.
+        /// Occurs when [item removed].
         /// </summary>
         /// </summary>
-        private readonly object _rootFolderSyncLock = new object();
+        public event EventHandler<ItemChangeEventArgs> ItemRemoved;
 
 
         /// <summary>
         /// <summary>
         /// Gets the root folder.
         /// Gets the root folder.
@@ -241,7 +186,68 @@ namespace Emby.Server.Implementations.Library
             }
             }
         }
         }
 
 
-        private bool _wizardCompleted;
+        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
+
+        private IProviderManager ProviderManager => _providerManagerFactory.Value;
+
+        private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
+
+        /// <summary>
+        /// Gets or sets the postscan tasks.
+        /// </summary>
+        /// <value>The postscan tasks.</value>
+        private ILibraryPostScanTask[] PostscanTasks { get; set; }
+
+        /// <summary>
+        /// Gets or sets the intro providers.
+        /// </summary>
+        /// <value>The intro providers.</value>
+        private IIntroProvider[] IntroProviders { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of entity resolution ignore rules.
+        /// </summary>
+        /// <value>The entity resolution ignore rules.</value>
+        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of currently registered entity resolvers.
+        /// </summary>
+        /// <value>The entity resolvers enumerable.</value>
+        private IItemResolver[] EntityResolvers { get; set; }
+
+        private IMultiItemResolver[] MultiItemResolvers { get; set; }
+
+        /// <summary>
+        /// Gets or sets the comparers.
+        /// </summary>
+        /// <value>The comparers.</value>
+        private IBaseItemComparer[] Comparers { get; set; }
+
+        public bool IsScanRunning { get; private set; }
+
+        /// <summary>
+        /// Adds the parts.
+        /// </summary>
+        /// <param name="rules">The rules.</param>
+        /// <param name="resolvers">The resolvers.</param>
+        /// <param name="introProviders">The intro providers.</param>
+        /// <param name="itemComparers">The item comparers.</param>
+        /// <param name="postscanTasks">The post scan tasks.</param>
+        public void AddParts(
+            IEnumerable<IResolverIgnoreRule> rules,
+            IEnumerable<IItemResolver> resolvers,
+            IEnumerable<IIntroProvider> introProviders,
+            IEnumerable<IBaseItemComparer> itemComparers,
+            IEnumerable<ILibraryPostScanTask> postscanTasks)
+        {
+            EntityResolutionIgnoreRules = rules.ToArray();
+            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
+            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
+            IntroProviders = introProviders.ToArray();
+            Comparers = itemComparers.ToArray();
+            PostscanTasks = postscanTasks.ToArray();
+        }
 
 
         /// <summary>
         /// <summary>
         /// Records the configuration values.
         /// Records the configuration values.
@@ -293,7 +299,7 @@ namespace Emby.Server.Implementations.Library
                 }
                 }
             }
             }
 
 
-            _libraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; });
+            _memoryCache.Set(item.Id, item);
         }
         }
 
 
         public void DeleteItem(BaseItem item, DeleteOptions options)
         public void DeleteItem(BaseItem item, DeleteOptions options)
@@ -341,7 +347,7 @@ namespace Emby.Server.Implementations.Library
             if (item is LiveTvProgram)
             if (item is LiveTvProgram)
             {
             {
                 _logger.LogDebug(
                 _logger.LogDebug(
-                    "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+                    "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
                     item.GetType().Name,
                     item.GetType().Name,
                     item.Name ?? "Unknown name",
                     item.Name ?? "Unknown name",
                     item.Path ?? string.Empty,
                     item.Path ?? string.Empty,
@@ -350,7 +356,7 @@ namespace Emby.Server.Implementations.Library
             else
             else
             {
             {
                 _logger.LogInformation(
                 _logger.LogInformation(
-                    "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+                    "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
                     item.GetType().Name,
                     item.GetType().Name,
                     item.Name ?? "Unknown name",
                     item.Name ?? "Unknown name",
                     item.Path ?? string.Empty,
                     item.Path ?? string.Empty,
@@ -368,7 +374,12 @@ namespace Emby.Server.Implementations.Library
                     continue;
                     continue;
                 }
                 }
 
 
-                _logger.LogDebug("Deleting path {MetadataPath}", metadataPath);
+                _logger.LogDebug(
+                    "Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+                    item.GetType().Name,
+                    item.Name ?? "Unknown name",
+                    metadataPath,
+                    item.Id);
 
 
                 try
                 try
                 {
                 {
@@ -392,7 +403,13 @@ namespace Emby.Server.Implementations.Library
                     {
                     {
                         try
                         try
                         {
                         {
-                            _logger.LogDebug("Deleting path {path}", fileSystemInfo.FullName);
+                            _logger.LogInformation(
+                                "Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+                                item.GetType().Name,
+                                item.Name ?? "Unknown name",
+                                fileSystemInfo.FullName,
+                                item.Id);
+
                             if (fileSystemInfo.IsDirectory)
                             if (fileSystemInfo.IsDirectory)
                             {
                             {
                                 Directory.Delete(fileSystemInfo.FullName, true);
                                 Directory.Delete(fileSystemInfo.FullName, true);
@@ -430,7 +447,7 @@ namespace Emby.Server.Implementations.Library
                 _itemRepository.DeleteItem(child.Id);
                 _itemRepository.DeleteItem(child.Id);
             }
             }
 
 
-            _libraryItemsCache.TryRemove(item.Id, out BaseItem removed);
+            _memoryCache.Remove(item.Id);
 
 
             ReportItemRemoved(item, parent);
             ReportItemRemoved(item, parent);
         }
         }
@@ -500,8 +517,8 @@ namespace Emby.Server.Implementations.Library
             {
             {
                 // Try to normalize paths located underneath program-data in an attempt to make them more portable
                 // 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("/", "\\");
+                    .TrimStart('/', '\\')
+                    .Replace('/', '\\');
             }
             }
 
 
             if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
             if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
@@ -764,14 +781,11 @@ namespace Emby.Server.Implementations.Library
             return rootFolder;
             return rootFolder;
         }
         }
 
 
-        private volatile UserRootFolder _userRootFolder;
-        private readonly object _syncLock = new object();
-
         public Folder GetUserRootFolder()
         public Folder GetUserRootFolder()
         {
         {
             if (_userRootFolder == null)
             if (_userRootFolder == null)
             {
             {
-                lock (_syncLock)
+                lock (_userRootFolderSyncLock)
                 {
                 {
                     if (_userRootFolder == null)
                     if (_userRootFolder == null)
                     {
                     {
@@ -1234,7 +1248,7 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentException("Guid can't be empty", nameof(id));
                 throw new ArgumentException("Guid can't be empty", nameof(id));
             }
             }
 
 
-            if (_libraryItemsCache.TryGetValue(id, out BaseItem item))
+            if (_memoryCache.TryGetValue(id, out BaseItem item))
             {
             {
                 return item;
                 return item;
             }
             }
@@ -1321,7 +1335,7 @@ namespace Emby.Server.Implementations.Library
 
 
             return new QueryResult<BaseItem>
             return new QueryResult<BaseItem>
             {
             {
-                Items = _itemRepository.GetItemList(query).ToArray()
+                Items = _itemRepository.GetItemList(query)
             };
             };
         }
         }
 
 
@@ -1452,11 +1466,9 @@ namespace Emby.Server.Implementations.Library
                 return _itemRepository.GetItems(query);
                 return _itemRepository.GetItems(query);
             }
             }
 
 
-            var list = _itemRepository.GetItemList(query);
-
             return new QueryResult<BaseItem>
             return new QueryResult<BaseItem>
             {
             {
-                Items = list
+                Items = _itemRepository.GetItemList(query)
             };
             };
         }
         }
 
 
@@ -1579,7 +1591,6 @@ namespace Emby.Server.Implementations.Library
         public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
         public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
         {
         {
             var tasks = IntroProviders
             var tasks = IntroProviders
-                .OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
                 .Take(1)
                 .Take(1)
                 .Select(i => GetIntros(i, item, user));
                 .Select(i => GetIntros(i, item, user));
 
 
@@ -1865,7 +1876,8 @@ namespace Emby.Server.Implementations.Library
             }
             }
 
 
             var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
             var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
-            if (outdated.Length == 0)
+            // Skip image processing if current or live tv source
+            if (outdated.Length == 0 || item.SourceType != SourceType.Library)
             {
             {
                 RegisterItem(item);
                 RegisterItem(item);
                 return;
                 return;
@@ -1934,12 +1946,9 @@ namespace Emby.Server.Implementations.Library
         /// <summary>
         /// <summary>
         /// Updates the item.
         /// Updates the item.
         /// </summary>
         /// </summary>
-        public void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
+        public void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
         {
         {
-            // Don't iterate multiple times
-            var itemsList = items.ToList();
-
-            foreach (var item in itemsList)
+            foreach (var item in items)
             {
             {
                 if (item.IsFileProtocol)
                 if (item.IsFileProtocol)
                 {
                 {
@@ -1951,11 +1960,11 @@ namespace Emby.Server.Implementations.Library
                 UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
                 UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
             }
             }
 
 
-            _itemRepository.SaveItems(itemsList, cancellationToken);
+            _itemRepository.SaveItems(items, cancellationToken);
 
 
             if (ItemUpdated != null)
             if (ItemUpdated != null)
             {
             {
-                foreach (var item in itemsList)
+                foreach (var item in items)
                 {
                 {
                     // With the live tv guide this just creates too much noise
                     // With the live tv guide this just creates too much noise
                     if (item.SourceType != SourceType.Library)
                     if (item.SourceType != SourceType.Library)
@@ -2178,8 +2187,6 @@ namespace Emby.Server.Implementations.Library
                 .FirstOrDefault(i => !string.IsNullOrEmpty(i));
                 .FirstOrDefault(i => !string.IsNullOrEmpty(i));
         }
         }
 
 
-        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
-
         public UserView GetNamedView(
         public UserView GetNamedView(
             User user,
             User user,
             string name,
             string name,
@@ -2477,14 +2484,9 @@ namespace Emby.Server.Implementations.Library
 
 
             var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
             var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 
 
-            var episodeInfo = episode.IsFileProtocol ?
-                resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) :
-                new Naming.TV.EpisodeInfo();
-
-            if (episodeInfo == null)
-            {
-                episodeInfo = new Naming.TV.EpisodeInfo();
-            }
+            var episodeInfo = episode.IsFileProtocol
+                ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
+                : new Naming.TV.EpisodeInfo();
 
 
             try
             try
             {
             {
@@ -2492,11 +2494,13 @@ namespace Emby.Server.Implementations.Library
                 if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
                 if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     // Read from metadata
                     // Read from metadata
-                    var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest
-                    {
-                        MediaSource = episode.GetMediaSources(false)[0],
-                        MediaType = DlnaProfileType.Video
-                    }, CancellationToken.None).GetAwaiter().GetResult();
+                    var mediaInfo = _mediaEncoder.GetMediaInfo(
+                        new MediaInfoRequest
+                        {
+                            MediaSource = episode.GetMediaSources(false)[0],
+                            MediaType = DlnaProfileType.Video
+                        },
+                        CancellationToken.None).GetAwaiter().GetResult();
                     if (mediaInfo.ParentIndexNumber > 0)
                     if (mediaInfo.ParentIndexNumber > 0)
                     {
                     {
                         episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
                         episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
@@ -2654,7 +2658,7 @@ namespace Emby.Server.Implementations.Library
 
 
             var videos = videoListResolver.Resolve(fileSystemChildren);
             var videos = videoListResolver.Resolve(fileSystemChildren);
 
 
-            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
+            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
 
 
             if (currentVideo != null)
             if (currentVideo != null)
             {
             {
@@ -2671,9 +2675,7 @@ namespace Emby.Server.Implementations.Library
                 .Select(video =>
                 .Select(video =>
                 {
                 {
                     // Try to retrieve it from the db. If we don't find it, use the resolved version
                     // Try to retrieve it from the db. If we don't find it, use the resolved version
-                    var dbItem = GetItemById(video.Id) as Trailer;
-
-                    if (dbItem != null)
+                    if (GetItemById(video.Id) is Trailer dbItem)
                     {
                     {
                         video = dbItem;
                         video = dbItem;
                     }
                     }
@@ -3000,23 +3002,6 @@ namespace Emby.Server.Implementations.Library
             });
             });
         }
         }
 
 
-        private static bool ValidateNetworkPath(string path)
-        {
-            // if (Environment.OSVersion.Platform == PlatformID.Win32NT)
-            //{
-            //    // We can't validate protocol-based paths, so just allow them
-            //    if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1)
-            //    {
-            //        return Directory.Exists(path);
-            //    }
-            //}
-
-            // Without native support for unc, we cannot validate this when running under mono
-            return true;
-        }
-
-        private const string ShortcutFileExtension = ".mblink";
-
         public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
         public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
         {
         {
             AddMediaPathInternal(virtualFolderName, pathInfo, true);
             AddMediaPathInternal(virtualFolderName, pathInfo, true);
@@ -3041,11 +3026,6 @@ namespace Emby.Server.Implementations.Library
                 throw new FileNotFoundException("The path does not exist.");
                 throw new FileNotFoundException("The path does not exist.");
             }
             }
 
 
-            if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
-            {
-                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 virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 
 
@@ -3084,11 +3064,6 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentNullException(nameof(pathInfo));
                 throw new ArgumentNullException(nameof(pathInfo));
             }
             }
 
 
-            if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
-            {
-                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 virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 
 
@@ -3220,7 +3195,8 @@ namespace Emby.Server.Implementations.Library
 
 
             if (!Directory.Exists(virtualFolderPath))
             if (!Directory.Exists(virtualFolderPath))
             {
             {
-                throw new FileNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName));
+                throw new FileNotFoundException(
+                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
             }
             }
 
 
             var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
             var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)

+ 12 - 12
Emby.Server.Implementations/Library/LiveStreamHelper.cs

@@ -23,9 +23,8 @@ namespace Emby.Server.Implementations.Library
     {
     {
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly ILogger _logger;
         private readonly ILogger _logger;
-
-        private IJsonSerializer _json;
-        private IApplicationPaths _appPaths;
+        private readonly IJsonSerializer _json;
+        private readonly IApplicationPaths _appPaths;
 
 
         public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
         public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
         {
         {
@@ -72,13 +71,14 @@ namespace Emby.Server.Implementations.Library
 
 
                 mediaSource.AnalyzeDurationMs = 3000;
                 mediaSource.AnalyzeDurationMs = 3000;
 
 
-                mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
-                {
-                    MediaSource = mediaSource,
-                    MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
-                    ExtractChapters = false
-
-                }, cancellationToken).ConfigureAwait(false);
+                mediaInfo = await _mediaEncoder.GetMediaInfo(
+                    new MediaInfoRequest
+                    {
+                        MediaSource = mediaSource,
+                        MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
+                        ExtractChapters = false
+                    },
+                    cancellationToken).ConfigureAwait(false);
 
 
                 if (cacheFilePath != null)
                 if (cacheFilePath != null)
                 {
                 {
@@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library
                 mediaSource.RunTimeTicks = null;
                 mediaSource.RunTimeTicks = null;
             }
             }
 
 
-            var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio);
+            var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
 
 
             if (audioStream == null || audioStream.Index == -1)
             if (audioStream == null || audioStream.Index == -1)
             {
             {
@@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.Library
                 mediaSource.DefaultAudioStreamIndex = audioStream.Index;
                 mediaSource.DefaultAudioStreamIndex = audioStream.Index;
             }
             }
 
 
-            var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video);
+            var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
             if (videoStream != null)
             if (videoStream != null)
             {
             {
                 if (!videoStream.BitRate.HasValue)
                 if (!videoStream.BitRate.HasValue)

+ 23 - 25
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -29,6 +29,9 @@ namespace Emby.Server.Implementations.Library
 {
 {
     public class MediaSourceManager : IMediaSourceManager, IDisposable
     public class MediaSourceManager : IMediaSourceManager, IDisposable
     {
     {
+        // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
+        private const char LiveStreamIdDelimeter = '_';
+
         private readonly IItemRepository _itemRepo;
         private readonly IItemRepository _itemRepo;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
@@ -40,6 +43,9 @@ namespace Emby.Server.Implementations.Library
         private readonly ILocalizationManager _localizationManager;
         private readonly ILocalizationManager _localizationManager;
         private readonly IApplicationPaths _appPaths;
         private readonly IApplicationPaths _appPaths;
 
 
+        private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
+        private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
+
         private IMediaSourceProvider[] _providers;
         private IMediaSourceProvider[] _providers;
 
 
         public MediaSourceManager(
         public MediaSourceManager(
@@ -368,7 +374,6 @@ namespace Emby.Server.Implementations.Library
                 }
                 }
             }
             }
 
 
-
             var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
             var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
                 ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
                 ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
 
 
@@ -451,9 +456,6 @@ namespace Emby.Server.Implementations.Library
             .ToList();
             .ToList();
         }
         }
 
 
-        private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
-        private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
-
         public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
         public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
         {
         {
             await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
             await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -619,12 +621,14 @@ namespace Emby.Server.Implementations.Library
 
 
             if (liveStreamInfo is IDirectStreamProvider)
             if (liveStreamInfo is IDirectStreamProvider)
             {
             {
-                var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
-                {
-                    MediaSource = mediaSource,
-                    ExtractChapters = false,
-                    MediaType = DlnaProfileType.Video
-                }, cancellationToken).ConfigureAwait(false);
+                var info = await _mediaEncoder.GetMediaInfo(
+                    new MediaInfoRequest
+                    {
+                        MediaSource = mediaSource,
+                        ExtractChapters = false,
+                        MediaType = DlnaProfileType.Video
+                    },
+                    cancellationToken).ConfigureAwait(false);
 
 
                 mediaSource.MediaStreams = info.MediaStreams;
                 mediaSource.MediaStreams = info.MediaStreams;
                 mediaSource.Container = info.Container;
                 mediaSource.Container = info.Container;
@@ -855,24 +859,21 @@ namespace Emby.Server.Implementations.Library
             }
             }
         }
         }
 
 
-        // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
-        private const char LiveStreamIdDelimeter = '_';
-
-        private Tuple<IMediaSourceProvider, string> GetProvider(string key)
+        private (IMediaSourceProvider, string) GetProvider(string key)
         {
         {
             if (string.IsNullOrEmpty(key))
             if (string.IsNullOrEmpty(key))
             {
             {
-                throw new ArgumentException("key");
+                throw new ArgumentException("Key can't be empty.", nameof(key));
             }
             }
 
 
             var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
             var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
 
 
             var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
             var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
 
 
-            var splitIndex = key.IndexOf(LiveStreamIdDelimeter);
+            var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
             var keyId = key.Substring(splitIndex + 1);
             var keyId = key.Substring(splitIndex + 1);
 
 
-            return new Tuple<IMediaSourceProvider, string>(provider, keyId);
+            return (provider, keyId);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -881,9 +882,9 @@ namespace Emby.Server.Implementations.Library
         public void Dispose()
         public void Dispose()
         {
         {
             Dispose(true);
             Dispose(true);
+            GC.SuppressFinalize(this);
         }
         }
 
 
-        private readonly object _disposeLock = new object();
         /// <summary>
         /// <summary>
         /// Releases unmanaged and - optionally - managed resources.
         /// Releases unmanaged and - optionally - managed resources.
         /// </summary>
         /// </summary>
@@ -892,15 +893,12 @@ namespace Emby.Server.Implementations.Library
         {
         {
             if (dispose)
             if (dispose)
             {
             {
-                lock (_disposeLock)
+                foreach (var key in _openStreams.Keys.ToList())
                 {
                 {
-                    foreach (var key in _openStreams.Keys.ToList())
-                    {
-                        var task = CloseLiveStream(key);
-
-                        Task.WaitAll(task);
-                    }
+                    CloseLiveStream(key).GetAwaiter().GetResult();
                 }
                 }
+
+                _liveStreamSemaphore.Dispose();
             }
             }
         }
         }
     }
     }

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

@@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library
             }
             }
 
 
             // load forced subs if we have found no suitable full subtitles
             // load forced subs if we have found no suitable full subtitles
-            stream = stream ?? streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
+            stream ??= streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
 
 
             if (stream != null)
             if (stream != null)
             {
             {

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

@@ -4,12 +4,12 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 
 

+ 0 - 5
Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs

@@ -1,6 +1,5 @@
 using System.Globalization;
 using System.Globalization;
 using Emby.Naming.TV;
 using Emby.Naming.TV;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Globalization;
@@ -13,7 +12,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
     /// </summary>
     /// </summary>
     public class SeasonResolver : FolderResolver<Season>
     public class SeasonResolver : FolderResolver<Season>
     {
     {
-        private readonly IServerConfigurationManager _config;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILocalizationManager _localization;
         private readonly ILocalizationManager _localization;
         private readonly ILogger<SeasonResolver> _logger;
         private readonly ILogger<SeasonResolver> _logger;
@@ -21,17 +19,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="SeasonResolver"/> class.
         /// Initializes a new instance of the <see cref="SeasonResolver"/> class.
         /// </summary>
         /// </summary>
-        /// <param name="config">The config.</param>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="localization">The localization.</param>
         /// <param name="localization">The localization.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="logger">The logger.</param>
         public SeasonResolver(
         public SeasonResolver(
-            IServerConfigurationManager config,
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
             ILocalizationManager localization,
             ILocalizationManager localization,
             ILogger<SeasonResolver> logger)
             ILogger<SeasonResolver> logger)
         {
         {
-            _config = config;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _localization = localization;
             _localization = localization;
             _logger = logger;
             _logger = logger;

+ 7 - 13
Emby.Server.Implementations/Library/SearchEngine.cs

@@ -4,12 +4,12 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Search;
 using MediaBrowser.Model.Search;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -20,13 +20,11 @@ namespace Emby.Server.Implementations.Library
 {
 {
     public class SearchEngine : ISearchEngine
     public class SearchEngine : ISearchEngine
     {
     {
-        private readonly ILogger<SearchEngine> _logger;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
 
 
-        public SearchEngine(ILogger<SearchEngine> logger, ILibraryManager libraryManager, IUserManager userManager)
+        public SearchEngine(ILibraryManager libraryManager, IUserManager userManager)
         {
         {
-            _logger = logger;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _userManager = userManager;
             _userManager = userManager;
         }
         }
@@ -34,11 +32,7 @@ namespace Emby.Server.Implementations.Library
         public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
         public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
         {
         {
             User user = null;
             User user = null;
-
-            if (query.UserId.Equals(Guid.Empty))
-            {
-            }
-            else
+            if (query.UserId != Guid.Empty)
             {
             {
                 user = _userManager.GetUserById(query.UserId);
                 user = _userManager.GetUserById(query.UserId);
             }
             }
@@ -48,19 +42,19 @@ namespace Emby.Server.Implementations.Library
 
 
             if (query.StartIndex.HasValue)
             if (query.StartIndex.HasValue)
             {
             {
-                results = results.Skip(query.StartIndex.Value).ToList();
+                results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value);
             }
             }
 
 
             if (query.Limit.HasValue)
             if (query.Limit.HasValue)
             {
             {
-                results = results.Take(query.Limit.Value).ToList();
+                results = results.GetRange(0, query.Limit.Value);
             }
             }
 
 
             return new QueryResult<SearchHintInfo>
             return new QueryResult<SearchHintInfo>
             {
             {
                 TotalRecordCount = totalRecordCount,
                 TotalRecordCount = totalRecordCount,
 
 
-                Items = results.ToArray()
+                Items = results
             };
             };
         }
         }
 
 
@@ -85,7 +79,7 @@ namespace Emby.Server.Implementations.Library
 
 
             if (string.IsNullOrEmpty(searchTerm))
             if (string.IsNullOrEmpty(searchTerm))
             {
             {
-                throw new ArgumentNullException("SearchTerm can't be empty.", nameof(searchTerm));
+                throw new ArgumentException("SearchTerm can't be empty.", nameof(query));
             }
             }
 
 
             searchTerm = searchTerm.Trim().RemoveDiacritics();
             searchTerm = searchTerm.Trim().RemoveDiacritics();

+ 0 - 4
Emby.Server.Implementations/Library/UserDataManager.cs

@@ -13,7 +13,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
-using Microsoft.Extensions.Logging;
 using Book = MediaBrowser.Controller.Entities.Book;
 using Book = MediaBrowser.Controller.Entities.Book;
 
 
 namespace Emby.Server.Implementations.Library
 namespace Emby.Server.Implementations.Library
@@ -28,18 +27,15 @@ namespace Emby.Server.Implementations.Library
         private readonly ConcurrentDictionary<string, UserItemData> _userData =
         private readonly ConcurrentDictionary<string, UserItemData> _userData =
             new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
             new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
 
 
-        private readonly ILogger<UserDataManager> _logger;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
         private readonly IUserDataRepository _repository;
         private readonly IUserDataRepository _repository;
 
 
         public UserDataManager(
         public UserDataManager(
-            ILogger<UserDataManager> logger,
             IServerConfigurationManager config,
             IServerConfigurationManager config,
             IUserManager userManager,
             IUserManager userManager,
             IUserDataRepository repository)
             IUserDataRepository repository)
         {
         {
-            _logger = logger;
             _config = config;
             _config = config;
             _userManager = userManager;
             _userManager = userManager;
             _repository = repository;
             _repository = repository;

+ 1 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -12,6 +12,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using System.Xml;
 using System.Xml;
 using Emby.Server.Implementations.Library;
 using Emby.Server.Implementations.Library;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;

+ 1 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -230,7 +230,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
             if (filters.Count > 0)
             if (filters.Count > 0)
             {
             {
-                output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray()));
+                output += string.Format(CultureInfo.InvariantCulture, " -vf \"{0}\"", string.Join(",", filters.ToArray()));
             }
             }
 
 
             return output;
             return output;

+ 2 - 2
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -461,7 +461,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
         private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(
         private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(
             ListingsProviderInfo info,
             ListingsProviderInfo info,
             List<string> programIds,
             List<string> programIds,
-           CancellationToken cancellationToken)
+            CancellationToken cancellationToken)
         {
         {
             if (programIds.Count == 0)
             if (programIds.Count == 0)
             {
             {
@@ -474,7 +474,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             {
             {
                 var imageId = i.Substring(0, 10);
                 var imageId = i.Substring(0, 10);
 
 
-                if (!imageIdString.Contains(imageId))
+                if (!imageIdString.Contains(imageId, StringComparison.Ordinal))
                 {
                 {
                     imageIdString += "\"" + imageId + "\",";
                     imageIdString += "\"" + imageId + "\",";
                 }
                 }

+ 2 - 3
Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs

@@ -237,7 +237,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                     && !programInfo.IsRepeat
                     && !programInfo.IsRepeat
                     && (programInfo.EpisodeNumber ?? 0) == 0)
                     && (programInfo.EpisodeNumber ?? 0) == 0)
                 {
                 {
-                    programInfo.ShowId = programInfo.ShowId + programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture);
+                    programInfo.ShowId += programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture);
                 }
                 }
             }
             }
             else
             else
@@ -246,7 +246,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             }
             }
 
 
             // Construct an id from the channel and start date
             // Construct an id from the channel and start date
-            programInfo.Id = string.Format("{0}_{1:O}", program.ChannelId, program.StartDate);
+            programInfo.Id = string.Format(CultureInfo.InvariantCulture, "{0}_{1:O}", program.ChannelId, program.StartDate);
 
 
             if (programInfo.IsMovie)
             if (programInfo.IsMovie)
             {
             {
@@ -296,7 +296,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 Name = c.DisplayName,
                 Name = c.DisplayName,
                 ImageUrl = c.Icon != null && !string.IsNullOrEmpty(c.Icon.Source) ? c.Icon.Source : null,
                 ImageUrl = c.Icon != null && !string.IsNullOrEmpty(c.Icon.Source) ? c.Icon.Source : null,
                 Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
                 Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
-
             }).ToList();
             }).ToList();
         }
         }
     }
     }

+ 57 - 58
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
+using System.Text.Json;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.Library;
 using Emby.Server.Implementations.Library;
@@ -28,7 +29,6 @@ using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@@ -41,6 +41,7 @@ namespace Emby.Server.Implementations.LiveTv
     /// </summary>
     /// </summary>
     public class LiveTvManager : ILiveTvManager, IDisposable
     public class LiveTvManager : ILiveTvManager, IDisposable
     {
     {
+        private const int MaxGuideDays = 14;
         private const string ExternalServiceTag = "ExternalServiceId";
         private const string ExternalServiceTag = "ExternalServiceId";
 
 
         private const string EtagKey = "ProgramEtag";
         private const string EtagKey = "ProgramEtag";
@@ -54,7 +55,6 @@ namespace Emby.Server.Implementations.LiveTv
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ITaskManager _taskManager;
         private readonly ITaskManager _taskManager;
         private readonly ILocalizationManager _localization;
         private readonly ILocalizationManager _localization;
-        private readonly IJsonSerializer _jsonSerializer;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IChannelManager _channelManager;
         private readonly IChannelManager _channelManager;
         private readonly LiveTvDtoService _tvDtoService;
         private readonly LiveTvDtoService _tvDtoService;
@@ -73,7 +73,6 @@ namespace Emby.Server.Implementations.LiveTv
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
             ITaskManager taskManager,
             ITaskManager taskManager,
             ILocalizationManager localization,
             ILocalizationManager localization,
-            IJsonSerializer jsonSerializer,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             IChannelManager channelManager,
             IChannelManager channelManager,
             LiveTvDtoService liveTvDtoService)
             LiveTvDtoService liveTvDtoService)
@@ -85,7 +84,6 @@ namespace Emby.Server.Implementations.LiveTv
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _taskManager = taskManager;
             _taskManager = taskManager;
             _localization = localization;
             _localization = localization;
-            _jsonSerializer = jsonSerializer;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _dtoService = dtoService;
             _dtoService = dtoService;
             _userDataManager = userDataManager;
             _userDataManager = userDataManager;
@@ -563,7 +561,7 @@ namespace Emby.Server.Implementations.LiveTv
 
 
             item.Audio = info.Audio;
             item.Audio = info.Audio;
             item.ChannelId = channel.Id;
             item.ChannelId = channel.Id;
-            item.CommunityRating = item.CommunityRating ?? info.CommunityRating;
+            item.CommunityRating ??= info.CommunityRating;
             if ((item.CommunityRating ?? 0).Equals(0))
             if ((item.CommunityRating ?? 0).Equals(0))
             {
             {
                 item.CommunityRating = null;
                 item.CommunityRating = null;
@@ -648,8 +646,8 @@ namespace Emby.Server.Implementations.LiveTv
             item.IsSeries = isSeries;
             item.IsSeries = isSeries;
 
 
             item.Name = info.Name;
             item.Name = info.Name;
-            item.OfficialRating = item.OfficialRating ?? info.OfficialRating;
-            item.Overview = item.Overview ?? info.Overview;
+            item.OfficialRating ??= info.OfficialRating;
+            item.Overview ??= info.Overview;
             item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
             item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
             item.ProviderIds = info.ProviderIds;
             item.ProviderIds = info.ProviderIds;
 
 
@@ -686,19 +684,23 @@ namespace Emby.Server.Implementations.LiveTv
             {
             {
                 if (!string.IsNullOrWhiteSpace(info.ImagePath))
                 if (!string.IsNullOrWhiteSpace(info.ImagePath))
                 {
                 {
-                    item.SetImage(new ItemImageInfo
-                    {
-                        Path = info.ImagePath,
-                        Type = ImageType.Primary
-                    }, 0);
+                    item.SetImage(
+                        new ItemImageInfo
+                        {
+                            Path = info.ImagePath,
+                            Type = ImageType.Primary
+                        },
+                        0);
                 }
                 }
                 else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
                 else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
                 {
                 {
-                    item.SetImage(new ItemImageInfo
-                    {
-                        Path = info.ImageUrl,
-                        Type = ImageType.Primary
-                    }, 0);
+                    item.SetImage(
+                        new ItemImageInfo
+                        {
+                            Path = info.ImageUrl,
+                            Type = ImageType.Primary
+                        },
+                        0);
                 }
                 }
             }
             }
 
 
@@ -706,11 +708,13 @@ namespace Emby.Server.Implementations.LiveTv
             {
             {
                 if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
                 if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
                 {
                 {
-                    item.SetImage(new ItemImageInfo
-                    {
-                        Path = info.ThumbImageUrl,
-                        Type = ImageType.Thumb
-                    }, 0);
+                    item.SetImage(
+                        new ItemImageInfo
+                        {
+                            Path = info.ThumbImageUrl,
+                            Type = ImageType.Thumb
+                        },
+                        0);
                 }
                 }
             }
             }
 
 
@@ -718,11 +722,13 @@ namespace Emby.Server.Implementations.LiveTv
             {
             {
                 if (!string.IsNullOrWhiteSpace(info.LogoImageUrl))
                 if (!string.IsNullOrWhiteSpace(info.LogoImageUrl))
                 {
                 {
-                    item.SetImage(new ItemImageInfo
-                    {
-                        Path = info.LogoImageUrl,
-                        Type = ImageType.Logo
-                    }, 0);
+                    item.SetImage(
+                        new ItemImageInfo
+                        {
+                            Path = info.LogoImageUrl,
+                            Type = ImageType.Logo
+                        },
+                        0);
                 }
                 }
             }
             }
 
 
@@ -730,11 +736,13 @@ namespace Emby.Server.Implementations.LiveTv
             {
             {
                 if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
                 if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
                 {
                 {
-                    item.SetImage(new ItemImageInfo
-                    {
-                        Path = info.BackdropImageUrl,
-                        Type = ImageType.Backdrop
-                    }, 0);
+                    item.SetImage(
+                        new ItemImageInfo
+                        {
+                            Path = info.BackdropImageUrl,
+                            Type = ImageType.Backdrop
+                        },
+                        0);
                 }
                 }
             }
             }
 
 
@@ -789,7 +797,6 @@ namespace Emby.Server.Implementations.LiveTv
 
 
             if (query.OrderBy.Count == 0)
             if (query.OrderBy.Count == 0)
             {
             {
-
                 // Unless something else was specified, order by start date to take advantage of a specialized index
                 // Unless something else was specified, order by start date to take advantage of a specialized index
                 query.OrderBy = new[]
                 query.OrderBy = new[]
                 {
                 {
@@ -827,7 +834,7 @@ namespace Emby.Server.Implementations.LiveTv
 
 
             if (!string.IsNullOrWhiteSpace(query.SeriesTimerId))
             if (!string.IsNullOrWhiteSpace(query.SeriesTimerId))
             {
             {
-                var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery { }, cancellationToken).ConfigureAwait(false);
+                var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false);
                 var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.Id).ToString("N", CultureInfo.InvariantCulture), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase));
                 var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.Id).ToString("N", CultureInfo.InvariantCulture), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase));
                 if (seriesTimer != null)
                 if (seriesTimer != null)
                 {
                 {
@@ -850,13 +857,11 @@ namespace Emby.Server.Implementations.LiveTv
 
 
             var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user);
             var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user);
 
 
-            var result = new QueryResult<BaseItemDto>
+            return new QueryResult<BaseItemDto>
             {
             {
                 Items = returnArray,
                 Items = returnArray,
                 TotalRecordCount = queryResult.TotalRecordCount
                 TotalRecordCount = queryResult.TotalRecordCount
             };
             };
-
-            return result;
         }
         }
 
 
         public QueryResult<BaseItem> GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken)
         public QueryResult<BaseItem> GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken)
@@ -1176,7 +1181,6 @@ namespace Emby.Server.Implementations.LiveTv
 
 
                     var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
                     var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
                     {
                     {
-
                         IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
                         IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
                         ChannelIds = new Guid[] { currentChannel.Id },
                         ChannelIds = new Guid[] { currentChannel.Id },
                         DtoOptions = new DtoOptions(true)
                         DtoOptions = new DtoOptions(true)
@@ -1301,8 +1305,6 @@ namespace Emby.Server.Implementations.LiveTv
             }
             }
         }
         }
 
 
-        private const int MaxGuideDays = 14;
-
         private double GetGuideDays()
         private double GetGuideDays()
         {
         {
             var config = GetConfiguration();
             var config = GetConfiguration();
@@ -1715,7 +1717,7 @@ namespace Emby.Server.Implementations.LiveTv
 
 
             if (timer == null)
             if (timer == null)
             {
             {
-                throw new ResourceNotFoundException(string.Format("Timer with Id {0} not found", id));
+                throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "Timer with Id {0} not found", id));
             }
             }
 
 
             var service = GetService(timer.ServiceName);
             var service = GetService(timer.ServiceName);
@@ -1734,7 +1736,7 @@ namespace Emby.Server.Implementations.LiveTv
 
 
             if (timer == null)
             if (timer == null)
             {
             {
-                throw new ResourceNotFoundException(string.Format("SeriesTimer with Id {0} not found", id));
+                throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "SeriesTimer with Id {0} not found", id));
             }
             }
 
 
             var service = GetService(timer.ServiceName);
             var service = GetService(timer.ServiceName);
@@ -1746,10 +1748,12 @@ namespace Emby.Server.Implementations.LiveTv
 
 
         public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken)
         public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken)
         {
         {
-            var results = await GetTimers(new TimerQuery
-            {
-                Id = id
-            }, cancellationToken).ConfigureAwait(false);
+            var results = await GetTimers(
+                new TimerQuery
+                {
+                    Id = id
+                },
+                cancellationToken).ConfigureAwait(false);
 
 
             return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
             return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
         }
         }
@@ -1797,10 +1801,7 @@ namespace Emby.Server.Implementations.LiveTv
             }
             }
 
 
             var returnArray = timers
             var returnArray = timers
-                .Select(i =>
-                {
-                    return i.Item1;
-                })
+                .Select(i => i.Item1)
                 .ToArray();
                 .ToArray();
 
 
             return new QueryResult<SeriesTimerInfo>
             return new QueryResult<SeriesTimerInfo>
@@ -1971,7 +1972,7 @@ namespace Emby.Server.Implementations.LiveTv
 
 
             if (service == null)
             if (service == null)
             {
             {
-                service = _services.First();
+                service = _services[0];
             }
             }
 
 
             var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false);
             var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false);
@@ -1997,9 +1998,7 @@ namespace Emby.Server.Implementations.LiveTv
         {
         {
             var info = await GetNewTimerDefaultsInternal(cancellationToken).ConfigureAwait(false);
             var info = await GetNewTimerDefaultsInternal(cancellationToken).ConfigureAwait(false);
 
 
-            var obj = _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null);
-
-            return obj;
+            return _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null);
         }
         }
 
 
         public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken)
         public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken)
@@ -2128,6 +2127,7 @@ namespace Emby.Server.Implementations.LiveTv
         public void Dispose()
         public void Dispose()
         {
         {
             Dispose(true);
             Dispose(true);
+            GC.SuppressFinalize(this);
         }
         }
 
 
         private bool _disposed = false;
         private bool _disposed = false;
@@ -2234,7 +2234,7 @@ namespace Emby.Server.Implementations.LiveTv
 
 
         public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
         public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
         {
         {
-            info = _jsonSerializer.DeserializeFromString<TunerHostInfo>(_jsonSerializer.SerializeToString(info));
+            info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.Serialize(info));
 
 
             var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
             var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
 
 
@@ -2278,7 +2278,7 @@ namespace Emby.Server.Implementations.LiveTv
         {
         {
             // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
             // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
             // ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
             // ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
-            info = _jsonSerializer.DeserializeFromString<ListingsProviderInfo>(_jsonSerializer.SerializeToString(info));
+            info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.Serialize(info));
 
 
             var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
             var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
 
 
@@ -2450,8 +2450,7 @@ namespace Emby.Server.Implementations.LiveTv
                 .SelectMany(i => i.Locations)
                 .SelectMany(i => i.Locations)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Select(i => _libraryManager.FindByPath(i, true))
                 .Select(i => _libraryManager.FindByPath(i, true))
-                .Where(i => i != null)
-                .Where(i => i.IsVisibleStandalone(user))
+                .Where(i => i != null && i.IsVisibleStandalone(user))
                 .SelectMany(i => _libraryManager.GetCollectionFolders(i))
                 .SelectMany(i => _libraryManager.GetCollectionFolders(i))
                 .GroupBy(x => x.Id)
                 .GroupBy(x => x.Id)
                 .Select(x => x.First())
                 .Select(x => x.First())

+ 4 - 5
Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs

@@ -19,8 +19,7 @@ namespace Emby.Server.Implementations.LiveTv
     public class LiveTvMediaSourceProvider : IMediaSourceProvider
     public class LiveTvMediaSourceProvider : IMediaSourceProvider
     {
     {
         // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
         // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
-        private const char StreamIdDelimeter = '_';
-        private const string StreamIdDelimeterString = "_";
+        private const char StreamIdDelimiter = '_';
 
 
         private readonly ILiveTvManager _liveTvManager;
         private readonly ILiveTvManager _liveTvManager;
         private readonly ILogger<LiveTvMediaSourceProvider> _logger;
         private readonly ILogger<LiveTvMediaSourceProvider> _logger;
@@ -47,7 +46,7 @@ namespace Emby.Server.Implementations.LiveTv
                 }
                 }
             }
             }
 
 
-            return Task.FromResult<IEnumerable<MediaSourceInfo>>(Array.Empty<MediaSourceInfo>());
+            return Task.FromResult(Enumerable.Empty<MediaSourceInfo>());
         }
         }
 
 
         private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
         private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
@@ -98,7 +97,7 @@ namespace Emby.Server.Implementations.LiveTv
                         source.Id ?? string.Empty
                         source.Id ?? string.Empty
                     };
                     };
 
 
-                    source.OpenToken = string.Join(StreamIdDelimeterString, openKeys);
+                    source.OpenToken = string.Join(StreamIdDelimiter, openKeys);
                 }
                 }
 
 
                 // Dummy this up so that direct play checks can still run
                 // Dummy this up so that direct play checks can still run
@@ -116,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv
         /// <inheritdoc />
         /// <inheritdoc />
         public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
         public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
         {
         {
-            var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
+            var keys = openToken.Split(StreamIdDelimiter, 3);
             var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
             var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
 
 
             var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false);
             var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false);

+ 14 - 22
Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs

@@ -1,10 +1,10 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
-using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using System.Text.Json;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
@@ -14,7 +14,7 @@ using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.LiveTv.TunerHosts
 namespace Emby.Server.Implementations.LiveTv.TunerHosts
@@ -23,17 +23,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
     {
     {
         protected readonly IServerConfigurationManager Config;
         protected readonly IServerConfigurationManager Config;
         protected readonly ILogger<BaseTunerHost> Logger;
         protected readonly ILogger<BaseTunerHost> Logger;
-        protected IJsonSerializer JsonSerializer;
         protected readonly IFileSystem FileSystem;
         protected readonly IFileSystem FileSystem;
 
 
-        private readonly ConcurrentDictionary<string, ChannelCache> _channelCache =
-            new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase);
+        private readonly IMemoryCache _memoryCache;
 
 
-        protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem)
+        protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache)
         {
         {
             Config = config;
             Config = config;
             Logger = logger;
             Logger = logger;
-            JsonSerializer = jsonSerializer;
+            _memoryCache = memoryCache;
             FileSystem = fileSystem;
             FileSystem = fileSystem;
         }
         }
 
 
@@ -44,23 +42,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
 
         public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
         public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
         {
         {
-            ChannelCache cache = null;
             var key = tuner.Id;
             var key = tuner.Id;
 
 
-            if (enableCache && !string.IsNullOrEmpty(key) && _channelCache.TryGetValue(key, out cache))
+            if (enableCache && !string.IsNullOrEmpty(key) && _memoryCache.TryGetValue(key, out List<ChannelInfo> cache))
             {
             {
-                return cache.Channels.ToList();
+                return cache;
             }
             }
 
 
-            var result = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
-            var list = result.ToList();
+            var list = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
             // logger.LogInformation("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
             // logger.LogInformation("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
 
 
             if (!string.IsNullOrEmpty(key) && list.Count > 0)
             if (!string.IsNullOrEmpty(key) && list.Count > 0)
             {
             {
-                cache = cache ?? new ChannelCache();
-                cache.Channels = list;
-                _channelCache.AddOrUpdate(key, cache, (k, v) => cache);
+                _memoryCache.Set(key, list);
             }
             }
 
 
             return list;
             return list;
@@ -95,7 +89,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                         try
                         try
                         {
                         {
                             Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
                             Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
-                            JsonSerializer.SerializeToFile(channels, channelCacheFile);
+                            await using var writeStream = File.OpenWrite(channelCacheFile);
+                            await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false);
                         }
                         }
                         catch (IOException)
                         catch (IOException)
                         {
                         {
@@ -110,7 +105,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                     {
                     {
                         try
                         try
                         {
                         {
-                            var channels = JsonSerializer.DeserializeFromFile<List<ChannelInfo>>(channelCacheFile);
+                            await using var readStream = File.OpenRead(channelCacheFile);
+                            var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken)
+                                .ConfigureAwait(false);
                             list.AddRange(channels);
                             list.AddRange(channels);
                         }
                         }
                         catch (IOException)
                         catch (IOException)
@@ -233,10 +230,5 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         {
         {
             return Config.GetConfiguration<LiveTvOptions>("livetv");
             return Config.GetConfiguration<LiveTvOptions>("livetv");
         }
         }
-
-        private class ChannelCache
-        {
-            public List<ChannelInfo> Channels;
-        }
     }
     }
 }
 }

+ 48 - 40
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs

@@ -7,6 +7,7 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Net;
 using System.Net;
 using System.Net.Http;
 using System.Net.Http;
+using System.Text.Json;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
@@ -23,7 +24,7 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@@ -36,17 +37,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         private readonly INetworkManager _networkManager;
         private readonly INetworkManager _networkManager;
         private readonly IStreamHelper _streamHelper;
         private readonly IStreamHelper _streamHelper;
 
 
+        private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
+
         public HdHomerunHost(
         public HdHomerunHost(
             IServerConfigurationManager config,
             IServerConfigurationManager config,
             ILogger<HdHomerunHost> logger,
             ILogger<HdHomerunHost> logger,
-            IJsonSerializer jsonSerializer,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             IHttpClient httpClient,
             IHttpClient httpClient,
             IServerApplicationHost appHost,
             IServerApplicationHost appHost,
             ISocketFactory socketFactory,
             ISocketFactory socketFactory,
             INetworkManager networkManager,
             INetworkManager networkManager,
-            IStreamHelper streamHelper)
-            : base(config, logger, jsonSerializer, fileSystem)
+            IStreamHelper streamHelper,
+            IMemoryCache memoryCache)
+            : base(config, logger, fileSystem, memoryCache)
         {
         {
             _httpClient = httpClient;
             _httpClient = httpClient;
             _appHost = appHost;
             _appHost = appHost;
@@ -75,18 +78,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                 BufferContent = false
                 BufferContent = false
             };
             };
 
 
-            using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false))
-            using (var stream = response.Content)
-            {
-                var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>();
-
-                if (info.ImportFavoritesOnly)
-                {
-                    lineup = lineup.Where(i => i.Favorite).ToList();
-                }
+            using var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
+            await using var stream = response.Content;
+            var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken)
+                .ConfigureAwait(false) ?? new List<Channels>();
 
 
-                return lineup.Where(i => !i.DRM).ToList();
+            if (info.ImportFavoritesOnly)
+            {
+                lineup = lineup.Where(i => i.Favorite).ToList();
             }
             }
+
+            return lineup.Where(i => !i.DRM).ToList();
         }
         }
 
 
         private class HdHomerunChannelInfo : ChannelInfo
         private class HdHomerunChannelInfo : ChannelInfo
@@ -114,7 +116,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             }).Cast<ChannelInfo>().ToList();
             }).Cast<ChannelInfo>().ToList();
         }
         }
 
 
-        private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
         private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
         private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
         {
         {
             var cacheKey = info.Id;
             var cacheKey = info.Id;
@@ -132,35 +133,35 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
 
             try
             try
             {
             {
-                using (var response = await _httpClient.SendAsync(new HttpRequestOptions()
+                using var response = await _httpClient.SendAsync(
+                    new HttpRequestOptions
                 {
                 {
-                    Url = string.Format("{0}/discover.json", GetApiUrl(info)),
+                    Url = string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)),
                     CancellationToken = cancellationToken,
                     CancellationToken = cancellationToken,
                     BufferContent = false
                     BufferContent = false
-                }, HttpMethod.Get).ConfigureAwait(false))
-                using (var stream = response.Content)
-                {
-                    var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false);
+                }, HttpMethod.Get).ConfigureAwait(false);
+                await using var stream = response.Content;
+                var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken)
+                    .ConfigureAwait(false);
 
 
-                    if (!string.IsNullOrEmpty(cacheKey))
+                if (!string.IsNullOrEmpty(cacheKey))
+                {
+                    lock (_modelCache)
                     {
                     {
-                        lock (_modelCache)
-                        {
-                            _modelCache[cacheKey] = discoverResponse;
-                        }
+                        _modelCache[cacheKey] = discoverResponse;
                     }
                     }
-
-                    return discoverResponse;
                 }
                 }
+
+                return discoverResponse;
             }
             }
             catch (HttpException ex)
             catch (HttpException ex)
             {
             {
-                if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
+                if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
                 {
                 {
-                    var defaultValue = "HDHR";
+                    const string DefaultValue = "HDHR";
                     var response = new DiscoverResponse
                     var response = new DiscoverResponse
                     {
                     {
-                        ModelNumber = defaultValue
+                        ModelNumber = DefaultValue
                     };
                     };
                     if (!string.IsNullOrEmpty(cacheKey))
                     if (!string.IsNullOrEmpty(cacheKey))
                     {
                     {
@@ -182,12 +183,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         {
         {
             var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
             var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
 
 
-            using (var response = await _httpClient.SendAsync(new HttpRequestOptions()
-            {
-                Url = string.Format("{0}/tuners.html", GetApiUrl(info)),
-                CancellationToken = cancellationToken,
-                BufferContent = false
-            }, HttpMethod.Get).ConfigureAwait(false))
+            using (var response = await _httpClient.SendAsync(
+                new HttpRequestOptions()
+                {
+                    Url = string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)),
+                    CancellationToken = cancellationToken,
+                    BufferContent = false
+                },
+                HttpMethod.Get).ConfigureAwait(false))
             using (var stream = response.Content)
             using (var stream = response.Content)
             using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
             using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
             {
             {
@@ -195,7 +198,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                 while (!sr.EndOfStream)
                 while (!sr.EndOfStream)
                 {
                 {
                     string line = StripXML(sr.ReadLine());
                     string line = StripXML(sr.ReadLine());
-                    if (line.Contains("Channel"))
+                    if (line.Contains("Channel", StringComparison.Ordinal))
                     {
                     {
                         LiveTvTunerStatus status;
                         LiveTvTunerStatus status;
                         var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
                         var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
@@ -226,6 +229,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
 
         private static string StripXML(string source)
         private static string StripXML(string source)
         {
         {
+            if (string.IsNullOrEmpty(source))
+            {
+                return string.Empty;
+            }
+
             char[] buffer = new char[source.Length];
             char[] buffer = new char[source.Length];
             int bufferIndex = 0;
             int bufferIndex = 0;
             bool inside = false;
             bool inside = false;
@@ -270,7 +278,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
 
                 for (int i = 0; i < model.TunerCount; ++i)
                 for (int i = 0; i < model.TunerCount; ++i)
                 {
                 {
-                    var name = string.Format("Tuner {0}", i + 1);
+                    var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1);
                     var currentChannel = "none"; // @todo Get current channel and map back to Station Id
                     var currentChannel = "none"; // @todo Get current channel and map back to Station Id
                     var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
                     var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
                     var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
                     var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
@@ -725,7 +733,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                 // Need a way to set the Receive timeout on the socket otherwise this might never timeout?
                 // Need a way to set the Receive timeout on the socket otherwise this might never timeout?
                 try
                 try
                 {
                 {
-                    await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken);
+                    await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken).ConfigureAwait(false);
                     var receiveBuffer = new byte[8192];
                     var receiveBuffer = new byte[8192];
 
 
                     while (!cancellationToken.IsCancellationRequested)
                     while (!cancellationToken.IsCancellationRequested)

+ 6 - 5
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs

@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         }
         }
     }
     }
 
 
-    public class HdHomerunManager : IDisposable
+    public sealed class HdHomerunManager : IDisposable
     {
     {
         public const int HdHomeRunPort = 65001;
         public const int HdHomeRunPort = 65001;
 
 
@@ -105,6 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                     StopStreaming(socket).GetAwaiter().GetResult();
                     StopStreaming(socket).GetAwaiter().GetResult();
                 }
                 }
             }
             }
+
+            GC.SuppressFinalize(this);
         }
         }
 
 
         public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
         public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
@@ -162,7 +164,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                     }
                     }
 
 
                     _activeTuner = i;
                     _activeTuner = i;
-                    var lockKeyString = string.Format("{0:d}", lockKeyValue);
+                    var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
                     var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
                     var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
                     await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
                     await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
                     int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
                     int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
@@ -173,8 +175,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                         continue;
                         continue;
                     }
                     }
 
 
-                    var commandList = commands.GetCommands();
-                    foreach (var command in commandList)
+                    foreach (var command in commands.GetCommands())
                     {
                     {
                         var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
                         var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
                         await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
                         await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
@@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                         }
                         }
                     }
                     }
 
 
-                    var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort);
+                    var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
                     var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
                     var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
 
 
                     await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
                     await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);

+ 4 - 4
Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -18,7 +18,7 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Microsoft.Net.Http.Headers;
 using Microsoft.Net.Http.Headers;
 
 
@@ -36,13 +36,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             IServerConfigurationManager config,
             IServerConfigurationManager config,
             IMediaSourceManager mediaSourceManager,
             IMediaSourceManager mediaSourceManager,
             ILogger<M3UTunerHost> logger,
             ILogger<M3UTunerHost> logger,
-            IJsonSerializer jsonSerializer,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             IHttpClient httpClient,
             IHttpClient httpClient,
             IServerApplicationHost appHost,
             IServerApplicationHost appHost,
             INetworkManager networkManager,
             INetworkManager networkManager,
-            IStreamHelper streamHelper)
-            : base(config, logger, jsonSerializer, fileSystem)
+            IStreamHelper streamHelper,
+            IMemoryCache memoryCache)
+            : base(config, logger, fileSystem, memoryCache)
         {
         {
             _httpClient = httpClient;
             _httpClient = httpClient;
             _appHost = appHost;
             _appHost = appHost;

+ 12 - 13
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -158,15 +158,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
         private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
         {
         {
             var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
             var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-            var nameInExtInf = nameParts.Length > 1 ? nameParts[nameParts.Length - 1].Trim() : null;
+            var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty;
 
 
             string numberString = null;
             string numberString = null;
             string attributeValue;
             string attributeValue;
-            double doubleValue;
 
 
             if (attributes.TryGetValue("tvg-chno", out attributeValue))
             if (attributes.TryGetValue("tvg-chno", out attributeValue))
             {
             {
-                if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue))
+                if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
                 {
                 {
                     numberString = attributeValue;
                     numberString = attributeValue;
                 }
                 }
@@ -176,36 +175,36 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             {
             {
                 if (attributes.TryGetValue("tvg-id", out attributeValue))
                 if (attributes.TryGetValue("tvg-id", out attributeValue))
                 {
                 {
-                    if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue))
+                    if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
                     {
                     {
                         numberString = attributeValue;
                         numberString = attributeValue;
                     }
                     }
                     else if (attributes.TryGetValue("channel-id", out attributeValue))
                     else if (attributes.TryGetValue("channel-id", out attributeValue))
                     {
                     {
-                        if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue))
+                        if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
                         {
                         {
                             numberString = attributeValue;
                             numberString = attributeValue;
                         }
                         }
                     }
                     }
                 }
                 }
 
 
-                if (String.IsNullOrWhiteSpace(numberString))
+                if (string.IsNullOrWhiteSpace(numberString))
                 {
                 {
                     // Using this as a fallback now as this leads to Problems with channels like "5 USA"
                     // Using this as a fallback now as this leads to Problems with channels like "5 USA"
                     // where 5 isnt ment to be the channel number
                     // where 5 isnt ment to be the channel number
                     // Check for channel number with the format from SatIp
                     // Check for channel number with the format from SatIp
                     // #EXTINF:0,84. VOX Schweiz
                     // #EXTINF:0,84. VOX Schweiz
                     // #EXTINF:0,84.0 - VOX Schweiz
                     // #EXTINF:0,84.0 - VOX Schweiz
-                    if (!string.IsNullOrWhiteSpace(nameInExtInf))
+                    if (!nameInExtInf.IsEmpty && !nameInExtInf.IsWhiteSpace())
                     {
                     {
                         var numberIndex = nameInExtInf.IndexOf(' ');
                         var numberIndex = nameInExtInf.IndexOf(' ');
                         if (numberIndex > 0)
                         if (numberIndex > 0)
                         {
                         {
-                            var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
+                            var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' });
 
 
-                            if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
+                            if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
                             {
                             {
-                                numberString = numberPart;
+                                numberString = numberPart.ToString();
                             }
                             }
                         }
                         }
                     }
                     }
@@ -231,7 +230,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 {
                 {
                     try
                     try
                     {
                     {
-                        numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/').Last());
+                        numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]);
 
 
                         if (!IsValidChannelNumber(numberString))
                         if (!IsValidChannelNumber(numberString))
                         {
                         {
@@ -258,7 +257,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 return false;
                 return false;
             }
             }
 
 
-            if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
+            if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
             {
             {
                 return false;
                 return false;
             }
             }
@@ -281,7 +280,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 {
                 {
                     var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
                     var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
 
 
-                    if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
+                    if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
                     {
                     {
                         // channel.Number = number.ToString();
                         // channel.Number = number.ToString();
                         nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });
                         nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });

+ 7 - 3
Emby.Server.Implementations/Localization/Core/af.json

@@ -19,8 +19,8 @@
     "Sync": "Sinkroniseer",
     "Sync": "Sinkroniseer",
     "HeaderFavoriteSongs": "Gunsteling Liedjies",
     "HeaderFavoriteSongs": "Gunsteling Liedjies",
     "Songs": "Liedjies",
     "Songs": "Liedjies",
-    "DeviceOnlineWithName": "{0} is verbind",
-    "DeviceOfflineWithName": "{0} het afgesluit",
+    "DeviceOnlineWithName": "{0} gekoppel is",
+    "DeviceOfflineWithName": "{0} is ontkoppel",
     "Collections": "Versamelings",
     "Collections": "Versamelings",
     "Inherit": "Ontvang",
     "Inherit": "Ontvang",
     "HeaderLiveTV": "Live TV",
     "HeaderLiveTV": "Live TV",
@@ -91,5 +91,9 @@
     "ChapterNameValue": "Hoofstuk",
     "ChapterNameValue": "Hoofstuk",
     "CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
     "CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
     "AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
     "AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
-    "Albums": "Albums"
+    "Albums": "Albums",
+    "TasksChannelsCategory": "Internet kanale",
+    "TasksApplicationCategory": "aansoek",
+    "TasksLibraryCategory": "biblioteek",
+    "TasksMaintenanceCategory": "onderhoud"
 }
 }

+ 19 - 19
Emby.Server.Implementations/Localization/Core/bn.json

@@ -1,26 +1,26 @@
 {
 {
     "DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
     "DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
     "DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
     "DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
-    "Collections": "সংকলন",
+    "Collections": "কলেক্শন",
     "ChapterNameValue": "অধ্যায় {0}",
     "ChapterNameValue": "অধ্যায় {0}",
     "Channels": "চ্যানেল",
     "Channels": "চ্যানেল",
-    "CameraImageUploadedFrom": "একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে {0} থেকে",
+    "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
     "Books": "বই",
     "Books": "বই",
-    "AuthenticationSucceededWithUserName": "{0} যাচাই সফল",
-    "Artists": "শিল্পী",
+    "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
+    "Artists": "শিল্পীরা",
     "Application": "অ্যাপ্লিকেশন",
     "Application": "অ্যাপ্লিকেশন",
     "Albums": "অ্যালবামগুলো",
     "Albums": "অ্যালবামগুলো",
     "HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
     "HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
     "HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
     "HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
     "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
     "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
     "HeaderContinueWatching": "দেখতে থাকুন",
     "HeaderContinueWatching": "দেখতে থাকুন",
-    "HeaderCameraUploads": "ক্যামেরার আপলোডগুলো",
-    "HeaderAlbumArtists": "এলবামের শিল্পী",
-    "Genres": "ঘরানা",
+    "HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ",
+    "HeaderAlbumArtists": "এলবাম শিল্পী",
+    "Genres": "জেনার",
     "Folders": "ফোল্ডারগুলো",
     "Folders": "ফোল্ডারগুলো",
-    "Favorites": "ফেভারিটগুলো",
-    "FailedLoginAttemptWithUserName": "{0} থেকে লগিন করতে ব্যর্থ",
-    "AppDeviceValues": "প: {0}, ডিভাইস: {0}",
+    "Favorites": "পছন্দসমূহ",
+    "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
+    "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
     "VersionNumber": "সংস্করণ {0}",
     "VersionNumber": "সংস্করণ {0}",
     "ValueSpecialEpisodeName": "বিশেষ - {0}",
     "ValueSpecialEpisodeName": "বিশেষ - {0}",
     "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
     "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
@@ -74,20 +74,20 @@
     "NameInstallFailed": "{0} ইন্সটল ব্যর্থ",
     "NameInstallFailed": "{0} ইন্সটল ব্যর্থ",
     "MusicVideos": "গানের ভিডিও",
     "MusicVideos": "গানের ভিডিও",
     "Music": "গান",
     "Music": "গান",
-    "Movies": "সিনেমা",
+    "Movies": "চলচ্চিত্র",
     "MixedContent": "মিশ্র কন্টেন্ট",
     "MixedContent": "মিশ্র কন্টেন্ট",
-    "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন হালনাগাদ করা হয়েছে",
-    "HeaderRecordingGroups": "রেকর্ডিং গ্রুপ",
-    "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসন অংশ আপডেট করা হয়েছে",
-    "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে হালনাগাদ করা হয়েছে",
-    "MessageApplicationUpdated": "জেলিফিন সার্ভার হালনাগাদ করা হয়েছে",
-    "Latest": "একদম নতুন",
+    "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
+    "HeaderRecordingGroups": "রেকর্ডিং দল",
+    "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে",
+    "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
+    "MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
+    "Latest": "সর্বশেষ",
     "LabelRunningTimeValue": "চলার সময়: {0}",
     "LabelRunningTimeValue": "চলার সময়: {0}",
-    "LabelIpAddressValue": "আইপি ঠিকানা: {0}",
+    "LabelIpAddressValue": "আইপি এড্রেস: {0}",
     "ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
     "ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
     "ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
     "ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
     "Inherit": "থেকে পাওয়া",
     "Inherit": "থেকে পাওয়া",
-    "HomeVideos": "বাসার ভিডিও",
+    "HomeVideos": "হোম ভিডিও",
     "HeaderNextUp": "এরপরে আসছে",
     "HeaderNextUp": "এরপরে আসছে",
     "HeaderLiveTV": "লাইভ টিভি",
     "HeaderLiveTV": "লাইভ টিভি",
     "HeaderFavoriteSongs": "প্রিয় গানগুলো",
     "HeaderFavoriteSongs": "প্রিয় গানগুলো",

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

@@ -3,9 +3,9 @@
     "AppDeviceValues": "App: {0}, Gerät: {1}",
     "AppDeviceValues": "App: {0}, Gerät: {1}",
     "Application": "Anwendung",
     "Application": "Anwendung",
     "Artists": "Interpreten",
     "Artists": "Interpreten",
-    "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert",
+    "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
     "Books": "Bücher",
     "Books": "Bücher",
-    "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
+    "CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
     "Channels": "Kanäle",
     "Channels": "Kanäle",
     "ChapterNameValue": "Kapitel {0}",
     "ChapterNameValue": "Kapitel {0}",
     "Collections": "Sammlungen",
     "Collections": "Sammlungen",
@@ -101,12 +101,12 @@
     "TaskCleanTranscode": "Lösche Transkodier Pfad",
     "TaskCleanTranscode": "Lösche Transkodier Pfad",
     "TaskUpdatePluginsDescription": "Lädt 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",
     "TaskUpdatePlugins": "Update Plugins",
-    "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.",
-    "TaskRefreshPeople": "Erneuere Schausteller",
+    "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
+    "TaskRefreshPeople": "Erneuere Schauspieler",
     "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
     "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
     "TaskCleanLogs": "Lösche Log Pfad",
     "TaskCleanLogs": "Lösche Log Pfad",
     "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
     "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
-    "TaskRefreshLibrary": "Scanne alle Bibliotheken",
+    "TaskRefreshLibrary": "Scanne Medien-Bibliothek",
     "TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.",
     "TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.",
     "TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder",
     "TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder",
     "TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",
     "TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",

+ 28 - 28
Emby.Server.Implementations/Localization/Core/he.json

@@ -18,13 +18,13 @@
     "HeaderAlbumArtists": "אמני האלבום",
     "HeaderAlbumArtists": "אמני האלבום",
     "HeaderCameraUploads": "העלאות ממצלמה",
     "HeaderCameraUploads": "העלאות ממצלמה",
     "HeaderContinueWatching": "המשך לצפות",
     "HeaderContinueWatching": "המשך לצפות",
-    "HeaderFavoriteAlbums": "אלבומים שאהבתי",
+    "HeaderFavoriteAlbums": "אלבומים מועדפים",
     "HeaderFavoriteArtists": "אמנים מועדפים",
     "HeaderFavoriteArtists": "אמנים מועדפים",
     "HeaderFavoriteEpisodes": "פרקים מועדפים",
     "HeaderFavoriteEpisodes": "פרקים מועדפים",
-    "HeaderFavoriteShows": "סדרות מועדפות",
+    "HeaderFavoriteShows": "תוכניות מועדפות",
     "HeaderFavoriteSongs": "שירים מועדפים",
     "HeaderFavoriteSongs": "שירים מועדפים",
     "HeaderLiveTV": "שידורים חיים",
     "HeaderLiveTV": "שידורים חיים",
-    "HeaderNextUp": "הבא",
+    "HeaderNextUp": "הבא בתור",
     "HeaderRecordingGroups": "קבוצות הקלטה",
     "HeaderRecordingGroups": "קבוצות הקלטה",
     "HomeVideos": "סרטונים בייתים",
     "HomeVideos": "סרטונים בייתים",
     "Inherit": "הורש",
     "Inherit": "הורש",
@@ -45,37 +45,37 @@
     "NameSeasonNumber": "עונה {0}",
     "NameSeasonNumber": "עונה {0}",
     "NameSeasonUnknown": "עונה לא ידועה",
     "NameSeasonUnknown": "עונה לא ידועה",
     "NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
     "NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
-    "NotificationOptionApplicationUpdateAvailable": "Application update available",
-    "NotificationOptionApplicationUpdateInstalled": "Application update installed",
-    "NotificationOptionAudioPlayback": "Audio playback started",
-    "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
-    "NotificationOptionCameraImageUploaded": "Camera image uploaded",
+    "NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
+    "NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
+    "NotificationOptionAudioPlayback": "ניגון שמע החל",
+    "NotificationOptionAudioPlaybackStopped": "ניגון שמע הופסק",
+    "NotificationOptionCameraImageUploaded": "תמונת מצלמה הועלתה",
     "NotificationOptionInstallationFailed": "התקנה נכשלה",
     "NotificationOptionInstallationFailed": "התקנה נכשלה",
-    "NotificationOptionNewLibraryContent": "New content added",
-    "NotificationOptionPluginError": "Plugin failure",
+    "NotificationOptionNewLibraryContent": "תוכן חדש הוסף",
+    "NotificationOptionPluginError": "כשלון בתוסף",
     "NotificationOptionPluginInstalled": "התוסף הותקן",
     "NotificationOptionPluginInstalled": "התוסף הותקן",
     "NotificationOptionPluginUninstalled": "התוסף הוסר",
     "NotificationOptionPluginUninstalled": "התוסף הוסר",
     "NotificationOptionPluginUpdateInstalled": "העדכון לתוסף הותקן",
     "NotificationOptionPluginUpdateInstalled": "העדכון לתוסף הותקן",
     "NotificationOptionServerRestartRequired": "יש לאתחל את השרת",
     "NotificationOptionServerRestartRequired": "יש לאתחל את השרת",
-    "NotificationOptionTaskFailed": "Scheduled task failure",
-    "NotificationOptionUserLockedOut": "User locked out",
-    "NotificationOptionVideoPlayback": "Video playback started",
-    "NotificationOptionVideoPlaybackStopped": "Video playback stopped",
+    "NotificationOptionTaskFailed": "משימה מתוזמנת נכשלה",
+    "NotificationOptionUserLockedOut": "משתמש ננעל",
+    "NotificationOptionVideoPlayback": "ניגון וידאו החל",
+    "NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק",
     "Photos": "תמונות",
     "Photos": "תמונות",
     "Playlists": "רשימות הפעלה",
     "Playlists": "רשימות הפעלה",
     "Plugin": "Plugin",
     "Plugin": "Plugin",
-    "PluginInstalledWithName": "{0} was installed",
-    "PluginUninstalledWithName": "{0} was uninstalled",
-    "PluginUpdatedWithName": "{0} was updated",
+    "PluginInstalledWithName": "{0} הותקן",
+    "PluginUninstalledWithName": "{0} הוסר",
+    "PluginUpdatedWithName": "{0} עודכן",
     "ProviderValue": "Provider: {0}",
     "ProviderValue": "Provider: {0}",
-    "ScheduledTaskFailedWithName": "{0} failed",
-    "ScheduledTaskStartedWithName": "{0} started",
-    "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
+    "ScheduledTaskFailedWithName": "{0} נכשל",
+    "ScheduledTaskStartedWithName": "{0} החל",
+    "ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
     "Shows": "סדרות",
     "Shows": "סדרות",
     "Songs": "שירים",
     "Songs": "שירים",
     "StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.",
     "StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
-    "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
+    "SubtitleDownloadFailureFromForItem": "הורדת כתוביות נכשלה מ-{0} עבור {1}",
     "Sync": "סנכרן",
     "Sync": "סנכרן",
     "System": "System",
     "System": "System",
     "TvShows": "סדרות טלוויזיה",
     "TvShows": "סדרות טלוויזיה",
@@ -83,14 +83,14 @@
     "UserCreatedWithName": "המשתמש {0} נוצר",
     "UserCreatedWithName": "המשתמש {0} נוצר",
     "UserDeletedWithName": "המשתמש {0} הוסר",
     "UserDeletedWithName": "המשתמש {0} הוסר",
     "UserDownloadingItemWithValues": "{0} מוריד את {1}",
     "UserDownloadingItemWithValues": "{0} מוריד את {1}",
-    "UserLockedOutWithName": "User {0} has been locked out",
-    "UserOfflineFromDevice": "{0} has disconnected from {1}",
-    "UserOnlineFromDevice": "{0} is online from {1}",
-    "UserPasswordChangedWithName": "Password has been changed for user {0}",
-    "UserPolicyUpdatedWithName": "User policy has been updated for {0}",
+    "UserLockedOutWithName": "המשתמש {0} ננעל",
+    "UserOfflineFromDevice": "{0} התנתק מ-{1}",
+    "UserOnlineFromDevice": "{0} מחובר מ-{1}",
+    "UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}",
+    "UserPolicyUpdatedWithName": "מדיניות המשתמש {0} עודכנה",
     "UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",
     "UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",
     "UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
     "UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
-    "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
+    "ValueHasBeenAddedToLibrary": "{0} התווסף לספריית המדיה שלך",
     "ValueSpecialEpisodeName": "מיוחד- {0}",
     "ValueSpecialEpisodeName": "מיוחד- {0}",
     "VersionNumber": "Version {0}",
     "VersionNumber": "Version {0}",
     "TaskRefreshLibrary": "סרוק ספריית מדיה",
     "TaskRefreshLibrary": "סרוק ספריית מדיה",
@@ -109,7 +109,7 @@
     "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
     "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
     "TasksChannelsCategory": "ערוצי אינטרנט",
     "TasksChannelsCategory": "ערוצי אינטרנט",
     "TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
     "TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
-    "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות.",
+    "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
     "TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
     "TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
     "TaskRefreshChannels": "רענן ערוץ",
     "TaskRefreshChannels": "רענן ערוץ",
     "TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.",
     "TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.",

+ 36 - 14
Emby.Server.Implementations/Localization/Core/id.json

@@ -7,8 +7,8 @@
     "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
     "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
     "Latest": "Terbaru",
     "Latest": "Terbaru",
     "LabelIpAddressValue": "Alamat IP: {0}",
     "LabelIpAddressValue": "Alamat IP: {0}",
-    "ItemRemovedWithName": "{0} sudah dikeluarkan dari perpustakaan",
-    "ItemAddedWithName": "{0} sudah dimasukkan ke dalam perpustakaan",
+    "ItemRemovedWithName": "{0} sudah dikeluarkan dari pustaka",
+    "ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka",
     "Inherit": "Warisan",
     "Inherit": "Warisan",
     "HomeVideos": "Video Rumah",
     "HomeVideos": "Video Rumah",
     "HeaderRecordingGroups": "Grup Rekaman",
     "HeaderRecordingGroups": "Grup Rekaman",
@@ -19,8 +19,8 @@
     "HeaderFavoriteEpisodes": "Episode Favorit",
     "HeaderFavoriteEpisodes": "Episode Favorit",
     "HeaderFavoriteArtists": "Artis Favorit",
     "HeaderFavoriteArtists": "Artis Favorit",
     "HeaderFavoriteAlbums": "Album Favorit",
     "HeaderFavoriteAlbums": "Album Favorit",
-    "HeaderContinueWatching": "Masih Melihat",
-    "HeaderCameraUploads": "Uplod Kamera",
+    "HeaderContinueWatching": "Lanjutkan Menonton",
+    "HeaderCameraUploads": "Unggahan Kamera",
     "HeaderAlbumArtists": "Album Artis",
     "HeaderAlbumArtists": "Album Artis",
     "Genres": "Genre",
     "Genres": "Genre",
     "Folders": "Folder",
     "Folders": "Folder",
@@ -32,11 +32,11 @@
     "ChapterNameValue": "Bagian {0}",
     "ChapterNameValue": "Bagian {0}",
     "Channels": "Saluran",
     "Channels": "Saluran",
     "TvShows": "Seri TV",
     "TvShows": "Seri TV",
-    "SubtitleDownloadFailureFromForItem": "Talop gagal diunduh dari {0} untuk {1}",
-    "StartupEmbyServerIsLoading": "Peladen Jellyfin sedang dimuat. Silakan coba kembali beberapa saat lagi.",
+    "SubtitleDownloadFailureFromForItem": "Subtitel gagal diunduh dari {0} untuk {1}",
+    "StartupEmbyServerIsLoading": "Server Jellyfin sedang dimuat. Silakan coba lagi nanti.",
     "Songs": "Lagu",
     "Songs": "Lagu",
     "Playlists": "Daftar putar",
     "Playlists": "Daftar putar",
-    "NotificationOptionPluginUninstalled": "Plugin dilepas",
+    "NotificationOptionPluginUninstalled": "Plugin dihapus",
     "MusicVideos": "Video musik",
     "MusicVideos": "Video musik",
     "VersionNumber": "Versi {0}",
     "VersionNumber": "Versi {0}",
     "ValueSpecialEpisodeName": "Spesial - {0}",
     "ValueSpecialEpisodeName": "Spesial - {0}",
@@ -65,7 +65,7 @@
     "Photos": "Foto",
     "Photos": "Foto",
     "NotificationOptionUserLockedOut": "Pengguna terkunci",
     "NotificationOptionUserLockedOut": "Pengguna terkunci",
     "NotificationOptionTaskFailed": "Kegagalan tugas terjadwal",
     "NotificationOptionTaskFailed": "Kegagalan tugas terjadwal",
-    "NotificationOptionServerRestartRequired": "Restart peladen dibutuhkan",
+    "NotificationOptionServerRestartRequired": "Muat ulang server dibutuhkan",
     "NotificationOptionPluginUpdateInstalled": "Pembaruan plugin terpasang",
     "NotificationOptionPluginUpdateInstalled": "Pembaruan plugin terpasang",
     "NotificationOptionPluginInstalled": "Plugin terpasang",
     "NotificationOptionPluginInstalled": "Plugin terpasang",
     "NotificationOptionPluginError": "Kegagalan plugin",
     "NotificationOptionPluginError": "Kegagalan plugin",
@@ -74,14 +74,14 @@
     "NotificationOptionCameraImageUploaded": "Gambar kamera terunggah",
     "NotificationOptionCameraImageUploaded": "Gambar kamera terunggah",
     "NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang",
     "NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang",
     "NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia",
     "NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia",
-    "NewVersionIsAvailable": "Sebuah versi baru dari Peladen Jellyfin tersedia untuk diunduh.",
+    "NewVersionIsAvailable": "Versi baru dari Jellyfin Server tersedia untuk diunduh.",
     "NameSeasonUnknown": "Musim tak diketahui",
     "NameSeasonUnknown": "Musim tak diketahui",
     "NameSeasonNumber": "Musim {0}",
     "NameSeasonNumber": "Musim {0}",
-    "NameInstallFailed": "{0} instalasi gagal",
+    "NameInstallFailed": "{0} penginstalan gagal",
     "Music": "Musik",
     "Music": "Musik",
     "Movies": "Film",
     "Movies": "Film",
-    "MessageServerConfigurationUpdated": "Konfigurasi peladen telah diperbarui",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi peladen bagian {0} telah diperbarui",
+    "MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
     "FailedLoginAttemptWithUserName": "Percobaan login gagal dari {0}",
     "FailedLoginAttemptWithUserName": "Percobaan login gagal dari {0}",
     "CameraImageUploadedFrom": "Sebuah gambar baru telah diunggah dari {0}",
     "CameraImageUploadedFrom": "Sebuah gambar baru telah diunggah dari {0}",
     "DeviceOfflineWithName": "{0} telah terputus",
     "DeviceOfflineWithName": "{0} telah terputus",
@@ -90,6 +90,28 @@
     "NotificationOptionVideoPlayback": "Pemutaran video dimulai",
     "NotificationOptionVideoPlayback": "Pemutaran video dimulai",
     "NotificationOptionAudioPlaybackStopped": "Pemutaran audio berhenti",
     "NotificationOptionAudioPlaybackStopped": "Pemutaran audio berhenti",
     "NotificationOptionAudioPlayback": "Pemutaran audio dimulai",
     "NotificationOptionAudioPlayback": "Pemutaran audio dimulai",
-    "MixedContent": "Konten campur",
-    "PluginUninstalledWithName": "{0} telah dihapus"
+    "MixedContent": "Konten campuran",
+    "PluginUninstalledWithName": "{0} telah dihapus",
+    "TaskRefreshChapterImagesDescription": "Membuat gambar mini untuk video yang memiliki bagian.",
+    "TaskRefreshChapterImages": "Ekstrak Gambar Bagian",
+    "TaskCleanCacheDescription": "Menghapus file cache yang tidak lagi dibutuhkan oleh sistem.",
+    "TaskCleanCache": "Bersihkan Cache Direktori",
+    "TasksLibraryCategory": "Pustaka",
+    "TasksMaintenanceCategory": "Perbaikan",
+    "TasksApplicationCategory": "Aplikasi",
+    "TaskRefreshPeopleDescription": "Memperbarui metadata untuk aktor dan sutradara di pustaka media Anda.",
+    "TaskRefreshLibraryDescription": "Memindai Pustaka media Anda untuk mencari file baru dan memperbarui metadata.",
+    "TasksChannelsCategory": "Saluran Online",
+    "TaskDownloadMissingSubtitlesDescription": "Mencari di internet untuk subtitle yang hilang berdasarkan konfigurasi metadata.",
+    "TaskDownloadMissingSubtitles": "Unduh subtitle yang hilang",
+    "TaskRefreshChannelsDescription": "Segarkan informasi saluran internet.",
+    "TaskRefreshChannels": "Segarkan Saluran",
+    "TaskCleanTranscodeDescription": "Menghapus file transcode yang berumur lebih dari satu hari.",
+    "TaskCleanTranscode": "Bersihkan Direktori Transcode",
+    "TaskUpdatePluginsDescription": "Unduh dan instal pembaruan untuk plugin yang dikonfigurasi untuk memperbarui secara otomatis.",
+    "TaskUpdatePlugins": "Perbarui Plugin",
+    "TaskRefreshPeople": "Muat ulang Orang",
+    "TaskCleanLogsDescription": "Menghapus file log yang lebih dari {0} hari.",
+    "TaskCleanLogs": "Bersihkan Log Direktori",
+    "TaskRefreshLibrary": "Pindai Pustaka Media"
 }
 }

+ 4 - 4
Emby.Server.Implementations/Localization/Core/it.json

@@ -84,8 +84,8 @@
     "UserDeletedWithName": "L'utente {0} è stato rimosso",
     "UserDeletedWithName": "L'utente {0} è stato rimosso",
     "UserDownloadingItemWithValues": "{0} sta scaricando {1}",
     "UserDownloadingItemWithValues": "{0} sta scaricando {1}",
     "UserLockedOutWithName": "L'utente {0} è stato bloccato",
     "UserLockedOutWithName": "L'utente {0} è stato bloccato",
-    "UserOfflineFromDevice": "{0} è stato disconnesso da {1}",
-    "UserOnlineFromDevice": "{0} è online da {1}",
+    "UserOfflineFromDevice": "{0} si è disconnesso su {1}",
+    "UserOnlineFromDevice": "{0} è online su {1}",
     "UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
     "UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
     "UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
     "UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
     "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1} su {2}",
     "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1} su {2}",
@@ -102,11 +102,11 @@
     "TaskUpdatePluginsDescription": "Scarica e installa gli aggiornamenti per i plugin che sono stati configurati per essere aggiornati contemporaneamente.",
     "TaskUpdatePluginsDescription": "Scarica e installa gli aggiornamenti per i plugin che sono stati configurati per essere aggiornati contemporaneamente.",
     "TaskUpdatePlugins": "Aggiorna i Plugin",
     "TaskUpdatePlugins": "Aggiorna i Plugin",
     "TaskRefreshPeopleDescription": "Aggiorna i metadati per gli attori e registi nella tua libreria multimediale.",
     "TaskRefreshPeopleDescription": "Aggiorna i metadati per gli attori e registi nella tua libreria multimediale.",
-    "TaskRefreshPeople": "Aggiorna persone",
+    "TaskRefreshPeople": "Aggiornamento Persone",
     "TaskCleanLogsDescription": "Rimuovi i file di log più vecchi di {0} giorni.",
     "TaskCleanLogsDescription": "Rimuovi i file di log più vecchi di {0} giorni.",
     "TaskCleanLogs": "Pulisci la cartella dei log",
     "TaskCleanLogs": "Pulisci la cartella dei log",
     "TaskRefreshLibraryDescription": "Analizza la tua libreria multimediale per nuovi file e rinnova i metadati.",
     "TaskRefreshLibraryDescription": "Analizza la tua libreria multimediale per nuovi file e rinnova i metadati.",
-    "TaskRefreshLibrary": "Analizza la libreria dei contenuti multimediali",
+    "TaskRefreshLibrary": "Scan Librerie",
     "TaskRefreshChapterImagesDescription": "Crea le thumbnail per i video che hanno capitoli.",
     "TaskRefreshChapterImagesDescription": "Crea le thumbnail per i video che hanno capitoli.",
     "TaskRefreshChapterImages": "Estrai immagini capitolo",
     "TaskRefreshChapterImages": "Estrai immagini capitolo",
     "TaskCleanCacheDescription": "Cancella i file di cache non più necessari al sistema.",
     "TaskCleanCacheDescription": "Cancella i file di cache non più necessari al sistema.",

+ 3 - 1
Emby.Server.Implementations/Localization/Core/mr.json

@@ -57,5 +57,7 @@
     "HeaderCameraUploads": "कॅमेरा अपलोड",
     "HeaderCameraUploads": "कॅमेरा अपलोड",
     "CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
     "CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
     "Application": "अ‍ॅप्लिकेशन",
     "Application": "अ‍ॅप्लिकेशन",
-    "AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}"
+    "AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}",
+    "Collections": "संग्रह",
+    "ChapterNameValue": "धडा {0}"
 }
 }

+ 32 - 32
Emby.Server.Implementations/Localization/Core/ms.json

@@ -5,47 +5,47 @@
     "Artists": "Artis",
     "Artists": "Artis",
     "AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
     "AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
     "Books": "Buku-buku",
     "Books": "Buku-buku",
-    "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
+    "CameraImageUploadedFrom": "Ada gambar dari kamera yang baru dimuat naik melalui {0}",
     "Channels": "Saluran",
     "Channels": "Saluran",
-    "ChapterNameValue": "Chapter {0}",
+    "ChapterNameValue": "Bab {0}",
     "Collections": "Koleksi",
     "Collections": "Koleksi",
-    "DeviceOfflineWithName": "{0} has disconnected",
-    "DeviceOnlineWithName": "{0} is connected",
+    "DeviceOfflineWithName": "{0} telah diputuskan sambungan",
+    "DeviceOnlineWithName": "{0} telah disambung",
     "FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}",
     "FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}",
-    "Favorites": "Favorites",
-    "Folders": "Folders",
+    "Favorites": "Kegemaran",
+    "Folders": "Fail-fail",
     "Genres": "Genre-genre",
     "Genres": "Genre-genre",
-    "HeaderAlbumArtists": "Album Artists",
+    "HeaderAlbumArtists": "Album Artis-artis",
     "HeaderCameraUploads": "Muatnaik Kamera",
     "HeaderCameraUploads": "Muatnaik Kamera",
     "HeaderContinueWatching": "Terus Menonton",
     "HeaderContinueWatching": "Terus Menonton",
-    "HeaderFavoriteAlbums": "Favorite Albums",
-    "HeaderFavoriteArtists": "Favorite Artists",
-    "HeaderFavoriteEpisodes": "Favorite Episodes",
-    "HeaderFavoriteShows": "Favorite Shows",
-    "HeaderFavoriteSongs": "Favorite Songs",
-    "HeaderLiveTV": "Live TV",
-    "HeaderNextUp": "Next Up",
-    "HeaderRecordingGroups": "Recording Groups",
-    "HomeVideos": "Home videos",
-    "Inherit": "Inherit",
-    "ItemAddedWithName": "{0} was added to the library",
-    "ItemRemovedWithName": "{0} was removed from the library",
+    "HeaderFavoriteAlbums": "Album-album Kegemaran",
+    "HeaderFavoriteArtists": "Artis-artis Kegemaran",
+    "HeaderFavoriteEpisodes": "Episod-episod Kegemaran",
+    "HeaderFavoriteShows": "Rancangan-rancangan Kegemaran",
+    "HeaderFavoriteSongs": "Lagu-lagu Kegemaran",
+    "HeaderLiveTV": "TV Siaran Langsung",
+    "HeaderNextUp": "Seterusnya",
+    "HeaderRecordingGroups": "Kumpulan-kumpulan Rakaman",
+    "HomeVideos": "Video Personal",
+    "Inherit": "Mewarisi",
+    "ItemAddedWithName": "{0} telah ditambahkan ke dalam pustaka",
+    "ItemRemovedWithName": "{0} telah dibuang daripada pustaka",
     "LabelIpAddressValue": "Alamat IP: {0}",
     "LabelIpAddressValue": "Alamat IP: {0}",
-    "LabelRunningTimeValue": "Running time: {0}",
-    "Latest": "Latest",
-    "MessageApplicationUpdated": "Jellyfin Server has been updated",
-    "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
-    "MessageServerConfigurationUpdated": "Server configuration has been updated",
-    "MixedContent": "Mixed content",
-    "Movies": "Movies",
+    "LabelRunningTimeValue": "Masa berjalan: {0}",
+    "Latest": "Terbaru",
+    "MessageApplicationUpdated": "Jellyfin Server telah dikemas kini",
+    "MessageApplicationUpdatedTo": "Jellyfin Server telah dikemas kini ke {0}",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini",
+    "MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini",
+    "MixedContent": "Kandungan campuran",
+    "Movies": "Filem",
     "Music": "Muzik",
     "Music": "Muzik",
     "MusicVideos": "Video muzik",
     "MusicVideos": "Video muzik",
-    "NameInstallFailed": "{0} installation failed",
-    "NameSeasonNumber": "Season {0}",
-    "NameSeasonUnknown": "Season Unknown",
-    "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
-    "NotificationOptionApplicationUpdateAvailable": "Application update available",
+    "NameInstallFailed": "{0} pemasangan gagal",
+    "NameSeasonNumber": "Musim {0}",
+    "NameSeasonUnknown": "Musim Tidak Diketahui",
+    "NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.",
+    "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia",
     "NotificationOptionApplicationUpdateInstalled": "Application update installed",
     "NotificationOptionApplicationUpdateInstalled": "Application update installed",
     "NotificationOptionAudioPlayback": "Audio playback started",
     "NotificationOptionAudioPlayback": "Audio playback started",
     "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
     "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",

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

@@ -104,5 +104,14 @@
     "TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
     "TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
     "TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.",
     "TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.",
     "TasksChannelsCategory": "Canais de Internet",
     "TasksChannelsCategory": "Canais de Internet",
-    "TaskRefreshChapterImages": "Extrair Imagens do Capítulo"
+    "TaskRefreshChapterImages": "Extrair Imagens do Capítulo",
+    "TaskDownloadMissingSubtitlesDescription": "Pesquisa na Internet as legendas em falta com base na configuração de metadados.",
+    "TaskDownloadMissingSubtitles": "Download das legendas em falta",
+    "TaskRefreshChannelsDescription": "Atualiza as informações do canal da Internet.",
+    "TaskCleanTranscodeDescription": "Apagar os ficheiros com mais de um dia, de Transcode.",
+    "TaskCleanTranscode": "Limpar o diretório de Transcode",
+    "TaskUpdatePluginsDescription": "Download e instala as atualizações para plug-ins configurados para atualização automática.",
+    "TaskRefreshPeopleDescription": "Atualiza os metadados para atores e diretores na tua biblioteca de media.",
+    "TaskRefreshPeople": "Atualizar pessoas",
+    "TaskRefreshLibraryDescription": "Pesquisa a tua biblioteca de media por novos ficheiros e atualiza os metadados."
 }
 }

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

@@ -21,7 +21,7 @@
     "HeaderFavoriteAlbums": "Избранные альбомы",
     "HeaderFavoriteAlbums": "Избранные альбомы",
     "HeaderFavoriteArtists": "Избранные исполнители",
     "HeaderFavoriteArtists": "Избранные исполнители",
     "HeaderFavoriteEpisodes": "Избранные эпизоды",
     "HeaderFavoriteEpisodes": "Избранные эпизоды",
-    "HeaderFavoriteShows": "Избранные передачи",
+    "HeaderFavoriteShows": "Избранные сериалы",
     "HeaderFavoriteSongs": "Избранные композиции",
     "HeaderFavoriteSongs": "Избранные композиции",
     "HeaderLiveTV": "Эфир",
     "HeaderLiveTV": "Эфир",
     "HeaderNextUp": "Очередное",
     "HeaderNextUp": "Очередное",

+ 21 - 3
Emby.Server.Implementations/Localization/Core/ta.json

@@ -45,7 +45,7 @@
     "TvShows": "தொலைக்காட்சித் தொடர்கள்",
     "TvShows": "தொலைக்காட்சித் தொடர்கள்",
     "Sync": "ஒத்திசைவு",
     "Sync": "ஒத்திசைவு",
     "StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
     "StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
-    "Songs": "பாட்டுகள்",
+    "Songs": "பாட்கள்",
     "Shows": "தொடர்கள்",
     "Shows": "தொடர்கள்",
     "ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
     "ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
     "ScheduledTaskStartedWithName": "{0} துவங்கியது",
     "ScheduledTaskStartedWithName": "{0} துவங்கியது",
@@ -93,7 +93,25 @@
     "Channels": "சேனல்கள்",
     "Channels": "சேனல்கள்",
     "Books": "புத்தகங்கள்",
     "Books": "புத்தகங்கள்",
     "AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது",
     "AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது",
-    "Artists": "கலைஞர்கள்",
+    "Artists": "கலைஞர்",
     "Application": "செயலி",
     "Application": "செயலி",
-    "Albums": "ஆல்பங்கள்"
+    "Albums": "ஆல்பங்கள்",
+    "NewVersionIsAvailable": "ஜெல்லிஃபின் சேவையகத்தின் புதிய பதிப்பு பதிவிறக்கத்திற்கு கிடைக்கிறது.",
+    "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0 புதுப்பிக்கப்பட்டது",
+    "TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.",
+    "UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
+    "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0 } இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
+    "TaskDownloadMissingSubtitlesDescription": "மெட்டாடேட்டா உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
+    "TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.",
+    "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட செருகுநிரல்களுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
+    "TaskRefreshPeopleDescription": "உங்கள் மீடியா நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மெட்டாடேட்டாவை புதுப்பிக்கும்.",
+    "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
+    "TaskCleanLogs": "பதிவு அடைவு சுத்தம் செய்யுங்கள்",
+    "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் மீடியா நூலகத்தை ஸ்கேன் செய்து மீத்தரவை புதுப்பிக்கும்.",
+    "TaskRefreshChapterImagesDescription": "அத்தியாயங்களைக் கொண்ட வீடியோக்களுக்கான சிறு உருவங்களை உருவாக்குகிறது.",
+    "ValueHasBeenAddedToLibrary": "உங்கள் மீடியா நூலகத்தில் {0} சேர்க்கப்பட்டது",
+    "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
+    "HomeVideos": "முகப்பு வீடியோக்கள்",
+    "UserStoppedPlayingItemWithValues": "{2} இல் {1} முடித்துவிட்டது",
+    "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது"
 }
 }

+ 3 - 1
Emby.Server.Implementations/Localization/Core/th.json

@@ -67,5 +67,7 @@
     "Artists": "นักแสดง",
     "Artists": "นักแสดง",
     "Application": "แอปพลิเคชั่น",
     "Application": "แอปพลิเคชั่น",
     "AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
     "AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
-    "Albums": "อัลบั้ม"
+    "Albums": "อัลบั้ม",
+    "ScheduledTaskStartedWithName": "{0} เริ่มต้น",
+    "ScheduledTaskFailedWithName": "{0} ล้มเหลว"
 }
 }

+ 94 - 13
Emby.Server.Implementations/Localization/Core/uk.json

@@ -1,13 +1,13 @@
 {
 {
-    "MusicVideos": "Музичні відео",
+    "MusicVideos": "Музичні кліпи",
     "Music": "Музика",
     "Music": "Музика",
     "Movies": "Фільми",
     "Movies": "Фільми",
-    "MessageApplicationUpdatedTo": "Jellyfin Server був оновлений до версії {0}",
-    "MessageApplicationUpdated": "Jellyfin Server був оновлений",
+    "MessageApplicationUpdatedTo": "Jellyfin Server оновлено до версії {0}",
+    "MessageApplicationUpdated": "Jellyfin Server оновлено",
     "Latest": "Останні",
     "Latest": "Останні",
-    "LabelIpAddressValue": "IP-адреси: {0}",
-    "ItemRemovedWithName": "{0} видалено з бібліотеки",
-    "ItemAddedWithName": "{0} додано до бібліотеки",
+    "LabelIpAddressValue": "IP-адреса: {0}",
+    "ItemRemovedWithName": "{0} видалено з медіатеки",
+    "ItemAddedWithName": "{0} додано до медіатеки",
     "HeaderNextUp": "Наступний",
     "HeaderNextUp": "Наступний",
     "HeaderLiveTV": "Ефірне ТБ",
     "HeaderLiveTV": "Ефірне ТБ",
     "HeaderFavoriteSongs": "Улюблені пісні",
     "HeaderFavoriteSongs": "Улюблені пісні",
@@ -17,20 +17,101 @@
     "HeaderFavoriteAlbums": "Улюблені альбоми",
     "HeaderFavoriteAlbums": "Улюблені альбоми",
     "HeaderContinueWatching": "Продовжити перегляд",
     "HeaderContinueWatching": "Продовжити перегляд",
     "HeaderCameraUploads": "Завантажено з камери",
     "HeaderCameraUploads": "Завантажено з камери",
-    "HeaderAlbumArtists": "Виконавці альбомів",
+    "HeaderAlbumArtists": "Виконавці альбому",
     "Genres": "Жанри",
     "Genres": "Жанри",
-    "Folders": "Директорії",
+    "Folders": "Каталоги",
     "Favorites": "Улюблені",
     "Favorites": "Улюблені",
-    "DeviceOnlineWithName": "{0} під'єднано",
-    "DeviceOfflineWithName": "{0} від'єднано",
+    "DeviceOnlineWithName": "Пристрій {0} підключився",
+    "DeviceOfflineWithName": "Пристрій {0} відключився",
     "Collections": "Колекції",
     "Collections": "Колекції",
-    "ChapterNameValue": "Глава {0}",
+    "ChapterNameValue": "Розділ {0}",
     "Channels": "Канали",
     "Channels": "Канали",
     "CameraImageUploadedFrom": "Нова фотографія завантажена з {0}",
     "CameraImageUploadedFrom": "Нова фотографія завантажена з {0}",
     "Books": "Книги",
     "Books": "Книги",
-    "AuthenticationSucceededWithUserName": "{0} успішно авторизовані",
+    "AuthenticationSucceededWithUserName": "{0} успішно авторизований",
     "Artists": "Виконавці",
     "Artists": "Виконавці",
     "Application": "Додаток",
     "Application": "Додаток",
     "AppDeviceValues": "Додаток: {0}, Пристрій: {1}",
     "AppDeviceValues": "Додаток: {0}, Пристрій: {1}",
-    "Albums": "Альбоми"
+    "Albums": "Альбоми",
+    "NotificationOptionServerRestartRequired": "Необхідно перезапустити сервер",
+    "NotificationOptionPluginUpdateInstalled": "Встановлено оновлення плагіна",
+    "NotificationOptionPluginUninstalled": "Плагін видалено",
+    "NotificationOptionPluginInstalled": "Плагін встановлено",
+    "NotificationOptionPluginError": "Помилка плагіна",
+    "NotificationOptionNewLibraryContent": "Додано новий контент",
+    "HomeVideos": "Домашнє відео",
+    "FailedLoginAttemptWithUserName": "Невдала спроба входу від {0}",
+    "LabelRunningTimeValue": "Тривалість: {0}",
+    "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}",
+    "ValueSpecialEpisodeName": "Спецепізод - {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": "Система",
+    "Sync": "Синхронізація",
+    "SubtitleDownloadFailureFromForItem": "Не вдалося завантажити субтитри з {0} для {1}",
+    "StartupEmbyServerIsLoading": "Jellyfin Server завантажується. Будь ласка, спробуйте трішки пізніше.",
+    "Songs": "Пісні",
+    "Shows": "Шоу",
+    "ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити",
+    "ScheduledTaskStartedWithName": "{0} розпочато",
+    "ScheduledTaskFailedWithName": "Помилка {0}",
+    "ProviderValue": "Постачальник: {0}",
+    "PluginUpdatedWithName": "{0} оновлено",
+    "PluginUninstalledWithName": "{0} видалено",
+    "PluginInstalledWithName": "{0} встановлено",
+    "Plugin": "Плагін",
+    "Playlists": "Плейлисти",
+    "Photos": "Фотографії",
+    "NotificationOptionVideoPlaybackStopped": "Відтворення відео зупинено",
+    "NotificationOptionVideoPlayback": "Розпочато відтворення відео",
+    "NotificationOptionUserLockedOut": "Користувача заблоковано",
+    "NotificationOptionTaskFailed": "Помилка запланованого завдання",
+    "NotificationOptionInstallationFailed": "Помилка встановлення",
+    "NotificationOptionCameraImageUploaded": "Фотографію завантажено",
+    "NotificationOptionAudioPlaybackStopped": "Відтворення аудіо зупинено",
+    "NotificationOptionAudioPlayback": "Розпочато відтворення аудіо",
+    "NotificationOptionApplicationUpdateInstalled": "Встановлено оновлення додатка",
+    "NotificationOptionApplicationUpdateAvailable": "Доступне оновлення додатка",
+    "NewVersionIsAvailable": "Для завантаження доступна нова версія Jellyfin Server.",
+    "NameSeasonUnknown": "Сезон Невідомий",
+    "NameSeasonNumber": "Сезон {0}",
+    "NameInstallFailed": "Не вдалося встановити {0}",
+    "MixedContent": "Змішаний контент",
+    "MessageServerConfigurationUpdated": "Конфігурація сервера оновлена",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Розділ конфігурації сервера {0} оновлено",
+    "Inherit": "Успадкувати",
+    "HeaderRecordingGroups": "Групи запису"
 }
 }

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

@@ -92,7 +92,7 @@
     "HeaderRecordingGroups": "錄製組",
     "HeaderRecordingGroups": "錄製組",
     "Inherit": "繼承",
     "Inherit": "繼承",
     "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
     "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
-    "TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。",
+    "TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
     "TaskDownloadMissingSubtitles": "下載遺失的字幕",
     "TaskDownloadMissingSubtitles": "下載遺失的字幕",
     "TaskRefreshChannels": "重新整理頻道",
     "TaskRefreshChannels": "重新整理頻道",
     "TaskUpdatePlugins": "更新插件",
     "TaskUpdatePlugins": "更新插件",

+ 4 - 4
Emby.Server.Implementations/Localization/LocalizationManager.cs

@@ -247,7 +247,7 @@ namespace Emby.Server.Implementations.Localization
             }
             }
 
 
             // Try splitting by : to handle "Germany: FSK 18"
             // Try splitting by : to handle "Germany: FSK 18"
-            var index = rating.IndexOf(':');
+            var index = rating.IndexOf(':', StringComparison.Ordinal);
             if (index != -1)
             if (index != -1)
             {
             {
                 rating = rating.Substring(index).TrimStart(':').Trim();
                 rating = rating.Substring(index).TrimStart(':').Trim();
@@ -312,12 +312,12 @@ namespace Emby.Server.Implementations.Localization
                 throw new ArgumentNullException(nameof(culture));
                 throw new ArgumentNullException(nameof(culture));
             }
             }
 
 
-            const string prefix = "Core";
-            var key = prefix + culture;
+            const string Prefix = "Core";
+            var key = Prefix + culture;
 
 
             return _dictionaries.GetOrAdd(
             return _dictionaries.GetOrAdd(
                 key,
                 key,
-                f => GetDictionary(prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
+                f => GetDictionary(Prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
         }
         }
 
 
         private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename)
         private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename)

+ 23 - 20
Emby.Server.Implementations/Net/UdpSocket.cs

@@ -15,13 +15,11 @@ namespace Emby.Server.Implementations.Net
     public sealed class UdpSocket : ISocket, IDisposable
     public sealed class UdpSocket : ISocket, IDisposable
     {
     {
         private Socket _socket;
         private Socket _socket;
-        private int _localPort;
+        private readonly int _localPort;
         private bool _disposed = false;
         private bool _disposed = false;
 
 
         public Socket Socket => _socket;
         public Socket Socket => _socket;
 
 
-        public IPAddress LocalIPAddress { get; }
-
         private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
         private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
         {
         {
             SocketFlags = SocketFlags.None
             SocketFlags = SocketFlags.None
@@ -51,18 +49,33 @@ namespace Emby.Server.Implementations.Net
             InitReceiveSocketAsyncEventArgs();
             InitReceiveSocketAsyncEventArgs();
         }
         }
 
 
+        public UdpSocket(Socket socket, IPEndPoint endPoint)
+        {
+            if (socket == null)
+            {
+                throw new ArgumentNullException(nameof(socket));
+            }
+
+            _socket = socket;
+            _socket.Connect(endPoint);
+
+            InitReceiveSocketAsyncEventArgs();
+        }
+
+        public IPAddress LocalIPAddress { get; }
+
         private void InitReceiveSocketAsyncEventArgs()
         private void InitReceiveSocketAsyncEventArgs()
         {
         {
             var receiveBuffer = new byte[8192];
             var receiveBuffer = new byte[8192];
             _receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
             _receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
-            _receiveSocketAsyncEventArgs.Completed += _receiveSocketAsyncEventArgs_Completed;
+            _receiveSocketAsyncEventArgs.Completed += OnReceiveSocketAsyncEventArgsCompleted;
 
 
             var sendBuffer = new byte[8192];
             var sendBuffer = new byte[8192];
             _sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
             _sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
-            _sendSocketAsyncEventArgs.Completed += _sendSocketAsyncEventArgs_Completed;
+            _sendSocketAsyncEventArgs.Completed += OnSendSocketAsyncEventArgsCompleted;
         }
         }
 
 
-        private void _receiveSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
+        private void OnReceiveSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
         {
         {
             var tcs = _currentReceiveTaskCompletionSource;
             var tcs = _currentReceiveTaskCompletionSource;
             if (tcs != null)
             if (tcs != null)
@@ -86,7 +99,7 @@ namespace Emby.Server.Implementations.Net
             }
             }
         }
         }
 
 
-        private void _sendSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
+        private void OnSendSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
         {
         {
             var tcs = _currentSendTaskCompletionSource;
             var tcs = _currentSendTaskCompletionSource;
             if (tcs != null)
             if (tcs != null)
@@ -104,19 +117,6 @@ namespace Emby.Server.Implementations.Net
             }
             }
         }
         }
 
 
-        public UdpSocket(Socket socket, IPEndPoint endPoint)
-        {
-            if (socket == null)
-            {
-                throw new ArgumentNullException(nameof(socket));
-            }
-
-            _socket = socket;
-            _socket.Connect(endPoint);
-
-            InitReceiveSocketAsyncEventArgs();
-        }
-
         public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback)
         public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback)
         {
         {
             ThrowIfDisposed();
             ThrowIfDisposed();
@@ -247,6 +247,7 @@ namespace Emby.Server.Implementations.Net
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
         public void Dispose()
         public void Dispose()
         {
         {
             if (_disposed)
             if (_disposed)
@@ -255,6 +256,8 @@ namespace Emby.Server.Implementations.Net
             }
             }
 
 
             _socket?.Dispose();
             _socket?.Dispose();
+            _receiveSocketAsyncEventArgs.Dispose();
+            _sendSocketAsyncEventArgs.Dispose();
             _currentReceiveTaskCompletionSource?.TrySetCanceled();
             _currentReceiveTaskCompletionSource?.TrySetCanceled();
             _currentSendTaskCompletionSource?.TrySetCanceled();
             _currentSendTaskCompletionSource?.TrySetCanceled();
 
 

+ 14 - 3
Emby.Server.Implementations/Networking/NetworkManager.cs

@@ -152,7 +152,12 @@ namespace Emby.Server.Implementations.Networking
                 return true;
                 return true;
             }
             }
 
 
-            byte[] octet = IPAddress.Parse(endpoint).GetAddressBytes();
+            if (!IPAddress.TryParse(endpoint, out var ipAddress))
+            {
+                return false;
+            }
+
+            byte[] octet = ipAddress.GetAddressBytes();
 
 
             if ((octet[0] == 10) ||
             if ((octet[0] == 10) ||
                 (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918
                 (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918
@@ -160,7 +165,7 @@ namespace Emby.Server.Implementations.Networking
                 (octet[0] == 127) || // RFC1122
                 (octet[0] == 127) || // RFC1122
                 (octet[0] == 169 && octet[1] == 254)) // RFC3927
                 (octet[0] == 169 && octet[1] == 254)) // RFC3927
             {
             {
-                return false;
+                return true;
             }
             }
 
 
             if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
             if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
@@ -268,6 +273,12 @@ namespace Emby.Server.Implementations.Networking
             string excludeAddress = "[" + addressString + "]";
             string excludeAddress = "[" + addressString + "]";
             var subnets = LocalSubnetsFn();
             var subnets = LocalSubnetsFn();
 
 
+            // Include any address if LAN subnets aren't specified
+            if (subnets.Length == 0)
+            {
+                return true;
+            }
+
             // Exclude any addresses if they appear in the LAN list in [ ]
             // Exclude any addresses if they appear in the LAN list in [ ]
             if (Array.IndexOf(subnets, excludeAddress) != -1)
             if (Array.IndexOf(subnets, excludeAddress) != -1)
             {
             {
@@ -379,7 +390,7 @@ namespace Emby.Server.Implementations.Networking
                         var host = uri.DnsSafeHost;
                         var host = uri.DnsSafeHost;
                         _logger.LogDebug("Resolving host {0}", host);
                         _logger.LogDebug("Resolving host {0}", host);
 
 
-                        address = GetIpAddresses(host).Result.FirstOrDefault();
+                        address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault();
 
 
                         if (address != null)
                         if (address != null)
                         {
                         {

+ 20 - 44
Emby.Server.Implementations/Playlists/PlaylistManager.cs

@@ -349,16 +349,14 @@ namespace Emby.Server.Implementations.Playlists
                         AlbumTitle = child.Album
                         AlbumTitle = child.Album
                     };
                     };
 
 
-                    var hasAlbumArtist = child as IHasAlbumArtist;
-                    if (hasAlbumArtist != null)
+                    if (child is IHasAlbumArtist hasAlbumArtist)
                     {
                     {
-                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
                     }
                     }
 
 
-                    var hasArtist = child as IHasArtist;
-                    if (hasArtist != null)
+                    if (child is IHasArtist hasArtist)
                     {
                     {
-                        entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
+                        entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
                     }
                     }
 
 
                     if (child.RunTimeTicks.HasValue)
                     if (child.RunTimeTicks.HasValue)
@@ -385,16 +383,14 @@ namespace Emby.Server.Implementations.Playlists
                         AlbumTitle = child.Album
                         AlbumTitle = child.Album
                     };
                     };
 
 
-                    var hasAlbumArtist = child as IHasAlbumArtist;
-                    if (hasAlbumArtist != null)
+                    if (child is IHasAlbumArtist hasAlbumArtist)
                     {
                     {
-                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
                     }
                     }
 
 
-                    var hasArtist = child as IHasArtist;
-                    if (hasArtist != null)
+                    if (child is IHasArtist hasArtist)
                     {
                     {
-                        entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
+                        entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
                     }
                     }
 
 
                     if (child.RunTimeTicks.HasValue)
                     if (child.RunTimeTicks.HasValue)
@@ -411,8 +407,10 @@ namespace Emby.Server.Implementations.Playlists
 
 
             if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
             if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
             {
             {
-                var playlist = new M3uPlaylist();
-                playlist.IsExtended = true;
+                var playlist = new M3uPlaylist
+                {
+                    IsExtended = true
+                };
                 foreach (var child in item.GetLinkedChildren())
                 foreach (var child in item.GetLinkedChildren())
                 {
                 {
                     var entry = new M3uPlaylistEntry()
                     var entry = new M3uPlaylistEntry()
@@ -422,10 +420,9 @@ namespace Emby.Server.Implementations.Playlists
                         Album = child.Album
                         Album = child.Album
                     };
                     };
 
 
-                    var hasAlbumArtist = child as IHasAlbumArtist;
-                    if (hasAlbumArtist != null)
+                    if (child is IHasAlbumArtist hasAlbumArtist)
                     {
                     {
-                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
                     }
                     }
 
 
                     if (child.RunTimeTicks.HasValue)
                     if (child.RunTimeTicks.HasValue)
@@ -453,10 +450,9 @@ namespace Emby.Server.Implementations.Playlists
                         Album = child.Album
                         Album = child.Album
                     };
                     };
 
 
-                    var hasAlbumArtist = child as IHasAlbumArtist;
-                    if (hasAlbumArtist != null)
+                    if (child is IHasAlbumArtist hasAlbumArtist)
                     {
                     {
-                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
                     }
                     }
 
 
                     if (child.RunTimeTicks.HasValue)
                     if (child.RunTimeTicks.HasValue)
@@ -514,7 +510,7 @@ namespace Emby.Server.Implementations.Playlists
 
 
             if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
             if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
             {
             {
-                folderPath = folderPath + Path.DirectorySeparatorChar;
+                folderPath += Path.DirectorySeparatorChar;
             }
             }
 
 
             var folderUri = new Uri(folderPath);
             var folderUri = new Uri(folderPath);
@@ -537,32 +533,12 @@ namespace Emby.Server.Implementations.Playlists
             return relativePath;
             return relativePath;
         }
         }
 
 
-        private static string UnEscape(string content)
-        {
-            if (content == null)
-            {
-                return content;
-            }
-
-            return content.Replace("&amp;", "&").Replace("&apos;", "'").Replace("&quot;", "\"").Replace("&gt;", ">").Replace("&lt;", "<");
-        }
-
-        private static string Escape(string content)
-        {
-            if (content == null)
-            {
-                return null;
-            }
-
-            return content.Replace("&", "&amp;").Replace("'", "&apos;").Replace("\"", "&quot;").Replace(">", "&gt;").Replace("<", "&lt;");
-        }
-
         public Folder GetPlaylistsFolder(Guid userId)
         public Folder GetPlaylistsFolder(Guid userId)
         {
         {
-            var typeName = "PlaylistsFolder";
+            const string TypeName = "PlaylistsFolder";
 
 
-            return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal)) ??
-                _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal));
+            return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)) ??
+                _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal));
         }
         }
     }
     }
 }
 }

+ 1 - 6
Emby.Server.Implementations/ScheduledTasks/TaskManager.cs

@@ -7,7 +7,6 @@ using System.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Events;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -37,7 +36,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IApplicationPaths _applicationPaths;
         private readonly IApplicationPaths _applicationPaths;
         private readonly ILogger<TaskManager> _logger;
         private readonly ILogger<TaskManager> _logger;
-        private readonly IFileSystem _fileSystem;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="TaskManager" /> class.
         /// Initializes a new instance of the <see cref="TaskManager" /> class.
@@ -45,17 +43,14 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// <param name="applicationPaths">The application paths.</param>
         /// <param name="applicationPaths">The application paths.</param>
         /// <param name="jsonSerializer">The json serializer.</param>
         /// <param name="jsonSerializer">The json serializer.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="logger">The logger.</param>
-        /// <param name="fileSystem">The filesystem manager.</param>
         public TaskManager(
         public TaskManager(
             IApplicationPaths applicationPaths,
             IApplicationPaths applicationPaths,
             IJsonSerializer jsonSerializer,
             IJsonSerializer jsonSerializer,
-            ILogger<TaskManager> logger,
-            IFileSystem fileSystem)
+            ILogger<TaskManager> logger)
         {
         {
             _applicationPaths = applicationPaths;
             _applicationPaths = applicationPaths;
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
             _logger = logger;
             _logger = logger;
-            _fileSystem = fileSystem;
 
 
             ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
             ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
         }
         }

+ 0 - 8
Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs

@@ -14,7 +14,6 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Logging;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Globalization;
 
 
 namespace Emby.Server.Implementations.ScheduledTasks
 namespace Emby.Server.Implementations.ScheduledTasks
@@ -24,11 +23,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
     /// </summary>
     /// </summary>
     public class ChapterImagesTask : IScheduledTask
     public class ChapterImagesTask : IScheduledTask
     {
     {
-        /// <summary>
-        /// The _logger.
-        /// </summary>
-        private readonly ILogger<ChapterImagesTask> _logger;
-
         /// <summary>
         /// <summary>
         /// The _library manager.
         /// The _library manager.
         /// </summary>
         /// </summary>
@@ -46,7 +40,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
         /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
         /// </summary>
         /// </summary>
         public ChapterImagesTask(
         public ChapterImagesTask(
-            ILoggerFactory loggerFactory,
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
             IItemRepository itemRepo,
             IItemRepository itemRepo,
             IApplicationPaths appPaths,
             IApplicationPaths appPaths,
@@ -54,7 +47,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             ILocalizationManager localization)
             ILocalizationManager localization)
         {
         {
-            _logger = loggerFactory.CreateLogger<ChapterImagesTask>();
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _itemRepo = itemRepo;
             _itemRepo = itemRepo;
             _appPaths = appPaths;
             _appPaths = appPaths;

+ 6 - 4
Emby.Server.Implementations/Serialization/MyXmlSerializer.cs

@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
 using System.IO;
 using System.IO;
 using System.Xml;
 using System.Xml;
 using System.Xml.Serialization;
 using System.Xml.Serialization;
+using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 
 
 namespace Emby.Server.Implementations.Serialization
 namespace Emby.Server.Implementations.Serialization
@@ -53,10 +54,11 @@ namespace Emby.Server.Implementations.Serialization
         /// <param name="stream">The stream.</param>
         /// <param name="stream">The stream.</param>
         public void SerializeToStream(object obj, Stream stream)
         public void SerializeToStream(object obj, Stream stream)
         {
         {
-            using (var writer = new XmlTextWriter(stream, null))
+            using (var writer = new StreamWriter(stream, null, IODefaults.StreamWriterBufferSize, true))
+            using (var textWriter = new XmlTextWriter(writer))
             {
             {
-                writer.Formatting = Formatting.Indented;
-                SerializeToWriter(obj, writer);
+                textWriter.Formatting = Formatting.Indented;
+                SerializeToWriter(obj, textWriter);
             }
             }
         }
         }
 
 
@@ -95,7 +97,7 @@ namespace Emby.Server.Implementations.Serialization
         /// <returns>System.Object.</returns>
         /// <returns>System.Object.</returns>
         public object DeserializeFromBytes(Type type, byte[] buffer)
         public object DeserializeFromBytes(Type type, byte[] buffer)
         {
         {
-            using (var stream = new MemoryStream(buffer))
+            using (var stream = new MemoryStream(buffer, 0, buffer.Length, false, true))
             {
             {
                 return DeserializeFromStream(type, stream);
                 return DeserializeFromStream(type, stream);
             }
             }

+ 14 - 5
Emby.Server.Implementations/Services/ServiceController.cs

@@ -2,6 +2,7 @@
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.HttpServer;
 using Emby.Server.Implementations.HttpServer;
 using MediaBrowser.Model.Services;
 using MediaBrowser.Model.Services;
@@ -91,12 +92,22 @@ namespace Emby.Server.Implementations.Services
         {
         {
             if (restPath.Path[0] != '/')
             if (restPath.Path[0] != '/')
             {
             {
-                throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName()));
+                throw new ArgumentException(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "Route '{0}' on '{1}' must start with a '/'",
+                        restPath.Path,
+                        restPath.RequestType.GetMethodName()));
             }
             }
 
 
             if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
             if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
             {
             {
-                throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName()));
+                throw new ArgumentException(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "Route '{0}' on '{1}' contains invalid chars. ",
+                        restPath.Path,
+                        restPath.RequestType.GetMethodName()));
             }
             }
 
 
             if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
             if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
@@ -179,8 +190,7 @@ namespace Emby.Server.Implementations.Services
 
 
             var service = httpHost.CreateInstance(serviceType);
             var service = httpHost.CreateInstance(serviceType);
 
 
-            var serviceRequiresContext = service as IRequiresRequest;
-            if (serviceRequiresContext != null)
+            if (service is IRequiresRequest serviceRequiresContext)
             {
             {
                 serviceRequiresContext.Request = req;
                 serviceRequiresContext.Request = req;
             }
             }
@@ -189,5 +199,4 @@ namespace Emby.Server.Implementations.Services
             return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
             return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
         }
         }
     }
     }
-
 }
 }

+ 8 - 1
Emby.Server.Implementations/Services/ServiceExec.cs

@@ -2,6 +2,7 @@
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Linq.Expressions;
 using System.Linq.Expressions;
 using System.Reflection;
 using System.Reflection;
@@ -105,7 +106,13 @@ namespace Emby.Server.Implementations.Services
             }
             }
 
 
             var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant();
             var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant();
-            throw new NotImplementedException(string.Format("Could not find method named {1}({0}) or Any({0}) on Service {2}", requestDto.GetType().GetMethodName(), expectedMethodName, serviceType.GetMethodName()));
+            throw new NotImplementedException(
+                string.Format(
+                    CultureInfo.InvariantCulture,
+                    "Could not find method named {1}({0}) or Any({0}) on Service {2}",
+                    requestDto.GetType().GetMethodName(),
+                    expectedMethodName,
+                    serviceType.GetMethodName()));
         }
         }
 
 
         private static async Task<object> GetTaskResult(Task task)
         private static async Task<object> GetTaskResult(Task task)

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