2
0
Эх сурвалжийг харах

Merge remote-tracking branch 'upstream/master' into api-doc-css

crobibero 4 жил өмнө
parent
commit
bebb0afb52
100 өөрчлөгдсөн 1342 нэмэгдсэн , 4263 устгасан
  1. 0 3
      .ci/azure-pipelines-abi.yml
  2. 62 14
      .ci/azure-pipelines-package.yml
  3. 10 4
      .ci/azure-pipelines.yml
  4. 1 0
      CONTRIBUTORS.md
  5. 1 1
      Emby.Dlna/Common/ServiceAction.cs
  6. 2 1
      Emby.Dlna/Common/StateVariable.cs
  7. 0 16
      Emby.Dlna/ConfigurationExtension.cs
  8. 4 6
      Emby.Dlna/ConnectionManager/ConnectionManagerService.cs
  9. 2 2
      Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
  10. 3 3
      Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
  11. 3 2
      Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
  12. 66 92
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  13. 23 0
      Emby.Dlna/ContentDirectory/ServerItem.cs
  14. 28 0
      Emby.Dlna/ContentDirectory/StubType.cs
  15. 6 6
      Emby.Dlna/ControlRequest.cs
  16. 1 1
      Emby.Dlna/ControlResponse.cs
  17. 69 88
      Emby.Dlna/Didl/DidlBuilder.cs
  18. 1 3
      Emby.Dlna/Didl/Filter.cs
  19. 1 1
      Emby.Dlna/Didl/StringWriterWithEncoding.cs
  20. 24 0
      Emby.Dlna/DlnaConfigurationFactory.cs
  21. 16 15
      Emby.Dlna/DlnaManager.cs
  22. 1 1
      Emby.Dlna/Emby.Dlna.csproj
  23. 1 1
      Emby.Dlna/EventSubscriptionResponse.cs
  24. 7 5
      Emby.Dlna/Eventing/DlnaEventManager.cs
  25. 1 1
      Emby.Dlna/IConnectionManager.cs
  26. 1 1
      Emby.Dlna/IContentDirectory.cs
  27. 11 1
      Emby.Dlna/IDlnaEventManager.cs
  28. 1 1
      Emby.Dlna/IMediaReceiverRegistrar.cs
  29. 33 23
      Emby.Dlna/Main/DlnaEntryPoint.cs
  30. 3 3
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs
  31. 2 1
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
  32. 116 109
      Emby.Dlna/PlayTo/Device.cs
  33. 3 2
      Emby.Dlna/PlayTo/DeviceInfo.cs
  34. 13 0
      Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
  35. 127 124
      Emby.Dlna/PlayTo/PlayToController.cs
  36. 29 28
      Emby.Dlna/PlayTo/PlayToManager.cs
  37. 1 1
      Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
  38. 1 1
      Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
  39. 1 8
      Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
  40. 0 13
      Emby.Dlna/PlayTo/TRANSPORTSTATE.cs
  41. 31 39
      Emby.Dlna/PlayTo/TransportCommands.cs
  42. 14 0
      Emby.Dlna/PlayTo/TransportState.cs
  43. 8 8
      Emby.Dlna/PlayTo/UpnpContainer.cs
  44. 13 12
      Emby.Dlna/PlayTo/uBaseObject.cs
  45. 58 32
      Emby.Dlna/PlayTo/uPnpNamespaces.cs
  46. 2 2
      Emby.Dlna/Profiles/DefaultProfile.cs
  47. 6 6
      Emby.Dlna/Profiles/DishHopperJoeyProfile.cs
  48. 2 2
      Emby.Dlna/Profiles/LgTvProfile.cs
  49. 1 1
      Emby.Dlna/Profiles/LinksysDMA2100Profile.cs
  50. 1 1
      Emby.Dlna/Profiles/PanasonicVieraProfile.cs
  51. 7 7
      Emby.Dlna/Profiles/PopcornHourProfile.cs
  52. 1 1
      Emby.Dlna/Profiles/SamsungSmartTvProfile.cs
  53. 3 3
      Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs
  54. 3 3
      Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs
  55. 3 3
      Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs
  56. 3 3
      Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs
  57. 5 5
      Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs
  58. 22 22
      Emby.Dlna/Profiles/SonyBravia2010Profile.cs
  59. 21 21
      Emby.Dlna/Profiles/SonyBravia2011Profile.cs
  60. 17 17
      Emby.Dlna/Profiles/SonyBravia2012Profile.cs
  61. 18 19
      Emby.Dlna/Profiles/SonyBravia2013Profile.cs
  62. 18 19
      Emby.Dlna/Profiles/SonyBravia2014Profile.cs
  63. 7 7
      Emby.Dlna/Profiles/SonyPs3Profile.cs
  64. 7 7
      Emby.Dlna/Profiles/SonyPs4Profile.cs
  65. 4 4
      Emby.Dlna/Profiles/WdtvLiveProfile.cs
  66. 7 7
      Emby.Dlna/Profiles/XboxOneProfile.cs
  67. 17 17
      Emby.Dlna/Service/BaseControlHandler.cs
  68. 8 6
      Emby.Dlna/Service/BaseService.cs
  69. 5 5
      Emby.Dlna/Service/ControlErrorHandler.cs
  70. 1 1
      Emby.Dlna/Service/ServiceXmlBuilder.cs
  71. 9 10
      Emby.Dlna/Ssdp/DeviceDiscovery.cs
  72. 1 1
      Emby.Dlna/Ssdp/SsdpExtensions.cs
  73. 15 1
      Emby.Naming/Emby.Naming.csproj
  74. 1 1
      Emby.Notifications/NotificationEntryPoint.cs
  75. 0 590
      Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
  76. 109 88
      Emby.Server.Implementations/ApplicationHost.cs
  77. 15 6
      Emby.Server.Implementations/Channels/ChannelManager.cs
  78. 20 31
      Emby.Server.Implementations/Collections/CollectionManager.cs
  79. 1 1
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  80. 1 1
      Emby.Server.Implementations/ConfigurationOptions.cs
  81. 7 4
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  82. 1 1
      Emby.Server.Implementations/Devices/DeviceManager.cs
  83. 3 3
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  84. 1 1
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  85. 1 1
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  86. 5 4
      Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
  87. 0 210
      Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs
  88. 0 250
      Emby.Server.Implementations/HttpServer/FileWriter.cs
  89. 0 766
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  90. 0 721
      Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
  91. 0 212
      Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
  92. 0 113
      Emby.Server.Implementations/HttpServer/ResponseFilter.cs
  93. 1 212
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  94. 9 15
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  95. 7 13
      Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
  96. 0 120
      Emby.Server.Implementations/HttpServer/StreamWriter.cs
  97. 7 1
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  98. 102 0
      Emby.Server.Implementations/HttpServer/WebSocketManager.cs
  99. 3 24
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  100. 35 0
      Emby.Server.Implementations/IO/LibraryMonitorStartup.cs

+ 0 - 3
.ci/azure-pipelines-abi.yml

@@ -62,7 +62,6 @@ jobs:
 
       - task: DownloadPipelineArtifact@2
         displayName: 'Download Reference Assembly Build Artifact'
-        enabled: false
         inputs:
           source: "specific"
           artifact: "$(NugetPackageName)"
@@ -74,7 +73,6 @@ jobs:
 
       - task: CopyFiles@2
         displayName: 'Copy Reference Assembly Build Artifact'
-        enabled: false
         inputs:
           sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
           contents: '**/*.dll'
@@ -85,7 +83,6 @@ jobs:
 
       - task: DotNetCoreCLI@2
         displayName: 'Execute ABI Compatibility Check Tool'
-        enabled: false
         inputs:
           command: custom
           custom: compat

+ 62 - 14
.ci/azure-pipelines-package.yml

@@ -42,7 +42,7 @@ jobs:
 
   - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
     displayName: 'Run Dockerfile (stable)'
-    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 
   - task: PublishPipelineArtifact@1
     displayName: 'Publish Release'
@@ -87,7 +87,7 @@ jobs:
   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')
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 
   - task: Docker@2
     displayName: 'Push Unstable Image'
@@ -104,7 +104,7 @@ jobs:
 
   - task: Docker@2
     displayName: 'Push Stable Image'
-    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
     inputs:
       repository: 'jellyfin/jellyfin-server'
       command: buildAndPush
@@ -116,8 +116,9 @@ jobs:
         $(JellyfinVersion)-$(BuildConfiguration)
 
 - job: CollectArtifacts
-  timeoutInMinutes: 10
+  timeoutInMinutes: 20
   displayName: 'Collect Artifacts'
+  continueOnError: true
   dependsOn:
   - BuildPackage
   - BuildDocker
@@ -129,38 +130,85 @@ jobs:
   steps:
   - task: SSH@0
     displayName: 'Update Unstable Repository'
+    continueOnError: true
     condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
     inputs:
       sshEndpoint: repository
       runOptions: 'commands'
-      commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
+      commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
 
   - task: SSH@0
     displayName: 'Update Stable Repository'
-    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+    continueOnError: true
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
     inputs:
       sshEndpoint: repository
       runOptions: 'commands'
-      commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
-      
+      commands: sudo nohup -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'))
+  condition: succeeded('BuildPackage')
 
   pool:
     vmImage: 'ubuntu-latest'
 
   steps:
-  - task: NuGetCommand@2
+  - task: DotNetCoreCLI@2
+    displayName: 'Build Stable Nuget packages'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
     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)'
+      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'
+      versioningScheme: 'off'
+
+  - task: DotNetCoreCLI@2
+    displayName: 'Build Unstable Nuget packages'
+    inputs:
+      command: 'custom'
+      projects: |
+        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
+      custom: 'pack'
+      arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
+
+  - task: PublishBuildArtifacts@1
+    displayName: 'Publish Nuget packages'
+    inputs:
+      pathToPublish: $(Build.ArtifactStagingDirectory)
+      artifactName: Jellyfin Nuget Packages
+
+  - task: NuGetAuthenticate@0
+    displayName: 'Authenticate to stable Nuget feed'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+    inputs:
+      nuGetServiceConnections: 'NugetOrg'
 
   - task: NuGetCommand@2
+    displayName: 'Push Nuget packages to stable feed'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+    inputs:
+      command: 'push'
+      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg'
+      nuGetFeedType: 'external'
+      publishFeedCredentials: 'NugetOrg'
+      allowPackageConflicts: true # This ignores an error if the version already exists
+
+  - task: NuGetAuthenticate@0
+    displayName: 'Authenticate to unstable Nuget feed'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+
+  - task: NuGetCommand@2
+    displayName: 'Push Nuget packages to unstable feed'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
     inputs:
       command: 'push'
-      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
-      includeNugetOrg: 'true'
+      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' # No symbols since Azure Artifact does not support it
+      nuGetFeedType: 'internal'
+      publishVstsFeed: '7cce6c46-d610-45e3-9fb7-65a6bfd1b671/a5746b79-f369-42db-93ff-59cd066f9327'
+      allowPackageConflicts: true # This ignores an error if the version already exists

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

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

+ 1 - 0
CONTRIBUTORS.md

@@ -16,6 +16,7 @@
  - [bugfixin](https://github.com/bugfixin)
  - [chaosinnovator](https://github.com/chaosinnovator)
  - [ckcr4lyf](https://github.com/ckcr4lyf)
+ - [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
  - [crankdoofus](https://github.com/crankdoofus)
  - [crobibero](https://github.com/crobibero)
  - [cromefire](https://github.com/cromefire)

+ 1 - 1
Emby.Dlna/Common/ServiceAction.cs

@@ -13,7 +13,7 @@ namespace Emby.Dlna.Common
 
         public string Name { get; set; }
 
-        public List<Argument> ArgumentList { get; set; }
+        public List<Argument> ArgumentList { get; }
 
         /// <inheritdoc />
         public override string ToString()

+ 2 - 1
Emby.Dlna/Common/StateVariable.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Collections.Generic;
 
 namespace Emby.Dlna.Common
 {
@@ -17,7 +18,7 @@ namespace Emby.Dlna.Common
 
         public bool SendsEvents { get; set; }
 
-        public string[] AllowedValues { get; set; }
+        public IReadOnlyList<string> AllowedValues { get; set; }
 
         /// <inheritdoc />
         public override string ToString()

+ 0 - 16
Emby.Dlna/ConfigurationExtension.cs

@@ -1,7 +1,6 @@
 #nullable enable
 #pragma warning disable CS1591
 
-using System.Collections.Generic;
 using Emby.Dlna.Configuration;
 using MediaBrowser.Common.Configuration;
 
@@ -14,19 +13,4 @@ namespace Emby.Dlna
             return manager.GetConfiguration<DlnaOptions>("dlna");
         }
     }
-
-    public class DlnaConfigurationFactory : IConfigurationFactory
-    {
-        public IEnumerable<ConfigurationStore> GetConfigurations()
-        {
-            return new ConfigurationStore[]
-            {
-                new ConfigurationStore
-                {
-                    Key = "dlna",
-                    ConfigurationType = typeof (DlnaOptions)
-                }
-            };
-        }
-    }
 }

+ 4 - 6
Emby.Dlna/ConnectionManager/ConnectionManager.cs → Emby.Dlna/ConnectionManager/ConnectionManagerService.cs

@@ -9,22 +9,20 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.ConnectionManager
 {
-    public class ConnectionManager : BaseService, IConnectionManager
+    public class ConnectionManagerService : BaseService, IConnectionManager
     {
         private readonly IDlnaManager _dlna;
-        private readonly ILogger _logger;
         private readonly IServerConfigurationManager _config;
 
-        public ConnectionManager(
+        public ConnectionManagerService(
             IDlnaManager dlna,
             IServerConfigurationManager config,
-            ILogger<ConnectionManager> logger,
+            ILogger<ConnectionManagerService> logger,
             IHttpClient httpClient)
             : base(logger, httpClient)
         {
             _dlna = dlna;
             _config = config;
-            _logger = logger;
         }
 
         /// <inheritdoc />
@@ -39,7 +37,7 @@ namespace Emby.Dlna.ConnectionManager
             var profile = _dlna.GetProfile(request.Headers) ??
                          _dlna.GetDefaultProfile();
 
-            return new ControlHandler(_config, _logger, profile).ProcessControlRequestAsync(request);
+            return new ControlHandler(_config, Logger, profile).ProcessControlRequestAsync(request);
         }
     }
 }

+ 2 - 2
Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs

@@ -44,7 +44,7 @@ namespace Emby.Dlna.ConnectionManager
                 DataType = "string",
                 SendsEvents = false,
 
-                AllowedValues = new string[]
+                AllowedValues = new[]
                 {
                     "OK",
                     "ContentFormatMismatch",
@@ -67,7 +67,7 @@ namespace Emby.Dlna.ConnectionManager
                 DataType = "string",
                 SendsEvents = false,
 
-                AllowedValues = new string[]
+                AllowedValues = new[]
                 {
                     "Output",
                     "Input"

+ 3 - 3
Emby.Dlna/ContentDirectory/ContentDirectory.cs → Emby.Dlna/ContentDirectory/ContentDirectoryService.cs

@@ -19,7 +19,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.ContentDirectory
 {
-    public class ContentDirectory : BaseService, IContentDirectory
+    public class ContentDirectoryService : BaseService, IContentDirectory
     {
         private readonly ILibraryManager _libraryManager;
         private readonly IImageProcessor _imageProcessor;
@@ -33,14 +33,14 @@ namespace Emby.Dlna.ContentDirectory
         private readonly IMediaEncoder _mediaEncoder;
         private readonly ITVSeriesManager _tvSeriesManager;
 
-        public ContentDirectory(
+        public ContentDirectoryService(
             IDlnaManager dlna,
             IUserDataManager userDataManager,
             IImageProcessor imageProcessor,
             ILibraryManager libraryManager,
             IServerConfigurationManager config,
             IUserManager userManager,
-            ILogger<ContentDirectory> logger,
+            ILogger<ContentDirectoryService> logger,
             IHttpClient httpClient,
             ILocalizationManager localization,
             IMediaSourceManager mediaSourceManager,

+ 3 - 2
Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs

@@ -10,7 +10,8 @@ namespace Emby.Dlna.ContentDirectory
     {
         public string GetXml()
         {
-            return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
+            return new ServiceXmlBuilder().GetXml(
+                new ServiceActionListBuilder().GetActions(),
                 GetStateVariables());
         }
 
@@ -101,7 +102,7 @@ namespace Emby.Dlna.ContentDirectory
                 DataType = "string",
                 SendsEvents = false,
 
-                AllowedValues = new string[]
+                AllowedValues = new[]
                 {
                     "BrowseMetadata",
                     "BrowseDirectChildren"

+ 66 - 92
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -40,6 +40,11 @@ namespace Emby.Dlna.ContentDirectory
 {
     public class ControlHandler : BaseControlHandler
     {
+        private const string NsDc = "http://purl.org/dc/elements/1.1/";
+        private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
+        private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
+        private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
+
         private readonly ILibraryManager _libraryManager;
         private readonly IUserDataManager _userDataManager;
         private readonly IServerConfigurationManager _config;
@@ -47,11 +52,6 @@ namespace Emby.Dlna.ContentDirectory
         private readonly IUserViewManager _userViewManager;
         private readonly ITVSeriesManager _tvSeriesManager;
 
-        private const string NS_DC = "http://purl.org/dc/elements/1.1/";
-        private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
-        private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
-        private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
-
         private readonly int _systemUpdateId;
 
         private readonly DidlBuilder _didlBuilder;
@@ -181,7 +181,11 @@ namespace Emby.Dlna.ContentDirectory
 
             userdata.PlaybackPositionTicks = TimeSpan.FromSeconds(newbookmark).Ticks;
 
-            _userDataManager.SaveUserData(_user, item, userdata, UserDataSaveReason.TogglePlayed,
+            _userDataManager.SaveUserData(
+                _user,
+                item,
+                userdata,
+                UserDataSaveReason.TogglePlayed,
                 CancellationToken.None);
         }
 
@@ -253,7 +257,7 @@ namespace Emby.Dlna.ContentDirectory
             var id = sparams["ObjectID"];
             var flag = sparams["BrowseFlag"];
             var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
-            var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", ""));
+            var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
 
             var provided = 0;
 
@@ -286,18 +290,17 @@ namespace Emby.Dlna.ContentDirectory
 
                 using (var writer = XmlWriter.Create(builder, settings))
                 {
-                    writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
+                    writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
 
-                    writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
-                    writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
-                    writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
+                    writer.WriteAttributeString("xmlns", "dc", null, NsDc);
+                    writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
+                    writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
 
                     DidlBuilder.WriteXmlRootAttributes(_profile, writer);
 
                     var serverItem = GetItemFromObjectId(id);
                     var item = serverItem.Item;
 
-
                     if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal))
                     {
                         totalCount = 1;
@@ -362,8 +365,8 @@ namespace Emby.Dlna.ContentDirectory
 
         private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
         {
-            var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", ""));
-            var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", ""));
+            var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty));
+            var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
             var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
 
             // sort example: dc:title, dc:date
@@ -397,11 +400,11 @@ namespace Emby.Dlna.ContentDirectory
 
                 using (var writer = XmlWriter.Create(builder, settings))
                 {
-                    writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
+                    writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
 
-                    writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
-                    writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
-                    writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
+                    writer.WriteAttributeString("xmlns", "dc", null, NsDc);
+                    writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
+                    writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
 
                     DidlBuilder.WriteXmlRootAttributes(_profile, writer);
 
@@ -783,11 +786,14 @@ namespace Emby.Dlna.ContentDirectory
                 })
                 .ToArray();
 
-            return ApplyPaging(new QueryResult<ServerItem>
-            {
-                Items = folders,
-                TotalRecordCount = folders.Length
-            }, startIndex, limit);
+            return ApplyPaging(
+                new QueryResult<ServerItem>
+                {
+                    Items = folders,
+                    TotalRecordCount = folders.Length
+                },
+                startIndex,
+                limit);
         }
 
         private QueryResult<ServerItem> GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit)
@@ -1135,14 +1141,16 @@ namespace Emby.Dlna.ContentDirectory
         {
             query.OrderBy = Array.Empty<(string, SortOrder)>();
 
-            var items = _userViewManager.GetLatestItems(new LatestItemsQuery
-            {
-                UserId = user.Id,
-                Limit = 50,
-                IncludeItemTypes = new[] { nameof(Audio) },
-                ParentId = parent?.Id ?? Guid.Empty,
-                GroupItems = true
-            }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
+            var items = _userViewManager.GetLatestItems(
+                new LatestItemsQuery
+                {
+                    UserId = user.Id,
+                    Limit = 50,
+                    IncludeItemTypes = new[] { nameof(Audio) },
+                    ParentId = parent?.Id ?? Guid.Empty,
+                    GroupItems = true
+                },
+                query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
 
             return ToResult(items);
         }
@@ -1151,12 +1159,15 @@ namespace Emby.Dlna.ContentDirectory
         {
             query.OrderBy = Array.Empty<(string, SortOrder)>();
 
-            var result = _tvSeriesManager.GetNextUp(new NextUpQuery
-            {
-                Limit = query.Limit,
-                StartIndex = query.StartIndex,
-                UserId = query.User.Id
-            }, new[] { parent }, query.DtoOptions);
+            var result = _tvSeriesManager.GetNextUp(
+                new NextUpQuery
+                {
+                    Limit = query.Limit,
+                    StartIndex = query.StartIndex,
+                    UserId = query.User.Id
+                },
+                new[] { parent },
+                query.DtoOptions);
 
             return ToResult(result);
         }
@@ -1165,14 +1176,16 @@ namespace Emby.Dlna.ContentDirectory
         {
             query.OrderBy = Array.Empty<(string, SortOrder)>();
 
-            var items = _userViewManager.GetLatestItems(new LatestItemsQuery
-            {
-                UserId = user.Id,
-                Limit = 50,
-                IncludeItemTypes = new[] { typeof(Episode).Name },
-                ParentId = parent == null ? Guid.Empty : parent.Id,
-                GroupItems = false
-            }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
+            var items = _userViewManager.GetLatestItems(
+                new LatestItemsQuery
+                {
+                    UserId = user.Id,
+                    Limit = 50,
+                    IncludeItemTypes = new[] { typeof(Episode).Name },
+                    ParentId = parent == null ? Guid.Empty : parent.Id,
+                    GroupItems = false
+                },
+                query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
 
             return ToResult(items);
         }
@@ -1183,13 +1196,14 @@ namespace Emby.Dlna.ContentDirectory
 
             var items = _userViewManager.GetLatestItems(
                 new LatestItemsQuery
-            {
-                UserId = user.Id,
-                Limit = 50,
-                IncludeItemTypes = new[] { nameof(Movie) },
-                ParentId = parent?.Id ?? Guid.Empty,
-                GroupItems = true
-            }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
+                {
+                    UserId = user.Id,
+                    Limit = 50,
+                    IncludeItemTypes = new[] { nameof(Movie) },
+                    ParentId = parent?.Id ?? Guid.Empty,
+                    GroupItems = true
+                },
+                query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
 
             return ToResult(items);
         }
@@ -1354,44 +1368,4 @@ namespace Emby.Dlna.ContentDirectory
             return new ServerItem(_libraryManager.GetUserRootFolder());
         }
     }
-
-    internal class ServerItem
-    {
-        public BaseItem Item { get; set; }
-
-        public StubType? StubType { get; set; }
-
-        public ServerItem(BaseItem item)
-        {
-            Item = item;
-
-            if (item is IItemByName && !(item is Folder))
-            {
-                StubType = Dlna.ContentDirectory.StubType.Folder;
-            }
-        }
-    }
-
-    public enum StubType
-    {
-        Folder = 0,
-        Latest = 2,
-        Playlists = 3,
-        Albums = 4,
-        AlbumArtists = 5,
-        Artists = 6,
-        Songs = 7,
-        Genres = 8,
-        FavoriteSongs = 9,
-        FavoriteArtists = 10,
-        FavoriteAlbums = 11,
-        ContinueWatching = 12,
-        Movies = 13,
-        Collections = 14,
-        Favorites = 15,
-        NextUp = 16,
-        Series = 17,
-        FavoriteSeries = 18,
-        FavoriteEpisodes = 19
-    }
 }

+ 23 - 0
Emby.Dlna/ContentDirectory/ServerItem.cs

@@ -0,0 +1,23 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities;
+
+namespace Emby.Dlna.ContentDirectory
+{
+    internal class ServerItem
+    {
+        public ServerItem(BaseItem item)
+        {
+            Item = item;
+
+            if (item is IItemByName && !(item is Folder))
+            {
+                StubType = Dlna.ContentDirectory.StubType.Folder;
+            }
+        }
+
+        public BaseItem Item { get; set; }
+
+        public StubType? StubType { get; set; }
+    }
+}

+ 28 - 0
Emby.Dlna/ContentDirectory/StubType.cs

@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1602
+
+namespace Emby.Dlna.ContentDirectory
+{
+    public enum StubType
+    {
+        Folder = 0,
+        Latest = 2,
+        Playlists = 3,
+        Albums = 4,
+        AlbumArtists = 5,
+        Artists = 6,
+        Songs = 7,
+        Genres = 8,
+        FavoriteSongs = 9,
+        FavoriteArtists = 10,
+        FavoriteAlbums = 11,
+        ContinueWatching = 12,
+        Movies = 13,
+        Collections = 14,
+        Favorites = 15,
+        NextUp = 16,
+        Series = 17,
+        FavoriteSeries = 18,
+        FavoriteEpisodes = 19
+    }
+}

+ 6 - 6
Emby.Dlna/ControlRequest.cs

@@ -7,17 +7,17 @@ namespace Emby.Dlna
 {
     public class ControlRequest
     {
-        public IHeaderDictionary Headers { get; set; }
+        public ControlRequest(IHeaderDictionary headers)
+        {
+            Headers = headers;
+        }
+
+        public IHeaderDictionary Headers { get; }
 
         public Stream InputXml { get; set; }
 
         public string TargetServerUuId { get; set; }
 
         public string RequestedUrl { get; set; }
-
-        public ControlRequest()
-        {
-            Headers = new HeaderDictionary();
-        }
     }
 }

+ 1 - 1
Emby.Dlna/ControlResponse.cs

@@ -11,7 +11,7 @@ namespace Emby.Dlna
             Headers = new Dictionary<string, string>();
         }
 
-        public IDictionary<string, string> Headers { get; set; }
+        public IDictionary<string, string> Headers { get; }
 
         public string Xml { get; set; }
 

+ 69 - 88
Emby.Dlna/Didl/DidlBuilder.cs

@@ -34,12 +34,12 @@ namespace Emby.Dlna.Didl
 {
     public class DidlBuilder
     {
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
+        private const string NsDc = "http://purl.org/dc/elements/1.1/";
+        private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
+        private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
 
-        private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
-        private const string NS_DC = "http://purl.org/dc/elements/1.1/";
-        private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
-        private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
         private readonly DeviceProfile _profile;
         private readonly IImageProcessor _imageProcessor;
@@ -100,11 +100,11 @@ namespace Emby.Dlna.Didl
                 {
                     // writer.WriteStartDocument();
 
-                    writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
+                    writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
 
-                    writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
-                    writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
-                    writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
+                    writer.WriteAttributeString("xmlns", "dc", null, NsDc);
+                    writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
+                    writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
                     // didl.SetAttribute("xmlns:sec", NS_SEC);
 
                     WriteXmlRootAttributes(_profile, writer);
@@ -147,7 +147,7 @@ namespace Emby.Dlna.Didl
         {
             var clientId = GetClientId(item, null);
 
-            writer.WriteStartElement(string.Empty, "item", NS_DIDL);
+            writer.WriteStartElement(string.Empty, "item", NsDidl);
 
             writer.WriteAttributeString("restricted", "1");
             writer.WriteAttributeString("id", clientId);
@@ -207,7 +207,8 @@ namespace Emby.Dlna.Didl
             var targetWidth = streamInfo.TargetWidth;
             var targetHeight = streamInfo.TargetHeight;
 
-            var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(streamInfo.Container,
+            var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
+                streamInfo.Container,
                 streamInfo.TargetVideoCodec.FirstOrDefault(),
                 streamInfo.TargetAudioCodec.FirstOrDefault(),
                 targetWidth,
@@ -279,7 +280,7 @@ namespace Emby.Dlna.Didl
             }
             else if (string.Equals(subtitleMode, "smi", StringComparison.OrdinalIgnoreCase))
             {
-                writer.WriteStartElement(string.Empty, "res", NS_DIDL);
+                writer.WriteStartElement(string.Empty, "res", NsDidl);
 
                 writer.WriteAttributeString("protocolInfo", "http-get:*:smi/caption:*");
 
@@ -288,7 +289,7 @@ namespace Emby.Dlna.Didl
             }
             else
             {
-                writer.WriteStartElement(string.Empty, "res", NS_DIDL);
+                writer.WriteStartElement(string.Empty, "res", NsDidl);
                 var protocolInfo = string.Format(
                     CultureInfo.InvariantCulture,
                     "http-get:*:text/{0}:*",
@@ -304,7 +305,7 @@ namespace Emby.Dlna.Didl
 
         private void AddVideoResource(XmlWriter writer, Filter filter, string contentFeatures, StreamInfo streamInfo)
         {
-            writer.WriteStartElement(string.Empty, "res", NS_DIDL);
+            writer.WriteStartElement(string.Empty, "res", NsDidl);
 
             var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
 
@@ -526,7 +527,7 @@ namespace Emby.Dlna.Didl
 
         private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
         {
-            writer.WriteStartElement(string.Empty, "res", NS_DIDL);
+            writer.WriteStartElement(string.Empty, "res", NsDidl);
 
             if (streamInfo == null)
             {
@@ -583,7 +584,8 @@ namespace Emby.Dlna.Didl
                 writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
             }
 
-            var mediaProfile = _profile.GetAudioMediaProfile(streamInfo.Container,
+            var mediaProfile = _profile.GetAudioMediaProfile(
+                streamInfo.Container,
                 streamInfo.TargetAudioCodec.FirstOrDefault(),
                 targetChannels,
                 targetAudioBitrate,
@@ -596,7 +598,8 @@ namespace Emby.Dlna.Didl
                 ? MimeTypes.GetMimeType(filename)
                 : mediaProfile.MimeType;
 
-            var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(streamInfo.Container,
+            var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
+                streamInfo.Container,
                 streamInfo.TargetAudioCodec.FirstOrDefault(),
                 targetAudioBitrate,
                 targetSampleRate,
@@ -627,7 +630,7 @@ namespace Emby.Dlna.Didl
 
         public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null)
         {
-            writer.WriteStartElement(string.Empty, "container", NS_DIDL);
+            writer.WriteStartElement(string.Empty, "container", NsDidl);
 
             writer.WriteAttributeString("restricted", "1");
             writer.WriteAttributeString("searchable", "1");
@@ -714,7 +717,7 @@ namespace Emby.Dlna.Didl
             // MediaMonkey for example won't display content without a title
             // if (filter.Contains("dc:title"))
             {
-                AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC);
+                AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NsDc);
             }
 
             WriteObjectClass(writer, item, itemStubType);
@@ -723,7 +726,7 @@ namespace Emby.Dlna.Didl
             {
                 if (item.PremiereDate.HasValue)
                 {
-                    AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NS_DC);
+                    AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc);
                 }
             }
 
@@ -731,13 +734,13 @@ namespace Emby.Dlna.Didl
             {
                 foreach (var genre in item.Genres)
                 {
-                    AddValue(writer, "upnp", "genre", genre, NS_UPNP);
+                    AddValue(writer, "upnp", "genre", genre, NsUpnp);
                 }
             }
 
             foreach (var studio in item.Studios)
             {
-                AddValue(writer, "upnp", "publisher", studio, NS_UPNP);
+                AddValue(writer, "upnp", "publisher", studio, NsUpnp);
             }
 
             if (!(item is Folder))
@@ -748,28 +751,29 @@ namespace Emby.Dlna.Didl
 
                     if (!string.IsNullOrWhiteSpace(desc))
                     {
-                        AddValue(writer, "dc", "description", desc, NS_DC);
+                        AddValue(writer, "dc", "description", desc, NsDc);
                     }
                 }
+
                 // if (filter.Contains("upnp:longDescription"))
-                //{
+                // {
                 //    if (!string.IsNullOrWhiteSpace(item.Overview))
                 //    {
-                //        AddValue(writer, "upnp", "longDescription", item.Overview, NS_UPNP);
+                //        AddValue(writer, "upnp", "longDescription", item.Overview, NsUpnp);
                 //    }
-                //}
+                // }
             }
 
             if (!string.IsNullOrEmpty(item.OfficialRating))
             {
                 if (filter.Contains("dc:rating"))
                 {
-                    AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC);
+                    AddValue(writer, "dc", "rating", item.OfficialRating, NsDc);
                 }
 
                 if (filter.Contains("upnp:rating"))
                 {
-                    AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP);
+                    AddValue(writer, "upnp", "rating", item.OfficialRating, NsUpnp);
                 }
             }
 
@@ -781,7 +785,7 @@ namespace Emby.Dlna.Didl
             // More types here
             // http://oss.linn.co.uk/repos/Public/LibUpnpCil/DidlLite/UpnpAv/Test/TestDidlLite.cs
 
-            writer.WriteStartElement("upnp", "class", NS_UPNP);
+            writer.WriteStartElement("upnp", "class", NsUpnp);
 
             if (item.IsDisplayedAsFolder || stubType.HasValue)
             {
@@ -882,7 +886,7 @@ namespace Emby.Dlna.Didl
                 var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
                     ?? PersonType.Actor;
 
-                AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NS_UPNP);
+                AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp);
             }
         }
 
@@ -896,8 +900,8 @@ namespace Emby.Dlna.Didl
             {
                 foreach (var artist in hasArtists.Artists)
                 {
-                    AddValue(writer, "upnp", "artist", artist, NS_UPNP);
-                    AddValue(writer, "dc", "creator", artist, NS_DC);
+                    AddValue(writer, "upnp", "artist", artist, NsUpnp);
+                    AddValue(writer, "dc", "creator", artist, NsDc);
 
                     // If it doesn't support album artists (musicvideo), then tag as both
                     if (hasAlbumArtists == null)
@@ -917,16 +921,16 @@ namespace Emby.Dlna.Didl
 
             if (!string.IsNullOrWhiteSpace(item.Album))
             {
-                AddValue(writer, "upnp", "album", item.Album, NS_UPNP);
+                AddValue(writer, "upnp", "album", item.Album, NsUpnp);
             }
 
             if (item.IndexNumber.HasValue)
             {
-                AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP);
+                AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
 
                 if (item is Episode)
                 {
-                    AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP);
+                    AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
                 }
             }
         }
@@ -935,7 +939,7 @@ namespace Emby.Dlna.Didl
         {
             try
             {
-                writer.WriteStartElement("upnp", "artist", NS_UPNP);
+                writer.WriteStartElement("upnp", "artist", NsUpnp);
                 writer.WriteAttributeString("role", "AlbumArtist");
 
                 writer.WriteString(name);
@@ -971,14 +975,14 @@ namespace Emby.Dlna.Didl
 
             var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg");
 
-            writer.WriteStartElement("upnp", "albumArtURI", NS_UPNP);
-            writer.WriteAttributeString("dlna", "profileID", NS_DLNA, _profile.AlbumArtPn);
-            writer.WriteString(albumartUrlInfo.Url);
+            writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
+            writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
+            writer.WriteString(albumartUrlInfo.url);
             writer.WriteFullEndElement();
 
             // TOOD: Remove these default values
             var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg");
-            writer.WriteElementString("upnp", "icon", NS_UPNP, iconUrlInfo.Url);
+            writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
 
             if (!_profile.EnableAlbumArtInDidl)
             {
@@ -1021,12 +1025,12 @@ namespace Emby.Dlna.Didl
 
             var albumartUrlInfo = GetImageUrl(imageInfo, maxWidth, maxHeight, format);
 
-            writer.WriteStartElement(string.Empty, "res", NS_DIDL);
+            writer.WriteStartElement(string.Empty, "res", NsDidl);
 
             // Images must have a reported size or many clients (Bubble upnp), will only use the first thumbnail
             // rather than using a larger one when available
-            var width = albumartUrlInfo.Width ?? maxWidth;
-            var height = albumartUrlInfo.Height ?? maxHeight;
+            var width = albumartUrlInfo.width ?? maxWidth;
+            var height = albumartUrlInfo.height ?? maxHeight;
 
             var contentFeatures = new ContentFeatureBuilder(_profile)
                 .BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
@@ -1043,7 +1047,7 @@ namespace Emby.Dlna.Didl
                 "resolution",
                 string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height));
 
-            writer.WriteString(albumartUrlInfo.Url);
+            writer.WriteString(albumartUrlInfo.url);
 
             writer.WriteFullEndElement();
         }
@@ -1139,7 +1143,6 @@ namespace Emby.Dlna.Didl
 
             if (width == 0 || height == 0)
             {
-                // _imageProcessor.GetImageSize(item, imageInfo);
                 width = null;
                 height = null;
             }
@@ -1149,18 +1152,6 @@ namespace Emby.Dlna.Didl
                 height = null;
             }
 
-            // try
-            //{
-            //    var size = _imageProcessor.GetImageSize(imageInfo);
-
-            //    width = size.Width;
-            //    height = size.Height;
-            //}
-            // catch
-            //{
-
-            //}
-
             var inputFormat = (Path.GetExtension(imageInfo.Path) ?? string.Empty)
                 .TrimStart('.')
                 .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
@@ -1177,30 +1168,6 @@ namespace Emby.Dlna.Didl
             };
         }
 
-        private class ImageDownloadInfo
-        {
-            internal Guid ItemId;
-            internal string ImageTag;
-            internal ImageType Type;
-
-            internal int? Width;
-            internal int? Height;
-
-            internal bool IsDirectStream;
-
-            internal string Format;
-
-            internal ItemImageInfo ItemImageInfo;
-        }
-
-        private class ImageUrlInfo
-        {
-            internal string Url;
-
-            internal int? Width;
-            internal int? Height;
-        }
-
         public static string GetClientId(BaseItem item, StubType? stubType)
         {
             return GetClientId(item.Id, stubType);
@@ -1218,7 +1185,7 @@ namespace Emby.Dlna.Didl
             return id;
         }
 
-        private ImageUrlInfo GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
+        private (string url, int? width, int? height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
         {
             var url = string.Format(
                 CultureInfo.InvariantCulture,
@@ -1256,12 +1223,26 @@ namespace Emby.Dlna.Didl
             // just lie
             info.IsDirectStream = true;
 
-            return new ImageUrlInfo
-            {
-                Url = url,
-                Width = width,
-                Height = height
-            };
+            return (url, width, height);
+        }
+
+        private class ImageDownloadInfo
+        {
+            internal Guid ItemId { get; set; }
+
+            internal string ImageTag { get; set; }
+
+            internal ImageType Type { get; set; }
+
+            internal int? Width { get; set; }
+
+            internal int? Height { get; set; }
+
+            internal bool IsDirectStream { get; set; }
+
+            internal string Format { get; set; }
+
+            internal ItemImageInfo ItemImageInfo { get; set; }
         }
     }
 }

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

@@ -23,9 +23,7 @@ namespace Emby.Dlna.Didl
 
         public bool Contains(string field)
         {
-            // Don't bother with this. Some clients (media monkey) use the filter and then don't display very well when very little data comes back.
-            return true;
-            // return _all || ListHelper.ContainsIgnoreCase(_fields, field);
+            return _all || Array.Exists(_fields, x => x.Equals(field, StringComparison.OrdinalIgnoreCase));
         }
     }
 }

+ 1 - 1
Emby.Dlna/Didl/StringWriterWithEncoding.cs

@@ -1,4 +1,5 @@
 #pragma warning disable CS1591
+#pragma warning disable CA1305
 
 using System;
 using System.IO;
@@ -29,7 +30,6 @@ namespace Emby.Dlna.Didl
         {
         }
 
-
         public StringWriterWithEncoding(Encoding encoding)
         {
             _encoding = encoding;

+ 24 - 0
Emby.Dlna/DlnaConfigurationFactory.cs

@@ -0,0 +1,24 @@
+#nullable enable
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using Emby.Dlna.Configuration;
+using MediaBrowser.Common.Configuration;
+
+namespace Emby.Dlna
+{
+    public class DlnaConfigurationFactory : IConfigurationFactory
+    {
+        public IEnumerable<ConfigurationStore> GetConfigurations()
+        {
+            return new[]
+            {
+                new ConfigurationStore
+                {
+                    Key = "dlna",
+                    ConfigurationType = typeof(DlnaOptions)
+                }
+            };
+        }
+    }
+}

+ 16 - 15
Emby.Dlna/DlnaManager.cs

@@ -54,11 +54,15 @@ namespace Emby.Dlna
             _appHost = appHost;
         }
 
+        private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
+
+        private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
+
         public async Task InitProfilesAsync()
         {
             try
             {
-                await ExtractSystemProfilesAsync();
+                await ExtractSystemProfilesAsync().ConfigureAwait(false);
                 LoadProfiles();
             }
             catch (Exception ex)
@@ -240,7 +244,7 @@ namespace Emby.Dlna
             }
             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);
             }
 
@@ -280,10 +284,6 @@ namespace Emby.Dlna
             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)
         {
             try
@@ -495,8 +495,8 @@ namespace Emby.Dlna
         /// Recreates the object using serialization, to ensure it's not a subclass.
         /// If it's a subclass it may not serlialize properly to xml (different root element tag name).
         /// </summary>
-        /// <param name="profile"></param>
-        /// <returns></returns>
+        /// <param name="profile">The device profile.</param>
+        /// <returns>The reserialized device profile.</returns>
         private DeviceProfile ReserializeProfile(DeviceProfile profile)
         {
             if (profile.GetType() == typeof(DeviceProfile))
@@ -509,13 +509,6 @@ namespace Emby.Dlna
             return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
         }
 
-        private class InternalProfileInfo
-        {
-            internal DeviceProfileInfo Info { get; set; }
-
-            internal string Path { get; set; }
-        }
-
         public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
         {
             var profile = GetProfile(headers) ??
@@ -540,7 +533,15 @@ namespace Emby.Dlna
                 Stream = _assembly.GetManifestResourceStream(resource)
             };
         }
+
+        private class InternalProfileInfo
+        {
+            internal DeviceProfileInfo Info { get; set; }
+
+            internal string Path { get; set; }
+        }
     }
+
     /*
     class DlnaProfileEntryPoint : IServerEntryPoint
     {

+ 1 - 1
Emby.Dlna/Emby.Dlna.csproj

@@ -20,7 +20,7 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
   </PropertyGroup>
 
   <!-- Code Analyzers-->

+ 1 - 1
Emby.Dlna/EventSubscriptionResponse.cs

@@ -15,6 +15,6 @@ namespace Emby.Dlna
 
         public string ContentType { get; set; }
 
-        public Dictionary<string, string> Headers { get; set; }
+        public Dictionary<string, string> Headers { get; }
     }
 }

+ 7 - 5
Emby.Dlna/Eventing/EventManager.cs → Emby.Dlna/Eventing/DlnaEventManager.cs

@@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.Eventing
 {
-    public class EventManager : IEventManager
+    public class DlnaEventManager : IDlnaEventManager
     {
         private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions =
             new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
@@ -22,7 +22,9 @@ namespace Emby.Dlna.Eventing
         private readonly ILogger _logger;
         private readonly IHttpClient _httpClient;
 
-        public EventManager(ILogger logger, IHttpClient httpClient)
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+        public DlnaEventManager(ILogger logger, IHttpClient httpClient)
         {
             _httpClient = httpClient;
             _logger = logger;
@@ -58,7 +60,8 @@ namespace Emby.Dlna.Eventing
             var timeout = ParseTimeout(requestedTimeoutString) ?? 300;
             var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
 
-            _logger.LogDebug("Creating event subscription for {0} with timeout of {1} to {2}",
+            _logger.LogDebug(
+                "Creating event subscription for {0} with timeout of {1} to {2}",
                 notificationType,
                 timeout,
                 callbackUrl);
@@ -94,7 +97,7 @@ namespace Emby.Dlna.Eventing
         {
             _logger.LogDebug("Cancelling event subscription {0}", subscriptionId);
 
-            _subscriptions.TryRemove(subscriptionId, out EventSubscription sub);
+            _subscriptions.TryRemove(subscriptionId, out _);
 
             return new EventSubscriptionResponse
             {
@@ -103,7 +106,6 @@ namespace Emby.Dlna.Eventing
             };
         }
 
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
         {
             var response = new EventSubscriptionResponse

+ 1 - 1
Emby.Dlna/IConnectionManager.cs

@@ -2,7 +2,7 @@
 
 namespace Emby.Dlna
 {
-    public interface IConnectionManager : IEventManager, IUpnpService
+    public interface IConnectionManager : IDlnaEventManager, IUpnpService
     {
     }
 }

+ 1 - 1
Emby.Dlna/IContentDirectory.cs

@@ -2,7 +2,7 @@
 
 namespace Emby.Dlna
 {
-    public interface IContentDirectory : IEventManager, IUpnpService
+    public interface IContentDirectory : IDlnaEventManager, IUpnpService
     {
     }
 }

+ 11 - 1
Emby.Dlna/IEventManager.cs → Emby.Dlna/IDlnaEventManager.cs

@@ -2,22 +2,32 @@
 
 namespace Emby.Dlna
 {
-    public interface IEventManager
+    public interface IDlnaEventManager
     {
         /// <summary>
         /// Cancels the event subscription.
         /// </summary>
         /// <param name="subscriptionId">The subscription identifier.</param>
+        /// <returns>The response.</returns>
         EventSubscriptionResponse CancelEventSubscription(string subscriptionId);
 
         /// <summary>
         /// Renews the event subscription.
         /// </summary>
+        /// <param name="subscriptionId">The subscription identifier.</param>
+        /// <param name="notificationType">The notification type.</param>
+        /// <param name="requestedTimeoutString">The requested timeout as a sting.</param>
+        /// <param name="callbackUrl">The callback url.</param>
+        /// <returns>The response.</returns>
         EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl);
 
         /// <summary>
         /// Creates the event subscription.
         /// </summary>
+        /// <param name="notificationType">The notification type.</param>
+        /// <param name="requestedTimeoutString">The requested timeout as a sting.</param>
+        /// <param name="callbackUrl">The callback url.</param>
+        /// <returns>The response.</returns>
         EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl);
     }
 }

+ 1 - 1
Emby.Dlna/IMediaReceiverRegistrar.cs

@@ -2,7 +2,7 @@
 
 namespace Emby.Dlna
 {
-    public interface IMediaReceiverRegistrar : IEventManager, IUpnpService
+    public interface IMediaReceiverRegistrar : IDlnaEventManager, IUpnpService
     {
     }
 }

+ 33 - 23
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -30,7 +30,7 @@ using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
 
 namespace Emby.Dlna.Main
 {
-    public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
+    public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
     {
         private readonly IServerConfigurationManager _config;
         private readonly ILogger<DlnaEntryPoint> _logger;
@@ -54,13 +54,7 @@ namespace Emby.Dlna.Main
         private SsdpDevicePublisher _publisher;
         private ISsdpCommunicationsServer _communicationsServer;
 
-        public IContentDirectory ContentDirectory { get; private set; }
-
-        public IConnectionManager ConnectionManager { get; private set; }
-
-        public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
-
-        public static DlnaEntryPoint Current;
+        private bool _disposed;
 
         public DlnaEntryPoint(
             IServerConfigurationManager config,
@@ -99,14 +93,14 @@ namespace Emby.Dlna.Main
             _networkManager = networkManager;
             _logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
 
-            ContentDirectory = new ContentDirectory.ContentDirectory(
+            ContentDirectory = new ContentDirectory.ContentDirectoryService(
                 dlnaManager,
                 userDataManager,
                 imageProcessor,
                 libraryManager,
                 config,
                 userManager,
-                loggerFactory.CreateLogger<ContentDirectory.ContentDirectory>(),
+                loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
                 httpClient,
                 localizationManager,
                 mediaSourceManager,
@@ -114,19 +108,27 @@ namespace Emby.Dlna.Main
                 mediaEncoder,
                 tvSeriesManager);
 
-            ConnectionManager = new ConnectionManager.ConnectionManager(
+            ConnectionManager = new ConnectionManager.ConnectionManagerService(
                 dlnaManager,
                 config,
-                loggerFactory.CreateLogger<ConnectionManager.ConnectionManager>(),
+                loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
                 httpClient);
 
-            MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(
-                loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrar>(),
+            MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
+                loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
                 httpClient,
                 config);
             Current = this;
         }
 
+        public static DlnaEntryPoint Current { get; private set; }
+
+        public IContentDirectory ContentDirectory { get; private set; }
+
+        public IConnectionManager ConnectionManager { get; private set; }
+
+        public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
+
         public async Task RunAsync()
         {
             await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
@@ -399,8 +401,24 @@ namespace Emby.Dlna.Main
             }
         }
 
+        public void DisposeDevicePublisher()
+        {
+            if (_publisher != null)
+            {
+                _logger.LogInformation("Disposing SsdpDevicePublisher");
+                _publisher.Dispose();
+                _publisher = null;
+            }
+        }
+
+        /// <inheritdoc />
         public void Dispose()
         {
+            if (_disposed)
+            {
+                return;
+            }
+
             DisposeDevicePublisher();
             DisposePlayToManager();
             DisposeDeviceDiscovery();
@@ -416,16 +434,8 @@ namespace Emby.Dlna.Main
             ConnectionManager = null;
             MediaReceiverRegistrar = null;
             Current = null;
-        }
 
-        public void DisposeDevicePublisher()
-        {
-            if (_publisher != null)
-            {
-                _logger.LogInformation("Disposing SsdpDevicePublisher");
-                _publisher.Dispose();
-                _publisher = null;
-            }
+            _disposed = true;
         }
     }
 }

+ 3 - 3
Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs → Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs

@@ -8,12 +8,12 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.MediaReceiverRegistrar
 {
-    public class MediaReceiverRegistrar : BaseService, IMediaReceiverRegistrar
+    public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
     {
         private readonly IServerConfigurationManager _config;
 
-        public MediaReceiverRegistrar(
-            ILogger<MediaReceiverRegistrar> logger,
+        public MediaReceiverRegistrarService(
+            ILogger<MediaReceiverRegistrarService> logger,
             IHttpClient httpClient,
             IServerConfigurationManager config)
             : base(logger, httpClient)

+ 2 - 1
Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs

@@ -10,7 +10,8 @@ namespace Emby.Dlna.MediaReceiverRegistrar
     {
         public string GetXml()
         {
-            return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
+            return new ServiceXmlBuilder().GetXml(
+                new ServiceActionListBuilder().GetActions(),
                 GetStateVariables());
         }
 

+ 116 - 109
Emby.Dlna/PlayTo/Device.cs

@@ -19,15 +19,40 @@ namespace Emby.Dlna.PlayTo
 {
     public class Device : IDisposable
     {
+        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+        private readonly IHttpClient _httpClient;
+
+        private readonly ILogger _logger;
+
+        private readonly object _timerLock = new object();
         private Timer _timer;
+        private int _muteVol;
+        private int _volume;
+        private DateTime _lastVolumeRefresh;
+        private bool _volumeRefreshActive;
+        private int _connectFailureCount;
+        private bool _disposed;
+
+        public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger)
+        {
+            Properties = deviceProperties;
+            _httpClient = httpClient;
+            _logger = logger;
+        }
+
+        public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
+
+        public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
+
+        public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
+
+        public event EventHandler<MediaChangedEventArgs> MediaChanged;
 
         public DeviceInfo Properties { get; set; }
 
-        private int _muteVol;
         public bool IsMuted { get; set; }
 
-        private int _volume;
-
         public int Volume
         {
             get
@@ -43,29 +68,21 @@ namespace Emby.Dlna.PlayTo
 
         public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0);
 
-        public TRANSPORTSTATE TransportState { get; private set; }
-
-        public bool IsPlaying => TransportState == TRANSPORTSTATE.PLAYING;
+        public TransportState TransportState { get; private set; }
 
-        public bool IsPaused => TransportState == TRANSPORTSTATE.PAUSED || TransportState == TRANSPORTSTATE.PAUSED_PLAYBACK;
+        public bool IsPlaying => TransportState == TransportState.Playing;
 
-        public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED;
+        public bool IsPaused => TransportState == TransportState.Paused || TransportState == TransportState.PausedPlayback;
 
-        private readonly IHttpClient _httpClient;
+        public bool IsStopped => TransportState == TransportState.Stopped;
 
-        private readonly ILogger _logger;
+        public Action OnDeviceUnavailable { get; set; }
 
-        private readonly IServerConfigurationManager _config;
+        private TransportCommands AvCommands { get; set; }
 
-        public Action OnDeviceUnavailable { get; set; }
+        private TransportCommands RendererCommands { get; set; }
 
-        public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config)
-        {
-            Properties = deviceProperties;
-            _httpClient = httpClient;
-            _logger = logger;
-            _config = config;
-        }
+        public UBaseObject CurrentMediaInfo { get; private set; }
 
         public void Start()
         {
@@ -73,8 +90,6 @@ namespace Emby.Dlna.PlayTo
             _timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite);
         }
 
-        private DateTime _lastVolumeRefresh;
-        private bool _volumeRefreshActive;
         private Task RefreshVolumeIfNeeded()
         {
             if (_volumeRefreshActive
@@ -105,7 +120,6 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        private readonly object _timerLock = new object();
         private void RestartTimer(bool immediate = false)
         {
             lock (_timerLock)
@@ -233,6 +247,9 @@ namespace Emby.Dlna.PlayTo
         /// <summary>
         /// Sets volume on a scale of 0-100.
         /// </summary>
+        /// <param name="value">The volume on a scale of 0-100.</param>
+        /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
         public async Task SetVolume(int value, CancellationToken cancellationToken)
         {
             var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
@@ -275,7 +292,7 @@ namespace Emby.Dlna.PlayTo
                 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);
 
             RestartTimer(true);
@@ -285,7 +302,7 @@ namespace Emby.Dlna.PlayTo
         {
             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);
 
@@ -297,8 +314,8 @@ namespace Emby.Dlna.PlayTo
 
             var dictionary = new Dictionary<string, string>
             {
-                {"CurrentURI", url},
-                {"CurrentURIMetaData", CreateDidlMeta(metaData)}
+                { "CurrentURI", url },
+                { "CurrentURIMetaData", CreateDidlMeta(metaData) }
             };
 
             var service = GetAvTransportService();
@@ -401,13 +418,11 @@ namespace Emby.Dlna.PlayTo
             await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
                 .ConfigureAwait(false);
 
-            TransportState = TRANSPORTSTATE.PAUSED;
+            TransportState = TransportState.Paused;
 
             RestartTimer(true);
         }
 
-        private int _connectFailureCount;
-
         private async void TimerCallback(object sender)
         {
             if (_disposed)
@@ -436,7 +451,7 @@ namespace Emby.Dlna.PlayTo
                 if (transportState.HasValue)
                 {
                     // If we're not playing anything no need to get additional data
-                    if (transportState.Value == TRANSPORTSTATE.STOPPED)
+                    if (transportState.Value == TransportState.Stopped)
                     {
                         UpdateMediaInfo(null, transportState.Value);
                     }
@@ -465,7 +480,7 @@ namespace Emby.Dlna.PlayTo
                     }
 
                     // If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive
-                    if (transportState.Value == TRANSPORTSTATE.STOPPED)
+                    if (transportState.Value == TransportState.Stopped)
                     {
                         RestartTimerInactive();
                     }
@@ -539,7 +554,7 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var volume = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
+            var volume = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
             var volumeValue = volume?.Value;
 
             if (string.IsNullOrWhiteSpace(volumeValue))
@@ -589,14 +604,14 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse")
+            var valueNode = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetMuteResponse")
                                             .Select(i => i.Element("CurrentMute"))
                                             .FirstOrDefault(i => i != null);
 
             IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase);
         }
 
-        private async Task<TRANSPORTSTATE?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
+        private async Task<TransportState?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
         {
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo");
             if (command == null)
@@ -623,12 +638,12 @@ namespace Emby.Dlna.PlayTo
             }
 
             var transportState =
-                result.Document.Descendants(uPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
+                result.Document.Descendants(UPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
 
             var transportStateValue = transportState?.Value;
 
             if (transportStateValue != null
-                && Enum.TryParse(transportStateValue, true, out TRANSPORTSTATE state))
+                && Enum.TryParse(transportStateValue, true, out TransportState state))
             {
                 return state;
             }
@@ -636,7 +651,7 @@ namespace Emby.Dlna.PlayTo
             return null;
         }
 
-        private async Task<uBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
+        private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
         {
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
             if (command == null)
@@ -671,7 +686,7 @@ namespace Emby.Dlna.PlayTo
                 return null;
             }
 
-            var e = track.Element(uPnpNamespaces.items) ?? track;
+            var e = track.Element(UPnpNamespaces.Items) ?? track;
 
             var elementString = (string)e;
 
@@ -687,13 +702,13 @@ namespace Emby.Dlna.PlayTo
                 return null;
             }
 
-            e = track.Element(uPnpNamespaces.items) ?? track;
+            e = track.Element(UPnpNamespaces.Items) ?? track;
 
             elementString = (string)e;
 
             if (!string.IsNullOrWhiteSpace(elementString))
             {
-                return new uBaseObject
+                return new UBaseObject
                 {
                     Url = elementString
                 };
@@ -702,7 +717,7 @@ namespace Emby.Dlna.PlayTo
             return null;
         }
 
-        private async Task<(bool, uBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
+        private async Task<(bool, UBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
         {
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
             if (command == null)
@@ -731,11 +746,11 @@ namespace Emby.Dlna.PlayTo
                 return (false, 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 trackUriElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
+            var trackUri = trackUriElem?.Value;
 
-            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 durationElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
+            var duration = durationElem?.Value;
 
             if (!string.IsNullOrWhiteSpace(duration)
                 && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
@@ -747,8 +762,8 @@ namespace Emby.Dlna.PlayTo
                 Duration = 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 positionElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
+            var position = positionElem?.Value;
 
             if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
             {
@@ -787,7 +802,7 @@ namespace Emby.Dlna.PlayTo
                 return (true, null);
             }
 
-            var e = uPnpResponse.Element(uPnpNamespaces.items);
+            var e = uPnpResponse.Element(UPnpNamespaces.Items);
 
             var uTrack = CreateUBaseObject(e, trackUri);
 
@@ -819,7 +834,7 @@ namespace Emby.Dlna.PlayTo
             // some devices send back invalid xml
             try
             {
-                return XElement.Parse(xml.Replace("&", "&amp;"));
+                return XElement.Parse(xml.Replace("&", "&amp;", StringComparison.Ordinal));
             }
             catch (XmlException)
             {
@@ -828,27 +843,27 @@ namespace Emby.Dlna.PlayTo
             return null;
         }
 
-        private static uBaseObject CreateUBaseObject(XElement container, string trackUri)
+        private static UBaseObject CreateUBaseObject(XElement container, string trackUri)
         {
             if (container == null)
             {
                 throw new ArgumentNullException(nameof(container));
             }
 
-            var url = container.GetValue(uPnpNamespaces.Res);
+            var url = container.GetValue(UPnpNamespaces.Res);
 
             if (string.IsNullOrWhiteSpace(url))
             {
                 url = trackUri;
             }
 
-            return new uBaseObject
+            return new UBaseObject
             {
-                Id = container.GetAttributeValue(uPnpNamespaces.Id),
-                ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
-                Title = container.GetValue(uPnpNamespaces.title),
-                IconUrl = container.GetValue(uPnpNamespaces.Artwork),
-                SecondText = "",
+                Id = container.GetAttributeValue(UPnpNamespaces.Id),
+                ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId),
+                Title = container.GetValue(UPnpNamespaces.Title),
+                IconUrl = container.GetValue(UPnpNamespaces.Artwork),
+                SecondText = string.Empty,
                 Url = url,
                 ProtocolInfo = GetProtocolInfo(container),
                 MetaData = container.ToString()
@@ -862,11 +877,11 @@ namespace Emby.Dlna.PlayTo
                 throw new ArgumentNullException(nameof(container));
             }
 
-            var resElement = container.Element(uPnpNamespaces.Res);
+            var resElement = container.Element(UPnpNamespaces.Res);
 
             if (resElement != null)
             {
-                var info = resElement.Attribute(uPnpNamespaces.ProtocolInfo);
+                var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
 
                 if (info != null && !string.IsNullOrWhiteSpace(info.Value))
                 {
@@ -941,12 +956,12 @@ namespace Emby.Dlna.PlayTo
                 return url;
             }
 
-            if (!url.Contains("/"))
+            if (!url.Contains('/', StringComparison.Ordinal))
             {
                 url = "/dmr/" + url;
             }
 
-            if (!url.StartsWith("/"))
+            if (!url.StartsWith("/", StringComparison.Ordinal))
             {
                 url = "/" + url;
             }
@@ -954,11 +969,7 @@ namespace Emby.Dlna.PlayTo
             return baseUrl + url;
         }
 
-        private TransportCommands AvCommands { get; set; }
-
-        private TransportCommands RendererCommands { get; set; }
-
-        public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, CancellationToken cancellationToken)
+        public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, ILogger logger, CancellationToken cancellationToken)
         {
             var ssdpHttpClient = new SsdpHttpClient(httpClient);
 
@@ -966,13 +977,13 @@ namespace Emby.Dlna.PlayTo
 
             var friendlyNames = new List<string>();
 
-            var name = document.Descendants(uPnpNamespaces.ud.GetName("friendlyName")).FirstOrDefault();
+            var name = document.Descendants(UPnpNamespaces.Ud.GetName("friendlyName")).FirstOrDefault();
             if (name != null && !string.IsNullOrWhiteSpace(name.Value))
             {
                 friendlyNames.Add(name.Value);
             }
 
-            var room = document.Descendants(uPnpNamespaces.ud.GetName("roomName")).FirstOrDefault();
+            var room = document.Descendants(UPnpNamespaces.Ud.GetName("roomName")).FirstOrDefault();
             if (room != null && !string.IsNullOrWhiteSpace(room.Value))
             {
                 friendlyNames.Add(room.Value);
@@ -981,77 +992,77 @@ namespace Emby.Dlna.PlayTo
             var deviceProperties = new DeviceInfo()
             {
                 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();
             if (model != null)
             {
                 deviceProperties.ModelName = model.Value;
             }
 
-            var modelNumber = document.Descendants(uPnpNamespaces.ud.GetName("modelNumber")).FirstOrDefault();
+            var modelNumber = document.Descendants(UPnpNamespaces.Ud.GetName("modelNumber")).FirstOrDefault();
             if (modelNumber != null)
             {
                 deviceProperties.ModelNumber = modelNumber.Value;
             }
 
-            var uuid = document.Descendants(uPnpNamespaces.ud.GetName("UDN")).FirstOrDefault();
+            var uuid = document.Descendants(UPnpNamespaces.Ud.GetName("UDN")).FirstOrDefault();
             if (uuid != null)
             {
                 deviceProperties.UUID = uuid.Value;
             }
 
-            var manufacturer = document.Descendants(uPnpNamespaces.ud.GetName("manufacturer")).FirstOrDefault();
+            var manufacturer = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturer")).FirstOrDefault();
             if (manufacturer != null)
             {
                 deviceProperties.Manufacturer = manufacturer.Value;
             }
 
-            var manufacturerUrl = document.Descendants(uPnpNamespaces.ud.GetName("manufacturerURL")).FirstOrDefault();
+            var manufacturerUrl = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturerURL")).FirstOrDefault();
             if (manufacturerUrl != null)
             {
                 deviceProperties.ManufacturerUrl = manufacturerUrl.Value;
             }
 
-            var presentationUrl = document.Descendants(uPnpNamespaces.ud.GetName("presentationURL")).FirstOrDefault();
+            var presentationUrl = document.Descendants(UPnpNamespaces.Ud.GetName("presentationURL")).FirstOrDefault();
             if (presentationUrl != null)
             {
                 deviceProperties.PresentationUrl = presentationUrl.Value;
             }
 
-            var modelUrl = document.Descendants(uPnpNamespaces.ud.GetName("modelURL")).FirstOrDefault();
+            var modelUrl = document.Descendants(UPnpNamespaces.Ud.GetName("modelURL")).FirstOrDefault();
             if (modelUrl != null)
             {
                 deviceProperties.ModelUrl = modelUrl.Value;
             }
 
-            var serialNumber = document.Descendants(uPnpNamespaces.ud.GetName("serialNumber")).FirstOrDefault();
+            var serialNumber = document.Descendants(UPnpNamespaces.Ud.GetName("serialNumber")).FirstOrDefault();
             if (serialNumber != null)
             {
                 deviceProperties.SerialNumber = serialNumber.Value;
             }
 
-            var modelDescription = document.Descendants(uPnpNamespaces.ud.GetName("modelDescription")).FirstOrDefault();
+            var modelDescription = document.Descendants(UPnpNamespaces.Ud.GetName("modelDescription")).FirstOrDefault();
             if (modelDescription != null)
             {
                 deviceProperties.ModelDescription = modelDescription.Value;
             }
 
-            var icon = document.Descendants(uPnpNamespaces.ud.GetName("icon")).FirstOrDefault();
+            var icon = document.Descendants(UPnpNamespaces.Ud.GetName("icon")).FirstOrDefault();
             if (icon != null)
             {
                 deviceProperties.Icon = CreateIcon(icon);
             }
 
-            foreach (var services in document.Descendants(uPnpNamespaces.ud.GetName("serviceList")))
+            foreach (var services in document.Descendants(UPnpNamespaces.Ud.GetName("serviceList")))
             {
                 if (services == null)
                 {
                     continue;
                 }
 
-                var servicesList = services.Descendants(uPnpNamespaces.ud.GetName("service"));
+                var servicesList = services.Descendants(UPnpNamespaces.Ud.GetName("service"));
                 if (servicesList == null)
                 {
                     continue;
@@ -1068,10 +1079,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)
         {
             if (element == null)
@@ -1079,11 +1089,11 @@ namespace Emby.Dlna.PlayTo
                 throw new ArgumentNullException(nameof(element));
             }
 
-            var mimeType = element.GetDescendantValue(uPnpNamespaces.ud.GetName("mimetype"));
-            var width = element.GetDescendantValue(uPnpNamespaces.ud.GetName("width"));
-            var height = element.GetDescendantValue(uPnpNamespaces.ud.GetName("height"));
-            var depth = element.GetDescendantValue(uPnpNamespaces.ud.GetName("depth"));
-            var url = element.GetDescendantValue(uPnpNamespaces.ud.GetName("url"));
+            var mimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype"));
+            var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width"));
+            var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height"));
+            var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
+            var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
 
             var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
             var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
@@ -1100,11 +1110,11 @@ namespace Emby.Dlna.PlayTo
 
         private static DeviceService Create(XElement element)
         {
-            var type = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceType"));
-            var id = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceId"));
-            var scpdUrl = element.GetDescendantValue(uPnpNamespaces.ud.GetName("SCPDURL"));
-            var controlURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("controlURL"));
-            var eventSubURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("eventSubURL"));
+            var type = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType"));
+            var id = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId"));
+            var scpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL"));
+            var controlURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL"));
+            var eventSubURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL"));
 
             return new DeviceService
             {
@@ -1116,14 +1126,7 @@ namespace Emby.Dlna.PlayTo
             };
         }
 
-        public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
-        public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
-        public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
-        public event EventHandler<MediaChangedEventArgs> MediaChanged;
-
-        public uBaseObject CurrentMediaInfo { get; private set; }
-
-        private void UpdateMediaInfo(uBaseObject mediaInfo, TRANSPORTSTATE state)
+        private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state)
         {
             TransportState = state;
 
@@ -1132,7 +1135,7 @@ namespace Emby.Dlna.PlayTo
 
             if (previousMediaInfo == null && mediaInfo != null)
             {
-                if (state != TRANSPORTSTATE.STOPPED)
+                if (state != TransportState.Stopped)
                 {
                     OnPlaybackStart(mediaInfo);
                 }
@@ -1151,7 +1154,7 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        private void OnPlaybackStart(uBaseObject mediaInfo)
+        private void OnPlaybackStart(UBaseObject mediaInfo)
         {
             if (string.IsNullOrWhiteSpace(mediaInfo.Url))
             {
@@ -1164,7 +1167,7 @@ namespace Emby.Dlna.PlayTo
             });
         }
 
-        private void OnPlaybackProgress(uBaseObject mediaInfo)
+        private void OnPlaybackProgress(UBaseObject mediaInfo)
         {
             if (string.IsNullOrWhiteSpace(mediaInfo.Url))
             {
@@ -1177,7 +1180,7 @@ namespace Emby.Dlna.PlayTo
             });
         }
 
-        private void OnPlaybackStop(uBaseObject mediaInfo)
+        private void OnPlaybackStop(UBaseObject mediaInfo)
         {
             PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
             {
@@ -1185,7 +1188,7 @@ namespace Emby.Dlna.PlayTo
             });
         }
 
-        private void OnMediaChanged(uBaseObject old, uBaseObject newMedia)
+        private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
         {
             MediaChanged?.Invoke(this, new MediaChangedEventArgs
             {
@@ -1194,14 +1197,17 @@ namespace Emby.Dlna.PlayTo
             });
         }
 
-        bool _disposed;
-
+        /// <inheritdoc />
         public void Dispose()
         {
             Dispose(true);
             GC.SuppressFinalize(this);
         }
 
+        /// <summary>
+        /// Releases unmanaged and optionally managed resources.
+        /// </summary>
+        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
         protected virtual void Dispose(bool disposing)
         {
             if (_disposed)
@@ -1220,9 +1226,10 @@ namespace Emby.Dlna.PlayTo
             _disposed = true;
         }
 
+        /// <inheritdoc />
         public override string ToString()
         {
-            return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);
+            return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl);
         }
     }
 }

+ 3 - 2
Emby.Dlna/PlayTo/DeviceInfo.cs

@@ -8,6 +8,9 @@ namespace Emby.Dlna.PlayTo
 {
     public class DeviceInfo
     {
+        private readonly List<DeviceService> _services = new List<DeviceService>();
+        private string _baseUrl = string.Empty;
+
         public DeviceInfo()
         {
             Name = "Generic Device";
@@ -33,7 +36,6 @@ namespace Emby.Dlna.PlayTo
 
         public string PresentationUrl { get; set; }
 
-        private string _baseUrl = string.Empty;
         public string BaseUrl
         {
             get => _baseUrl;
@@ -42,7 +44,6 @@ namespace Emby.Dlna.PlayTo
 
         public DeviceIcon Icon { get; set; }
 
-        private readonly List<DeviceService> _services = new List<DeviceService>();
         public List<DeviceService> Services => _services;
 
         public DeviceIdentification ToDeviceIdentification()

+ 13 - 0
Emby.Dlna/PlayTo/MediaChangedEventArgs.cs

@@ -0,0 +1,13 @@
+#pragma warning disable CS1591
+
+using System;
+
+namespace Emby.Dlna.PlayTo
+{
+    public class MediaChangedEventArgs : EventArgs
+    {
+        public UBaseObject OldMediaInfo { get; set; }
+
+        public UBaseObject NewMediaInfo { get; set; }
+    }
+}

+ 127 - 124
Emby.Dlna/PlayTo/PlayToController.cs

@@ -8,6 +8,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Emby.Dlna.Didl;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
@@ -18,7 +19,6 @@ using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Session;
 using Microsoft.AspNetCore.WebUtilities;
@@ -31,7 +31,6 @@ namespace Emby.Dlna.PlayTo
     {
         private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
 
-        private Device _device;
         private readonly SessionInfo _session;
         private readonly ISessionManager _sessionManager;
         private readonly ILibraryManager _libraryManager;
@@ -50,6 +49,7 @@ namespace Emby.Dlna.PlayTo
         private readonly string _accessToken;
 
         private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
+        private Device _device;
         private int _currentPlaylistIndex;
 
         private bool _disposed;
@@ -372,8 +372,13 @@ namespace Emby.Dlna.PlayTo
 
             if (!command.ControllingUserId.Equals(Guid.Empty))
             {
-                _sessionManager.LogSessionActivity(_session.Client, _session.ApplicationVersion, _session.DeviceId,
-                       _session.DeviceName, _session.RemoteEndPoint, user);
+                _sessionManager.LogSessionActivity(
+                    _session.Client,
+                    _session.ApplicationVersion,
+                    _session.DeviceId,
+                    _session.DeviceName,
+                    _session.RemoteEndPoint,
+                    user);
             }
 
             return PlayItems(playlist, cancellationToken);
@@ -498,42 +503,44 @@ namespace Emby.Dlna.PlayTo
             if (streamInfo.MediaType == DlnaProfileType.Audio)
             {
                 return new ContentFeatureBuilder(profile)
-                    .BuildAudioHeader(streamInfo.Container,
-                    streamInfo.TargetAudioCodec.FirstOrDefault(),
-                    streamInfo.TargetAudioBitrate,
-                    streamInfo.TargetAudioSampleRate,
-                    streamInfo.TargetAudioChannels,
-                    streamInfo.TargetAudioBitDepth,
-                    streamInfo.IsDirectStream,
-                    streamInfo.RunTimeTicks ?? 0,
-                    streamInfo.TranscodeSeekInfo);
+                    .BuildAudioHeader(
+                        streamInfo.Container,
+                        streamInfo.TargetAudioCodec.FirstOrDefault(),
+                        streamInfo.TargetAudioBitrate,
+                        streamInfo.TargetAudioSampleRate,
+                        streamInfo.TargetAudioChannels,
+                        streamInfo.TargetAudioBitDepth,
+                        streamInfo.IsDirectStream,
+                        streamInfo.RunTimeTicks ?? 0,
+                        streamInfo.TranscodeSeekInfo);
             }
 
             if (streamInfo.MediaType == DlnaProfileType.Video)
             {
                 var list = new ContentFeatureBuilder(profile)
-                    .BuildVideoHeader(streamInfo.Container,
-                    streamInfo.TargetVideoCodec.FirstOrDefault(),
-                    streamInfo.TargetAudioCodec.FirstOrDefault(),
-                    streamInfo.TargetWidth,
-                    streamInfo.TargetHeight,
-                    streamInfo.TargetVideoBitDepth,
-                    streamInfo.TargetVideoBitrate,
-                    streamInfo.TargetTimestamp,
-                    streamInfo.IsDirectStream,
-                    streamInfo.RunTimeTicks ?? 0,
-                    streamInfo.TargetVideoProfile,
-                    streamInfo.TargetVideoLevel,
-                    streamInfo.TargetFramerate ?? 0,
-                    streamInfo.TargetPacketLength,
-                    streamInfo.TranscodeSeekInfo,
-                    streamInfo.IsTargetAnamorphic,
-                    streamInfo.IsTargetInterlaced,
-                    streamInfo.TargetRefFrames,
-                    streamInfo.TargetVideoStreamCount,
-                    streamInfo.TargetAudioStreamCount,
-                    streamInfo.TargetVideoCodecTag,
-                    streamInfo.IsTargetAVC);
+                    .BuildVideoHeader(
+                        streamInfo.Container,
+                        streamInfo.TargetVideoCodec.FirstOrDefault(),
+                        streamInfo.TargetAudioCodec.FirstOrDefault(),
+                        streamInfo.TargetWidth,
+                        streamInfo.TargetHeight,
+                        streamInfo.TargetVideoBitDepth,
+                        streamInfo.TargetVideoBitrate,
+                        streamInfo.TargetTimestamp,
+                        streamInfo.IsDirectStream,
+                        streamInfo.RunTimeTicks ?? 0,
+                        streamInfo.TargetVideoProfile,
+                        streamInfo.TargetVideoLevel,
+                        streamInfo.TargetFramerate ?? 0,
+                        streamInfo.TargetPacketLength,
+                        streamInfo.TranscodeSeekInfo,
+                        streamInfo.IsTargetAnamorphic,
+                        streamInfo.IsTargetInterlaced,
+                        streamInfo.TargetRefFrames,
+                        streamInfo.TargetVideoStreamCount,
+                        streamInfo.TargetAudioStreamCount,
+                        streamInfo.TargetVideoCodecTag,
+                        streamInfo.IsTargetAVC);
 
                 return list.Count == 0 ? null : list[0];
             }
@@ -633,6 +640,10 @@ namespace Emby.Dlna.PlayTo
             GC.SuppressFinalize(this);
         }
 
+        /// <summary>
+        /// Releases unmanaged and optionally managed resources.
+        /// </summary>
+        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
         protected virtual void Dispose(bool disposing)
         {
             if (_disposed)
@@ -673,48 +684,41 @@ namespace Emby.Dlna.PlayTo
                     case GeneralCommandType.ToggleMute:
                         return _device.ToggleMute(cancellationToken);
                     case GeneralCommandType.SetAudioStreamIndex:
+                        if (command.Arguments.TryGetValue("Index", out string index))
                         {
-                            if (command.Arguments.TryGetValue("Index", out string arg))
+                            if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
                             {
-                                if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val))
-                                {
-                                    return SetAudioStreamIndex(val);
-                                }
-
-                                throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
+                                return SetAudioStreamIndex(val);
                             }
 
-                            throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
+                            throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
                         }
+
+                        throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
                     case GeneralCommandType.SetSubtitleStreamIndex:
+                        if (command.Arguments.TryGetValue("Index", out index))
                         {
-                            if (command.Arguments.TryGetValue("Index", out string arg))
+                            if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
                             {
-                                if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val))
-                                {
-                                    return SetSubtitleStreamIndex(val);
-                                }
-
-                                throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
+                                return SetSubtitleStreamIndex(val);
                             }
 
-                            throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
+                            throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
                         }
+
+                        throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
                     case GeneralCommandType.SetVolume:
+                        if (command.Arguments.TryGetValue("Volume", out string vol))
                         {
-                            if (command.Arguments.TryGetValue("Volume", out string arg))
+                            if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
                             {
-                                if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var volume))
-                                {
-                                    return _device.SetVolume(volume, cancellationToken);
-                                }
-
-                                throw new ArgumentException("Unsupported volume value supplied.");
+                                return _device.SetVolume(volume, cancellationToken);
                             }
 
-                            throw new ArgumentException("Volume argument cannot be null");
+                            throw new ArgumentException("Unsupported volume value supplied.");
                         }
 
+                        throw new ArgumentException("Volume argument cannot be null");
                     default:
                         return Task.CompletedTask;
                 }
@@ -778,7 +782,7 @@ namespace Emby.Dlna.PlayTo
             const int maxWait = 15000000;
             const int interval = 500;
             var currentWait = 0;
-            while (_device.TransportState != TRANSPORTSTATE.PLAYING && currentWait < maxWait)
+            while (_device.TransportState != TransportState.Playing && currentWait < maxWait)
             {
                 await Task.Delay(interval).ConfigureAwait(false);
                 currentWait += interval;
@@ -787,8 +791,67 @@ namespace Emby.Dlna.PlayTo
             await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false);
         }
 
+        private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
+        {
+            var value = values.GetValueOrDefault(name);
+
+            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+            {
+                return result;
+            }
+
+            return null;
+        }
+
+        private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
+        {
+            var value = values.GetValueOrDefault(name);
+
+            if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+            {
+                return result;
+            }
+
+            return 0;
+        }
+
+        /// <inheritdoc />
+        public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
+        {
+            if (_disposed)
+            {
+                throw new ObjectDisposedException(GetType().Name);
+            }
+
+            if (_device == null)
+            {
+                return Task.CompletedTask;
+            }
+
+            if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
+            {
+                return SendPlayCommand(data as PlayRequest, cancellationToken);
+            }
+
+            if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
+            {
+                return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
+            }
+
+            if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
+            {
+                return SendGeneralCommand(data as GeneralCommand, cancellationToken);
+            }
+
+            // Not supported or needed right now
+            return Task.CompletedTask;
+        }
+
         private class StreamParams
         {
+            private MediaSourceInfo mediaSource;
+            private IMediaSourceManager _mediaSourceManager;
+
             public Guid ItemId { get; set; }
 
             public bool IsDirectStream { get; set; }
@@ -809,15 +872,11 @@ namespace Emby.Dlna.PlayTo
 
             public BaseItem Item { get; set; }
 
-            private MediaSourceInfo MediaSource;
-
-            private IMediaSourceManager _mediaSourceManager;
-
             public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
             {
-                if (MediaSource != null)
+                if (mediaSource != null)
                 {
-                    return MediaSource;
+                    return mediaSource;
                 }
 
                 var hasMediaSources = Item as IHasMediaSources;
@@ -827,9 +886,9 @@ namespace Emby.Dlna.PlayTo
                     return null;
                 }
 
-                MediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
+                mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
 
-                return MediaSource;
+                return mediaSource;
             }
 
             private static Guid GetItemId(string url)
@@ -901,61 +960,5 @@ namespace Emby.Dlna.PlayTo
                 return request;
             }
         }
-
-        private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
-        {
-            var value = values.GetValueOrDefault(name);
-
-            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
-            {
-                return result;
-            }
-
-            return null;
-        }
-
-        private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
-        {
-            var value = values.GetValueOrDefault(name);
-
-            if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
-            {
-                return result;
-            }
-
-            return 0;
-        }
-
-        /// <inheritdoc />
-        public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
-        {
-            if (_disposed)
-            {
-                throw new ObjectDisposedException(GetType().Name);
-            }
-
-            if (_device == null)
-            {
-                return Task.CompletedTask;
-            }
-
-            if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
-            {
-                return SendPlayCommand(data as PlayRequest, cancellationToken);
-            }
-
-            if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
-            {
-                return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
-            }
-
-            if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
-            {
-                return SendGeneralCommand(data as GeneralCommand, cancellationToken);
-            }
-
-            // Not supported or needed right now
-            return Task.CompletedTask;
-        }
     }
 }

+ 29 - 28
Emby.Dlna/PlayTo/PlayToManager.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Net;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
@@ -16,7 +17,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
@@ -92,7 +92,7 @@ namespace Emby.Dlna.PlayTo
 
             // It has to report that it's a media renderer
             if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 &&
-                     nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
+                nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
             {
                 // _logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location);
                 return;
@@ -174,7 +174,7 @@ namespace Emby.Dlna.PlayTo
 
             if (controller == null)
             {
-                var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger, cancellationToken).ConfigureAwait(false);
+                var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _logger, cancellationToken).ConfigureAwait(false);
 
                 string deviceName = device.Properties.Name;
 
@@ -192,20 +192,20 @@ namespace Emby.Dlna.PlayTo
 
                 controller = new PlayToController(
                     sessionInfo,
-                   _sessionManager,
-                   _libraryManager,
-                   _logger,
-                   _dlnaManager,
-                   _userManager,
-                   _imageProcessor,
-                   serverAddress,
-                   null,
-                   _deviceDiscovery,
-                   _userDataManager,
-                   _localization,
-                   _mediaSourceManager,
-                   _config,
-                   _mediaEncoder);
+                    _sessionManager,
+                    _libraryManager,
+                    _logger,
+                    _dlnaManager,
+                    _userManager,
+                    _imageProcessor,
+                    serverAddress,
+                    null,
+                    _deviceDiscovery,
+                    _userDataManager,
+                    _localization,
+                    _mediaSourceManager,
+                    _config,
+                    _mediaEncoder);
 
                 sessionInfo.AddController(controller);
 
@@ -218,17 +218,17 @@ namespace Emby.Dlna.PlayTo
                 {
                     PlayableMediaTypes = profile.GetSupportedMediaTypes(),
 
-                    SupportedCommands = new string[]
+                    SupportedCommands = new[]
                     {
-                            GeneralCommandType.VolumeDown.ToString(),
-                            GeneralCommandType.VolumeUp.ToString(),
-                            GeneralCommandType.Mute.ToString(),
-                            GeneralCommandType.Unmute.ToString(),
-                            GeneralCommandType.ToggleMute.ToString(),
-                            GeneralCommandType.SetVolume.ToString(),
-                            GeneralCommandType.SetAudioStreamIndex.ToString(),
-                            GeneralCommandType.SetSubtitleStreamIndex.ToString(),
-                            GeneralCommandType.PlayMediaSource.ToString()
+                        GeneralCommandType.VolumeDown.ToString(),
+                        GeneralCommandType.VolumeUp.ToString(),
+                        GeneralCommandType.Mute.ToString(),
+                        GeneralCommandType.Unmute.ToString(),
+                        GeneralCommandType.ToggleMute.ToString(),
+                        GeneralCommandType.SetVolume.ToString(),
+                        GeneralCommandType.SetAudioStreamIndex.ToString(),
+                        GeneralCommandType.SetSubtitleStreamIndex.ToString(),
+                        GeneralCommandType.PlayMediaSource.ToString()
                     },
 
                     SupportsMediaControl = true
@@ -247,8 +247,9 @@ namespace Emby.Dlna.PlayTo
             {
                 _disposeCancellationTokenSource.Cancel();
             }
-            catch
+            catch (Exception ex)
             {
+                _logger.LogDebug(ex, "Error while disposing PlayToManager");
             }
 
             _sessionLock.Dispose();

+ 1 - 1
Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs

@@ -6,6 +6,6 @@ namespace Emby.Dlna.PlayTo
 {
     public class PlaybackProgressEventArgs : EventArgs
     {
-        public uBaseObject MediaInfo { get; set; }
+        public UBaseObject MediaInfo { get; set; }
     }
 }

+ 1 - 1
Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs

@@ -6,6 +6,6 @@ namespace Emby.Dlna.PlayTo
 {
     public class PlaybackStartEventArgs : EventArgs
     {
-        public uBaseObject MediaInfo { get; set; }
+        public UBaseObject MediaInfo { get; set; }
     }
 }

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

@@ -6,13 +6,6 @@ namespace Emby.Dlna.PlayTo
 {
     public class PlaybackStoppedEventArgs : EventArgs
     {
-        public uBaseObject MediaInfo { get; set; }
-    }
-
-    public class MediaChangedEventArgs : EventArgs
-    {
-        public uBaseObject OldMediaInfo { get; set; }
-
-        public uBaseObject NewMediaInfo { get; set; }
+        public UBaseObject MediaInfo { get; set; }
     }
 }

+ 0 - 13
Emby.Dlna/PlayTo/TRANSPORTSTATE.cs

@@ -1,13 +0,0 @@
-#pragma warning disable CS1591
-
-namespace Emby.Dlna.PlayTo
-{
-    public enum TRANSPORTSTATE
-    {
-        STOPPED,
-        PLAYING,
-        TRANSITIONING,
-        PAUSED_PLAYBACK,
-        PAUSED
-    }
-}

+ 31 - 39
Emby.Dlna/PlayTo/TransportCommands.cs

@@ -2,6 +2,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Linq;
 using System.Xml.Linq;
 using Emby.Dlna.Common;
@@ -11,36 +12,30 @@ namespace Emby.Dlna.PlayTo
 {
     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>();
-        public List<StateVariable> StateVariables
-        {
-            get => _stateVariables;
-            set => _stateVariables = value;
-        }
-
         private List<ServiceAction> _serviceActions = new List<ServiceAction>();
-        public List<ServiceAction> ServiceActions
-        {
-            get => _serviceActions;
-            set => _serviceActions = value;
-        }
+
+        public List<StateVariable> StateVariables => _stateVariables;
+
+        public List<ServiceAction> ServiceActions => _serviceActions;
 
         public static TransportCommands Create(XDocument document)
         {
             var command = new TransportCommands();
 
-            var actionList = document.Descendants(uPnpNamespaces.svc + "actionList");
+            var actionList = document.Descendants(UPnpNamespaces.Svc + "actionList");
 
-            foreach (var container in actionList.Descendants(uPnpNamespaces.svc + "action"))
+            foreach (var container in actionList.Descendants(UPnpNamespaces.Svc + "action"))
             {
                 command.ServiceActions.Add(ServiceActionFromXml(container));
             }
 
-            var stateValues = document.Descendants(uPnpNamespaces.ServiceStateTable).FirstOrDefault();
+            var stateValues = document.Descendants(UPnpNamespaces.ServiceStateTable).FirstOrDefault();
 
             if (stateValues != null)
             {
-                foreach (var container in stateValues.Elements(uPnpNamespaces.svc + "stateVariable"))
+                foreach (var container in stateValues.Elements(UPnpNamespaces.Svc + "stateVariable"))
                 {
                     command.StateVariables.Add(FromXml(container));
                 }
@@ -51,19 +46,19 @@ namespace Emby.Dlna.PlayTo
 
         private static ServiceAction ServiceActionFromXml(XElement container)
         {
-            var argumentList = new List<Argument>();
+            var serviceAction = new ServiceAction
+            {
+                Name = container.GetValue(UPnpNamespaces.Svc + "name"),
+            };
 
-            foreach (var arg in container.Descendants(uPnpNamespaces.svc + "argument"))
+            var argumentList = serviceAction.ArgumentList;
+
+            foreach (var arg in container.Descendants(UPnpNamespaces.Svc + "argument"))
             {
                 argumentList.Add(ArgumentFromXml(arg));
             }
 
-            return new ServiceAction
-            {
-                Name = container.GetValue(uPnpNamespaces.svc + "name"),
-
-                ArgumentList = argumentList
-            };
+            return serviceAction;
         }
 
         private static Argument ArgumentFromXml(XElement container)
@@ -75,29 +70,29 @@ namespace Emby.Dlna.PlayTo
 
             return new Argument
             {
-                Name = container.GetValue(uPnpNamespaces.svc + "name"),
-                Direction = container.GetValue(uPnpNamespaces.svc + "direction"),
-                RelatedStateVariable = container.GetValue(uPnpNamespaces.svc + "relatedStateVariable")
+                Name = container.GetValue(UPnpNamespaces.Svc + "name"),
+                Direction = container.GetValue(UPnpNamespaces.Svc + "direction"),
+                RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable")
             };
         }
 
         private static StateVariable FromXml(XElement container)
         {
             var allowedValues = new List<string>();
-            var element = container.Descendants(uPnpNamespaces.svc + "allowedValueList")
+            var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList")
                 .FirstOrDefault();
 
             if (element != null)
             {
-                var values = element.Descendants(uPnpNamespaces.svc + "allowedValue");
+                var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue");
 
                 allowedValues.AddRange(values.Select(child => child.Value));
             }
 
             return new StateVariable
             {
-                Name = container.GetValue(uPnpNamespaces.svc + "name"),
-                DataType = container.GetValue(uPnpNamespaces.svc + "dataType"),
+                Name = container.GetValue(UPnpNamespaces.Svc + "name"),
+                DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
                 AllowedValues = allowedValues.ToArray()
             };
         }
@@ -123,7 +118,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 = "")
@@ -147,7 +142,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)
@@ -170,7 +165,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 = "")
@@ -180,15 +175,12 @@ namespace Emby.Dlna.PlayTo
             if (state != null)
             {
                 var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
-                                 state.AllowedValues.FirstOrDefault() ??
-                                 value;
+                    (state.AllowedValues.Count > 0 ? state.AllowedValues[0] : 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>";
     }
 }

+ 14 - 0
Emby.Dlna/PlayTo/TransportState.cs

@@ -0,0 +1,14 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1602
+
+namespace Emby.Dlna.PlayTo
+{
+    public enum TransportState
+    {
+        Stopped,
+        Playing,
+        Transitioning,
+        PausedPlayback,
+        Paused
+    }
+}

+ 8 - 8
Emby.Dlna/PlayTo/UpnpContainer.cs

@@ -6,22 +6,22 @@ using Emby.Dlna.Ssdp;
 
 namespace Emby.Dlna.PlayTo
 {
-    public class UpnpContainer : uBaseObject
+    public class UpnpContainer : UBaseObject
     {
-        public static uBaseObject Create(XElement container)
+        public static UBaseObject Create(XElement container)
         {
             if (container == null)
             {
                 throw new ArgumentNullException(nameof(container));
             }
 
-            return new uBaseObject
+            return new UBaseObject
             {
-                Id = container.GetAttributeValue(uPnpNamespaces.Id),
-                ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
-                Title = container.GetValue(uPnpNamespaces.title),
-                IconUrl = container.GetValue(uPnpNamespaces.Artwork),
-                UpnpClass = container.GetValue(uPnpNamespaces.uClass)
+                Id = container.GetAttributeValue(UPnpNamespaces.Id),
+                ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId),
+                Title = container.GetValue(UPnpNamespaces.Title),
+                IconUrl = container.GetValue(UPnpNamespaces.Artwork),
+                UpnpClass = container.GetValue(UPnpNamespaces.Class)
             };
         }
     }

+ 13 - 12
Emby.Dlna/PlayTo/uBaseObject.cs

@@ -1,10 +1,11 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Collections.Generic;
 
 namespace Emby.Dlna.PlayTo
 {
-    public class uBaseObject
+    public class UBaseObject
     {
         public string Id { get; set; }
 
@@ -20,20 +21,10 @@ namespace Emby.Dlna.PlayTo
 
         public string Url { get; set; }
 
-        public string[] ProtocolInfo { get; set; }
+        public IReadOnlyList<string> ProtocolInfo { get; set; }
 
         public string UpnpClass { get; set; }
 
-        public bool Equals(uBaseObject obj)
-        {
-            if (obj == null)
-            {
-                throw new ArgumentNullException(nameof(obj));
-            }
-
-            return string.Equals(Id, obj.Id);
-        }
-
         public string MediaType
         {
             get
@@ -58,5 +49,15 @@ namespace Emby.Dlna.PlayTo
                 return null;
             }
         }
+
+        public bool Equals(UBaseObject obj)
+        {
+            if (obj == null)
+            {
+                throw new ArgumentNullException(nameof(obj));
+            }
+
+            return string.Equals(Id, obj.Id, StringComparison.Ordinal);
+        }
     }
 }

+ 58 - 32
Emby.Dlna/PlayTo/uPnpNamespaces.cs

@@ -4,38 +4,64 @@ using System.Xml.Linq;
 
 namespace Emby.Dlna.PlayTo
 {
-    public class uPnpNamespaces
+    public static class UPnpNamespaces
     {
-        public static XNamespace dc = "http://purl.org/dc/elements/1.1/";
-        public static XNamespace ns = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
-        public static XNamespace svc = "urn:schemas-upnp-org:service-1-0";
-        public static XNamespace ud = "urn:schemas-upnp-org:device-1-0";
-        public static XNamespace upnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
-        public static XNamespace RenderingControl = "urn:schemas-upnp-org:service:RenderingControl:1";
-        public static XNamespace AvTransport = "urn:schemas-upnp-org:service:AVTransport:1";
-        public static XNamespace ContentDirectory = "urn:schemas-upnp-org:service:ContentDirectory:1";
-
-        public static XName containers = ns + "container";
-        public static XName items = ns + "item";
-        public static XName title = dc + "title";
-        public static XName creator = dc + "creator";
-        public static XName artist = upnp + "artist";
-        public static XName Id = "id";
-        public static XName ParentId = "parentID";
-        public static XName uClass = upnp + "class";
-        public static XName Artwork = upnp + "albumArtURI";
-        public static XName Description = dc + "description";
-        public static XName LongDescription = upnp + "longDescription";
-        public static XName Album = upnp + "album";
-        public static XName Author = upnp + "author";
-        public static XName Director = upnp + "director";
-        public static XName PlayCount = upnp + "playbackCount";
-        public static XName Tracknumber = upnp + "originalTrackNumber";
-        public static XName Res = ns + "res";
-        public static XName Duration = "duration";
-        public static XName ProtocolInfo = "protocolInfo";
-
-        public static XName ServiceStateTable = svc + "serviceStateTable";
-        public static XName StateVariable = svc + "stateVariable";
+        public static XNamespace Dc { get; } = "http://purl.org/dc/elements/1.1/";
+
+        public static XNamespace Ns { get; } = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
+
+        public static XNamespace Svc { get; } = "urn:schemas-upnp-org:service-1-0";
+
+        public static XNamespace Ud { get; } = "urn:schemas-upnp-org:device-1-0";
+
+        public static XNamespace UPnp { get; } = "urn:schemas-upnp-org:metadata-1-0/upnp/";
+
+        public static XNamespace RenderingControl { get; } = "urn:schemas-upnp-org:service:RenderingControl:1";
+
+        public static XNamespace AvTransport { get; } = "urn:schemas-upnp-org:service:AVTransport:1";
+
+        public static XNamespace ContentDirectory { get; } = "urn:schemas-upnp-org:service:ContentDirectory:1";
+
+        public static XName Containers { get; } = Ns + "container";
+
+        public static XName Items { get; } = Ns + "item";
+
+        public static XName Title { get; } = Dc + "title";
+
+        public static XName Creator { get; } = Dc + "creator";
+
+        public static XName Artist { get; } = UPnp + "artist";
+
+        public static XName Id { get; } = "id";
+
+        public static XName ParentId { get; } = "parentID";
+
+        public static XName Class { get; } = UPnp + "class";
+
+        public static XName Artwork { get; } = UPnp + "albumArtURI";
+
+        public static XName Description { get; } = Dc + "description";
+
+        public static XName LongDescription { get; } = UPnp + "longDescription";
+
+        public static XName Album { get; } = UPnp + "album";
+
+        public static XName Author { get; } = UPnp + "author";
+
+        public static XName Director { get; } = UPnp + "director";
+
+        public static XName PlayCount { get; } = UPnp + "playbackCount";
+
+        public static XName Tracknumber { get; } = UPnp + "originalTrackNumber";
+
+        public static XName Res { get; } = Ns + "res";
+
+        public static XName Duration { get; } = "duration";
+
+        public static XName ProtocolInfo { get; } = "protocolInfo";
+
+        public static XName ServiceStateTable { get; } = Svc + "serviceStateTable";
+
+        public static XName StateVariable { get; } = Svc + "stateVariable";
     }
 }

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

@@ -64,14 +64,14 @@ namespace Emby.Dlna.Profiles
                 new DirectPlayProfile
                 {
                     // play all
-                    Container = "",
+                    Container = string.Empty,
                     Type = DlnaProfileType.Video
                 },
 
                 new DirectPlayProfile
                 {
                     // play all
-                    Container = "",
+                    Container = string.Empty,
                     Type = DlnaProfileType.Audio
                 }
             };

+ 6 - 6
Emby.Dlna/Profiles/DishHopperJoeyProfile.cs

@@ -24,7 +24,7 @@ namespace Emby.Dlna.Profiles
                     {
                          Match = HeaderMatchType.Substring,
                          Name = "User-Agent",
-                         Value ="Zip_"
+                         Value = "Zip_"
                     }
                 }
             };
@@ -81,7 +81,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -124,7 +124,7 @@ namespace Emby.Dlna.Profiles
                 new CodecProfile
                 {
                     Type = CodecType.Video,
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -161,7 +161,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "ac3,he-aac",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -177,7 +177,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -192,7 +192,7 @@ namespace Emby.Dlna.Profiles
                 new CodecProfile
                 {
                     Type = CodecType.VideoAudio,
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         // The device does not have any audio switching capabilities
                         new ProfileCondition

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

@@ -84,7 +84,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -191,7 +191,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[]
+            ResponseProfiles = new[]
             {
                 new ResponseProfile
                 {

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

@@ -32,7 +32,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[]
+            ResponseProfiles = new[]
             {
                 new ResponseProfile
                 {

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

@@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 7 - 7
Emby.Dlna/Profiles/PopcornHourProfile.cs

@@ -93,8 +93,8 @@ namespace Emby.Dlna.Profiles
                 new CodecProfile
                 {
                     Type = CodecType.Video,
-                    Codec="h264",
-                    Conditions = new []
+                    Codec = "h264",
+                    Conditions = new[]
                     {
                         new ProfileCondition(ProfileConditionType.EqualsAny, ProfileConditionValue.VideoProfile, "baseline|constrained baseline"),
                         new ProfileCondition
@@ -122,7 +122,7 @@ namespace Emby.Dlna.Profiles
                 new CodecProfile
                 {
                     Type = CodecType.Video,
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -150,7 +150,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -166,7 +166,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Audio,
                     Codec = "aac",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -182,7 +182,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Audio,
                     Codec = "mp3",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -202,7 +202,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[]
+            ResponseProfiles = new[]
             {
                 new ResponseProfile
                 {

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

@@ -139,7 +139,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 3 - 3
Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs

@@ -150,7 +150,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -178,7 +178,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -197,7 +197,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 3 - 3
Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs

@@ -150,7 +150,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -178,7 +178,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -197,7 +197,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 3 - 3
Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs

@@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -166,7 +166,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -185,7 +185,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 3 - 3
Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs

@@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -166,7 +166,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -185,7 +185,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 5 - 5
Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs

@@ -114,7 +114,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -156,7 +156,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -172,7 +172,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -191,7 +191,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -217,7 +217,7 @@ namespace Emby.Dlna.Profiles
                     VideoCodec = "h264,mpeg4,vc1",
                     AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
+                    OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
                     Type = DlnaProfileType.Video
                 },
 

+ 22 - 22
Emby.Dlna/Profiles/SonyBravia2010Profile.cs

@@ -102,13 +102,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
+                    OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -128,13 +128,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/mpeg",
-                    OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
+                    OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -148,28 +148,28 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
+                    OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="mpeg2video",
+                    VideoCodec = "mpeg2video",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
+                    OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "mpeg",
-                    VideoCodec="mpeg1video,mpeg2video",
+                    VideoCodec = "mpeg1video,mpeg2video",
                     MimeType = "video/mpeg",
-                    OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
+                    OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
                     Type = DlnaProfileType.Video
                 }
             };
@@ -180,7 +180,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -204,7 +204,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -243,7 +243,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "mpeg2video",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -275,7 +275,7 @@ namespace Emby.Dlna.Profiles
                 new CodecProfile
                 {
                     Type = CodecType.Video,
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -303,7 +303,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -319,7 +319,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -341,7 +341,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "mp3,mp2",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 21 - 21
Emby.Dlna/Profiles/SonyBravia2011Profile.cs

@@ -120,7 +120,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -143,13 +143,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
+                    OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -169,13 +169,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/mpeg",
-                    OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
+                    OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -189,28 +189,28 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
+                    OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="mpeg2video",
+                    VideoCodec = "mpeg2video",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
+                    OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "mpeg",
-                    VideoCodec="mpeg1video,mpeg2video",
+                    VideoCodec = "mpeg1video,mpeg2video",
                     MimeType = "video/mpeg",
-                    OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
+                    OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
                     Type = DlnaProfileType.Video
                 },
                 new ResponseProfile
@@ -227,7 +227,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -266,7 +266,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "mpeg2video",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -298,7 +298,7 @@ namespace Emby.Dlna.Profiles
                 new CodecProfile
                 {
                     Type = CodecType.Video,
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -326,7 +326,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -364,7 +364,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "mp3,mp2",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 17 - 17
Emby.Dlna/Profiles/SonyBravia2012Profile.cs

@@ -131,13 +131,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
+                    OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -157,13 +157,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/mpeg",
-                    OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
+                    OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -177,28 +177,28 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
+                    OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="mpeg2video",
+                    VideoCodec = "mpeg2video",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
+                    OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "mpeg",
-                    VideoCodec="mpeg1video,mpeg2video",
+                    VideoCodec = "mpeg1video,mpeg2video",
                     MimeType = "video/mpeg",
-                    OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
+                    OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
                     Type = DlnaProfileType.Video
                 },
                 new ResponseProfile
@@ -215,7 +215,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -282,7 +282,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "mp3,mp2",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 18 - 19
Emby.Dlna/Profiles/SonyBravia2013Profile.cs

@@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -187,13 +187,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
+                    OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -213,13 +213,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/mpeg",
-                    OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
+                    OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -233,28 +233,28 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
+                    OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="mpeg2video",
+                    VideoCodec = "mpeg2video",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
+                    OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "mpeg",
-                    VideoCodec="mpeg1video,mpeg2video",
+                    VideoCodec = "mpeg1video,mpeg2video",
                     MimeType = "video/mpeg",
-                    OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
+                    OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
                     Type = DlnaProfileType.Video
                 },
                 new ResponseProfile
@@ -265,14 +265,13 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-
             CodecProfiles = new[]
             {
                 new CodecProfile
                 {
                     Type = CodecType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -300,7 +299,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "mp3,mp2",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 18 - 19
Emby.Dlna/Profiles/SonyBravia2014Profile.cs

@@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -187,13 +187,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
+                    OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -213,13 +213,13 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/mpeg",
-                    OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
+                    OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
                     Type = DlnaProfileType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -233,28 +233,28 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="h264",
-                    AudioCodec="ac3,aac,mp3",
+                    VideoCodec = "h264",
+                    AudioCodec = "ac3,aac,mp3",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
+                    OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "ts,mpegts",
-                    VideoCodec="mpeg2video",
+                    VideoCodec = "mpeg2video",
                     MimeType = "video/vnd.dlna.mpeg-tts",
-                    OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
+                    OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
                     Type = DlnaProfileType.Video
                 },
 
                 new ResponseProfile
                 {
                     Container = "mpeg",
-                    VideoCodec="mpeg1video,mpeg2video",
+                    VideoCodec = "mpeg1video,mpeg2video",
                     MimeType = "video/mpeg",
-                    OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
+                    OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
                     Type = DlnaProfileType.Video
                 },
                 new ResponseProfile
@@ -265,14 +265,13 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-
             CodecProfiles = new[]
             {
                 new CodecProfile
                 {
                     Type = CodecType.Video,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -300,7 +299,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "mp3,mp2",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 7 - 7
Emby.Dlna/Profiles/SonyPs3Profile.cs

@@ -108,7 +108,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -133,7 +133,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.Video,
                     Codec = "h264",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -176,7 +176,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -201,7 +201,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "wmapro",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -217,7 +217,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -235,7 +235,7 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "mp4,mov",
-                    AudioCodec="aac",
+                    AudioCodec = "aac",
                     MimeType = "video/mp4",
                     Type = DlnaProfileType.Video
                 },
@@ -244,7 +244,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Container = "avi",
                     MimeType = "video/divx",
-                    OrgPn="AVI",
+                    OrgPn = "AVI",
                     Type = DlnaProfileType.Video
                 },
 

+ 7 - 7
Emby.Dlna/Profiles/SonyPs4Profile.cs

@@ -110,7 +110,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -135,7 +135,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.Video,
                     Codec = "h264",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -178,7 +178,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "ac3",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -203,7 +203,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "wmapro",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -219,7 +219,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -237,7 +237,7 @@ namespace Emby.Dlna.Profiles
                 new ResponseProfile
                 {
                     Container = "mp4,mov",
-                    AudioCodec="aac",
+                    AudioCodec = "aac",
                     MimeType = "video/mp4",
                     Type = DlnaProfileType.Video
                 },
@@ -246,7 +246,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Container = "avi",
                     MimeType = "video/divx",
-                    OrgPn="AVI",
+                    OrgPn = "AVI",
                     Type = DlnaProfileType.Video
                 },
 

+ 4 - 4
Emby.Dlna/Profiles/WdtvLiveProfile.cs

@@ -20,7 +20,7 @@ namespace Emby.Dlna.Profiles
 
                 Headers = new[]
                 {
-                    new HttpHeaderInfo {Name = "User-Agent", Value = "alphanetworks", Match = HeaderMatchType.Substring},
+                    new HttpHeaderInfo { Name = "User-Agent", Value = "alphanetworks", Match = HeaderMatchType.Substring },
                     new HttpHeaderInfo
                     {
                         Name = "User-Agent",
@@ -168,7 +168,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = DlnaProfileType.Photo,
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -193,7 +193,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.Video,
                     Codec = "h264",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -221,7 +221,7 @@ namespace Emby.Dlna.Profiles
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 7 - 7
Emby.Dlna/Profiles/XboxOneProfile.cs

@@ -119,7 +119,7 @@ namespace Emby.Dlna.Profiles
                     Type = DlnaProfileType.Video,
                     Container = "mp4,mov",
 
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "mpeg4",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -187,7 +187,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "h264",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -236,7 +236,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.Video,
                     Codec = "wmv2,wmv3,vc1",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -284,7 +284,7 @@ namespace Emby.Dlna.Profiles
                 new CodecProfile
                 {
                     Type = CodecType.Video,
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -307,7 +307,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "ac3,wmav2,wmapro",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {
@@ -323,7 +323,7 @@ namespace Emby.Dlna.Profiles
                 {
                     Type = CodecType.VideoAudio,
                     Codec = "aac",
-                    Conditions = new []
+                    Conditions = new[]
                     {
                         new ProfileCondition
                         {

+ 17 - 17
Emby.Dlna/Service/BaseControlHandler.cs

@@ -15,11 +15,7 @@ namespace Emby.Dlna.Service
 {
     public abstract class BaseControlHandler
     {
-        private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
-
-        protected IServerConfigurationManager Config { get; }
-
-        protected ILogger Logger { get; }
+        private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
 
         protected BaseControlHandler(IServerConfigurationManager config, ILogger logger)
         {
@@ -27,6 +23,10 @@ namespace Emby.Dlna.Service
             Logger = logger;
         }
 
+        protected IServerConfigurationManager Config { get; }
+
+        protected ILogger Logger { get; }
+
         public async Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
         {
             try
@@ -80,10 +80,10 @@ namespace Emby.Dlna.Service
             {
                 writer.WriteStartDocument(true);
 
-                writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV);
-                writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/");
+                writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
+                writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
 
-                writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV);
+                writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
                 writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI);
 
                 WriteResult(requestInfo.LocalName, requestInfo.Headers, writer);
@@ -210,15 +210,6 @@ namespace Emby.Dlna.Service
             }
         }
 
-        private class ControlRequestInfo
-        {
-            public string LocalName { get; set; }
-
-            public string NamespaceURI { get; set; }
-
-            public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-        }
-
         protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter);
 
         private void LogRequest(ControlRequest request)
@@ -240,5 +231,14 @@ namespace Emby.Dlna.Service
 
             Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml);
         }
+
+        private class ControlRequestInfo
+        {
+            public string LocalName { get; set; }
+
+            public string NamespaceURI { get; set; }
+
+            public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+        }
     }
 }

+ 8 - 6
Emby.Dlna/Service/BaseService.cs

@@ -6,20 +6,22 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.Service
 {
-    public class BaseService : IEventManager
+    public class BaseService : IDlnaEventManager
     {
-        protected IEventManager EventManager;
-        protected IHttpClient HttpClient;
-        protected ILogger Logger;
-
         protected BaseService(ILogger<BaseService> logger, IHttpClient httpClient)
         {
             Logger = logger;
             HttpClient = httpClient;
 
-            EventManager = new EventManager(logger, HttpClient);
+            EventManager = new DlnaEventManager(logger, HttpClient);
         }
 
+        protected IDlnaEventManager EventManager { get; }
+
+        protected IHttpClient HttpClient { get; }
+
+        protected ILogger Logger { get; }
+
         public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)
         {
             return EventManager.CancelEventSubscription(subscriptionId);

+ 5 - 5
Emby.Dlna/Service/ControlErrorHandler.cs

@@ -10,7 +10,7 @@ namespace Emby.Dlna.Service
 {
     public static class ControlErrorHandler
     {
-        private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
+        private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
 
         public static ControlResponse GetResponse(Exception ex)
         {
@@ -26,11 +26,11 @@ namespace Emby.Dlna.Service
             {
                 writer.WriteStartDocument(true);
 
-                writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV);
-                writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/");
+                writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
+                writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
 
-                writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV);
-                writer.WriteStartElement("SOAP-ENV", "Fault", NS_SOAPENV);
+                writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
+                writer.WriteStartElement("SOAP-ENV", "Fault", NsSoapEnv);
 
                 writer.WriteElementString("faultcode", "500");
                 writer.WriteElementString("faultstring", ex.Message);

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

@@ -87,7 +87,7 @@ namespace Emby.Dlna.Service
                     .Append(SecurityElement.Escape(item.DataType ?? string.Empty))
                     .Append("</dataType>");
 
-                if (item.AllowedValues.Length > 0)
+                if (item.AllowedValues.Count > 0)
                 {
                     builder.Append("<allowedValueList>");
                     foreach (var allowedValue in item.AllowedValues)

+ 9 - 10
Emby.Dlna/Ssdp/DeviceDiscovery.cs

@@ -3,9 +3,9 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Events;
 using Rssdp;
 using Rssdp.Infrastructure;
 
@@ -17,9 +17,17 @@ namespace Emby.Dlna.Ssdp
 
         private readonly IServerConfigurationManager _config;
 
+        private SsdpDeviceLocator _deviceLocator;
+        private ISsdpCommunicationsServer _commsServer;
+
         private int _listenerCount;
         private bool _disposed;
 
+        public DeviceDiscovery(IServerConfigurationManager config)
+        {
+            _config = config;
+        }
+
         private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal;
 
         /// <inheritdoc />
@@ -49,15 +57,6 @@ namespace Emby.Dlna.Ssdp
         /// <inheritdoc />
         public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft;
 
-        private SsdpDeviceLocator _deviceLocator;
-
-        private ISsdpCommunicationsServer _commsServer;
-
-        public DeviceDiscovery(IServerConfigurationManager config)
-        {
-            _config = config;
-        }
-
         // Call this method from somewhere in your code to start the search.
         public void Start(ISsdpCommunicationsServer communicationsServer)
         {

+ 1 - 1
Emby.Dlna/Ssdp/Extensions.cs → Emby.Dlna/Ssdp/SsdpExtensions.cs

@@ -5,7 +5,7 @@ using System.Xml.Linq;
 
 namespace Emby.Dlna.Ssdp
 {
-    public static class Extensions
+    public static class SsdpExtensions
     {
         public static string GetValue(this XElement container, XName name)
         {

+ 15 - 1
Emby.Naming/Emby.Naming.csproj

@@ -10,6 +10,15 @@
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <PublishRepositoryUrl>true</PublishRepositoryUrl>
+    <EmbedUntrackedSources>true</EmbedUntrackedSources>
+    <IncludeSymbols>true</IncludeSymbols>
+    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
+  </PropertyGroup>
+
+  <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
+    <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
+    <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
   </PropertyGroup>
 
   <ItemGroup>
@@ -23,10 +32,15 @@
   <PropertyGroup>
     <Authors>Jellyfin Contributors</Authors>
     <PackageId>Jellyfin.Naming</PackageId>
-    <PackageLicenseUrl>https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</PackageLicenseUrl>
+    <VersionPrefix>10.7.0</VersionPrefix>
     <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
+    <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
   </PropertyGroup>
 
+  <ItemGroup>
+    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
+  </ItemGroup>
+
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
     <!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->

+ 1 - 1
Emby.Notifications/NotificationEntryPoint.cs

@@ -4,6 +4,7 @@ using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
@@ -13,7 +14,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Notifications;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Notifications;
 using Microsoft.Extensions.Logging;

+ 0 - 590
Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs

@@ -1,590 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Controller.Subtitles;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Notifications;
-using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Updates;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Activity
-{
-    /// <summary>
-    /// Entry point for the activity logger.
-    /// </summary>
-    public sealed class ActivityLogEntryPoint : IServerEntryPoint
-    {
-        private readonly ILogger<ActivityLogEntryPoint> _logger;
-        private readonly IInstallationManager _installationManager;
-        private readonly ISessionManager _sessionManager;
-        private readonly ITaskManager _taskManager;
-        private readonly IActivityManager _activityManager;
-        private readonly ILocalizationManager _localization;
-        private readonly ISubtitleManager _subManager;
-        private readonly IUserManager _userManager;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
-        /// </summary>
-        /// <param name="logger">The logger.</param>
-        /// <param name="sessionManager">The session manager.</param>
-        /// <param name="taskManager">The task manager.</param>
-        /// <param name="activityManager">The activity manager.</param>
-        /// <param name="localization">The localization manager.</param>
-        /// <param name="installationManager">The installation manager.</param>
-        /// <param name="subManager">The subtitle manager.</param>
-        /// <param name="userManager">The user manager.</param>
-        public ActivityLogEntryPoint(
-            ILogger<ActivityLogEntryPoint> logger,
-            ISessionManager sessionManager,
-            ITaskManager taskManager,
-            IActivityManager activityManager,
-            ILocalizationManager localization,
-            IInstallationManager installationManager,
-            ISubtitleManager subManager,
-            IUserManager userManager)
-        {
-            _logger = logger;
-            _sessionManager = sessionManager;
-            _taskManager = taskManager;
-            _activityManager = activityManager;
-            _localization = localization;
-            _installationManager = installationManager;
-            _subManager = subManager;
-            _userManager = userManager;
-        }
-
-        /// <inheritdoc />
-        public Task RunAsync()
-        {
-            _taskManager.TaskCompleted += OnTaskCompleted;
-
-            _installationManager.PluginInstalled += OnPluginInstalled;
-            _installationManager.PluginUninstalled += OnPluginUninstalled;
-            _installationManager.PluginUpdated += OnPluginUpdated;
-            _installationManager.PackageInstallationFailed += OnPackageInstallationFailed;
-
-            _sessionManager.SessionStarted += OnSessionStarted;
-            _sessionManager.AuthenticationFailed += OnAuthenticationFailed;
-            _sessionManager.AuthenticationSucceeded += OnAuthenticationSucceeded;
-            _sessionManager.SessionEnded += OnSessionEnded;
-            _sessionManager.PlaybackStart += OnPlaybackStart;
-            _sessionManager.PlaybackStopped += OnPlaybackStopped;
-
-            _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure;
-
-            _userManager.OnUserCreated += OnUserCreated;
-            _userManager.OnUserPasswordChanged += OnUserPasswordChanged;
-            _userManager.OnUserDeleted += OnUserDeleted;
-            _userManager.OnUserLockedOut += OnUserLockedOut;
-
-            return Task.CompletedTask;
-        }
-
-        private async void OnUserLockedOut(object sender, GenericEventArgs<User> e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                    string.Format(
-                        CultureInfo.InvariantCulture,
-                        _localization.GetLocalizedString("UserLockedOutWithName"),
-                        e.Argument.Username),
-                    NotificationType.UserLockedOut.ToString(),
-                    e.Argument.Id)
-            {
-                LogSeverity = LogLevel.Error
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
-                    e.Provider,
-                    Notifications.NotificationEntryPoint.GetItemName(e.Item)),
-                "SubtitleDownloadFailure",
-                Guid.Empty)
-            {
-                ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
-                ShortOverview = e.Exception.Message
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
-        {
-            var item = e.MediaInfo;
-
-            if (item == null)
-            {
-                _logger.LogWarning("PlaybackStopped reported with null media info.");
-                return;
-            }
-
-            if (e.Item != null && e.Item.IsThemeMedia)
-            {
-                // Don't report theme song or local trailer playback
-                return;
-            }
-
-            if (e.Users.Count == 0)
-            {
-                return;
-            }
-
-            var user = e.Users[0];
-
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
-                    user.Username,
-                    GetItemName(item),
-                    e.DeviceName),
-                GetPlaybackStoppedNotificationType(item.MediaType),
-                user.Id))
-                .ConfigureAwait(false);
-        }
-
-        private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
-        {
-            var item = e.MediaInfo;
-
-            if (item == null)
-            {
-                _logger.LogWarning("PlaybackStart reported with null media info.");
-                return;
-            }
-
-            if (e.Item != null && e.Item.IsThemeMedia)
-            {
-                // Don't report theme song or local trailer playback
-                return;
-            }
-
-            if (e.Users.Count == 0)
-            {
-                return;
-            }
-
-            var user = e.Users.First();
-
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
-                    user.Username,
-                    GetItemName(item),
-                    e.DeviceName),
-                GetPlaybackNotificationType(item.MediaType),
-                user.Id))
-                .ConfigureAwait(false);
-        }
-
-        private static string GetItemName(BaseItemDto item)
-        {
-            var name = item.Name;
-
-            if (!string.IsNullOrEmpty(item.SeriesName))
-            {
-                name = item.SeriesName + " - " + name;
-            }
-
-            if (item.Artists != null && item.Artists.Count > 0)
-            {
-                name = item.Artists[0] + " - " + name;
-            }
-
-            return name;
-        }
-
-        private static string GetPlaybackNotificationType(string mediaType)
-        {
-            if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
-            {
-                return NotificationType.AudioPlayback.ToString();
-            }
-
-            if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
-            {
-                return NotificationType.VideoPlayback.ToString();
-            }
-
-            return null;
-        }
-
-        private static string GetPlaybackStoppedNotificationType(string mediaType)
-        {
-            if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
-            {
-                return NotificationType.AudioPlaybackStopped.ToString();
-            }
-
-            if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
-            {
-                return NotificationType.VideoPlaybackStopped.ToString();
-            }
-
-            return null;
-        }
-
-        private async void OnSessionEnded(object sender, SessionEventArgs e)
-        {
-            var session = e.SessionInfo;
-
-            if (string.IsNullOrEmpty(session.UserName))
-            {
-                return;
-            }
-
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserOfflineFromDevice"),
-                    session.UserName,
-                    session.DeviceName),
-                "SessionEnded",
-                session.UserId)
-            {
-                ShortOverview = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("LabelIpAddressValue"),
-                    session.RemoteEndPoint),
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
-        {
-            var user = e.Argument.User;
-
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
-                    user.Name),
-                "AuthenticationSucceeded",
-                user.Id)
-            {
-                ShortOverview = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("LabelIpAddressValue"),
-                    e.Argument.SessionInfo.RemoteEndPoint),
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
-                    e.Argument.Username),
-                "AuthenticationFailed",
-                Guid.Empty)
-            {
-                LogSeverity = LogLevel.Error,
-                ShortOverview = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("LabelIpAddressValue"),
-                    e.Argument.RemoteEndPoint),
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserDeletedWithName"),
-                    e.Argument.Username),
-                "UserDeleted",
-                Guid.Empty))
-                .ConfigureAwait(false);
-        }
-
-        private async void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserPasswordChangedWithName"),
-                    e.Argument.Username),
-                "UserPasswordChanged",
-                e.Argument.Id))
-                .ConfigureAwait(false);
-        }
-
-        private async void OnUserCreated(object sender, GenericEventArgs<User> e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserCreatedWithName"),
-                    e.Argument.Username),
-                "UserCreated",
-                e.Argument.Id))
-                .ConfigureAwait(false);
-        }
-
-        private async void OnSessionStarted(object sender, SessionEventArgs e)
-        {
-            var session = e.SessionInfo;
-
-            if (string.IsNullOrEmpty(session.UserName))
-            {
-                return;
-            }
-
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserOnlineFromDevice"),
-                    session.UserName,
-                    session.DeviceName),
-                "SessionStarted",
-                session.UserId)
-            {
-                ShortOverview = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("LabelIpAddressValue"),
-                    session.RemoteEndPoint)
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnPluginUpdated(object sender, InstallationInfo e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("PluginUpdatedWithName"),
-                    e.Name),
-                NotificationType.PluginUpdateInstalled.ToString(),
-                Guid.Empty)
-            {
-                ShortOverview = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("VersionNumber"),
-                    e.Version),
-                Overview = e.Changelog
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnPluginUninstalled(object sender, IPlugin e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("PluginUninstalledWithName"),
-                    e.Name),
-                NotificationType.PluginUninstalled.ToString(),
-                Guid.Empty))
-                .ConfigureAwait(false);
-        }
-
-        private async void OnPluginInstalled(object sender, InstallationInfo e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("PluginInstalledWithName"),
-                    e.Name),
-                NotificationType.PluginInstalled.ToString(),
-                Guid.Empty)
-            {
-                ShortOverview = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("VersionNumber"),
-                    e.Version)
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
-        {
-            var installationInfo = e.InstallationInfo;
-
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("NameInstallFailed"),
-                    installationInfo.Name),
-                NotificationType.InstallationFailed.ToString(),
-                Guid.Empty)
-            {
-                ShortOverview = string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("VersionNumber"),
-                    installationInfo.Version),
-                Overview = e.Exception.Message
-            }).ConfigureAwait(false);
-        }
-
-        private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
-        {
-            var result = e.Result;
-            var task = e.Task;
-
-            if (task.ScheduledTask is IConfigurableScheduledTask activityTask
-                && !activityTask.IsLogged)
-            {
-                return;
-            }
-
-            var time = result.EndTimeUtc - result.StartTimeUtc;
-            var runningTime = string.Format(
-                CultureInfo.InvariantCulture,
-                _localization.GetLocalizedString("LabelRunningTimeValue"),
-                ToUserFriendlyString(time));
-
-            if (result.Status == TaskCompletionStatus.Failed)
-            {
-                var vals = new List<string>();
-
-                if (!string.IsNullOrEmpty(e.Result.ErrorMessage))
-                {
-                    vals.Add(e.Result.ErrorMessage);
-                }
-
-                if (!string.IsNullOrEmpty(e.Result.LongErrorMessage))
-                {
-                    vals.Add(e.Result.LongErrorMessage);
-                }
-
-                await CreateLogEntry(new ActivityLog(
-                    string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
-                    NotificationType.TaskFailed.ToString(),
-                    Guid.Empty)
-                {
-                    LogSeverity = LogLevel.Error,
-                    Overview = string.Join(Environment.NewLine, vals),
-                    ShortOverview = runningTime
-                }).ConfigureAwait(false);
-            }
-        }
-
-        private async Task CreateLogEntry(ActivityLog entry)
-            => await _activityManager.CreateAsync(entry).ConfigureAwait(false);
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            _taskManager.TaskCompleted -= OnTaskCompleted;
-
-            _installationManager.PluginInstalled -= OnPluginInstalled;
-            _installationManager.PluginUninstalled -= OnPluginUninstalled;
-            _installationManager.PluginUpdated -= OnPluginUpdated;
-            _installationManager.PackageInstallationFailed -= OnPackageInstallationFailed;
-
-            _sessionManager.SessionStarted -= OnSessionStarted;
-            _sessionManager.AuthenticationFailed -= OnAuthenticationFailed;
-            _sessionManager.AuthenticationSucceeded -= OnAuthenticationSucceeded;
-            _sessionManager.SessionEnded -= OnSessionEnded;
-
-            _sessionManager.PlaybackStart -= OnPlaybackStart;
-            _sessionManager.PlaybackStopped -= OnPlaybackStopped;
-
-            _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure;
-
-            _userManager.OnUserCreated -= OnUserCreated;
-            _userManager.OnUserPasswordChanged -= OnUserPasswordChanged;
-            _userManager.OnUserDeleted -= OnUserDeleted;
-            _userManager.OnUserLockedOut -= OnUserLockedOut;
-        }
-
-        /// <summary>
-        /// Constructs a user-friendly string for this TimeSpan instance.
-        /// </summary>
-        private static string ToUserFriendlyString(TimeSpan span)
-        {
-            const int DaysInYear = 365;
-            const int DaysInMonth = 30;
-
-            // Get each non-zero value from TimeSpan component
-            var values = new List<string>();
-
-            // Number of years
-            int days = span.Days;
-            if (days >= DaysInYear)
-            {
-                int years = days / DaysInYear;
-                values.Add(CreateValueString(years, "year"));
-                days %= DaysInYear;
-            }
-
-            // Number of months
-            if (days >= DaysInMonth)
-            {
-                int months = days / DaysInMonth;
-                values.Add(CreateValueString(months, "month"));
-                days = days % DaysInMonth;
-            }
-
-            // Number of days
-            if (days >= 1)
-            {
-                values.Add(CreateValueString(days, "day"));
-            }
-
-            // Number of hours
-            if (span.Hours >= 1)
-            {
-                values.Add(CreateValueString(span.Hours, "hour"));
-            }
-
-            // Number of minutes
-            if (span.Minutes >= 1)
-            {
-                values.Add(CreateValueString(span.Minutes, "minute"));
-            }
-
-            // Number of seconds (include when 0 if no other components included)
-            if (span.Seconds >= 1 || values.Count == 0)
-            {
-                values.Add(CreateValueString(span.Seconds, "second"));
-            }
-
-            // Combine values into string
-            var builder = new StringBuilder();
-            for (int i = 0; i < values.Count; i++)
-            {
-                if (builder.Length > 0)
-                {
-                    builder.Append(i == values.Count - 1 ? " and " : ", ");
-                }
-
-                builder.Append(values[i]);
-            }
-
-            // Return result
-            return builder.ToString();
-        }
-
-        /// <summary>
-        /// Constructs a string description of a time-span value.
-        /// </summary>
-        /// <param name="value">The value of this item.</param>
-        /// <param name="description">The name of this item (singular form).</param>
-        private static string CreateValueString(int value, string description)
-        {
-            return string.Format(
-                CultureInfo.InvariantCulture,
-                "{0:#,##0} {1}",
-                value,
-                value == 1 ? description : string.Format(CultureInfo.InvariantCulture, "{0}s", description));
-        }
-    }
-}

+ 109 - 88
Emby.Server.Implementations/ApplicationHost.cs

@@ -37,10 +37,10 @@ using Emby.Server.Implementations.LiveTv;
 using Emby.Server.Implementations.Localization;
 using Emby.Server.Implementations.Net;
 using Emby.Server.Implementations.Playlists;
+using Emby.Server.Implementations.QuickConnect;
 using Emby.Server.Implementations.ScheduledTasks;
 using Emby.Server.Implementations.Security;
 using Emby.Server.Implementations.Serialization;
-using Emby.Server.Implementations.Services;
 using Emby.Server.Implementations.Session;
 using Emby.Server.Implementations.SyncPlay;
 using Emby.Server.Implementations.TV;
@@ -53,7 +53,6 @@ using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Collections;
@@ -72,6 +71,7 @@ using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.QuickConnect;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
@@ -89,7 +89,6 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Providers.Chapters;
@@ -97,11 +96,12 @@ using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Plugins.TheTvdb;
 using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.XbmcMetadata.Providers;
-using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Prometheus.DotNetRuntime;
 using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
+using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
 
 namespace Emby.Server.Implementations
 {
@@ -122,14 +122,18 @@ namespace Emby.Server.Implementations
 
         private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
-        private IHttpServer _httpServer;
+        private IWebSocketManager _webSocketManager;
         private IHttpClient _httpClient;
 
+        private string[] _urlPrefixes;
+
         /// <summary>
         /// Gets a value indicating whether this instance can self restart.
         /// </summary>
         public bool CanSelfRestart => _startupOptions.RestartPath != null;
 
+        public bool CoreStartupHasCompleted { get; private set; }
+
         public virtual bool CanLaunchWebBrowser
         {
             get
@@ -173,6 +177,8 @@ namespace Emby.Server.Implementations
         /// </summary>
         protected ILogger<ApplicationHost> Logger { get; }
 
+        protected IServiceCollection ServiceCollection { get; }
+
         private IPlugin[] _plugins;
 
         /// <summary>
@@ -238,9 +244,11 @@ namespace Emby.Server.Implementations
             ILoggerFactory loggerFactory,
             IStartupOptions options,
             IFileSystem fileSystem,
-            INetworkManager networkManager)
+            INetworkManager networkManager,
+            IServiceCollection serviceCollection)
         {
             _xmlSerializer = new MyXmlSerializer();
+            ServiceCollection = serviceCollection;
 
             _networkManager = networkManager;
             networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
@@ -440,8 +448,7 @@ namespace Emby.Server.Implementations
             Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
 
             Logger.LogInformation("Core startup complete");
-            _httpServer.GlobalResponse = null;
-
+            CoreStartupHasCompleted = true;
             stopWatch.Restart();
             await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
             Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
@@ -464,7 +471,7 @@ namespace Emby.Server.Implementations
         }
 
         /// <inheritdoc/>
-        public void Init(IServiceCollection serviceCollection)
+        public void Init()
         {
             HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber;
             HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber;
@@ -493,145 +500,143 @@ namespace Emby.Server.Implementations
 
             DiscoverTypes();
 
-            RegisterServices(serviceCollection);
+            RegisterServices();
         }
 
-        public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
-            => _httpServer.RequestHandler(context);
-
         /// <summary>
         /// Registers services/resources with the service collection that will be available via DI.
         /// </summary>
-        protected virtual void RegisterServices(IServiceCollection serviceCollection)
+        protected virtual void RegisterServices()
         {
-            serviceCollection.AddSingleton(_startupOptions);
-
-            serviceCollection.AddMemoryCache();
+            ServiceCollection.AddSingleton(_startupOptions);
 
-            serviceCollection.AddSingleton(ConfigurationManager);
-            serviceCollection.AddSingleton<IApplicationHost>(this);
+            ServiceCollection.AddMemoryCache();
 
-            serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
+            ServiceCollection.AddSingleton(ConfigurationManager);
+            ServiceCollection.AddSingleton<IApplicationHost>(this);
 
-            serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
+            ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
 
-            serviceCollection.AddSingleton(_fileSystemManager);
-            serviceCollection.AddSingleton<TvdbClientManager>();
+            ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
 
-            serviceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
+            ServiceCollection.AddSingleton(_fileSystemManager);
+            ServiceCollection.AddSingleton<TvdbClientManager>();
 
-            serviceCollection.AddSingleton(_networkManager);
+            ServiceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
 
-            serviceCollection.AddSingleton<IIsoManager, IsoManager>();
+            ServiceCollection.AddSingleton(_networkManager);
 
-            serviceCollection.AddSingleton<ITaskManager, TaskManager>();
+            ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
 
-            serviceCollection.AddSingleton(_xmlSerializer);
+            ServiceCollection.AddSingleton<ITaskManager, TaskManager>();
 
-            serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
+            ServiceCollection.AddSingleton(_xmlSerializer);
 
-            serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
+            ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>();
 
-            serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
+            ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
 
-            serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
+            ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>();
 
-            serviceCollection.AddSingleton<IZipClient, ZipClient>();
+            ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>();
 
-            serviceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>();
+            ServiceCollection.AddSingleton<IZipClient, ZipClient>();
 
-            serviceCollection.AddSingleton<IServerApplicationHost>(this);
-            serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
+            ServiceCollection.AddSingleton<IServerApplicationHost>(this);
+            ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
 
-            serviceCollection.AddSingleton(ServerConfigurationManager);
+            ServiceCollection.AddSingleton(ServerConfigurationManager);
 
-            serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
+            ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
 
-            serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
+            ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
 
-            serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
-            serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
+            ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
+            ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>();
 
-            serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
+            ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
 
-            serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
+            ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
 
             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
-            serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
+            ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
 
             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
-            serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
-            serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
+            ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
+            ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
 
             // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
-            serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
-            serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
-            serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
-            serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
+            ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
+            ServiceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
+            ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
+            ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>();
 
-            serviceCollection.AddSingleton<IMusicManager, MusicManager>();
+            ServiceCollection.AddSingleton<IMusicManager, MusicManager>();
 
-            serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
+            ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
 
-            serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
+            ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
 
-            serviceCollection.AddSingleton<ServiceController>();
-            serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
+            ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
 
-            serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
+            ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
 
-            serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
+            ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
 
-            serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
+            ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
 
-            serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
+            ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
 
-            serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
+            ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
 
-            serviceCollection.AddSingleton<IProviderManager, ProviderManager>();
+            ServiceCollection.AddSingleton<IProviderManager, ProviderManager>();
 
             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
-            serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
-            serviceCollection.AddSingleton<IDtoService, DtoService>();
+            ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
+            ServiceCollection.AddSingleton<IDtoService, DtoService>();
 
-            serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
+            ServiceCollection.AddSingleton<IChannelManager, ChannelManager>();
 
-            serviceCollection.AddSingleton<ISessionManager, SessionManager>();
+            ServiceCollection.AddSingleton<ISessionManager, SessionManager>();
 
-            serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
+            ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>();
 
-            serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
+            ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>();
 
-            serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
+            ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
 
-            serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
+            ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
 
-            serviceCollection.AddSingleton<LiveTvDtoService>();
-            serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
+            ServiceCollection.AddSingleton<LiveTvDtoService>();
+            ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
 
-            serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
+            ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>();
 
-            serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
+            ServiceCollection.AddSingleton<INotificationManager, NotificationManager>();
 
-            serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
+            ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
 
-            serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
+            ServiceCollection.AddSingleton<IChapterManager, ChapterManager>();
 
-            serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
+            ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
 
-            serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
-            serviceCollection.AddSingleton<ISessionContext, SessionContext>();
+            ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
+            ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
 
-            serviceCollection.AddSingleton<IAuthService, AuthService>();
+            ServiceCollection.AddSingleton<IAuthService, AuthService>();
+            ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
 
-            serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
+            ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
 
-            serviceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
-            serviceCollection.AddSingleton<EncodingHelper>();
+            ServiceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
+            ServiceCollection.AddSingleton<EncodingHelper>();
 
-            serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
+            ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
 
-            serviceCollection.AddSingleton<TranscodingJobHelper>();
+            ServiceCollection.AddSingleton<TranscodingJobHelper>();
+            ServiceCollection.AddScoped<MediaInfoHelper>();
+            ServiceCollection.AddScoped<AudioHelper>();
+            ServiceCollection.AddScoped<DynamicHlsHelper>();
         }
 
         /// <summary>
@@ -645,7 +650,7 @@ namespace Emby.Server.Implementations
 
             _mediaEncoder = Resolve<IMediaEncoder>();
             _sessionManager = Resolve<ISessionManager>();
-            _httpServer = Resolve<IHttpServer>();
+            _webSocketManager = Resolve<IWebSocketManager>();
             _httpClient = Resolve<IHttpClient>();
 
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
@@ -747,7 +752,6 @@ namespace Emby.Server.Implementations
             CollectionFolder.XmlSerializer = _xmlSerializer;
             CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
             CollectionFolder.ApplicationHost = this;
-            AuthenticatedAttribute.AuthService = Resolve<IAuthService>();
         }
 
         /// <summary>
@@ -767,7 +771,8 @@ namespace Emby.Server.Implementations
                         .Where(i => i != null)
                         .ToArray();
 
-            _httpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes());
+            _urlPrefixes = GetUrlPrefixes().ToArray();
+            _webSocketManager.Init(GetExports<IWebSocketListener>());
 
             Resolve<ILibraryManager>().AddParts(
                 GetExports<IResolverIgnoreRule>(),
@@ -831,6 +836,8 @@ namespace Emby.Server.Implementations
                 {
                     hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
                 }
+
+                plugin.RegisterServices(ServiceCollection);
             }
             catch (Exception ex)
             {
@@ -931,7 +938,7 @@ namespace Emby.Server.Implementations
                 }
             }
 
-            if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
+            if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
             {
                 requiresRestart = true;
             }
@@ -1385,6 +1392,20 @@ namespace Emby.Server.Implementations
             _plugins = list.ToArray();
         }
 
+        public IEnumerable<Assembly> GetApiPluginAssemblies()
+        {
+            var assemblies = _allConcreteTypes
+                .Where(i => typeof(ControllerBase).IsAssignableFrom(i))
+                .Select(i => i.Assembly)
+                .Distinct();
+
+            foreach (var assembly in assemblies)
+            {
+                Logger.LogDebug("Found API endpoints in plugin {name}", assembly.FullName);
+                yield return assembly;
+            }
+        }
+
         public virtual void LaunchUrl(string url)
         {
             if (!CanLaunchWebBrowser)

+ 15 - 6
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -746,12 +746,21 @@ namespace Emby.Server.Implementations.Channels
             // null if came from cache
             if (itemsResult != null)
             {
-                var internalItems = itemsResult.Items
-                    .Select(i => GetChannelItemEntity(i, channelProvider, channel.Id, parentItem, cancellationToken))
-                    .ToArray();
+                var items = itemsResult.Items;
+                var itemsLen = items.Count;
+                var internalItems = new Guid[itemsLen];
+                for (int i = 0; i < itemsLen; i++)
+                {
+                    internalItems[i] = (await GetChannelItemEntityAsync(
+                        items[i],
+                        channelProvider,
+                        channel.Id,
+                        parentItem,
+                        cancellationToken).ConfigureAwait(false)).Id;
+                }
 
                 var existingIds = _libraryManager.GetItemIds(query);
-                var deadIds = existingIds.Except(internalItems.Select(i => i.Id))
+                var deadIds = existingIds.Except(internalItems)
                     .ToArray();
 
                 foreach (var deadId in deadIds)
@@ -963,7 +972,7 @@ namespace Emby.Server.Implementations.Channels
             return item;
         }
 
-        private BaseItem GetChannelItemEntity(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken)
+        private async Task<BaseItem> GetChannelItemEntityAsync(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken)
         {
             var parentFolderId = parentFolder.Id;
 
@@ -1165,7 +1174,7 @@ namespace Emby.Server.Implementations.Channels
             }
             else if (forceUpdate)
             {
-                item.UpdateToRepository(ItemUpdateType.None, cancellationToken);
+                await item.UpdateToRepositoryAsync(ItemUpdateType.None, cancellationToken).ConfigureAwait(false);
             }
 
             if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media)

+ 20 - 31
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.Collections
         }
 
         /// <inheritdoc />
-        public BoxSet CreateCollection(CollectionCreationOptions options)
+        public async Task<BoxSet> CreateCollectionAsync(CollectionCreationOptions options)
         {
             var name = options.Name;
 
@@ -141,7 +141,7 @@ namespace Emby.Server.Implementations.Collections
             // This could cause it to get re-resolved as a plain folder
             var folderName = _fileSystem.GetValidFilename(name) + " [boxset]";
 
-            var parentFolder = GetCollectionsFolder(true).GetAwaiter().GetResult();
+            var parentFolder = await GetCollectionsFolder(true).ConfigureAwait(false);
 
             if (parentFolder == null)
             {
@@ -169,12 +169,16 @@ namespace Emby.Server.Implementations.Collections
 
                 if (options.ItemIdList.Length > 0)
                 {
-                    AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(new DirectoryService(_fileSystem))
-                    {
-                        // The initial adding of items is going to create a local metadata file
-                        // This will cause internet metadata to be skipped as a result
-                        MetadataRefreshMode = MetadataRefreshMode.FullRefresh
-                    });
+                    await AddToCollectionAsync(
+                        collection.Id,
+                        options.ItemIdList.Select(x => new Guid(x)),
+                        false,
+                        new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+                        {
+                            // The initial adding of items is going to create a local metadata file
+                            // This will cause internet metadata to be skipped as a result
+                            MetadataRefreshMode = MetadataRefreshMode.FullRefresh
+                        }).ConfigureAwait(false);
                 }
                 else
                 {
@@ -197,18 +201,10 @@ namespace Emby.Server.Implementations.Collections
         }
 
         /// <inheritdoc />
-        public void AddToCollection(Guid collectionId, IEnumerable<string> ids)
-        {
-            AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
-        }
+        public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids)
+            => AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
 
-        /// <inheritdoc />
-        public void AddToCollection(Guid collectionId, IEnumerable<Guid> ids)
-        {
-            AddToCollection(collectionId, ids.Select(i => i.ToString("N", CultureInfo.InvariantCulture)), true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
-        }
-
-        private void AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
+        private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
         {
             var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
             if (collection == null)
@@ -224,15 +220,14 @@ namespace Emby.Server.Implementations.Collections
 
             foreach (var id in ids)
             {
-                var guidId = new Guid(id);
-                var item = _libraryManager.GetItemById(guidId);
+                var item = _libraryManager.GetItemById(id);
 
                 if (item == null)
                 {
                     throw new ArgumentException("No item exists with the supplied Id");
                 }
 
-                if (!currentLinkedChildrenIds.Contains(guidId))
+                if (!currentLinkedChildrenIds.Contains(id))
                 {
                     itemList.Add(item);
 
@@ -249,7 +244,7 @@ namespace Emby.Server.Implementations.Collections
 
                 collection.UpdateRatingToItems(linkedChildrenList);
 
-                collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+                await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
 
                 refreshOptions.ForceSave = true;
                 _providerManager.QueueRefresh(collection.Id, refreshOptions, RefreshPriority.High);
@@ -266,13 +261,7 @@ namespace Emby.Server.Implementations.Collections
         }
 
         /// <inheritdoc />
-        public void RemoveFromCollection(Guid collectionId, IEnumerable<string> itemIds)
-        {
-            RemoveFromCollection(collectionId, itemIds.Select(i => new Guid(i)));
-        }
-
-        /// <inheritdoc />
-        public void RemoveFromCollection(Guid collectionId, IEnumerable<Guid> itemIds)
+        public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
         {
             var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
 
@@ -309,7 +298,7 @@ namespace Emby.Server.Implementations.Collections
                 collection.LinkedChildren = collection.LinkedChildren.Except(list).ToArray();
             }
 
-            collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+            await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
             _providerManager.QueueRefresh(
                 collection.Id,
                 new MetadataRefreshOptions(new DirectoryService(_fileSystem))

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

@@ -2,11 +2,11 @@ using System;
 using System.Globalization;
 using System.IO;
 using Emby.Server.Implementations.AppBase;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;

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

@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations
         public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string>
         {
             { HostWebClientKey, bool.TrueString },
-            { HttpListenerHost.DefaultRedirectKey, "web/index.html" },
+            { DefaultRedirectKey, "web/index.html" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegAnalyzeDurationKey, "200M" },
             { PlaylistsAllowDuplicatesKey, bool.TrueString },

+ 7 - 4
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -4308,7 +4308,7 @@ namespace Emby.Server.Implementations.Data
                 whereClauses.Add("ProductionYear=@Years");
                 if (statement != null)
                 {
-                    statement.TryBind("@Years", query.Years[0].ToString());
+                    statement.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture));
                 }
             }
             else if (query.Years.Length > 1)
@@ -4560,13 +4560,13 @@ namespace Emby.Server.Implementations.Data
             if (query.AncestorIds.Length > 1)
             {
                 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))
             {
                 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)
                 {
                     statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
@@ -5170,7 +5170,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                     insertText.Append(',');
                 }
 
-                insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
+                insertText.AppendFormat(
+                    CultureInfo.InvariantCulture,
+                    "(@ItemId, @AncestorId{0}, @AncestorIdText{0})",
+                    i.ToString(CultureInfo.InvariantCulture));
             }
 
             using (var statement = PrepareStatement(db, insertText.ToString()))

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

@@ -7,13 +7,13 @@ using System.IO;
 using System.Linq;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Session;

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

@@ -22,7 +22,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="IPNetwork2" Version="2.5.211" />
+    <PackageReference Include="IPNetwork2" Version="2.5.224" />
     <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
@@ -37,11 +37,11 @@
     <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.4.0" />
     <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="DotNet.Glob" Version="3.0.9" />
+    <PackageReference Include="DotNet.Glob" Version="3.1.0" />
   </ItemGroup>
 
   <ItemGroup>

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

@@ -7,11 +7,11 @@ using System.Net;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Events;
 using Microsoft.Extensions.Logging;
 using Mono.Nat;
 

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

@@ -7,6 +7,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -15,7 +16,6 @@ using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints

+ 5 - 4
Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs

@@ -5,6 +5,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Plugins;
@@ -43,22 +44,22 @@ namespace Emby.Server.Implementations.EntryPoints
             return Task.CompletedTask;
         }
 
-        private async void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
         {
             await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false);
         }
 
-        private async void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
         {
             await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false);
         }
 
-        private async void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
         {
             await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false);
         }
 
-        private async void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
         {
             await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false);
         }

+ 0 - 210
Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs

@@ -1,210 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Updates;
-
-namespace Emby.Server.Implementations.EntryPoints
-{
-    /// <summary>
-    /// Class WebSocketEvents.
-    /// </summary>
-    public class ServerEventNotifier : IServerEntryPoint
-    {
-        /// <summary>
-        /// The user manager.
-        /// </summary>
-        private readonly IUserManager _userManager;
-
-        /// <summary>
-        /// The installation manager.
-        /// </summary>
-        private readonly IInstallationManager _installationManager;
-
-        /// <summary>
-        /// The kernel.
-        /// </summary>
-        private readonly IServerApplicationHost _appHost;
-
-        /// <summary>
-        /// The task manager.
-        /// </summary>
-        private readonly ITaskManager _taskManager;
-
-        private readonly ISessionManager _sessionManager;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ServerEventNotifier"/> class.
-        /// </summary>
-        /// <param name="appHost">The application host.</param>
-        /// <param name="userManager">The user manager.</param>
-        /// <param name="installationManager">The installation manager.</param>
-        /// <param name="taskManager">The task manager.</param>
-        /// <param name="sessionManager">The session manager.</param>
-        public ServerEventNotifier(
-            IServerApplicationHost appHost,
-            IUserManager userManager,
-            IInstallationManager installationManager,
-            ITaskManager taskManager,
-            ISessionManager sessionManager)
-        {
-            _userManager = userManager;
-            _installationManager = installationManager;
-            _appHost = appHost;
-            _taskManager = taskManager;
-            _sessionManager = sessionManager;
-        }
-
-        /// <inheritdoc />
-        public Task RunAsync()
-        {
-            _userManager.OnUserDeleted += OnUserDeleted;
-            _userManager.OnUserUpdated += OnUserUpdated;
-
-            _appHost.HasPendingRestartChanged += OnHasPendingRestartChanged;
-
-            _installationManager.PluginUninstalled += OnPluginUninstalled;
-            _installationManager.PackageInstalling += OnPackageInstalling;
-            _installationManager.PackageInstallationCancelled += OnPackageInstallationCancelled;
-            _installationManager.PackageInstallationCompleted += OnPackageInstallationCompleted;
-            _installationManager.PackageInstallationFailed += OnPackageInstallationFailed;
-
-            _taskManager.TaskCompleted += OnTaskCompleted;
-
-            return Task.CompletedTask;
-        }
-
-        private async void OnPackageInstalling(object sender, InstallationInfo e)
-        {
-            await SendMessageToAdminSessions("PackageInstalling", e).ConfigureAwait(false);
-        }
-
-        private async void OnPackageInstallationCancelled(object sender, InstallationInfo e)
-        {
-            await SendMessageToAdminSessions("PackageInstallationCancelled", e).ConfigureAwait(false);
-        }
-
-        private async void OnPackageInstallationCompleted(object sender, InstallationInfo e)
-        {
-            await SendMessageToAdminSessions("PackageInstallationCompleted", e).ConfigureAwait(false);
-        }
-
-        private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
-        {
-            await SendMessageToAdminSessions("PackageInstallationFailed", e.InstallationInfo).ConfigureAwait(false);
-        }
-
-        private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
-        {
-            await SendMessageToAdminSessions("ScheduledTaskEnded", e.Result).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Installations the manager_ plugin uninstalled.
-        /// </summary>
-        /// <param name="sender">The sender.</param>
-        /// <param name="e">The e.</param>
-        private async void OnPluginUninstalled(object sender, IPlugin e)
-        {
-            await SendMessageToAdminSessions("PluginUninstalled", e).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Handles the HasPendingRestartChanged event of the kernel control.
-        /// </summary>
-        /// <param name="sender">The source of the event.</param>
-        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
-        private async void OnHasPendingRestartChanged(object sender, EventArgs e)
-        {
-            await _sessionManager.SendRestartRequiredNotification(CancellationToken.None).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Users the manager_ user updated.
-        /// </summary>
-        /// <param name="sender">The sender.</param>
-        /// <param name="e">The e.</param>
-        private async void OnUserUpdated(object sender, GenericEventArgs<User> e)
-        {
-            var dto = _userManager.GetUserDto(e.Argument);
-
-            await SendMessageToUserSession(e.Argument, "UserUpdated", dto).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Users the manager_ user deleted.
-        /// </summary>
-        /// <param name="sender">The sender.</param>
-        /// <param name="e">The e.</param>
-        private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
-        {
-            await SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)).ConfigureAwait(false);
-        }
-
-        private async Task SendMessageToAdminSessions<T>(string name, T data)
-        {
-            try
-            {
-                await _sessionManager.SendMessageToAdminSessions(name, data, CancellationToken.None).ConfigureAwait(false);
-            }
-            catch (Exception)
-            {
-            }
-        }
-
-        private async Task SendMessageToUserSession<T>(User user, string name, T data)
-        {
-            try
-            {
-                await _sessionManager.SendMessageToUserSessions(
-                    new List<Guid> { user.Id },
-                    name,
-                    data,
-                    CancellationToken.None).ConfigureAwait(false);
-            }
-            catch (Exception)
-            {
-            }
-        }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources.
-        /// </summary>
-        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
-        protected virtual void Dispose(bool dispose)
-        {
-            if (dispose)
-            {
-                _userManager.OnUserDeleted -= OnUserDeleted;
-                _userManager.OnUserUpdated -= OnUserUpdated;
-
-                _installationManager.PluginUninstalled -= OnPluginUninstalled;
-                _installationManager.PackageInstalling -= OnPackageInstalling;
-                _installationManager.PackageInstallationCancelled -= OnPackageInstallationCancelled;
-                _installationManager.PackageInstallationCompleted -= OnPackageInstallationCompleted;
-                _installationManager.PackageInstallationFailed -= OnPackageInstallationFailed;
-
-                _appHost.HasPendingRestartChanged -= OnHasPendingRestartChanged;
-
-                _taskManager.TaskCompleted -= OnTaskCompleted;
-            }
-        }
-    }
-}

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

@@ -1,250 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Runtime.InteropServices;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    public class FileWriter : IHttpResult
-    {
-        private static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
-
-        private static readonly string[] _skipLogExtensions = {
-            ".js",
-            ".html",
-            ".css"
-        };
-
-        private readonly IStreamHelper _streamHelper;
-        private readonly ILogger _logger;
-
-        /// <summary>
-        /// The _options.
-        /// </summary>
-        private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
-
-        /// <summary>
-        /// The _requested ranges.
-        /// </summary>
-        private List<KeyValuePair<long, long?>> _requestedRanges;
-
-        public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem, IStreamHelper streamHelper)
-        {
-            if (string.IsNullOrEmpty(contentType))
-            {
-                throw new ArgumentNullException(nameof(contentType));
-            }
-
-            _streamHelper = streamHelper;
-
-            Path = path;
-            _logger = logger;
-            RangeHeader = rangeHeader;
-
-            Headers[HeaderNames.ContentType] = contentType;
-
-            TotalContentLength = fileSystem.GetFileInfo(path).Length;
-            Headers[HeaderNames.AcceptRanges] = "bytes";
-
-            if (string.IsNullOrWhiteSpace(rangeHeader))
-            {
-                Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture);
-                StatusCode = HttpStatusCode.OK;
-            }
-            else
-            {
-                StatusCode = HttpStatusCode.PartialContent;
-                SetRangeValues();
-            }
-
-            FileShare = FileShare.Read;
-            Cookies = new List<Cookie>();
-        }
-
-        private string RangeHeader { get; set; }
-
-        private bool IsHeadRequest { get; set; }
-
-        private long RangeStart { get; set; }
-
-        private long RangeEnd { get; set; }
-
-        private long RangeLength { get; set; }
-
-        public long TotalContentLength { get; set; }
-
-        public Action OnComplete { get; set; }
-
-        public Action OnError { get; set; }
-
-        public List<Cookie> Cookies { get; private set; }
-
-        public FileShare FileShare { get; set; }
-
-        /// <summary>
-        /// Gets the options.
-        /// </summary>
-        /// <value>The options.</value>
-        public IDictionary<string, string> Headers => _options;
-
-        public string Path { get; set; }
-
-        /// <summary>
-        /// Gets the requested ranges.
-        /// </summary>
-        /// <value>The requested ranges.</value>
-        protected List<KeyValuePair<long, long?>> RequestedRanges
-        {
-            get
-            {
-                if (_requestedRanges == null)
-                {
-                    _requestedRanges = new List<KeyValuePair<long, long?>>();
-
-                    // Example: bytes=0-,32-63
-                    var ranges = RangeHeader.Split('=')[1].Split(',');
-
-                    foreach (var range in ranges)
-                    {
-                        var vals = range.Split('-');
-
-                        long start = 0;
-                        long? end = null;
-
-                        if (!string.IsNullOrEmpty(vals[0]))
-                        {
-                            start = long.Parse(vals[0], UsCulture);
-                        }
-
-                        if (!string.IsNullOrEmpty(vals[1]))
-                        {
-                            end = long.Parse(vals[1], UsCulture);
-                        }
-
-                        _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
-                    }
-                }
-
-                return _requestedRanges;
-            }
-        }
-
-        public string ContentType { get; set; }
-
-        public IRequest RequestContext { get; set; }
-
-        public object Response { get; set; }
-
-        public int Status { get; set; }
-
-        public HttpStatusCode StatusCode
-        {
-            get => (HttpStatusCode)Status;
-            set => Status = (int)value;
-        }
-
-        /// <summary>
-        /// Sets the range values.
-        /// </summary>
-        private void SetRangeValues()
-        {
-            var requestedRange = RequestedRanges[0];
-
-            // If the requested range is "0-", we can optimize by just doing a stream copy
-            if (!requestedRange.Value.HasValue)
-            {
-                RangeEnd = TotalContentLength - 1;
-            }
-            else
-            {
-                RangeEnd = requestedRange.Value.Value;
-            }
-
-            RangeStart = requestedRange.Key;
-            RangeLength = 1 + RangeEnd - RangeStart;
-
-            // Content-Length is the length of what we're serving, not the original content
-            var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture);
-            Headers[HeaderNames.ContentLength] = lengthString;
-            var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
-            Headers[HeaderNames.ContentRange] = rangeString;
-
-            _logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString);
-        }
-
-        public async Task WriteToAsync(HttpResponse response, CancellationToken cancellationToken)
-        {
-            try
-            {
-                // Headers only
-                if (IsHeadRequest)
-                {
-                    return;
-                }
-
-                var path = Path;
-                var offset = RangeStart;
-                var count = RangeLength;
-
-                if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1)
-                {
-                    var extension = System.IO.Path.GetExtension(path);
-
-                    if (extension == null || !_skipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
-                    {
-                        _logger.LogDebug("Transmit file {0}", path);
-                    }
-
-                    offset = 0;
-                    count = 0;
-                }
-
-                await TransmitFile(response.Body, path, offset, count, FileShare, cancellationToken).ConfigureAwait(false);
-            }
-            finally
-            {
-                OnComplete?.Invoke();
-            }
-        }
-
-        public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShare fileShare, CancellationToken cancellationToken)
-        {
-            var fileOptions = FileOptions.SequentialScan;
-
-            // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
-            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-            {
-                fileOptions |= FileOptions.Asynchronous;
-            }
-
-            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, fileOptions))
-            {
-                if (offset > 0)
-                {
-                    fs.Position = offset;
-                }
-
-                if (count > 0)
-                {
-                    await _streamHelper.CopyToAsync(fs, stream, count, cancellationToken).ConfigureAwait(false);
-                }
-                else
-                {
-                    await fs.CopyToAsync(stream, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false);
-                }
-            }
-        }
-    }
-}

+ 0 - 766
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -1,766 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Net.Sockets;
-using System.Net.WebSockets;
-using System.Reflection;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Services;
-using Emby.Server.Implementations.SocketSharp;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.AspNetCore.WebUtilities;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
-using ServiceStack.Text.Jsv;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    public class HttpListenerHost : IHttpServer
-    {
-        /// <summary>
-        /// The key for a setting that specifies the default redirect path
-        /// to use for requests where the URL base prefix is invalid or missing.
-        /// </summary>
-        public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
-
-        private readonly ILogger<HttpListenerHost> _logger;
-        private readonly ILoggerFactory _loggerFactory;
-        private readonly IServerConfigurationManager _config;
-        private readonly INetworkManager _networkManager;
-        private readonly IServerApplicationHost _appHost;
-        private readonly IJsonSerializer _jsonSerializer;
-        private readonly IXmlSerializer _xmlSerializer;
-        private readonly Func<Type, Func<string, object>> _funcParseFn;
-        private readonly string _defaultRedirectPath;
-        private readonly string _baseUrlPrefix;
-
-        private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
-        private readonly IHostEnvironment _hostEnvironment;
-
-        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
-        private bool _disposed = false;
-
-        public HttpListenerHost(
-            IServerApplicationHost applicationHost,
-            ILogger<HttpListenerHost> logger,
-            IServerConfigurationManager config,
-            IConfiguration configuration,
-            INetworkManager networkManager,
-            IJsonSerializer jsonSerializer,
-            IXmlSerializer xmlSerializer,
-            ILocalizationManager localizationManager,
-            ServiceController serviceController,
-            IHostEnvironment hostEnvironment,
-            ILoggerFactory loggerFactory)
-        {
-            _appHost = applicationHost;
-            _logger = logger;
-            _config = config;
-            _defaultRedirectPath = configuration[DefaultRedirectKey];
-            _baseUrlPrefix = _config.Configuration.BaseUrl;
-            _networkManager = networkManager;
-            _jsonSerializer = jsonSerializer;
-            _xmlSerializer = xmlSerializer;
-            ServiceController = serviceController;
-            _hostEnvironment = hostEnvironment;
-            _loggerFactory = loggerFactory;
-
-            _funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
-
-            Instance = this;
-            ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
-            GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
-        }
-
-        public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
-
-        public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; }
-
-        public static HttpListenerHost Instance { get; protected set; }
-
-        public string[] UrlPrefixes { get; private set; }
-
-        public string GlobalResponse { get; set; }
-
-        public ServiceController ServiceController { get; }
-
-        public object CreateInstance(Type type)
-        {
-            return _appHost.CreateInstance(type);
-        }
-
-        private static string NormalizeUrlPath(string path)
-        {
-            if (path.Length > 0 && path[0] == '/')
-            {
-                // If the path begins with a leading slash, just return it as-is
-                return path;
-            }
-            else
-            {
-                // If the path does not begin with a leading slash, append one for consistency
-                return "/" + path;
-            }
-        }
-
-        /// <summary>
-        /// Applies the request filters. Returns whether or not the request has been handled
-        /// and no more processing should be done.
-        /// </summary>
-        /// <returns></returns>
-        public void ApplyRequestFilters(IRequest req, HttpResponse res, object requestDto)
-        {
-            // Exec all RequestFilter attributes with Priority < 0
-            var attributes = GetRequestFilterAttributes(requestDto.GetType());
-
-            int count = attributes.Count;
-            int i = 0;
-            for (; i < count && attributes[i].Priority < 0; i++)
-            {
-                var attribute = attributes[i];
-                attribute.RequestFilter(req, res, requestDto);
-            }
-
-            // Exec remaining RequestFilter attributes with Priority >= 0
-            for (; i < count && attributes[i].Priority >= 0; i++)
-            {
-                var attribute = attributes[i];
-                attribute.RequestFilter(req, res, requestDto);
-            }
-        }
-
-        public Type GetServiceTypeByRequest(Type requestType)
-        {
-            _serviceOperationsMap.TryGetValue(requestType, out var serviceType);
-            return serviceType;
-        }
-
-        public void AddServiceInfo(Type serviceType, Type requestType)
-        {
-            _serviceOperationsMap[requestType] = serviceType;
-        }
-
-        private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
-        {
-            var attributes = requestDtoType.GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
-
-            var serviceType = GetServiceTypeByRequest(requestDtoType);
-            if (serviceType != null)
-            {
-                attributes.AddRange(serviceType.GetCustomAttributes(true).OfType<IHasRequestFilter>());
-            }
-
-            attributes.Sort((x, y) => x.Priority - y.Priority);
-
-            return attributes;
-        }
-
-        private static Exception GetActualException(Exception ex)
-        {
-            if (ex is AggregateException agg)
-            {
-                var inner = agg.InnerException;
-                if (inner != null)
-                {
-                    return GetActualException(inner);
-                }
-                else
-                {
-                    var inners = agg.InnerExceptions;
-                    if (inners.Count > 0)
-                    {
-                        return GetActualException(inners[0]);
-                    }
-                }
-            }
-
-            return ex;
-        }
-
-        private int GetStatusCode(Exception ex)
-        {
-            switch (ex)
-            {
-                case ArgumentException _: return 400;
-                case AuthenticationException _: return 401;
-                case SecurityException _: return 403;
-                case DirectoryNotFoundException _:
-                case FileNotFoundException _:
-                case ResourceNotFoundException _: return 404;
-                case MethodNotAllowedException _: return 405;
-                default: return 500;
-            }
-        }
-
-        private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace)
-        {
-            if (ignoreStackTrace)
-            {
-                _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
-            }
-            else
-            {
-                _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
-            }
-
-            var httpRes = httpReq.Response;
-
-            if (httpRes.HasStarted)
-            {
-                return;
-            }
-
-            httpRes.StatusCode = statusCode;
-
-            var errContent = _hostEnvironment.IsDevelopment()
-                    ? (NormalizeExceptionMessage(ex) ?? string.Empty)
-                    : "Error processing request.";
-            httpRes.ContentType = "text/plain";
-            httpRes.ContentLength = errContent.Length;
-            await httpRes.WriteAsync(errContent).ConfigureAwait(false);
-        }
-
-        private string NormalizeExceptionMessage(Exception ex)
-        {
-            // Do not expose the exception message for AuthenticationException
-            if (ex is AuthenticationException)
-            {
-                return null;
-            }
-
-            // Strip any information we don't want to reveal
-            return ex.Message
-                ?.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase)
-                .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
-        }
-
-        public static string RemoveQueryStringByKey(string url, string key)
-        {
-            var uri = new Uri(url);
-
-            // this gets all the query string key value pairs as a collection
-            var newQueryString = QueryHelpers.ParseQuery(uri.Query);
-
-            var originalCount = newQueryString.Count;
-
-            if (originalCount == 0)
-            {
-                return url;
-            }
-
-            // this removes the key if exists
-            newQueryString.Remove(key);
-
-            if (originalCount == newQueryString.Count)
-            {
-                return url;
-            }
-
-            // this gets the page path from root without QueryString
-            string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0];
-
-            return newQueryString.Count > 0
-                ? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString()))
-                : pagePathWithoutQueryString;
-        }
-
-        private static string GetUrlToLog(string url)
-        {
-            url = RemoveQueryStringByKey(url, "api_key");
-
-            return url;
-        }
-
-        private static string NormalizeConfiguredLocalAddress(string address)
-        {
-            var add = address.AsSpan().Trim('/');
-            int index = add.IndexOf('/');
-            if (index != -1)
-            {
-                add = add.Slice(index + 1);
-            }
-
-            return add.TrimStart('/').ToString();
-        }
-
-        private bool ValidateHost(string host)
-        {
-            var hosts = _config
-                .Configuration
-                .LocalNetworkAddresses
-                .Select(NormalizeConfiguredLocalAddress)
-                .ToList();
-
-            if (hosts.Count == 0)
-            {
-                return true;
-            }
-
-            host ??= string.Empty;
-
-            if (_networkManager.IsInPrivateAddressSpace(host))
-            {
-                hosts.Add("localhost");
-                hosts.Add("127.0.0.1");
-
-                return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1);
-            }
-
-            return true;
-        }
-
-        private bool ValidateRequest(string remoteIp, bool isLocal)
-        {
-            if (isLocal)
-            {
-                return true;
-            }
-
-            if (_config.Configuration.EnableRemoteAccess)
-            {
-                var addressFilter = _config.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
-
-                if (addressFilter.Length > 0 && !_networkManager.IsInLocalNetwork(remoteIp))
-                {
-                    if (_config.Configuration.IsRemoteIPFilterBlacklist)
-                    {
-                        return !_networkManager.IsAddressInSubnets(remoteIp, addressFilter);
-                    }
-                    else
-                    {
-                        return _networkManager.IsAddressInSubnets(remoteIp, addressFilter);
-                    }
-                }
-            }
-            else
-            {
-                if (!_networkManager.IsInLocalNetwork(remoteIp))
-                {
-                    return false;
-                }
-            }
-
-            return true;
-        }
-
-        /// <summary>
-        /// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required.
-        /// </summary>
-        /// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns>
-        private bool ValidateSsl(string remoteIp, string urlString)
-        {
-            if (_config.Configuration.RequireHttps
-                && _appHost.ListenWithHttps
-                && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
-            {
-                // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
-                if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
-                    || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
-                {
-                    return true;
-                }
-
-                if (!_networkManager.IsInLocalNetwork(remoteIp))
-                {
-                    return false;
-                }
-            }
-
-            return true;
-        }
-
-        /// <inheritdoc />
-        public Task RequestHandler(HttpContext context)
-        {
-            if (context.WebSockets.IsWebSocketRequest)
-            {
-                return WebSocketRequestHandler(context);
-            }
-
-            var request = context.Request;
-            var response = context.Response;
-            var localPath = context.Request.Path.ToString();
-
-            var req = new WebSocketSharpRequest(request, response, request.Path);
-            return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
-        }
-
-        /// <summary>
-        /// Overridable method that can be used to implement a custom handler.
-        /// </summary>
-        private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
-        {
-            var stopWatch = new Stopwatch();
-            stopWatch.Start();
-            var httpRes = httpReq.Response;
-            string urlToLog = GetUrlToLog(urlString);
-            string remoteIp = httpReq.RemoteIp;
-
-            try
-            {
-                if (_disposed)
-                {
-                    httpRes.StatusCode = 503;
-                    httpRes.ContentType = "text/plain";
-                    await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false);
-                    return;
-                }
-
-                if (!ValidateHost(host))
-                {
-                    httpRes.StatusCode = 400;
-                    httpRes.ContentType = "text/plain";
-                    await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false);
-                    return;
-                }
-
-                if (!ValidateRequest(remoteIp, httpReq.IsLocal))
-                {
-                    httpRes.StatusCode = 403;
-                    httpRes.ContentType = "text/plain";
-                    await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false);
-                    return;
-                }
-
-                if (!ValidateSsl(httpReq.RemoteIp, urlString))
-                {
-                    RedirectToSecureUrl(httpReq, httpRes, urlString);
-                    return;
-                }
-
-                if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
-                {
-                    httpRes.StatusCode = 200;
-                    foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
-                    {
-                        httpRes.Headers.Add(key, value);
-                    }
-
-                    httpRes.ContentType = "text/plain";
-                    await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
-                    return;
-                }
-
-                if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
-                    || string.IsNullOrEmpty(localPath)
-                    || !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
-                {
-                    // Always redirect back to the default path if the base prefix is invalid or missing
-                    _logger.LogDebug("Normalizing a URL at {0}", localPath);
-                    httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath);
-                    return;
-                }
-
-                if (!string.IsNullOrEmpty(GlobalResponse))
-                {
-                    // We don't want the address pings in ApplicationHost to fail
-                    if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)
-                    {
-                        httpRes.StatusCode = 503;
-                        httpRes.ContentType = "text/html";
-                        await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false);
-                        return;
-                    }
-                }
-
-                var handler = GetServiceHandler(httpReq);
-                if (handler != null)
-                {
-                    await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
-                }
-                else
-                {
-                    throw new FileNotFoundException();
-                }
-            }
-            catch (Exception requestEx)
-            {
-                try
-                {
-                    var requestInnerEx = GetActualException(requestEx);
-                    var statusCode = GetStatusCode(requestInnerEx);
-
-                    foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
-                    {
-                        if (!httpRes.Headers.ContainsKey(key))
-                        {
-                            httpRes.Headers.Add(key, value);
-                        }
-                    }
-
-                    bool ignoreStackTrace =
-                        requestInnerEx is SocketException
-                        || requestInnerEx is IOException
-                        || requestInnerEx is OperationCanceledException
-                        || requestInnerEx is SecurityException
-                        || requestInnerEx is AuthenticationException
-                        || requestInnerEx is FileNotFoundException;
-
-                    // Do not handle 500 server exceptions manually when in development mode.
-                    // Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware.
-                    // However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored,
-                    // because it will log the stack trace when it handles the exception.
-                    if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment())
-                    {
-                        throw;
-                    }
-
-                    await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
-                }
-                catch (Exception handlerException)
-                {
-                    var aggregateEx = new AggregateException("Error while handling request exception", requestEx, handlerException);
-                    _logger.LogError(aggregateEx, "Error while handling exception in response to {Url}", urlToLog);
-
-                    if (_hostEnvironment.IsDevelopment())
-                    {
-                        throw aggregateEx;
-                    }
-                }
-            }
-            finally
-            {
-                if (httpRes.StatusCode >= 500)
-                {
-                    _logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog);
-                }
-
-                stopWatch.Stop();
-                var elapsed = stopWatch.Elapsed;
-                if (elapsed.TotalMilliseconds > 500)
-                {
-                    _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog);
-                }
-            }
-        }
-
-        private async Task WebSocketRequestHandler(HttpContext context)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            try
-            {
-                _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
-
-                WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
-
-                using var connection = new WebSocketConnection(
-                    _loggerFactory.CreateLogger<WebSocketConnection>(),
-                    webSocket,
-                    context.Connection.RemoteIpAddress,
-                    context.Request.Query)
-                {
-                    OnReceive = ProcessWebSocketMessageReceived
-                };
-
-                WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
-
-                await connection.ProcessAsync().ConfigureAwait(false);
-                _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
-            }
-            catch (Exception ex) // Otherwise ASP.Net will ignore the exception
-            {
-                _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
-                if (!context.Response.HasStarted)
-                {
-                    context.Response.StatusCode = 500;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Get the default CORS headers.
-        /// </summary>
-        /// <param name="req"></param>
-        /// <returns></returns>
-        public IDictionary<string, string> GetDefaultCorsHeaders(IRequest req)
-        {
-            var origin = req.Headers["Origin"];
-            if (origin == StringValues.Empty)
-            {
-                origin = req.Headers["Host"];
-                if (origin == StringValues.Empty)
-                {
-                    origin = "*";
-                }
-            }
-
-            var headers = new Dictionary<string, string>();
-            headers.Add("Access-Control-Allow-Origin", origin);
-            headers.Add("Access-Control-Allow-Credentials", "true");
-            headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
-            headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie");
-            return headers;
-        }
-
-        // Entry point for HttpListener
-        public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
-        {
-            var pathInfo = httpReq.PathInfo;
-
-            pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType);
-            var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo);
-            if (restPath != null)
-            {
-                return new ServiceHandler(restPath, contentType);
-            }
-
-            _logger.LogError("Could not find handler for {PathInfo}", pathInfo);
-            return null;
-        }
-
-        private void RedirectToSecureUrl(IHttpRequest httpReq, HttpResponse httpRes, string url)
-        {
-            if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
-            {
-                var builder = new UriBuilder(uri)
-                {
-                    Port = _config.Configuration.PublicHttpsPort,
-                    Scheme = "https"
-                };
-                url = builder.Uri.ToString();
-            }
-
-            httpRes.Redirect(url);
-        }
-
-        /// <summary>
-        /// Adds the rest handlers.
-        /// </summary>
-        /// <param name="serviceTypes">The service types to register with the <see cref="ServiceController"/>.</param>
-        /// <param name="listeners">The web socket listeners.</param>
-        /// <param name="urlPrefixes">The URL prefixes. See <see cref="UrlPrefixes"/>.</param>
-        public void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listeners, IEnumerable<string> urlPrefixes)
-        {
-            _webSocketListeners = listeners.ToArray();
-            UrlPrefixes = urlPrefixes.ToArray();
-
-            ServiceController.Init(this, serviceTypes);
-
-            ResponseFilters = new Action<IRequest, HttpResponse, object>[]
-            {
-                new ResponseFilter(this, _logger).FilterResponse
-            };
-        }
-
-        public RouteAttribute[] GetRouteAttributes(Type requestType)
-        {
-            var routes = requestType.GetTypeInfo().GetCustomAttributes<RouteAttribute>(true).ToList();
-            var clone = routes.ToList();
-
-            foreach (var route in clone)
-            {
-                routes.Add(new RouteAttribute(NormalizeCustomRoutePath(route.Path), route.Verbs)
-                {
-                    Notes = route.Notes,
-                    Priority = route.Priority,
-                    Summary = route.Summary
-                });
-
-                routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs)
-                {
-                    Notes = route.Notes,
-                    Priority = route.Priority,
-                    Summary = route.Summary
-                });
-
-                routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs)
-                {
-                    Notes = route.Notes,
-                    Priority = route.Priority,
-                    Summary = route.Summary
-                });
-            }
-
-            return routes.ToArray();
-        }
-
-        public Func<string, object> GetParseFn(Type propertyType)
-        {
-            return _funcParseFn(propertyType);
-        }
-
-        public void SerializeToJson(object o, Stream stream)
-        {
-            _jsonSerializer.SerializeToStream(o, stream);
-        }
-
-        public void SerializeToXml(object o, Stream stream)
-        {
-            _xmlSerializer.SerializeToStream(o, stream);
-        }
-
-        public Task<object> DeserializeXml(Type type, Stream stream)
-        {
-            return Task.FromResult(_xmlSerializer.DeserializeFromStream(type, stream));
-        }
-
-        public Task<object> DeserializeJson(Type type, Stream stream)
-        {
-            return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
-        }
-
-        private string NormalizeEmbyRoutePath(string path)
-        {
-            _logger.LogDebug("Normalizing /emby route");
-            return _baseUrlPrefix + "/emby" + NormalizeUrlPath(path);
-        }
-
-        private string NormalizeMediaBrowserRoutePath(string path)
-        {
-            _logger.LogDebug("Normalizing /mediabrowser route");
-            return _baseUrlPrefix + "/mediabrowser" + NormalizeUrlPath(path);
-        }
-
-        private string NormalizeCustomRoutePath(string path)
-        {
-            _logger.LogDebug("Normalizing custom route {0}", path);
-            return _baseUrlPrefix + NormalizeUrlPath(path);
-        }
-
-        /// <summary>
-        /// Processes the web socket message received.
-        /// </summary>
-        /// <param name="result">The result.</param>
-        private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
-        {
-            if (_disposed)
-            {
-                return Task.CompletedTask;
-            }
-
-            IEnumerable<Task> GetTasks()
-            {
-                foreach (var x in _webSocketListeners)
-                {
-                    yield return x.ProcessMessageAsync(result);
-                }
-            }
-
-            return Task.WhenAll(GetTasks());
-        }
-    }
-}

+ 0 - 721
Emby.Server.Implementations/HttpServer/HttpResultFactory.cs

@@ -1,721 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.IO.Compression;
-using System.Net;
-using System.Runtime.Serialization;
-using System.Text;
-using System.Threading.Tasks;
-using System.Xml;
-using Emby.Server.Implementations.Services;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
-using Microsoft.Net.Http.Headers;
-using IRequest = MediaBrowser.Model.Services.IRequest;
-using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    /// <summary>
-    /// Class HttpResultFactory.
-    /// </summary>
-    public class HttpResultFactory : IHttpResultFactory
-    {
-        // Last-Modified and If-Modified-Since must follow strict date format,
-        // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
-        private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\"";
-        // We specifically use en-US culture because both day of week and month names require it
-        private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false);
-
-        /// <summary>
-        /// The logger.
-        /// </summary>
-        private readonly ILogger<HttpResultFactory> _logger;
-        private readonly IFileSystem _fileSystem;
-        private readonly IJsonSerializer _jsonSerializer;
-        private readonly IStreamHelper _streamHelper;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="HttpResultFactory" /> class.
-        /// </summary>
-        public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper)
-        {
-            _fileSystem = fileSystem;
-            _jsonSerializer = jsonSerializer;
-            _streamHelper = streamHelper;
-            _logger = loggerfactory.CreateLogger<HttpResultFactory>();
-        }
-
-        /// <summary>
-        /// Gets the result.
-        /// </summary>
-        /// <param name="requestContext">The request context.</param>
-        /// <param name="content">The content.</param>
-        /// <param name="contentType">Type of the content.</param>
-        /// <param name="responseHeaders">The response headers.</param>
-        /// <returns>System.Object.</returns>
-        public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null)
-        {
-            return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
-        }
-
-        public object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null)
-        {
-            return GetHttpResult(null, content, contentType, true, responseHeaders);
-        }
-
-        public object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null)
-        {
-            return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
-        }
-
-        public object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null)
-        {
-            return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
-        }
-
-        public object GetRedirectResult(string url)
-        {
-            var responseHeaders = new Dictionary<string, string>();
-            responseHeaders[HeaderNames.Location] = url;
-
-            var result = new HttpResult(Array.Empty<byte>(), "text/plain", HttpStatusCode.Redirect);
-
-            AddResponseHeaders(result, responseHeaders);
-
-            return result;
-        }
-
-        /// <summary>
-        /// Gets the HTTP result.
-        /// </summary>
-        private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
-        {
-            var result = new StreamWriter(content, contentType);
-
-            if (responseHeaders == null)
-            {
-                responseHeaders = new Dictionary<string, string>();
-            }
-
-            if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out _))
-            {
-                responseHeaders[HeaderNames.Expires] = "0";
-            }
-
-            AddResponseHeaders(result, responseHeaders);
-
-            return result;
-        }
-
-        /// <summary>
-        /// Gets the HTTP result.
-        /// </summary>
-        private IHasHeaders GetHttpResult(IRequest requestContext, byte[] content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
-        {
-            string compressionType = null;
-            bool isHeadRequest = false;
-
-            if (requestContext != null)
-            {
-                compressionType = GetCompressionType(requestContext, content, contentType);
-                isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
-            }
-
-            IHasHeaders result;
-            if (string.IsNullOrEmpty(compressionType))
-            {
-                var contentLength = content.Length;
-
-                if (isHeadRequest)
-                {
-                    content = Array.Empty<byte>();
-                }
-
-                result = new StreamWriter(content, contentType, contentLength);
-            }
-            else
-            {
-                result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType);
-            }
-
-            if (responseHeaders == null)
-            {
-                responseHeaders = new Dictionary<string, string>();
-            }
-
-            if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
-            {
-                responseHeaders[HeaderNames.Expires] = "0";
-            }
-
-            AddResponseHeaders(result, responseHeaders);
-
-            return result;
-        }
-
-        /// <summary>
-        /// Gets the HTTP result.
-        /// </summary>
-        private IHasHeaders GetHttpResult(IRequest requestContext, string content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
-        {
-            IHasHeaders result;
-
-            var bytes = Encoding.UTF8.GetBytes(content);
-
-            var compressionType = requestContext == null ? null : GetCompressionType(requestContext, bytes, contentType);
-
-            var isHeadRequest = requestContext == null ? false : string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
-
-            if (string.IsNullOrEmpty(compressionType))
-            {
-                var contentLength = bytes.Length;
-
-                if (isHeadRequest)
-                {
-                    bytes = Array.Empty<byte>();
-                }
-
-                result = new StreamWriter(bytes, contentType, contentLength);
-            }
-            else
-            {
-                result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType);
-            }
-
-            if (responseHeaders == null)
-            {
-                responseHeaders = new Dictionary<string, string>();
-            }
-
-            if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
-            {
-                responseHeaders[HeaderNames.Expires] = "0";
-            }
-
-            AddResponseHeaders(result, responseHeaders);
-
-            return result;
-        }
-
-        /// <summary>
-        /// Gets the optimized result.
-        /// </summary>
-        /// <typeparam name="T"></typeparam>
-        public object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null)
-            where T : class
-        {
-            if (result == null)
-            {
-                throw new ArgumentNullException(nameof(result));
-            }
-
-            if (responseHeaders == null)
-            {
-                responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-            }
-
-            responseHeaders[HeaderNames.Expires] = "0";
-
-            return ToOptimizedResultInternal(requestContext, result, responseHeaders);
-        }
-
-        private string GetCompressionType(IRequest request, byte[] content, string responseContentType)
-        {
-            if (responseContentType == null)
-            {
-                return null;
-            }
-
-            // Per apple docs, hls manifests must be compressed
-            if (!responseContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) &&
-                responseContentType.IndexOf("json", StringComparison.OrdinalIgnoreCase) == -1 &&
-                responseContentType.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) == -1 &&
-                responseContentType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) == -1 &&
-                responseContentType.IndexOf("application/x-mpegURL", StringComparison.OrdinalIgnoreCase) == -1)
-            {
-                return null;
-            }
-
-            if (content.Length < 1024)
-            {
-                return null;
-            }
-
-            return GetCompressionType(request);
-        }
-
-        private static string GetCompressionType(IRequest request)
-        {
-            var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString();
-
-            if (!string.IsNullOrEmpty(acceptEncoding))
-            {
-                // if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
-                //    return "br";
-
-                if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase))
-                {
-                    return "deflate";
-                }
-
-                if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase))
-                {
-                    return "gzip";
-                }
-            }
-
-            return null;
-        }
-
-        /// <summary>
-        /// Returns the optimized result for the IRequestContext.
-        /// Does not use or store results in any cache.
-        /// </summary>
-        /// <param name="request"></param>
-        /// <param name="dto"></param>
-        /// <returns></returns>
-        public object ToOptimizedResult<T>(IRequest request, T dto)
-        {
-            return ToOptimizedResultInternal(request, dto);
-        }
-
-        private object ToOptimizedResultInternal<T>(IRequest request, T dto, IDictionary<string, string> responseHeaders = null)
-        {
-            // TODO: @bond use Span and .Equals
-            var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant();
-
-            switch (contentType)
-            {
-                case "application/xml":
-                case "text/xml":
-                case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
-                    return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders);
-
-                case "application/json":
-                case "text/json":
-                    return GetHttpResult(request, _jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders);
-                default:
-                    break;
-            }
-
-            var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase);
-
-            var ms = new MemoryStream();
-            var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
-
-            writerFn(dto, ms);
-
-            ms.Position = 0;
-
-            if (isHeadRequest)
-            {
-                using (ms)
-                {
-                    return GetHttpResult(request, Array.Empty<byte>(), contentType, true, responseHeaders);
-                }
-            }
-
-            return GetHttpResult(request, ms, contentType, true, responseHeaders);
-        }
-
-        private IHasHeaders GetCompressedResult(
-            byte[] content,
-            string requestedCompressionType,
-            IDictionary<string, string> responseHeaders,
-            bool isHeadRequest,
-            string contentType)
-        {
-            if (responseHeaders == null)
-            {
-                responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-            }
-
-            content = Compress(content, requestedCompressionType);
-            responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType;
-
-            responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding;
-
-            var contentLength = content.Length;
-
-            if (isHeadRequest)
-            {
-                var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
-                AddResponseHeaders(result, responseHeaders);
-                return result;
-            }
-            else
-            {
-                var result = new StreamWriter(content, contentType, contentLength);
-                AddResponseHeaders(result, responseHeaders);
-                return result;
-            }
-        }
-
-        private byte[] Compress(byte[] bytes, string compressionType)
-        {
-            if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase))
-            {
-                return Deflate(bytes);
-            }
-
-            if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase))
-            {
-                return GZip(bytes);
-            }
-
-            throw new NotSupportedException(compressionType);
-        }
-
-        private static byte[] Deflate(byte[] bytes)
-        {
-            // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream
-            // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream
-            using (var ms = new MemoryStream())
-            using (var zipStream = new DeflateStream(ms, CompressionMode.Compress))
-            {
-                zipStream.Write(bytes, 0, bytes.Length);
-                zipStream.Dispose();
-
-                return ms.ToArray();
-            }
-        }
-
-        private static byte[] GZip(byte[] buffer)
-        {
-            using (var ms = new MemoryStream())
-            using (var zipStream = new GZipStream(ms, CompressionMode.Compress))
-            {
-                zipStream.Write(buffer, 0, buffer.Length);
-                zipStream.Dispose();
-
-                return ms.ToArray();
-            }
-        }
-
-        private static string SerializeToXmlString(object from)
-        {
-            using (var ms = new MemoryStream())
-            {
-                var xwSettings = new XmlWriterSettings();
-                xwSettings.Encoding = new UTF8Encoding(false);
-                xwSettings.OmitXmlDeclaration = false;
-
-                using (var xw = XmlWriter.Create(ms, xwSettings))
-                {
-                    var serializer = new DataContractSerializer(from.GetType());
-                    serializer.WriteObject(xw, from);
-                    xw.Flush();
-                    ms.Seek(0, SeekOrigin.Begin);
-                    using (var reader = new StreamReader(ms))
-                    {
-                        return reader.ReadToEnd();
-                    }
-                }
-            }
-        }
-
-        /// <summary>
-        /// Pres the process optimized result.
-        /// </summary>
-        private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options)
-        {
-            bool noCache = requestContext.Headers[HeaderNames.CacheControl].ToString().IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
-            AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
-
-            if (!noCache)
-            {
-                if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ifModifiedSinceHeader))
-                {
-                    _logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]);
-                    return null;
-                }
-
-                if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified))
-                {
-                    AddAgeHeader(responseHeaders, options.DateLastModified);
-
-                    var result = new HttpResult(Array.Empty<byte>(), options.ContentType ?? "text/html", HttpStatusCode.NotModified);
-
-                    AddResponseHeaders(result, responseHeaders);
-
-                    return result;
-                }
-            }
-
-            return null;
-        }
-
-        public Task<object> GetStaticFileResult(IRequest requestContext,
-            string path,
-            FileShare fileShare = FileShare.Read)
-        {
-            if (string.IsNullOrEmpty(path))
-            {
-                throw new ArgumentNullException(nameof(path));
-            }
-
-            return GetStaticFileResult(requestContext, new StaticFileResultOptions
-            {
-                Path = path,
-                FileShare = fileShare
-            });
-        }
-
-        public Task<object> GetStaticFileResult(IRequest requestContext, StaticFileResultOptions options)
-        {
-            var path = options.Path;
-            var fileShare = options.FileShare;
-
-            if (string.IsNullOrEmpty(path))
-            {
-                throw new ArgumentException("Path can't be empty.", nameof(options));
-            }
-
-            if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite)
-            {
-                throw new ArgumentException("FileShare must be either Read or ReadWrite");
-            }
-
-            if (string.IsNullOrEmpty(options.ContentType))
-            {
-                options.ContentType = MimeTypes.GetMimeType(path);
-            }
-
-            if (!options.DateLastModified.HasValue)
-            {
-                options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path);
-            }
-
-            options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare));
-
-            options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
-            return GetStaticResult(requestContext, options);
-        }
-
-        /// <summary>
-        /// Gets the file stream.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="fileShare">The file share.</param>
-        /// <returns>Stream.</returns>
-        private Stream GetFileStream(string path, FileShare fileShare)
-        {
-            return new FileStream(path, FileMode.Open, FileAccess.Read, fileShare);
-        }
-
-        public Task<object> GetStaticResult(IRequest requestContext,
-            Guid cacheKey,
-            DateTime? lastDateModified,
-            TimeSpan? cacheDuration,
-            string contentType,
-            Func<Task<Stream>> factoryFn,
-            IDictionary<string, string> responseHeaders = null,
-            bool isHeadRequest = false)
-        {
-            return GetStaticResult(requestContext, new StaticResultOptions
-            {
-                CacheDuration = cacheDuration,
-                ContentFactory = factoryFn,
-                ContentType = contentType,
-                DateLastModified = lastDateModified,
-                IsHeadRequest = isHeadRequest,
-                ResponseHeaders = responseHeaders
-            });
-        }
-
-        public async Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options)
-        {
-            options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
-            var contentType = options.ContentType;
-            if (!StringValues.IsNullOrEmpty(requestContext.Headers[HeaderNames.IfModifiedSince]))
-            {
-                // See if the result is already cached in the browser
-                var result = GetCachedResult(requestContext, options.ResponseHeaders, options);
-
-                if (result != null)
-                {
-                    return result;
-                }
-            }
-
-            // TODO: We don't really need the option value
-            var isHeadRequest = options.IsHeadRequest || string.Equals(requestContext.Verb, "HEAD", StringComparison.OrdinalIgnoreCase);
-            var factoryFn = options.ContentFactory;
-            var responseHeaders = options.ResponseHeaders;
-            AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified);
-            AddAgeHeader(responseHeaders, options.DateLastModified);
-
-            var rangeHeader = requestContext.Headers[HeaderNames.Range];
-
-            if (!isHeadRequest && !string.IsNullOrEmpty(options.Path))
-            {
-                var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem, _streamHelper)
-                {
-                    OnComplete = options.OnComplete,
-                    OnError = options.OnError,
-                    FileShare = options.FileShare
-                };
-
-                AddResponseHeaders(hasHeaders, options.ResponseHeaders);
-                return hasHeaders;
-            }
-
-            var stream = await factoryFn().ConfigureAwait(false);
-
-            var totalContentLength = options.ContentLength;
-            if (!totalContentLength.HasValue)
-            {
-                try
-                {
-                    totalContentLength = stream.Length;
-                }
-                catch (NotSupportedException)
-                {
-                }
-            }
-
-            if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue)
-            {
-                var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest)
-                {
-                    OnComplete = options.OnComplete
-                };
-
-                AddResponseHeaders(hasHeaders, options.ResponseHeaders);
-                return hasHeaders;
-            }
-            else
-            {
-                if (totalContentLength.HasValue)
-                {
-                    responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture);
-                }
-
-                if (isHeadRequest)
-                {
-                    using (stream)
-                    {
-                        return GetHttpResult(requestContext, Array.Empty<byte>(), contentType, true, responseHeaders);
-                    }
-                }
-
-                var hasHeaders = new StreamWriter(stream, contentType)
-                {
-                    OnComplete = options.OnComplete,
-                    OnError = options.OnError
-                };
-
-                AddResponseHeaders(hasHeaders, options.ResponseHeaders);
-                return hasHeaders;
-            }
-        }
-
-        /// <summary>
-        /// Adds the caching responseHeaders.
-        /// </summary>
-        private void AddCachingHeaders(
-            IDictionary<string, string> responseHeaders,
-            TimeSpan? cacheDuration,
-            bool noCache,
-            DateTime? lastModifiedDate)
-        {
-            if (noCache)
-            {
-                responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate";
-                responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate";
-                return;
-            }
-
-            if (cacheDuration.HasValue)
-            {
-                responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds;
-            }
-            else
-            {
-                responseHeaders[HeaderNames.CacheControl] = "public";
-            }
-
-            if (lastModifiedDate.HasValue)
-            {
-                responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture);
-            }
-        }
-
-        /// <summary>
-        /// Adds the age header.
-        /// </summary>
-        /// <param name="responseHeaders">The responseHeaders.</param>
-        /// <param name="lastDateModified">The last date modified.</param>
-        private static void AddAgeHeader(IDictionary<string, string> responseHeaders, DateTime? lastDateModified)
-        {
-            if (lastDateModified.HasValue)
-            {
-                responseHeaders[HeaderNames.Age] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture);
-            }
-        }
-
-        /// <summary>
-        /// Determines whether [is not modified] [the specified if modified since].
-        /// </summary>
-        /// <param name="ifModifiedSince">If modified since.</param>
-        /// <param name="cacheDuration">Duration of the cache.</param>
-        /// <param name="dateModified">The date modified.</param>
-        /// <returns><c>true</c> if [is not modified] [the specified if modified since]; otherwise, <c>false</c>.</returns>
-        private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified)
-        {
-            if (dateModified.HasValue)
-            {
-                var lastModified = NormalizeDateForComparison(dateModified.Value);
-                ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
-
-                return lastModified <= ifModifiedSince;
-            }
-
-            if (cacheDuration.HasValue)
-            {
-                var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value);
-
-                if (DateTime.UtcNow < cacheExpirationDate)
-                {
-                    return true;
-                }
-            }
-
-            return false;
-        }
-
-
-        /// <summary>
-        /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that.
-        /// </summary>
-        /// <param name="date">The date.</param>
-        /// <returns>DateTime.</returns>
-        private static DateTime NormalizeDateForComparison(DateTime date)
-        {
-            return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
-        }
-
-        /// <summary>
-        /// Adds the response headers.
-        /// </summary>
-        /// <param name="hasHeaders">The has options.</param>
-        /// <param name="responseHeaders">The response headers.</param>
-        private static void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable<KeyValuePair<string, string>> responseHeaders)
-        {
-            foreach (var item in responseHeaders)
-            {
-                hasHeaders.Headers[item.Key] = item.Value;
-            }
-        }
-    }
-}

+ 0 - 212
Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs

@@ -1,212 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Buffers;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult
-    {
-        private const int BufferSize = 81920;
-
-        private readonly Dictionary<string, string> _options = new Dictionary<string, string>();
-
-        private List<KeyValuePair<long, long?>> _requestedRanges;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="RangeRequestWriter" /> class.
-        /// </summary>
-        /// <param name="rangeHeader">The range header.</param>
-        /// <param name="contentLength">The content length.</param>
-        /// <param name="source">The source.</param>
-        /// <param name="contentType">Type of the content.</param>
-        /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
-        public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest)
-        {
-            if (string.IsNullOrEmpty(contentType))
-            {
-                throw new ArgumentNullException(nameof(contentType));
-            }
-
-            RangeHeader = rangeHeader;
-            SourceStream = source;
-            IsHeadRequest = isHeadRequest;
-
-            ContentType = contentType;
-            Headers[HeaderNames.ContentType] = contentType;
-            Headers[HeaderNames.AcceptRanges] = "bytes";
-            StatusCode = HttpStatusCode.PartialContent;
-
-            SetRangeValues(contentLength);
-        }
-
-        /// <summary>
-        /// Gets or sets the source stream.
-        /// </summary>
-        /// <value>The source stream.</value>
-        private Stream SourceStream { get; set; }
-        private string RangeHeader { get; set; }
-        private bool IsHeadRequest { get; set; }
-
-        private long RangeStart { get; set; }
-        private long RangeEnd { get; set; }
-        private long RangeLength { get; set; }
-        private long TotalContentLength { get; set; }
-
-        public Action OnComplete { get; set; }
-
-        /// <summary>
-        /// Additional HTTP Headers
-        /// </summary>
-        /// <value>The headers.</value>
-        public IDictionary<string, string> Headers => _options;
-
-        /// <summary>
-        /// Gets the requested ranges.
-        /// </summary>
-        /// <value>The requested ranges.</value>
-        protected List<KeyValuePair<long, long?>> RequestedRanges
-        {
-            get
-            {
-                if (_requestedRanges == null)
-                {
-                    _requestedRanges = new List<KeyValuePair<long, long?>>();
-
-                    // Example: bytes=0-,32-63
-                    var ranges = RangeHeader.Split('=')[1].Split(',');
-
-                    foreach (var range in ranges)
-                    {
-                        var vals = range.Split('-');
-
-                        long start = 0;
-                        long? end = null;
-
-                        if (!string.IsNullOrEmpty(vals[0]))
-                        {
-                            start = long.Parse(vals[0], CultureInfo.InvariantCulture);
-                        }
-
-                        if (!string.IsNullOrEmpty(vals[1]))
-                        {
-                            end = long.Parse(vals[1], CultureInfo.InvariantCulture);
-                        }
-
-                        _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
-                    }
-                }
-
-                return _requestedRanges;
-            }
-        }
-
-        public string ContentType { get; set; }
-
-        public IRequest RequestContext { get; set; }
-
-        public object Response { get; set; }
-
-        public int Status { get; set; }
-
-        public HttpStatusCode StatusCode
-        {
-            get => (HttpStatusCode)Status;
-            set => Status = (int)value;
-        }
-
-        /// <summary>
-        /// Sets the range values.
-        /// </summary>
-        private void SetRangeValues(long contentLength)
-        {
-            var requestedRange = RequestedRanges[0];
-
-            TotalContentLength = contentLength;
-
-            // If the requested range is "0-", we can optimize by just doing a stream copy
-            if (!requestedRange.Value.HasValue)
-            {
-                RangeEnd = TotalContentLength - 1;
-            }
-            else
-            {
-                RangeEnd = requestedRange.Value.Value;
-            }
-
-            RangeStart = requestedRange.Key;
-            RangeLength = 1 + RangeEnd - RangeStart;
-
-            Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
-            Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
-
-            if (RangeStart > 0 && SourceStream.CanSeek)
-            {
-                SourceStream.Position = RangeStart;
-            }
-        }
-
-        public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
-        {
-            try
-            {
-                // Headers only
-                if (IsHeadRequest)
-                {
-                    return;
-                }
-
-                using (var source = SourceStream)
-                {
-                    // If the requested range is "0-", we can optimize by just doing a stream copy
-                    if (RangeEnd >= TotalContentLength - 1)
-                    {
-                        await source.CopyToAsync(responseStream, BufferSize, cancellationToken).ConfigureAwait(false);
-                    }
-                    else
-                    {
-                        await CopyToInternalAsync(source, responseStream, RangeLength, cancellationToken).ConfigureAwait(false);
-                    }
-                }
-            }
-            finally
-            {
-                OnComplete?.Invoke();
-            }
-        }
-
-        private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
-        {
-            var array = ArrayPool<byte>.Shared.Rent(BufferSize);
-            try
-            {
-                int bytesRead;
-                while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
-                {
-                    var bytesToCopy = Math.Min(bytesRead, copyLength);
-
-                    await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy), cancellationToken).ConfigureAwait(false);
-
-                    copyLength -= bytesToCopy;
-
-                    if (copyLength <= 0)
-                    {
-                        break;
-                    }
-                }
-            }
-            finally
-            {
-                ArrayPool<byte>.Shared.Return(array);
-            }
-        }
-    }
-}

+ 0 - 113
Emby.Server.Implementations/HttpServer/ResponseFilter.cs

@@ -1,113 +0,0 @@
-using System;
-using System.Globalization;
-using System.Text;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    /// <summary>
-    /// Class ResponseFilter.
-    /// </summary>
-    public class ResponseFilter
-    {
-        private readonly IHttpServer _server;
-        private readonly ILogger _logger;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ResponseFilter"/> class.
-        /// </summary>
-        /// <param name="server">The HTTP server.</param>
-        /// <param name="logger">The logger.</param>
-        public ResponseFilter(IHttpServer server, ILogger logger)
-        {
-            _server = server;
-            _logger = logger;
-        }
-
-        /// <summary>
-        /// Filters the response.
-        /// </summary>
-        /// <param name="req">The req.</param>
-        /// <param name="res">The res.</param>
-        /// <param name="dto">The dto.</param>
-        public void FilterResponse(IRequest req, HttpResponse res, object dto)
-        {
-            foreach(var (key, value) in _server.GetDefaultCorsHeaders(req))
-            {
-                res.Headers.Add(key, value);
-            }
-            // Try to prevent compatibility view
-            res.Headers["Access-Control-Allow-Headers"] = "Accept, Accept-Language, Authorization, Cache-Control, " +
-                "Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " +
-                "Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " +
-                "Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " +
-                "X-Emby-Authorization";
-
-            if (dto is Exception exception)
-            {
-                _logger.LogError(exception, "Error processing request for {RawUrl}", req.RawUrl);
-
-                if (!string.IsNullOrEmpty(exception.Message))
-                {
-                    var error = exception.Message.Replace(Environment.NewLine, " ", StringComparison.Ordinal);
-                    error = RemoveControlCharacters(error);
-
-                    res.Headers.Add("X-Application-Error-Code", error);
-                }
-            }
-
-            if (dto is IHasHeaders hasHeaders)
-            {
-                if (!hasHeaders.Headers.ContainsKey(HeaderNames.Server))
-                {
-                    hasHeaders.Headers[HeaderNames.Server] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50";
-                }
-
-                // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy
-                if (hasHeaders.Headers.TryGetValue(HeaderNames.ContentLength, out string contentLength)
-                    && !string.IsNullOrEmpty(contentLength))
-                {
-                    var length = long.Parse(contentLength, CultureInfo.InvariantCulture);
-
-                    if (length > 0)
-                    {
-                        res.ContentLength = length;
-                    }
-                }
-            }
-        }
-
-        /// <summary>
-        /// Removes the control characters.
-        /// </summary>
-        /// <param name="inString">The in string.</param>
-        /// <returns>System.String.</returns>
-        public static string RemoveControlCharacters(string inString)
-        {
-            if (inString == null)
-            {
-                return null;
-            }
-            else if (inString.Length == 0)
-            {
-                return inString;
-            }
-
-            var newString = new StringBuilder(inString.Length);
-
-            foreach (var ch in inString)
-            {
-                if (!char.IsControl(ch))
-                {
-                    newString.Append(ch);
-                }
-            }
-
-            return newString.ToString();
-        }
-    }
-}

+ 1 - 212
Emby.Server.Implementations/HttpServer/Security/AuthService.cs

@@ -1,17 +1,7 @@
 #pragma warning disable CS1591
 
-using System;
-using System.Linq;
-using Emby.Server.Implementations.SocketSharp;
-using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
 
 namespace Emby.Server.Implementations.HttpServer.Security
@@ -19,32 +9,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
     public class AuthService : IAuthService
     {
         private readonly IAuthorizationContext _authorizationContext;
-        private readonly ISessionManager _sessionManager;
-        private readonly IServerConfigurationManager _config;
-        private readonly INetworkManager _networkManager;
 
         public AuthService(
-            IAuthorizationContext authorizationContext,
-            IServerConfigurationManager config,
-            ISessionManager sessionManager,
-            INetworkManager networkManager)
+            IAuthorizationContext authorizationContext)
         {
             _authorizationContext = authorizationContext;
-            _config = config;
-            _sessionManager = sessionManager;
-            _networkManager = networkManager;
-        }
-
-        public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
-        {
-            ValidateUser(request, authAttributes);
-        }
-
-        public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
-        {
-            var req = new WebSocketSharpRequest(request, null, request.Path);
-            var user = ValidateUser(req, authAttributes);
-            return user;
         }
 
         public AuthorizationInfo Authenticate(HttpRequest request)
@@ -62,185 +31,5 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
             return auth;
         }
-
-        private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
-        {
-            // This code is executed before the service
-            var auth = _authorizationContext.GetAuthorizationInfo(request);
-
-            if (!IsExemptFromAuthenticationToken(authAttributes, request))
-            {
-                ValidateSecurityToken(request, auth.Token);
-            }
-
-            if (authAttributes.AllowLocalOnly && !request.IsLocal)
-            {
-                throw new SecurityException("Operation not found.");
-            }
-
-            var user = auth.User;
-
-            if (user == null && auth.UserId != Guid.Empty)
-            {
-                throw new AuthenticationException("User with Id " + auth.UserId + " not found");
-            }
-
-            if (user != null)
-            {
-                ValidateUserAccess(user, request, authAttributes);
-            }
-
-            var info = GetTokenInfo(request);
-
-            if (!IsExemptFromRoles(auth, authAttributes, request, info))
-            {
-                var roles = authAttributes.GetRoles();
-
-                ValidateRoles(roles, user);
-            }
-
-            if (!string.IsNullOrEmpty(auth.DeviceId) &&
-                !string.IsNullOrEmpty(auth.Client) &&
-                !string.IsNullOrEmpty(auth.Device))
-            {
-                _sessionManager.LogSessionActivity(
-                    auth.Client,
-                    auth.Version,
-                    auth.DeviceId,
-                    auth.Device,
-                    request.RemoteIp,
-                    user);
-            }
-
-            return user;
-        }
-
-        private void ValidateUserAccess(
-            User user,
-            IRequest request,
-            IAuthenticationAttributes authAttributes)
-        {
-            if (user.HasPermission(PermissionKind.IsDisabled))
-            {
-                throw new SecurityException("User account has been disabled.");
-            }
-
-            if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp))
-            {
-                throw new SecurityException("User account has been disabled.");
-            }
-
-            if (!user.HasPermission(PermissionKind.IsAdministrator)
-                && !authAttributes.EscapeParentalControl
-                && !user.IsParentalScheduleAllowed())
-            {
-                request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl");
-
-                throw new SecurityException("This user account is not allowed access at this time.");
-            }
-        }
-
-        private bool IsExemptFromAuthenticationToken(IAuthenticationAttributes authAttribtues, IRequest request)
-        {
-            if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
-            {
-                return true;
-            }
-
-            if (authAttribtues.AllowLocal && request.IsLocal)
-            {
-                return true;
-            }
-
-            if (authAttribtues.AllowLocalOnly && request.IsLocal)
-            {
-                return true;
-            }
-
-            if (authAttribtues.IgnoreLegacyAuth)
-            {
-                return true;
-            }
-
-            return false;
-        }
-
-        private bool IsExemptFromRoles(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request, AuthenticationInfo tokenInfo)
-        {
-            if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
-            {
-                return true;
-            }
-
-            if (authAttribtues.AllowLocal && request.IsLocal)
-            {
-                return true;
-            }
-
-            if (authAttribtues.AllowLocalOnly && request.IsLocal)
-            {
-                return true;
-            }
-
-            if (string.IsNullOrEmpty(auth.Token))
-            {
-                return true;
-            }
-
-            if (tokenInfo != null && tokenInfo.UserId.Equals(Guid.Empty))
-            {
-                return true;
-            }
-
-            return false;
-        }
-
-        private static void ValidateRoles(string[] roles, User user)
-        {
-            if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase))
-            {
-                if (user == null || !user.HasPermission(PermissionKind.IsAdministrator))
-                {
-                    throw new SecurityException("User does not have admin access.");
-                }
-            }
-
-            if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase))
-            {
-                if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion))
-                {
-                    throw new SecurityException("User does not have delete access.");
-                }
-            }
-
-            if (roles.Contains("download", StringComparer.OrdinalIgnoreCase))
-            {
-                if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading))
-                {
-                    throw new SecurityException("User does not have download access.");
-                }
-            }
-        }
-
-        private static AuthenticationInfo GetTokenInfo(IRequest request)
-        {
-            request.Items.TryGetValue("OriginalAuthenticationInfo", out var info);
-            return info as AuthenticationInfo;
-        }
-
-        private void ValidateSecurityToken(IRequest request, string token)
-        {
-            if (string.IsNullOrEmpty(token))
-            {
-                throw new AuthenticationException("Access token is required.");
-            }
-
-            var info = GetTokenInfo(request);
-
-            if (info == null)
-            {
-                throw new AuthenticationException("Access token is invalid or expired.");
-            }
-        }
     }
 }

+ 9 - 15
Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs

@@ -7,7 +7,6 @@ using System.Net;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
-using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Net.Http.Headers;
 
@@ -24,14 +23,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
             _userManager = userManager;
         }
 
-        public AuthorizationInfo GetAuthorizationInfo(object requestContext)
+        public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
         {
-            return GetAuthorizationInfo((IRequest)requestContext);
-        }
-
-        public AuthorizationInfo GetAuthorizationInfo(IRequest requestContext)
-        {
-            if (requestContext.Items.TryGetValue("AuthorizationInfo", out var cached))
+            if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
             {
                 return (AuthorizationInfo)cached;
             }
@@ -52,18 +46,18 @@ namespace Emby.Server.Implementations.HttpServer.Security
         /// </summary>
         /// <param name="httpReq">The HTTP req.</param>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        private AuthorizationInfo GetAuthorization(IRequest httpReq)
+        private AuthorizationInfo GetAuthorization(HttpContext httpReq)
         {
             var auth = GetAuthorizationDictionary(httpReq);
             var (authInfo, originalAuthInfo) =
-                GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
+                GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
 
             if (originalAuthInfo != null)
             {
-                httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
+                httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
             }
 
-            httpReq.Items["AuthorizationInfo"] = authInfo;
+            httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
             return authInfo;
         }
 
@@ -203,13 +197,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
         /// </summary>
         /// <param name="httpReq">The HTTP req.</param>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        private Dictionary<string, string> GetAuthorizationDictionary(IRequest httpReq)
+        private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
         {
-            var auth = httpReq.Headers["X-Emby-Authorization"];
+            var auth = httpReq.Request.Headers["X-Emby-Authorization"];
 
             if (string.IsNullOrEmpty(auth))
             {
-                auth = httpReq.Headers[HeaderNames.Authorization];
+                auth = httpReq.Request.Headers[HeaderNames.Authorization];
             }
 
             return GetAuthorization(auth);

+ 7 - 13
Emby.Server.Implementations/HttpServer/Security/SessionContext.cs

@@ -2,11 +2,11 @@
 
 using System;
 using Jellyfin.Data.Entities;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
 
 namespace Emby.Server.Implementations.HttpServer.Security
 {
@@ -23,26 +23,20 @@ namespace Emby.Server.Implementations.HttpServer.Security
             _sessionManager = sessionManager;
         }
 
-        public SessionInfo GetSession(IRequest requestContext)
+        public SessionInfo GetSession(HttpContext requestContext)
         {
             var authorization = _authContext.GetAuthorizationInfo(requestContext);
 
             var user = authorization.User;
-            return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user);
-        }
-
-        private AuthenticationInfo GetTokenInfo(IRequest request)
-        {
-            request.Items.TryGetValue("OriginalAuthenticationInfo", out var info);
-            return info as AuthenticationInfo;
+            return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.Request.RemoteIp(), user);
         }
 
         public SessionInfo GetSession(object requestContext)
         {
-            return GetSession((IRequest)requestContext);
+            return GetSession((HttpContext)requestContext);
         }
 
-        public User GetUser(IRequest requestContext)
+        public User GetUser(HttpContext requestContext)
         {
             var session = GetSession(requestContext);
 
@@ -51,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
         public User GetUser(object requestContext)
         {
-            return GetUser((IRequest)requestContext);
+            return GetUser((HttpContext)requestContext);
         }
     }
 }

+ 0 - 120
Emby.Server.Implementations/HttpServer/StreamWriter.cs

@@ -1,120 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    /// <summary>
-    /// Class StreamWriter.
-    /// </summary>
-    public class StreamWriter : IAsyncStreamWriter, IHasHeaders
-    {
-        /// <summary>
-        /// The options.
-        /// </summary>
-        private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="StreamWriter" /> class.
-        /// </summary>
-        /// <param name="source">The source.</param>
-        /// <param name="contentType">Type of the content.</param>
-        public StreamWriter(Stream source, string contentType)
-        {
-            if (string.IsNullOrEmpty(contentType))
-            {
-                throw new ArgumentNullException(nameof(contentType));
-            }
-
-            SourceStream = source;
-
-            Headers["Content-Type"] = contentType;
-
-            if (source.CanSeek)
-            {
-                Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture);
-            }
-
-            Headers[HeaderNames.ContentType] = contentType;
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="StreamWriter"/> class.
-        /// </summary>
-        /// <param name="source">The source.</param>
-        /// <param name="contentType">Type of the content.</param>
-        /// <param name="contentLength">The content length.</param>
-        public StreamWriter(byte[] source, string contentType, int contentLength)
-        {
-            if (string.IsNullOrEmpty(contentType))
-            {
-                throw new ArgumentNullException(nameof(contentType));
-            }
-
-            SourceBytes = source;
-
-            Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture);
-            Headers[HeaderNames.ContentType] = contentType;
-        }
-
-        /// <summary>
-        /// Gets or sets the source stream.
-        /// </summary>
-        /// <value>The source stream.</value>
-        private Stream SourceStream { get; set; }
-
-        private byte[] SourceBytes { get; set; }
-
-        /// <summary>
-        /// Gets the options.
-        /// </summary>
-        /// <value>The options.</value>
-        public IDictionary<string, string> Headers => _options;
-
-        /// <summary>
-        /// Fires when complete.
-        /// </summary>
-        public Action OnComplete { get; set; }
-
-        /// <summary>
-        /// Fires when an error occours.
-        /// </summary>
-        public Action OnError { get; set; }
-
-        /// <inheritdoc />
-        public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
-        {
-            try
-            {
-                var bytes = SourceBytes;
-
-                if (bytes != null)
-                {
-                    await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
-                }
-                else
-                {
-                    using (var src = SourceStream)
-                    {
-                        await src.CopyToAsync(responseStream, cancellationToken).ConfigureAwait(false);
-                    }
-                }
-            }
-            catch
-            {
-                OnError?.Invoke();
-
-                throw;
-            }
-            finally
-            {
-                OnComplete?.Invoke();
-            }
-        }
-    }
-}

+ 7 - 1
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -179,7 +179,7 @@ namespace Emby.Server.Implementations.HttpServer
                 return;
             }
 
-            WebSocketMessage<object> stub;
+            WebSocketMessage<object>? stub;
             try
             {
 
@@ -209,6 +209,12 @@ namespace Emby.Server.Implementations.HttpServer
                 return;
             }
 
+            if (stub == null)
+            {
+                _logger.LogError("Error processing web socket message");
+                return;
+            }
+
             // Tell the PipeReader how much of the buffer we have consumed
             reader.AdvanceTo(buffer.End);
 

+ 102 - 0
Emby.Server.Implementations/HttpServer/WebSocketManager.cs

@@ -0,0 +1,102 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using Jellyfin.Data.Events;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.HttpServer
+{
+    public class WebSocketManager : IWebSocketManager
+    {
+        private readonly ILogger<WebSocketManager> _logger;
+        private readonly ILoggerFactory _loggerFactory;
+
+        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
+        private bool _disposed = false;
+
+        public WebSocketManager(
+            ILogger<WebSocketManager> logger,
+            ILoggerFactory loggerFactory)
+        {
+            _logger = logger;
+            _loggerFactory = loggerFactory;
+        }
+
+        public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
+
+        /// <inheritdoc />
+        public async Task WebSocketRequestHandler(HttpContext context)
+        {
+            if (_disposed)
+            {
+                return;
+            }
+
+            try
+            {
+                _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
+
+                WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
+
+                using var connection = new WebSocketConnection(
+                    _loggerFactory.CreateLogger<WebSocketConnection>(),
+                    webSocket,
+                    context.Connection.RemoteIpAddress,
+                    context.Request.Query)
+                {
+                    OnReceive = ProcessWebSocketMessageReceived
+                };
+
+                WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
+
+                await connection.ProcessAsync().ConfigureAwait(false);
+                _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
+            }
+            catch (Exception ex) // Otherwise ASP.Net will ignore the exception
+            {
+                _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
+                if (!context.Response.HasStarted)
+                {
+                    context.Response.StatusCode = 500;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Adds the rest handlers.
+        /// </summary>
+        /// <param name="listeners">The web socket listeners.</param>
+        public void Init(IEnumerable<IWebSocketListener> listeners)
+        {
+            _webSocketListeners = listeners.ToArray();
+        }
+
+        /// <summary>
+        /// Processes the web socket message received.
+        /// </summary>
+        /// <param name="result">The result.</param>
+        private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
+        {
+            if (_disposed)
+            {
+                return Task.CompletedTask;
+            }
+
+            IEnumerable<Task> GetTasks()
+            {
+                foreach (var x in _webSocketListeners)
+                {
+                    yield return x.ProcessMessageAsync(result);
+                }
+            }
+
+            return Task.WhenAll(GetTasks());
+        }
+    }
+}

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

@@ -6,12 +6,11 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Threading.Tasks;
+using Emby.Server.Implementations.Library;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Model.IO;
-using Emby.Server.Implementations.Library;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.IO
@@ -38,6 +37,8 @@ namespace Emby.Server.Implementations.IO
         /// </summary>
         private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
+        private bool _disposed = false;
+
         /// <summary>
         /// Add the path to our temporary ignore list.  Use when writing to a path within our listening scope.
         /// </summary>
@@ -492,8 +493,6 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
-        private bool _disposed = false;
-
         /// <summary>
         /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
         /// </summary>
@@ -522,24 +521,4 @@ namespace Emby.Server.Implementations.IO
             _disposed = true;
         }
     }
-
-    public class LibraryMonitorStartup : IServerEntryPoint
-    {
-        private readonly ILibraryMonitor _monitor;
-
-        public LibraryMonitorStartup(ILibraryMonitor monitor)
-        {
-            _monitor = monitor;
-        }
-
-        public Task RunAsync()
-        {
-            _monitor.Start();
-            return Task.CompletedTask;
-        }
-
-        public void Dispose()
-        {
-        }
-    }
 }

+ 35 - 0
Emby.Server.Implementations/IO/LibraryMonitorStartup.cs

@@ -0,0 +1,35 @@
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Plugins;
+
+namespace Emby.Server.Implementations.IO
+{
+    /// <summary>
+    /// <see cref="IServerEntryPoint" /> which is responsible for starting the library monitor.
+    /// </summary>
+    public sealed class LibraryMonitorStartup : IServerEntryPoint
+    {
+        private readonly ILibraryMonitor _monitor;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LibraryMonitorStartup"/> class.
+        /// </summary>
+        /// <param name="monitor">The library monitor.</param>
+        public LibraryMonitorStartup(ILibraryMonitor monitor)
+        {
+            _monitor = monitor;
+        }
+
+        /// <inheritdoc />
+        public Task RunAsync()
+        {
+            _monitor.Start();
+            return Task.CompletedTask;
+        }
+
+        /// <inheritdoc />
+        public void Dispose()
+        {
+        }
+    }
+}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно