Bläddra i källkod

Merge remote-tracking branch 'upstream/master' into api-stream-return

crobibero 4 år sedan
förälder
incheckning
d5eb246557
96 ändrade filer med 839 tillägg och 1622 borttagningar
  1. 1 1
      Dockerfile
  2. 1 1
      Dockerfile.arm
  3. 1 1
      Dockerfile.arm64
  4. 3 3
      Emby.Dlna/ConnectionManager/ConnectionManagerService.cs
  5. 2 2
      Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
  6. 1 0
      Emby.Dlna/Emby.Dlna.csproj
  7. 12 18
      Emby.Dlna/Eventing/DlnaEventManager.cs
  8. 8 7
      Emby.Dlna/Main/DlnaEntryPoint.cs
  9. 3 3
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs
  10. 21 20
      Emby.Dlna/PlayTo/Device.cs
  11. 5 4
      Emby.Dlna/PlayTo/PlayToManager.cs
  12. 45 71
      Emby.Dlna/PlayTo/SsdpHttpClient.cs
  13. 3 7
      Emby.Dlna/Service/BaseService.cs
  14. 13 22
      Emby.Server.Implementations/ApplicationHost.cs
  15. 0 335
      Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
  16. 18 32
      Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
  17. 5 4
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  18. 138 240
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  19. 7 24
      Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
  20. 40 58
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  21. 7 6
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  22. 6 10
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  23. 17 30
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  24. 16 16
      Emby.Server.Implementations/Localization/Core/ta.json
  25. 36 52
      Emby.Server.Implementations/Updates/InstallationManager.cs
  26. 5 1
      Jellyfin.Api/BaseJellyfinApiController.cs
  27. 2 99
      Jellyfin.Api/Controllers/DashboardController.cs
  28. 2 1
      Jellyfin.Api/Controllers/LiveTvController.cs
  29. 2 1
      Jellyfin.Api/Controllers/RemoteImageController.cs
  30. 2 1
      Jellyfin.Api/Controllers/VideosController.cs
  31. 2 1
      Jellyfin.Api/Helpers/AudioHelper.cs
  32. 0 56
      Jellyfin.Api/MvcRoutePrefix.cs
  33. 10 3
      Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
  34. 5 6
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  35. 1 1
      Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
  36. 3 2
      Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
  37. 2 1
      Jellyfin.Server/Formatters/XmlOutputFormatter.cs
  38. 0 36
      Jellyfin.Server/HealthChecks/JellyfinDbHealthCheck.cs
  39. 1 0
      Jellyfin.Server/Jellyfin.Server.csproj
  40. 1 5
      Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
  41. 1 1
      Jellyfin.Server/Program.cs
  42. 60 38
      Jellyfin.Server/Startup.cs
  43. 1 4
      MediaBrowser.Common/Configuration/IApplicationPaths.cs
  44. 10 0
      MediaBrowser.Common/Json/JsonDefaults.cs
  45. 0 11
      MediaBrowser.Common/Net/CacheMode.cs
  46. 0 15
      MediaBrowser.Common/Net/CompressionMethods.cs
  47. 0 105
      MediaBrowser.Common/Net/HttpRequestOptions.cs
  48. 0 80
      MediaBrowser.Common/Net/HttpResponseInfo.cs
  49. 0 53
      MediaBrowser.Common/Net/IHttpClient.cs
  50. 51 8
      MediaBrowser.Controller/Entities/BaseItem.cs
  51. 123 56
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  52. 1 0
      MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
  53. 14 0
      MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
  54. 10 0
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  55. 12 15
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  56. 28 1
      MediaBrowser.Model/Configuration/EncodingOptions.cs
  57. 0 6
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  58. 12 6
      MediaBrowser.Model/Entities/MediaStream.cs
  59. 1 1
      MediaBrowser.Model/Net/MimeTypes.cs
  60. 1 1
      MediaBrowser.Providers/Manager/ProviderManager.cs
  61. 2 1
      MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
  62. 2 1
      MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
  63. 2 1
      MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
  64. 2 1
      MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
  65. 2 1
      MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs
  66. 2 1
      MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
  67. 3 2
      MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
  68. 3 2
      MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
  69. 2 1
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs
  70. 2 1
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
  71. 2 1
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs
  72. 2 1
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs
  73. 2 1
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs
  74. 2 1
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
  75. 2 1
      MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
  76. 2 1
      MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
  77. 2 1
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
  78. 3 2
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
  79. 2 1
      MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
  80. 2 1
      MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
  81. 2 1
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
  82. 2 1
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
  83. 2 1
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
  84. 2 1
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
  85. 2 1
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
  86. 2 1
      MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
  87. 3 2
      MediaBrowser.Providers/Studios/StudiosImageProvider.cs
  88. 1 1
      debian/rules
  89. 1 1
      deployment/Dockerfile.docker.amd64
  90. 1 1
      deployment/Dockerfile.docker.arm64
  91. 1 1
      deployment/Dockerfile.docker.armhf
  92. 1 1
      deployment/build.linux.amd64
  93. 1 1
      deployment/build.macos
  94. 1 1
      deployment/build.portable
  95. 1 1
      deployment/build.windows.amd64
  96. 1 1
      fedora/jellyfin.spec

+ 1 - 1
Dockerfile

@@ -14,7 +14,7 @@ COPY . .
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 # because of changes in docker and systemd we need to not build in parallel at the moment
 # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=true;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
 
 FROM debian:buster-slim
 

+ 1 - 1
Dockerfile.arm

@@ -21,7 +21,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 # Discard objs - may cause failures if exists
 RUN find . -type d -name obj | xargs -r rm -r
 # Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=true;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
 
 
 FROM multiarch/qemu-user-static:x86_64-arm as qemu

+ 1 - 1
Dockerfile.arm64

@@ -21,7 +21,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 # Discard objs - may cause failures if exists
 RUN find . -type d -name obj | xargs -r rm -r
 # Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=true;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
 
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
 FROM arm64v8/debian:buster-slim

+ 3 - 3
Emby.Dlna/ConnectionManager/ConnectionManagerService.cs

@@ -1,8 +1,8 @@
 #pragma warning disable CS1591
 
+using System.Net.Http;
 using System.Threading.Tasks;
 using Emby.Dlna.Service;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
 using Microsoft.Extensions.Logging;
@@ -18,8 +18,8 @@ namespace Emby.Dlna.ConnectionManager
             IDlnaManager dlna,
             IServerConfigurationManager config,
             ILogger<ConnectionManagerService> logger,
-            IHttpClient httpClient)
-            : base(logger, httpClient)
+            IHttpClientFactory httpClientFactory)
+            : base(logger, httpClientFactory)
         {
             _dlna = dlna;
             _config = config;

+ 2 - 2
Emby.Dlna/ContentDirectory/ContentDirectoryService.cs

@@ -2,11 +2,11 @@
 
 using System;
 using System.Linq;
+using System.Net.Http;
 using System.Threading.Tasks;
 using Emby.Dlna.Service;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
@@ -41,7 +41,7 @@ namespace Emby.Dlna.ContentDirectory
             IServerConfigurationManager config,
             IUserManager userManager,
             ILogger<ContentDirectoryService> logger,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClient,
             ILocalizationManager localization,
             IMediaSourceManager mediaSourceManager,
             IUserViewManager userViewManager,

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

@@ -80,6 +80,7 @@
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
     <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
   </ItemGroup>
 
 </Project>

+ 12 - 18
Emby.Dlna/Eventing/DlnaEventManager.cs

@@ -6,6 +6,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Net.Http;
+using System.Net.Mime;
 using System.Text;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
@@ -20,13 +21,13 @@ namespace Emby.Dlna.Eventing
             new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
 
         private readonly ILogger _logger;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
 
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
-        public DlnaEventManager(ILogger logger, IHttpClient httpClient)
+        public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _logger = logger;
         }
 
@@ -167,24 +168,17 @@ namespace Emby.Dlna.Eventing
 
             builder.Append("</e:propertyset>");
 
-            var options = new HttpRequestOptions
-            {
-                RequestContent = builder.ToString(),
-                RequestContentType = "text/xml",
-                Url = subscription.CallbackUrl,
-                BufferContent = false
-            };
-
-            options.RequestHeaders.Add("NT", subscription.NotificationType);
-            options.RequestHeaders.Add("NTS", "upnp:propchange");
-            options.RequestHeaders.Add("SID", subscription.Id);
-            options.RequestHeaders.Add("SEQ", subscription.TriggerCount.ToString(_usCulture));
+            using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"),  subscription.CallbackUrl);
+            options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
+            options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
+            options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
+            options.Headers.TryAddWithoutValidation("SID", subscription.Id);
+            options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture));
 
             try
             {
-                using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false))
-                {
-                }
+                using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                    .SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
             }
             catch (OperationCanceledException)
             {

+ 8 - 7
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -2,6 +2,7 @@
 
 using System;
 using System.Globalization;
+using System.Net.Http;
 using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
@@ -36,7 +37,7 @@ namespace Emby.Dlna.Main
         private readonly ILogger<DlnaEntryPoint> _logger;
         private readonly IServerApplicationHost _appHost;
         private readonly ISessionManager _sessionManager;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILibraryManager _libraryManager;
         private readonly IUserManager _userManager;
         private readonly IDlnaManager _dlnaManager;
@@ -61,7 +62,7 @@ namespace Emby.Dlna.Main
             ILoggerFactory loggerFactory,
             IServerApplicationHost appHost,
             ISessionManager sessionManager,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILibraryManager libraryManager,
             IUserManager userManager,
             IDlnaManager dlnaManager,
@@ -79,7 +80,7 @@ namespace Emby.Dlna.Main
             _config = config;
             _appHost = appHost;
             _sessionManager = sessionManager;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _libraryManager = libraryManager;
             _userManager = userManager;
             _dlnaManager = dlnaManager;
@@ -101,7 +102,7 @@ namespace Emby.Dlna.Main
                 config,
                 userManager,
                 loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
-                httpClient,
+                httpClientFactory,
                 localizationManager,
                 mediaSourceManager,
                 userViewManager,
@@ -112,11 +113,11 @@ namespace Emby.Dlna.Main
                 dlnaManager,
                 config,
                 loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
-                httpClient);
+                httpClientFactory);
 
             MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
                 loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
-                httpClient,
+                httpClientFactory,
                 config);
             Current = this;
         }
@@ -364,7 +365,7 @@ namespace Emby.Dlna.Main
                         _appHost,
                         _imageProcessor,
                         _deviceDiscovery,
-                        _httpClient,
+                        _httpClientFactory,
                         _config,
                         _userDataManager,
                         _localization,

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

@@ -1,8 +1,8 @@
 #pragma warning disable CS1591
 
+using System.Net.Http;
 using System.Threading.Tasks;
 using Emby.Dlna.Service;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using Microsoft.Extensions.Logging;
 
@@ -14,9 +14,9 @@ namespace Emby.Dlna.MediaReceiverRegistrar
 
         public MediaReceiverRegistrarService(
             ILogger<MediaReceiverRegistrarService> logger,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IServerConfigurationManager config)
-            : base(logger, httpClient)
+            : base(logger, httpClientFactory)
         {
             _config = config;
         }

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

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Net.Http;
 using System.Security;
 using System.Threading;
 using System.Threading.Tasks;
@@ -21,7 +22,7 @@ namespace Emby.Dlna.PlayTo
     {
         private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
 
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
 
         private readonly ILogger _logger;
 
@@ -34,10 +35,10 @@ namespace Emby.Dlna.PlayTo
         private int _connectFailureCount;
         private bool _disposed;
 
-        public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger)
+        public Device(DeviceInfo deviceProperties, IHttpClientFactory httpClientFactory, ILogger logger)
         {
             Properties = deviceProperties;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _logger = logger;
         }
 
@@ -236,7 +237,7 @@ namespace Emby.Dlna.PlayTo
             _logger.LogDebug("Setting mute");
             var value = mute ? 1 : 0;
 
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
                 .ConfigureAwait(false);
 
             IsMuted = mute;
@@ -271,7 +272,7 @@ namespace Emby.Dlna.PlayTo
             // Remote control will perform better
             Volume = value;
 
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
                 .ConfigureAwait(false);
         }
 
@@ -292,7 +293,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(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
+            await new SsdpHttpClient(_httpClientFactory).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);
@@ -326,7 +327,7 @@ namespace Emby.Dlna.PlayTo
             }
 
             var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
                 .ConfigureAwait(false);
 
             await Task.Delay(50).ConfigureAwait(false);
@@ -368,7 +369,7 @@ namespace Emby.Dlna.PlayTo
                 throw new InvalidOperationException("Unable to find service");
             }
 
-            return new SsdpHttpClient(_httpClient).SendCommandAsync(
+            return new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -397,7 +398,7 @@ namespace Emby.Dlna.PlayTo
 
             var service = GetAvTransportService();
 
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
                 .ConfigureAwait(false);
 
             RestartTimer(true);
@@ -415,7 +416,7 @@ namespace Emby.Dlna.PlayTo
 
             var service = GetAvTransportService();
 
-            await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
                 .ConfigureAwait(false);
 
             TransportState = TransportState.Paused;
@@ -542,7 +543,7 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
+            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -592,7 +593,7 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
+            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -625,7 +626,7 @@ namespace Emby.Dlna.PlayTo
                 return null;
             }
 
-            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
+            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -667,7 +668,7 @@ namespace Emby.Dlna.PlayTo
 
             var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
 
-            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
+            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -734,7 +735,7 @@ namespace Emby.Dlna.PlayTo
 
             var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
 
-            var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
+            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -912,7 +913,7 @@ namespace Emby.Dlna.PlayTo
 
             string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
 
-            var httpClient = new SsdpHttpClient(_httpClient);
+            var httpClient = new SsdpHttpClient(_httpClientFactory);
 
             var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
 
@@ -940,7 +941,7 @@ namespace Emby.Dlna.PlayTo
 
             string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
 
-            var httpClient = new SsdpHttpClient(_httpClient);
+            var httpClient = new SsdpHttpClient(_httpClientFactory);
             _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
             var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
 
@@ -969,9 +970,9 @@ namespace Emby.Dlna.PlayTo
             return baseUrl + url;
         }
 
-        public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, ILogger logger, CancellationToken cancellationToken)
+        public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
         {
-            var ssdpHttpClient = new SsdpHttpClient(httpClient);
+            var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
 
             var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
 
@@ -1079,7 +1080,7 @@ namespace Emby.Dlna.PlayTo
                 }
             }
 
-            return new Device(deviceProperties, httpClient, logger);
+            return new Device(deviceProperties, httpClientFactory, logger);
         }
 
         private static DeviceIcon CreateIcon(XElement element)

+ 5 - 4
Emby.Dlna/PlayTo/PlayToManager.cs

@@ -4,6 +4,7 @@ using System;
 using System.Globalization;
 using System.Linq;
 using System.Net;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Events;
@@ -33,7 +34,7 @@ namespace Emby.Dlna.PlayTo
         private readonly IDlnaManager _dlnaManager;
         private readonly IServerApplicationHost _appHost;
         private readonly IImageProcessor _imageProcessor;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerConfigurationManager _config;
         private readonly IUserDataManager _userDataManager;
         private readonly ILocalizationManager _localization;
@@ -46,7 +47,7 @@ namespace Emby.Dlna.PlayTo
         private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
         private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
 
-        public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
+        public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
         {
             _logger = logger;
             _sessionManager = sessionManager;
@@ -56,7 +57,7 @@ namespace Emby.Dlna.PlayTo
             _appHost = appHost;
             _imageProcessor = imageProcessor;
             _deviceDiscovery = deviceDiscovery;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _config = config;
             _userDataManager = userDataManager;
             _localization = localization;
@@ -174,7 +175,7 @@ namespace Emby.Dlna.PlayTo
 
             if (controller == null)
             {
-                var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _logger, cancellationToken).ConfigureAwait(false);
+                var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
 
                 string deviceName = device.Properties.Name;
 

+ 45 - 71
Emby.Dlna/PlayTo/SsdpHttpClient.cs

@@ -4,6 +4,8 @@ using System;
 using System.Globalization;
 using System.IO;
 using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
@@ -20,11 +22,11 @@ namespace Emby.Dlna.PlayTo
 
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
 
-        public SsdpHttpClient(IHttpClient httpClient)
+        public SsdpHttpClient(IHttpClientFactory httpClientFactory)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
         }
 
         public async Task<XDocument> SendCommandAsync(
@@ -36,20 +38,18 @@ namespace Emby.Dlna.PlayTo
             CancellationToken cancellationToken = default)
         {
             var url = NormalizeServiceUrl(baseUrl, service.ControlUrl);
-            using (var response = await PostSoapDataAsync(
-                url,
-                $"\"{service.ServiceType}#{command}\"",
-                postData,
-                header,
-                cancellationToken)
-                .ConfigureAwait(false))
-            using (var stream = response.Content)
-            using (var reader = new StreamReader(stream, Encoding.UTF8))
-            {
-                return XDocument.Parse(
-                    await reader.ReadToEndAsync().ConfigureAwait(false),
-                    LoadOptions.PreserveWhitespace);
-            }
+            using var response = await PostSoapDataAsync(
+                    url,
+                    $"\"{service.ServiceType}#{command}\"",
+                    postData,
+                    header,
+                    cancellationToken)
+                .ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            using var reader = new StreamReader(stream, Encoding.UTF8);
+            return XDocument.Parse(
+                await reader.ReadToEndAsync().ConfigureAwait(false),
+                LoadOptions.PreserveWhitespace);
         }
 
         private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
@@ -76,49 +76,32 @@ namespace Emby.Dlna.PlayTo
             int eventport,
             int timeOut = 3600)
         {
-            var options = new HttpRequestOptions
-            {
-                Url = url,
-                UserAgent = USERAGENT,
-                LogErrorResponseBody = true,
-                BufferContent = false,
-            };
-
-            options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture);
-            options.RequestHeaders["CALLBACK"] = "<" + localIp + ":" + eventport.ToString(_usCulture) + ">";
-            options.RequestHeaders["NT"] = "upnp:event";
-            options.RequestHeaders["TIMEOUT"] = "Second-" + timeOut.ToString(_usCulture);
-
-            using (await _httpClient.SendAsync(options, new HttpMethod("SUBSCRIBE")).ConfigureAwait(false))
-            {
-            }
+            using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
+            options.Headers.UserAgent.ParseAdd(USERAGENT);
+            options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(_usCulture));
+            options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">");
+            options.Headers.TryAddWithoutValidation("NT", "upnp:event");
+            options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture));
+
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                .SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
+                .ConfigureAwait(false);
         }
 
         public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken)
         {
-            var options = new HttpRequestOptions
-            {
-                Url = url,
-                UserAgent = USERAGENT,
-                LogErrorResponseBody = true,
-                BufferContent = false,
-
-                CancellationToken = cancellationToken
-            };
-
-            options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName;
-
-            using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false))
-            using (var stream = response.Content)
-            using (var reader = new StreamReader(stream, Encoding.UTF8))
-            {
-                return XDocument.Parse(
-                    await reader.ReadToEndAsync().ConfigureAwait(false),
-                    LoadOptions.PreserveWhitespace);
-            }
+            using var options = new HttpRequestMessage(HttpMethod.Get, url);
+            options.Headers.UserAgent.ParseAdd(USERAGENT);
+            options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            using var reader = new StreamReader(stream, Encoding.UTF8);
+            return XDocument.Parse(
+                await reader.ReadToEndAsync().ConfigureAwait(false),
+                LoadOptions.PreserveWhitespace);
         }
 
-        private Task<HttpResponseInfo> PostSoapDataAsync(
+        private Task<HttpResponseMessage> PostSoapDataAsync(
             string url,
             string soapAction,
             string postData,
@@ -130,29 +113,20 @@ namespace Emby.Dlna.PlayTo
                 soapAction = $"\"{soapAction}\"";
             }
 
-            var options = new HttpRequestOptions
-            {
-                Url = url,
-                UserAgent = USERAGENT,
-                LogErrorResponseBody = true,
-                BufferContent = false,
-
-                CancellationToken = cancellationToken
-            };
-
-            options.RequestHeaders["SOAPAction"] = soapAction;
-            options.RequestHeaders["Pragma"] = "no-cache";
-            options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName;
+            using var options = new HttpRequestMessage(HttpMethod.Post, url);
+            options.Headers.UserAgent.ParseAdd(USERAGENT);
+            options.Headers.TryAddWithoutValidation("SOAPACTION", soapAction);
+            options.Headers.TryAddWithoutValidation("Pragma", "no-cache");
+            options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
 
             if (!string.IsNullOrEmpty(header))
             {
-                options.RequestHeaders["contentFeatures.dlna.org"] = header;
+                options.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header);
             }
 
-            options.RequestContentType = "text/xml";
-            options.RequestContent = postData;
+            options.Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml);
 
-            return _httpClient.Post(options);
+            return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
         }
     }
 }

+ 3 - 7
Emby.Dlna/Service/BaseService.cs

@@ -1,25 +1,21 @@
 #pragma warning disable CS1591
 
+using System.Net.Http;
 using Emby.Dlna.Eventing;
-using MediaBrowser.Common.Net;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.Service
 {
     public class BaseService : IDlnaEventManager
     {
-        protected BaseService(ILogger<BaseService> logger, IHttpClient httpClient)
+        protected BaseService(ILogger<BaseService> logger, IHttpClientFactory httpClientFactory)
         {
             Logger = logger;
-            HttpClient = httpClient;
-
-            EventManager = new DlnaEventManager(logger, HttpClient);
+            EventManager = new DlnaEventManager(logger, httpClientFactory);
         }
 
         protected IDlnaEventManager EventManager { get; }
 
-        protected IHttpClient HttpClient { get; }
-
         protected ILogger Logger { get; }
 
         public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)

+ 13 - 22
Emby.Server.Implementations/ApplicationHost.cs

@@ -49,6 +49,7 @@ using Jellyfin.Api.Helpers;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Json;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
@@ -122,8 +123,8 @@ namespace Emby.Server.Implementations
 
         private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
+        private IHttpClientFactory _httpClientFactory;
         private IWebSocketManager _webSocketManager;
-        private IHttpClient _httpClient;
 
         private string[] _urlPrefixes;
 
@@ -526,8 +527,6 @@ namespace Emby.Server.Implementations
             ServiceCollection.AddSingleton(_fileSystemManager);
             ServiceCollection.AddSingleton<TvdbClientManager>();
 
-            ServiceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
-
             ServiceCollection.AddSingleton(_networkManager);
 
             ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
@@ -654,8 +653,8 @@ namespace Emby.Server.Implementations
 
             _mediaEncoder = Resolve<IMediaEncoder>();
             _sessionManager = Resolve<ISessionManager>();
+            _httpClientFactory = Resolve<IHttpClientFactory>();
             _webSocketManager = Resolve<IWebSocketManager>();
-            _httpClient = Resolve<IHttpClient>();
 
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
 
@@ -1300,25 +1299,17 @@ namespace Emby.Server.Implementations
 
             try
             {
-                using (var response = await _httpClient.SendAsync(
-                    new HttpRequestOptions
-                    {
-                        Url = apiUrl,
-                        LogErrorResponseBody = false,
-                        BufferContent = false,
-                        CancellationToken = cancellationToken
-                    }, HttpMethod.Post).ConfigureAwait(false))
-                {
-                    using (var reader = new StreamReader(response.Content))
-                    {
-                        var result = await reader.ReadToEndAsync().ConfigureAwait(false);
-                        var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
+                using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl);
+                using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                    .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
 
-                        _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid);
-                        Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid);
-                        return valid;
-                    }
-                }
+                await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                var result = await System.Text.Json.JsonSerializer.DeserializeAsync<string>(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false);
+                var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
+
+                _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid);
+                Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid);
+                return valid;
             }
             catch (OperationCanceledException)
             {

+ 0 - 335
Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs

@@ -1,335 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpClientManager
-{
-    /// <summary>
-    /// Class HttpClientManager.
-    /// </summary>
-    public class HttpClientManager : IHttpClient
-    {
-        private readonly ILogger<HttpClientManager> _logger;
-        private readonly IApplicationPaths _appPaths;
-        private readonly IFileSystem _fileSystem;
-        private readonly IApplicationHost _appHost;
-
-        /// <summary>
-        /// Holds a dictionary of http clients by host.  Use GetHttpClient(host) to retrieve or create a client for web requests.
-        /// DON'T dispose it after use.
-        /// </summary>
-        /// <value>The HTTP clients.</value>
-        private readonly ConcurrentDictionary<string, HttpClient> _httpClients = new ConcurrentDictionary<string, HttpClient>();
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="HttpClientManager" /> class.
-        /// </summary>
-        public HttpClientManager(
-            IApplicationPaths appPaths,
-            ILogger<HttpClientManager> logger,
-            IFileSystem fileSystem,
-            IApplicationHost appHost)
-        {
-            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
-            _fileSystem = fileSystem;
-            _appPaths = appPaths ?? throw new ArgumentNullException(nameof(appPaths));
-            _appHost = appHost;
-        }
-
-        /// <summary>
-        /// Gets the correct http client for the given url.
-        /// </summary>
-        /// <param name="url">The url.</param>
-        /// <returns>HttpClient.</returns>
-        private HttpClient GetHttpClient(string url)
-        {
-            var key = GetHostFromUrl(url);
-
-            if (!_httpClients.TryGetValue(key, out var client))
-            {
-                client = new HttpClient()
-                {
-                    BaseAddress = new Uri(url)
-                };
-
-                _httpClients.TryAdd(key, client);
-            }
-
-            return client;
-        }
-
-        private HttpRequestMessage GetRequestMessage(HttpRequestOptions options, HttpMethod method)
-        {
-            string url = options.Url;
-            var uriAddress = new Uri(url);
-            string userInfo = uriAddress.UserInfo;
-            if (!string.IsNullOrWhiteSpace(userInfo))
-            {
-                _logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url);
-                url = url.Replace(userInfo + '@', string.Empty, StringComparison.Ordinal);
-            }
-
-            var request = new HttpRequestMessage(method, url);
-
-            foreach (var header in options.RequestHeaders)
-            {
-                request.Headers.TryAddWithoutValidation(header.Key, header.Value);
-            }
-
-            if (options.EnableDefaultUserAgent
-                && !request.Headers.TryGetValues(HeaderNames.UserAgent, out _))
-            {
-                request.Headers.Add(HeaderNames.UserAgent, _appHost.ApplicationUserAgent);
-            }
-
-            switch (options.DecompressionMethod)
-            {
-                case CompressionMethods.Deflate | CompressionMethods.Gzip:
-                    request.Headers.Add(HeaderNames.AcceptEncoding, new[] { "gzip", "deflate" });
-                    break;
-                case CompressionMethods.Deflate:
-                    request.Headers.Add(HeaderNames.AcceptEncoding, "deflate");
-                    break;
-                case CompressionMethods.Gzip:
-                    request.Headers.Add(HeaderNames.AcceptEncoding, "gzip");
-                    break;
-                default:
-                    break;
-            }
-
-            if (options.EnableKeepAlive)
-            {
-                request.Headers.Add(HeaderNames.Connection, "Keep-Alive");
-            }
-
-            // request.Headers.Add(HeaderNames.CacheControl, "no-cache");
-
-            /*
-            if (!string.IsNullOrWhiteSpace(userInfo))
-            {
-                var parts = userInfo.Split(':');
-                if (parts.Length == 2)
-                {
-                    request.Headers.Add(HeaderNames., GetCredential(url, parts[0], parts[1]);
-                }
-            }
-            */
-
-            return request;
-        }
-
-        /// <summary>
-        /// Gets the response internal.
-        /// </summary>
-        /// <param name="options">The options.</param>
-        /// <returns>Task{HttpResponseInfo}.</returns>
-        public Task<HttpResponseInfo> GetResponse(HttpRequestOptions options)
-            => SendAsync(options, HttpMethod.Get);
-
-        /// <summary>
-        /// Performs a GET request and returns the resulting stream.
-        /// </summary>
-        /// <param name="options">The options.</param>
-        /// <returns>Task{Stream}.</returns>
-        public async Task<Stream> Get(HttpRequestOptions options)
-        {
-            var response = await GetResponse(options).ConfigureAwait(false);
-            return response.Content;
-        }
-
-        /// <summary>
-        /// send as an asynchronous operation.
-        /// </summary>
-        /// <param name="options">The options.</param>
-        /// <param name="httpMethod">The HTTP method.</param>
-        /// <returns>Task{HttpResponseInfo}.</returns>
-        public Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod)
-            => SendAsync(options, new HttpMethod(httpMethod));
-
-        /// <summary>
-        /// send as an asynchronous operation.
-        /// </summary>
-        /// <param name="options">The options.</param>
-        /// <param name="httpMethod">The HTTP method.</param>
-        /// <returns>Task{HttpResponseInfo}.</returns>
-        public async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, HttpMethod httpMethod)
-        {
-            if (options.CacheMode == CacheMode.None)
-            {
-                return await SendAsyncInternal(options, httpMethod).ConfigureAwait(false);
-            }
-
-            var url = options.Url;
-            var urlHash = url.ToUpperInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
-
-            var responseCachePath = Path.Combine(_appPaths.CachePath, "httpclient", urlHash);
-
-            var response = GetCachedResponse(responseCachePath, options.CacheLength, url);
-            if (response != null)
-            {
-                return response;
-            }
-
-            response = await SendAsyncInternal(options, httpMethod).ConfigureAwait(false);
-
-            if (response.StatusCode == HttpStatusCode.OK)
-            {
-                await CacheResponse(response, responseCachePath).ConfigureAwait(false);
-            }
-
-            return response;
-        }
-
-        private HttpResponseInfo GetCachedResponse(string responseCachePath, TimeSpan cacheLength, string url)
-        {
-            if (File.Exists(responseCachePath)
-                && _fileSystem.GetLastWriteTimeUtc(responseCachePath).Add(cacheLength) > DateTime.UtcNow)
-            {
-                var stream = new FileStream(responseCachePath, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true);
-
-                return new HttpResponseInfo
-                {
-                    ResponseUrl = url,
-                    Content = stream,
-                    StatusCode = HttpStatusCode.OK,
-                    ContentLength = stream.Length
-                };
-            }
-
-            return null;
-        }
-
-        private async Task CacheResponse(HttpResponseInfo response, string responseCachePath)
-        {
-            Directory.CreateDirectory(Path.GetDirectoryName(responseCachePath));
-
-            using (var fileStream = new FileStream(
-                responseCachePath,
-                FileMode.Create,
-                FileAccess.Write,
-                FileShare.None,
-                IODefaults.FileStreamBufferSize,
-                true))
-            {
-                await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
-
-                response.Content.Position = 0;
-            }
-        }
-
-        private async Task<HttpResponseInfo> SendAsyncInternal(HttpRequestOptions options, HttpMethod httpMethod)
-        {
-            ValidateParams(options);
-
-            options.CancellationToken.ThrowIfCancellationRequested();
-
-            var client = GetHttpClient(options.Url);
-
-            var httpWebRequest = GetRequestMessage(options, httpMethod);
-
-            if (!string.IsNullOrEmpty(options.RequestContent)
-                || httpMethod == HttpMethod.Post)
-            {
-                if (options.RequestContent != null)
-                {
-                    httpWebRequest.Content = new StringContent(
-                        options.RequestContent,
-                        null,
-                        options.RequestContentType);
-                }
-                else
-                {
-                    httpWebRequest.Content = new ByteArrayContent(Array.Empty<byte>());
-                }
-            }
-
-            options.CancellationToken.ThrowIfCancellationRequested();
-
-            var response = await client.SendAsync(
-                httpWebRequest,
-                options.BufferContent || options.CacheMode == CacheMode.Unconditional ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead,
-                options.CancellationToken).ConfigureAwait(false);
-
-            await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
-
-            options.CancellationToken.ThrowIfCancellationRequested();
-
-            var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
-            return new HttpResponseInfo(response.Headers, response.Content.Headers)
-            {
-                Content = stream,
-                StatusCode = response.StatusCode,
-                ContentType = response.Content.Headers.ContentType?.MediaType,
-                ContentLength = response.Content.Headers.ContentLength,
-                ResponseUrl = response.Content.Headers.ContentLocation?.ToString()
-            };
-        }
-
-        /// <inheritdoc />
-        public Task<HttpResponseInfo> Post(HttpRequestOptions options)
-            => SendAsync(options, HttpMethod.Post);
-
-        private void ValidateParams(HttpRequestOptions options)
-        {
-            if (string.IsNullOrEmpty(options.Url))
-            {
-                throw new ArgumentNullException(nameof(options));
-            }
-        }
-
-        /// <summary>
-        /// Gets the host from URL.
-        /// </summary>
-        /// <param name="url">The URL.</param>
-        /// <returns>System.String.</returns>
-        private static string GetHostFromUrl(string url)
-        {
-            var index = url.IndexOf("://", StringComparison.OrdinalIgnoreCase);
-
-            if (index != -1)
-            {
-                url = url.Substring(index + 3);
-                var host = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
-
-                if (!string.IsNullOrWhiteSpace(host))
-                {
-                    return host;
-                }
-            }
-
-            return url;
-        }
-
-        private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options)
-        {
-            if (response.IsSuccessStatusCode)
-            {
-                return;
-            }
-
-            if (options.LogErrorResponseBody)
-            {
-                string msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
-                _logger.LogError("HTTP request failed with message: {Message}", msg);
-            }
-
-            throw new HttpException(response.ReasonPhrase)
-            {
-                StatusCode = response.StatusCode
-            };
-        }
-    }
-}

+ 18 - 32
Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs

@@ -16,13 +16,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
     public class DirectRecorder : IRecorder
     {
         private readonly ILogger _logger;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IStreamHelper _streamHelper;
 
-        public DirectRecorder(ILogger logger, IHttpClient httpClient, IStreamHelper streamHelper)
+        public DirectRecorder(ILogger logger, IHttpClientFactory httpClientFactory, IStreamHelper streamHelper)
         {
             _logger = logger;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _streamHelper = streamHelper;
         }
 
@@ -63,42 +63,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
         private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
         {
-            var httpRequestOptions = new HttpRequestOptions
-            {
-                Url = mediaSource.Path,
-                BufferContent = false,
-
-                // Some remote urls will expect a user agent to be supplied
-                UserAgent = "Emby/3.0",
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                .GetAsync(mediaSource.Path, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
 
-                // Shouldn't matter but may cause issues
-                DecompressionMethod = CompressionMethods.None,
-                CancellationToken = cancellationToken
-            };
+            _logger.LogInformation("Opened recording stream from tuner provider");
 
-            using (var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false))
-            {
-                _logger.LogInformation("Opened recording stream from tuner provider");
+            Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
 
-                Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
+            await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read);
 
-                using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
-                {
-                    onStarted();
+            onStarted();
 
-                    _logger.LogInformation("Copying recording stream to file {0}", targetFile);
+            _logger.LogInformation("Copying recording stream to file {0}", targetFile);
 
-                    // The media source if infinite so we need to handle stopping ourselves
-                    using var durationToken = new CancellationTokenSource(duration);
-                    using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
+            // The media source if infinite so we need to handle stopping ourselves
+            var durationToken = new CancellationTokenSource(duration);
+            cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
 
-                    await _streamHelper.CopyUntilCancelled(
-                        response.Content,
-                        output,
-                        IODefaults.CopyToBufferSize,
-                        cancellationTokenSource.Token).ConfigureAwait(false);
-                }
-            }
+            await _streamHelper.CopyUntilCancelled(
+                await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+                output,
+                IODefaults.CopyToBufferSize,
+                cancellationToken).ConfigureAwait(false);
 
             _logger.LogInformation("Recording completed to file {0}", targetFile);
         }

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

@@ -7,6 +7,7 @@ using System.Diagnostics;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Net.Http;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
@@ -48,7 +49,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
         private readonly IServerApplicationHost _appHost;
         private readonly ILogger<EmbyTV> _logger;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerConfigurationManager _config;
         private readonly IJsonSerializer _jsonSerializer;
 
@@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             IMediaSourceManager mediaSourceManager,
             ILogger<EmbyTV> logger,
             IJsonSerializer jsonSerializer,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IServerConfigurationManager config,
             ILiveTvManager liveTvManager,
             IFileSystem fileSystem,
@@ -94,7 +95,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
             _appHost = appHost;
             _logger = logger;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _config = config;
             _fileSystem = fileSystem;
             _libraryManager = libraryManager;
@@ -1637,7 +1638,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer);
             }
 
-            return new DirectRecorder(_logger, _httpClient, _streamHelper);
+            return new DirectRecorder(_logger, _httpClientFactory, _streamHelper);
         }
 
         private void OnSuccessfulRecording(TimerInfo timer, string path)

+ 138 - 240
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -8,6 +8,8 @@ using System.IO;
 using System.Linq;
 using System.Net;
 using System.Net.Http;
+using System.Net.Mime;
+using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
@@ -28,19 +30,19 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
         private readonly ILogger<SchedulesDirect> _logger;
         private readonly IJsonSerializer _jsonSerializer;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
         private readonly IApplicationHost _appHost;
 
         public SchedulesDirect(
             ILogger<SchedulesDirect> logger,
             IJsonSerializer jsonSerializer,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IApplicationHost appHost)
         {
             _logger = logger;
             _jsonSerializer = jsonSerializer;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
         }
 
@@ -102,95 +104,78 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             var requestString = _jsonSerializer.SerializeToString(requestList);
             _logger.LogDebug("Request string for schedules is: {RequestString}", requestString);
 
-            var httpOptions = new HttpRequestOptions()
-            {
-                Url = ApiUrl + "/schedules",
-                UserAgent = UserAgent,
-                CancellationToken = cancellationToken,
-                LogErrorResponseBody = true,
-                RequestContent = requestString
-            };
+            using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules");
+            options.Content = new StringContent(requestString, Encoding.UTF8, MediaTypeNames.Application.Json);
+            options.Headers.TryAddWithoutValidation("token", token);
+            using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
+            await using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(responseStream).ConfigureAwait(false);
+            _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
 
-            httpOptions.RequestHeaders["token"] = token;
+            using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
+            programRequestOptions.Headers.TryAddWithoutValidation("token", token);
 
-            using (var response = await Post(httpOptions, true, info).ConfigureAwait(false))
-            {
-                var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(response.Content).ConfigureAwait(false);
-                _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
+            var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct();
+            programRequestOptions.Content = new StringContent("[\"" + string.Join("\", \"", programsID) + "\"]", Encoding.UTF8, MediaTypeNames.Application.Json);
 
-                httpOptions = new HttpRequestOptions()
-                {
-                    Url = ApiUrl + "/programs",
-                    UserAgent = UserAgent,
-                    CancellationToken = cancellationToken,
-                    LogErrorResponseBody = true
-                };
-
-                httpOptions.RequestHeaders["token"] = token;
+            using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
+            await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream).ConfigureAwait(false);
+            var programDict = programDetails.ToDictionary(p => p.programID, y => y);
 
-                var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct();
-                httpOptions.RequestContent = "[\"" + string.Join("\", \"", programsID) + "\"]";
+            var programIdsWithImages =
+                programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
+                    .ToList();
 
-                using (var innerResponse = await Post(httpOptions, true, info).ConfigureAwait(false))
-                {
-                    var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponse.Content).ConfigureAwait(false);
-                    var programDict = programDetails.ToDictionary(p => p.programID, y => y);
+            var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
 
-                    var programIdsWithImages =
-                        programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
-                        .ToList();
-
-                    var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
+            var programsInfo = new List<ProgramInfo>();
+            foreach (ScheduleDirect.Program schedule in dailySchedules.SelectMany(d => d.programs))
+            {
+                // _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
+                //              " which corresponds to channel " + channelNumber + " and program id " +
+                //              schedule.programID + " which says it has images? " +
+                //              programDict[schedule.programID].hasImageArtwork);
 
-                    var programsInfo = new List<ProgramInfo>();
-                    foreach (ScheduleDirect.Program schedule in dailySchedules.SelectMany(d => d.programs))
+                if (images != null)
+                {
+                    var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10));
+                    if (imageIndex > -1)
                     {
-                        // _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
-                        //              " which corresponds to channel " + channelNumber + " and program id " +
-                        //              schedule.programID + " which says it has images? " +
-                        //              programDict[schedule.programID].hasImageArtwork);
-
-                        if (images != null)
-                        {
-                            var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10));
-                            if (imageIndex > -1)
-                            {
-                                var programEntry = programDict[schedule.programID];
+                        var programEntry = programDict[schedule.programID];
 
-                                var allImages = images[imageIndex].data ?? new List<ScheduleDirect.ImageData>();
-                                var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase));
-                                var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase));
+                        var allImages = images[imageIndex].data ?? new List<ScheduleDirect.ImageData>();
+                        var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase));
+                        var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase));
 
-                                const double DesiredAspect = 2.0 / 3;
+                        const double DesiredAspect = 2.0 / 3;
 
-                                programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
-                                    GetProgramImage(ApiUrl, allImages, true, DesiredAspect);
+                        programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
+                                                    GetProgramImage(ApiUrl, allImages, true, DesiredAspect);
 
-                                const double WideAspect = 16.0 / 9;
+                        const double WideAspect = 16.0 / 9;
 
-                                programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
+                        programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
 
-                                // Don't supply the same image twice
-                                if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal))
-                                {
-                                    programEntry.thumbImage = null;
-                                }
-
-                                programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
-
-                                // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
-                                //    GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
-                                //    GetProgramImage(ApiUrl, data, "Banner-LO", false) ??
-                                //    GetProgramImage(ApiUrl, data, "Banner-LOT", false);
-                            }
+                        // Don't supply the same image twice
+                        if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal))
+                        {
+                            programEntry.thumbImage = null;
                         }
 
-                        programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID]));
-                    }
+                        programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
 
-                    return programsInfo;
+                        // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
+                        //    GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
+                        //    GetProgramImage(ApiUrl, data, "Banner-LO", false) ??
+                        //    GetProgramImage(ApiUrl, data, "Banner-LOT", false);
+                    }
                 }
+
+                programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID]));
             }
+
+            return programsInfo;
         }
 
         private static int GetSizeOrder(ScheduleDirect.ImageData image)
@@ -483,22 +468,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             imageIdString = imageIdString.TrimEnd(',') + "]";
 
-            var httpOptions = new HttpRequestOptions()
-            {
-                Url = ApiUrl + "/metadata/programs",
-                UserAgent = UserAgent,
-                CancellationToken = cancellationToken,
-                RequestContent = imageIdString,
-                LogErrorResponseBody = true,
-            };
+            using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs");
+            message.Content = new StringContent(imageIdString, Encoding.UTF8, MediaTypeNames.Application.Json);
 
             try
             {
-                using (var innerResponse2 = await Post(httpOptions, true, info).ConfigureAwait(false))
-                {
-                    return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>(
-                        innerResponse2.Content).ConfigureAwait(false);
-                }
+                using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
+                await using var response = await innerResponse2.Content.ReadAsStreamAsync();
+                return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>(
+                    response).ConfigureAwait(false);
             }
             catch (Exception ex)
             {
@@ -519,41 +497,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 return lineups;
             }
 
-            var options = new HttpRequestOptions()
-            {
-                Url = ApiUrl + "/headends?country=" + country + "&postalcode=" + location,
-                UserAgent = UserAgent,
-                CancellationToken = cancellationToken,
-                LogErrorResponseBody = true
-            };
-
-            options.RequestHeaders["token"] = token;
+            using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/headends?country=" + country + "&postalcode=" + location);
+            options.Headers.TryAddWithoutValidation("token", token);
 
             try
             {
-                using (var httpResponse = await Get(options, false, info).ConfigureAwait(false))
-                using (Stream responce = httpResponse.Content)
-                {
-                    var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(responce).ConfigureAwait(false);
+                using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
+                await using var response = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+
+                var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(response).ConfigureAwait(false);
 
-                    if (root != null)
+                if (root != null)
+                {
+                    foreach (ScheduleDirect.Headends headend in root)
                     {
-                        foreach (ScheduleDirect.Headends headend in root)
+                        foreach (ScheduleDirect.Lineup lineup in headend.lineups)
                         {
-                            foreach (ScheduleDirect.Lineup lineup in headend.lineups)
+                            lineups.Add(new NameIdPair
                             {
-                                lineups.Add(new NameIdPair
-                                {
-                                    Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name,
-                                    Id = lineup.uri.Substring(18)
-                                });
-                            }
+                                Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name,
+                                Id = lineup.uri.Substring(18)
+                            });
                         }
                     }
-                    else
-                    {
-                        _logger.LogInformation("No lineups available");
-                    }
+                }
+                else
+                {
+                    _logger.LogInformation("No lineups available");
                 }
             }
             catch (Exception ex)
@@ -634,17 +604,16 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             }
         }
 
-        private async Task<HttpResponseInfo> Post(
-            HttpRequestOptions options,
+        private async Task<HttpResponseMessage> Send(
+            HttpRequestMessage options,
             bool enableRetry,
-            ListingsProviderInfo providerInfo)
+            ListingsProviderInfo providerInfo,
+            CancellationToken cancellationToken,
+            HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
         {
-            // Schedules direct requires that the client support compression and will return a 400 response without it
-            options.DecompressionMethod = CompressionMethods.Deflate;
-
             try
             {
-                return await _httpClient.Post(options).ConfigureAwait(false);
+                return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
             }
             catch (HttpException ex)
             {
@@ -661,39 +630,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 }
             }
 
-            options.RequestHeaders["token"] = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false);
-            return await Post(options, false, providerInfo).ConfigureAwait(false);
-        }
-
-        private async Task<HttpResponseInfo> Get(
-            HttpRequestOptions options,
-            bool enableRetry,
-            ListingsProviderInfo providerInfo)
-        {
-            // Schedules direct requires that the client support compression and will return a 400 response without it
-            options.DecompressionMethod = CompressionMethods.Deflate;
-
-            try
-            {
-                return await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
-            }
-            catch (HttpException ex)
-            {
-                _tokens.Clear();
-
-                if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500)
-                {
-                    enableRetry = false;
-                }
-
-                if (!enableRetry)
-                {
-                    throw;
-                }
-            }
-
-            options.RequestHeaders["token"] = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false);
-            return await Get(options, false, providerInfo).ConfigureAwait(false);
+            options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false));
+            return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false);
         }
 
         private async Task<string> GetTokenInternal(
@@ -701,28 +639,19 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             string password,
             CancellationToken cancellationToken)
         {
-            var httpOptions = new HttpRequestOptions()
-            {
-                Url = ApiUrl + "/token",
-                UserAgent = UserAgent,
-                RequestContent = "{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}",
-                CancellationToken = cancellationToken,
-                LogErrorResponseBody = true
-            };
-            // _logger.LogInformation("Obtaining token from Schedules Direct from addres: " + httpOptions.Url + " with body " +
-            // httpOptions.RequestContent);
+            using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
+            options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
 
-            using (var response = await Post(httpOptions, false, null).ConfigureAwait(false))
+            using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
+            if (root.message == "OK")
             {
-                var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(response.Content).ConfigureAwait(false);
-                if (root.message == "OK")
-                {
-                    _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token);
-                    return root.token;
-                }
-
-                throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message);
+                _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token);
+                return root.token;
             }
+
+            throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message);
         }
 
         private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken)
@@ -741,20 +670,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             _logger.LogInformation("Adding new LineUp ");
 
-            var httpOptions = new HttpRequestOptions()
-            {
-                Url = ApiUrl + "/lineups/" + info.ListingsId,
-                UserAgent = UserAgent,
-                CancellationToken = cancellationToken,
-                LogErrorResponseBody = true,
-                BufferContent = false
-            };
-
-            httpOptions.RequestHeaders["token"] = token;
-
-            using (await _httpClient.SendAsync(httpOptions, HttpMethod.Put).ConfigureAwait(false))
-            {
-            }
+            using var options = new HttpRequestMessage(HttpMethod.Put, ApiUrl + "/lineups/" + info.ListingsId);
+            options.Headers.TryAddWithoutValidation("token", token);
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
         }
 
         private async Task<bool> HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken)
@@ -773,25 +691,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             _logger.LogInformation("Headends on account ");
 
-            var options = new HttpRequestOptions()
-            {
-                Url = ApiUrl + "/lineups",
-                UserAgent = UserAgent,
-                CancellationToken = cancellationToken,
-                LogErrorResponseBody = true
-            };
-
-            options.RequestHeaders["token"] = token;
+            using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups");
+            options.Headers.TryAddWithoutValidation("token", token);
 
             try
             {
-                using (var httpResponse = await Get(options, false, null).ConfigureAwait(false))
-                using (var response = httpResponse.Content)
-                {
-                    var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(response).ConfigureAwait(false);
+                using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
+                await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                using var response = httpResponse.Content;
+                var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
 
-                    return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
-                }
+                return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
             }
             catch (HttpException ex)
             {
@@ -856,55 +766,43 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 throw new Exception("token required");
             }
 
-            var httpOptions = new HttpRequestOptions()
-            {
-                Url = ApiUrl + "/lineups/" + listingsId,
-                UserAgent = UserAgent,
-                CancellationToken = cancellationToken,
-                LogErrorResponseBody = true,
-            };
-
-            httpOptions.RequestHeaders["token"] = token;
+            using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId);
+            options.Headers.TryAddWithoutValidation("token", token);
 
             var list = new List<ChannelInfo>();
 
-            using (var httpResponse = await Get(httpOptions, true, info).ConfigureAwait(false))
-            using (var response = httpResponse.Content)
-            {
-                var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(response).ConfigureAwait(false);
-                _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
-                _logger.LogInformation("Mapping Stations to Channel");
-
-                var allStations = root.stations ?? Enumerable.Empty<ScheduleDirect.Station>();
+            using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
+            await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(stream).ConfigureAwait(false);
+            _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
+            _logger.LogInformation("Mapping Stations to Channel");
 
-                foreach (ScheduleDirect.Map map in root.map)
-                {
-                    var channelNumber = GetChannelNumber(map);
+            var allStations = root.stations ?? Enumerable.Empty<ScheduleDirect.Station>();
 
-                    var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase));
-                    if (station == null)
-                    {
-                        station = new ScheduleDirect.Station
-                        {
-                            stationID = map.stationID
-                        };
-                    }
+            foreach (ScheduleDirect.Map map in root.map)
+            {
+                var channelNumber = GetChannelNumber(map);
 
-                    var channelInfo = new ChannelInfo
-                    {
-                        Id = station.stationID,
-                        CallSign = station.callsign,
-                        Number = channelNumber,
-                        Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name
-                    };
+                var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase));
+                if (station == null)
+                {
+                    station = new ScheduleDirect.Station { stationID = map.stationID };
+                }
 
-                    if (station.logo != null)
-                    {
-                        channelInfo.ImageUrl = station.logo.URL;
-                    }
+                var channelInfo = new ChannelInfo
+                {
+                    Id = station.stationID,
+                    CallSign = station.callsign,
+                    Number = channelNumber,
+                    Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name
+                };
 
-                    list.Add(channelInfo);
+                if (station.logo != null)
+                {
+                    channelInfo.ImageUrl = station.logo.URL;
                 }
+
+                list.Add(channelInfo);
             }
 
             return list;

+ 7 - 24
Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs

@@ -25,20 +25,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
     public class XmlTvListingsProvider : IListingsProvider
     {
         private readonly IServerConfigurationManager _config;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILogger<XmlTvListingsProvider> _logger;
         private readonly IFileSystem _fileSystem;
         private readonly IZipClient _zipClient;
 
         public XmlTvListingsProvider(
             IServerConfigurationManager config,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILogger<XmlTvListingsProvider> logger,
             IFileSystem fileSystem,
             IZipClient zipClient)
         {
             _config = config;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _logger = logger;
             _fileSystem = fileSystem;
             _zipClient = zipClient;
@@ -78,28 +78,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
 
-            using (var res = await _httpClient.SendAsync(
-                new HttpRequestOptions
-                {
-                    CancellationToken = cancellationToken,
-                    Url = path,
-                    DecompressionMethod = CompressionMethods.Gzip,
-                },
-                HttpMethod.Get).ConfigureAwait(false))
-            using (var stream = res.Content)
-            using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew))
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew))
             {
-                if (res.ContentHeaders.ContentEncoding.Contains("gzip"))
-                {
-                    using (var gzStream = new GZipStream(stream, CompressionMode.Decompress))
-                    {
-                        await gzStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
-                    }
-                }
-                else
-                {
-                    await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
-                }
+                await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
             }
 
             return UnzipIfNeeded(path, cacheFile);

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

@@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 {
     public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerApplicationHost _appHost;
         private readonly ISocketFactory _socketFactory;
         private readonly INetworkManager _networkManager;
@@ -43,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             IServerConfigurationManager config,
             ILogger<HdHomerunHost> logger,
             IFileSystem fileSystem,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IServerApplicationHost appHost,
             ISocketFactory socketFactory,
             INetworkManager networkManager,
@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             IMemoryCache memoryCache)
             : base(config, logger, fileSystem, memoryCache)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
             _socketFactory = socketFactory;
             _networkManager = networkManager;
@@ -71,15 +71,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         {
             var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
 
-            var options = new HttpRequestOptions
-            {
-                Url = model.LineupURL,
-                CancellationToken = cancellationToken,
-                BufferContent = false
-            };
-
-            using var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
-            await using var stream = response.Content;
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
             var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken)
                 .ConfigureAwait(false) ?? new List<Channels>();
 
@@ -133,14 +126,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
             try
             {
-                using var response = await _httpClient.SendAsync(
-                    new HttpRequestOptions
-                {
-                    Url = string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)),
-                    CancellationToken = cancellationToken,
-                    BufferContent = false
-                }, HttpMethod.Get).ConfigureAwait(false);
-                await using var stream = response.Content;
+                using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                    .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
+                    .ConfigureAwait(false);
+                await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
                 var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken)
                     .ConfigureAwait(false);
 
@@ -183,48 +172,41 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         {
             var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
 
-            using (var response = await _httpClient.SendAsync(
-                new HttpRequestOptions()
-                {
-                    Url = string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)),
-                    CancellationToken = cancellationToken,
-                    BufferContent = false
-                },
-                HttpMethod.Get).ConfigureAwait(false))
-            using (var stream = response.Content)
-            using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
-            {
-                var tuners = new List<LiveTvTunerInfo>();
-                while (!sr.EndOfStream)
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
+                .ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
+            var tuners = new List<LiveTvTunerInfo>();
+            while (!sr.EndOfStream)
+            {
+                string line = StripXML(sr.ReadLine());
+                if (line.Contains("Channel", StringComparison.Ordinal))
                 {
-                    string line = StripXML(sr.ReadLine());
-                    if (line.Contains("Channel", StringComparison.Ordinal))
+                    LiveTvTunerStatus status;
+                    var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
+                    var name = line.Substring(0, index - 1);
+                    var currentChannel = line.Substring(index + 7);
+                    if (currentChannel != "none")
                     {
-                        LiveTvTunerStatus status;
-                        var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
-                        var name = line.Substring(0, index - 1);
-                        var currentChannel = line.Substring(index + 7);
-                        if (currentChannel != "none")
-                        {
-                            status = LiveTvTunerStatus.LiveTv;
-                        }
-                        else
-                        {
-                            status = LiveTvTunerStatus.Available;
-                        }
-
-                        tuners.Add(new LiveTvTunerInfo
-                        {
-                            Name = name,
-                            SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
-                            ProgramName = currentChannel,
-                            Status = status
-                        });
+                        status = LiveTvTunerStatus.LiveTv;
+                    }
+                    else
+                    {
+                        status = LiveTvTunerStatus.Available;
                     }
-                }
 
-                return tuners;
+                    tuners.Add(new LiveTvTunerInfo
+                    {
+                        Name = name,
+                        SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
+                        ProgramName = currentChannel,
+                        Status = status
+                    });
+                }
             }
+
+            return tuners;
         }
 
         private static string StripXML(string source)
@@ -634,7 +616,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                     info,
                     streamId,
                     FileSystem,
-                    _httpClient,
+                    _httpClientFactory,
                     Logger,
                     Config,
                     _appHost,

+ 7 - 6
Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -5,6 +5,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
@@ -26,7 +27,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 {
     public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerApplicationHost _appHost;
         private readonly INetworkManager _networkManager;
         private readonly IMediaSourceManager _mediaSourceManager;
@@ -37,14 +38,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             IMediaSourceManager mediaSourceManager,
             ILogger<M3UTunerHost> logger,
             IFileSystem fileSystem,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IServerApplicationHost appHost,
             INetworkManager networkManager,
             IStreamHelper streamHelper,
             IMemoryCache memoryCache)
             : base(config, logger, fileSystem, memoryCache)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
             _networkManager = networkManager;
             _mediaSourceManager = mediaSourceManager;
@@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         {
             var channelIdPrefix = GetFullChannelIdPrefix(info);
 
-            return await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
+            return await new M3uParser(Logger, _httpClientFactory, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
         }
 
         public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
@@ -116,7 +117,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
                 if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
                 {
-                    return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config, _appHost, _streamHelper);
+                    return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
                 }
             }
 
@@ -125,7 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         public async Task Validate(TunerHostInfo info)
         {
-            using (var stream = await new M3uParser(Logger, _httpClient, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
+            using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
             {
             }
         }

+ 6 - 10
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -5,6 +5,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Net.Http;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
@@ -19,13 +20,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
     public class M3uParser
     {
         private readonly ILogger _logger;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerApplicationHost _appHost;
 
-        public M3uParser(ILogger logger, IHttpClient httpClient, IServerApplicationHost appHost)
+        public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory, IServerApplicationHost appHost)
         {
             _logger = logger;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
         }
 
@@ -51,13 +52,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         {
             if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
             {
-                return _httpClient.Get(new HttpRequestOptions
-                {
-                    Url = url,
-                    CancellationToken = cancellationToken,
-                    // Some data providers will require a user agent
-                    UserAgent = _appHost.ApplicationUserAgent
-                });
+                return _httpClientFactory.CreateClient(NamedClient.Default)
+                    .GetStreamAsync(url);
             }
 
             return Task.FromResult((Stream)File.OpenRead(url));

+ 17 - 30
Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs

@@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 {
     public class SharedHttpStream : LiveStream, IDirectStreamProvider
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerApplicationHost _appHost;
 
         public SharedHttpStream(
@@ -29,14 +29,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             TunerHostInfo tunerHostInfo,
             string originalStreamId,
             IFileSystem fileSystem,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILogger logger,
             IConfigurationManager configurationManager,
             IServerApplicationHost appHost,
             IStreamHelper streamHelper)
             : base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
             OriginalStreamId = originalStreamId;
             EnableStreamSharing = true;
@@ -55,25 +55,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             var typeName = GetType().Name;
             Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
 
-            var httpRequestOptions = new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = CancellationToken.None,
-                BufferContent = false,
-                DecompressionMethod = CompressionMethods.None
-            };
-
-            foreach (var header in mediaSource.RequiredHttpHeaders)
-            {
-                httpRequestOptions.RequestHeaders[header.Key] = header.Value;
-            }
-
-            var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false);
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
+                .ConfigureAwait(false);
 
             var extension = "ts";
             var requiresRemux = false;
 
-            var contentType = response.ContentType ?? string.Empty;
+            var contentType = response.Content.Headers.ContentType.ToString();
             if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1)
             {
                 requiresRemux = true;
@@ -132,24 +121,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             }
         }
 
-        private Task StartStreaming(HttpResponseInfo response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
+        private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
         {
             return Task.Run(async () =>
             {
                 try
                 {
                     Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
-                    using (response)
-                    using (var stream = response.Content)
-                    using (var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read))
-                    {
-                        await StreamHelper.CopyToAsync(
-                            stream,
-                            fileStream,
-                            IODefaults.CopyToBufferSize,
-                            () => Resolve(openTaskCompletionSource),
-                            cancellationToken).ConfigureAwait(false);
-                    }
+                    using var message = response;
+                    await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                    await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read);
+                    await StreamHelper.CopyToAsync(
+                        stream,
+                        fileStream,
+                        IODefaults.CopyToBufferSize,
+                        () => Resolve(openTaskCompletionSource),
+                        cancellationToken).ConfigureAwait(false);
                 }
                 catch (OperationCanceledException ex)
                 {

+ 16 - 16
Emby.Server.Implementations/Localization/Core/ta.json

@@ -18,7 +18,7 @@
     "MessageServerConfigurationUpdated": "சேவையக அமைப்புகள் புதுப்பிக்கப்பட்டன",
     "MessageApplicationUpdatedTo": "ஜெல்லிஃபின் சேவையகம் {0} இற்கு புதுப்பிக்கப்பட்டது",
     "MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது",
-    "Inherit": "மரபரிமையாகப் பெறு",
+    "Inherit": "மரபரிமையாகப் பெறு",
     "HeaderRecordingGroups": "பதிவு குழுக்கள்",
     "HeaderCameraUploads": "புகைப்பட பதிவேற்றங்கள்",
     "Folders": "கோப்புறைகள்",
@@ -31,7 +31,7 @@
     "TaskDownloadMissingSubtitles": "விடுபட்டுபோன வசன வரிகளைப் பதிவிறக்கு",
     "TaskRefreshChannels": "சேனல்களை புதுப்பி",
     "TaskUpdatePlugins": "உட்செருகிகளை புதுப்பி",
-    "TaskRefreshLibrary": "மீடியா நூலகத்தை ஆராய்",
+    "TaskRefreshLibrary": "ஊடக நூலகத்தை ஆராய்",
     "TasksChannelsCategory": "இணைய சேனல்கள்",
     "TasksApplicationCategory": "செயலி",
     "TasksLibraryCategory": "நூலகம்",
@@ -46,7 +46,7 @@
     "Sync": "ஒத்திசைவு",
     "StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
     "Songs": "பாடல்கள்",
-    "Shows": "தொடர்கள்",
+    "Shows": "நிகழ்ச்சிகள்",
     "ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
     "ScheduledTaskStartedWithName": "{0} துவங்கியது",
     "ScheduledTaskFailedWithName": "{0} தோல்வியடைந்தது",
@@ -67,20 +67,20 @@
     "NotificationOptionAudioPlayback": "ஒலி இசைக்கத் துவங்கியுள்ளது",
     "NotificationOptionApplicationUpdateInstalled": "செயலி புதுப்பிக்கப்பட்டது",
     "NotificationOptionApplicationUpdateAvailable": "செயலியினை புதுப்பிக்கலாம்",
-    "NameSeasonUnknown": "பருவம் அறியப்படாதவை",
+    "NameSeasonUnknown": "அறியப்படாத பருவம்",
     "NameSeasonNumber": "பருவம் {0}",
     "NameInstallFailed": "{0} நிறுவல் தோல்வியடைந்தது",
     "MusicVideos": "இசைப்படங்கள்",
     "Music": "இசை",
     "Movies": "திரைப்படங்கள்",
-    "Latest": "புதிய",
+    "Latest": "புதியவை",
     "LabelRunningTimeValue": "ஓடும் நேரம்: {0}",
     "LabelIpAddressValue": "ஐபி முகவரி: {0}",
     "ItemRemovedWithName": "{0} நூலகத்திலிருந்து அகற்றப்பட்டது",
     "ItemAddedWithName": "{0} நூலகத்தில் சேர்க்கப்பட்டது",
-    "HeaderNextUp": "அடுத்ததாக",
+    "HeaderNextUp": "அடுத்தத",
     "HeaderLiveTV": "நேரடித் தொலைக்காட்சி",
-    "HeaderFavoriteSongs": "பிடித்த பாட்டுகள்",
+    "HeaderFavoriteSongs": "பிடித்த பாட்கள்",
     "HeaderFavoriteShows": "பிடித்த தொடர்கள்",
     "HeaderFavoriteEpisodes": "பிடித்த அத்தியாயங்கள்",
     "HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்",
@@ -93,25 +93,25 @@
     "Channels": "சேனல்கள்",
     "Books": "புத்தகங்கள்",
     "AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது",
-    "Artists": "கலைஞர்",
+    "Artists": "கலைஞர்கள்",
     "Application": "செயலி",
     "Albums": "ஆல்பங்கள்",
     "NewVersionIsAvailable": "ஜெல்லிஃபின் சேவையகத்தின் புதிய பதிப்பு பதிவிறக்கத்திற்கு கிடைக்கிறது.",
-    "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0 புதுப்பிக்கப்பட்டது",
+    "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0} புதுப்பிக்கப்பட்டது",
     "TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.",
     "UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
-    "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0 } இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
-    "TaskDownloadMissingSubtitlesDescription": "மெட்டாடேட்டா உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
+    "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
+    "TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
     "TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.",
-    "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட செருகுநிரல்களுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
-    "TaskRefreshPeopleDescription": "உங்கள் மீடியா நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மெட்டாடேட்டாவை புதுப்பிக்கும்.",
+    "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
+    "TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.",
     "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
-    "TaskCleanLogs": "பதிவு அடைவ சுத்தம் செய்யுங்கள்",
-    "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் மீடியா நூலகத்தை ஸ்கேன் செய்து மீத்தரவை புதுப்பிக்கும்.",
+    "TaskCleanLogs": "பதிவு அடைவ சுத்தம் செய்யுங்கள்",
+    "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் ஊடக நூலகத்தை ஆராய்ந்து மீத்தரவை புதுப்பிக்கும்.",
     "TaskRefreshChapterImagesDescription": "அத்தியாயங்களைக் கொண்ட வீடியோக்களுக்கான சிறு உருவங்களை உருவாக்குகிறது.",
     "ValueHasBeenAddedToLibrary": "உங்கள் மீடியா நூலகத்தில் {0} சேர்க்கப்பட்டது",
     "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
     "HomeVideos": "முகப்பு வீடியோக்கள்",
-    "UserStoppedPlayingItemWithValues": "{2} இல் {1} முடித்துவிட்டது",
+    "UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது",
     "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது"
 }

+ 36 - 52
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Updates
         /// </summary>
         private readonly ILogger<InstallationManager> _logger;
         private readonly IApplicationPaths _appPaths;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IServerConfigurationManager _config;
         private readonly IFileSystem _fileSystem;
@@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Updates
             ILogger<InstallationManager> logger,
             IApplicationHost appHost,
             IApplicationPaths appPaths,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IJsonSerializer jsonSerializer,
             IServerConfigurationManager config,
             IFileSystem fileSystem,
@@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Updates
             _logger = logger;
             _applicationHost = appHost;
             _appPaths = appPaths;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _jsonSerializer = jsonSerializer;
             _config = config;
             _fileSystem = fileSystem;
@@ -116,26 +116,18 @@ namespace Emby.Server.Implementations.Updates
         {
             try
             {
-                using (var response = await _httpClient.SendAsync(
-                    new HttpRequestOptions
-                    {
-                        Url = manifest,
-                        CancellationToken = cancellationToken,
-                        CacheMode = CacheMode.Unconditional,
-                        CacheLength = TimeSpan.FromMinutes(3)
-                    },
-                    HttpMethod.Get).ConfigureAwait(false))
-                using (Stream stream = response.Content)
+                using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                    .GetAsync(manifest, cancellationToken).ConfigureAwait(false);
+                await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+
+                try
                 {
-                    try
-                    {
-                        return await _jsonSerializer.DeserializeFromStreamAsync<IReadOnlyList<PackageInfo>>(stream).ConfigureAwait(false);
-                    }
-                    catch (SerializationException ex)
-                    {
-                        _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
-                        return Array.Empty<PackageInfo>();
-                    }
+                    return await _jsonSerializer.DeserializeFromStreamAsync<IReadOnlyList<PackageInfo>>(stream).ConfigureAwait(false);
+                }
+                catch (SerializationException ex)
+                {
+                    _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
+                    return Array.Empty<PackageInfo>();
                 }
             }
             catch (UriFormatException ex)
@@ -360,42 +352,34 @@ namespace Emby.Server.Implementations.Updates
             // Always override the passed-in target (which is a file) and figure it out again
             string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
 
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                .GetAsync(package.SourceUrl, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+
             // CA5351: Do Not Use Broken Cryptographic Algorithms
 #pragma warning disable CA5351
-            using (var res = await _httpClient.SendAsync(
-                new HttpRequestOptions
-                {
-                    Url = package.SourceUrl,
-                    CancellationToken = cancellationToken,
-                    // We need it to be buffered for setting the position
-                    BufferContent = true
-                },
-                HttpMethod.Get).ConfigureAwait(false))
-            using (var stream = res.Content)
-            using (var md5 = MD5.Create())
-            {
-                cancellationToken.ThrowIfCancellationRequested();
+            using var md5 = MD5.Create();
+            cancellationToken.ThrowIfCancellationRequested();
 
-                var hash = Hex.Encode(md5.ComputeHash(stream));
-                if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
-                {
-                    _logger.LogError(
-                        "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}",
-                        package.Name,
-                        package.Checksum,
-                        hash);
-                    throw new InvalidDataException("The checksum of the received data doesn't match.");
-                }
-
-                if (Directory.Exists(targetDir))
-                {
-                    Directory.Delete(targetDir, true);
-                }
+            var hash = Hex.Encode(md5.ComputeHash(stream));
+            if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
+            {
+                _logger.LogError(
+                    "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}",
+                    package.Name,
+                    package.Checksum,
+                    hash);
+                throw new InvalidDataException("The checksum of the received data doesn't match.");
+            }
 
-                stream.Position = 0;
-                _zipClient.ExtractAllFromZip(stream, targetDir, true);
+            if (Directory.Exists(targetDir))
+            {
+                Directory.Delete(targetDir, true);
             }
 
+            stream.Position = 0;
+            _zipClient.ExtractAllFromZip(stream, targetDir, true);
+
 #pragma warning restore CA5351
         }
 

+ 5 - 1
Jellyfin.Api/BaseJellyfinApiController.cs

@@ -1,4 +1,5 @@
 using System.Net.Mime;
+using MediaBrowser.Common.Json;
 using Microsoft.AspNetCore.Mvc;
 
 namespace Jellyfin.Api
@@ -8,7 +9,10 @@ namespace Jellyfin.Api
     /// </summary>
     [ApiController]
     [Route("[controller]")]
-    [Produces(MediaTypeNames.Application.Json)]
+    [Produces(
+        MediaTypeNames.Application.Json,
+        JsonDefaults.CamelCaseMediaType,
+        JsonDefaults.PascalCaseMediaType)]
     public class BaseJellyfinApiController : ControllerBase
     {
     }

+ 2 - 99
Jellyfin.Api/Controllers/DashboardController.cs

@@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Http.Extensions;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -28,38 +29,20 @@ namespace Jellyfin.Api.Controllers
     {
         private readonly ILogger<DashboardController> _logger;
         private readonly IServerApplicationHost _appHost;
-        private readonly IConfiguration _appConfig;
-        private readonly IServerConfigurationManager _serverConfigurationManager;
-        private readonly IResourceFileManager _resourceFileManager;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="DashboardController"/> class.
         /// </summary>
         /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
         /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
-        /// <param name="appConfig">Instance of <see cref="IConfiguration"/> interface.</param>
-        /// <param name="resourceFileManager">Instance of <see cref="IResourceFileManager"/> interface.</param>
-        /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
         public DashboardController(
             ILogger<DashboardController> logger,
-            IServerApplicationHost appHost,
-            IConfiguration appConfig,
-            IResourceFileManager resourceFileManager,
-            IServerConfigurationManager serverConfigurationManager)
+            IServerApplicationHost appHost)
         {
             _logger = logger;
             _appHost = appHost;
-            _appConfig = appConfig;
-            _resourceFileManager = resourceFileManager;
-            _serverConfigurationManager = serverConfigurationManager;
         }
 
-        /// <summary>
-        /// Gets the path of the directory containing the static web interface content, or null if the server is not
-        /// hosting the web client.
-        /// </summary>
-        private string? WebClientUiPath => GetWebClientUiPath(_appConfig, _serverConfigurationManager);
-
         /// <summary>
         /// Gets the configuration pages.
         /// </summary>
@@ -172,86 +155,6 @@ namespace Jellyfin.Api.Controllers
             return NotFound();
         }
 
-        /// <summary>
-        /// Gets the robots.txt.
-        /// </summary>
-        /// <response code="200">Robots.txt returned.</response>
-        /// <returns>The robots.txt.</returns>
-        [HttpGet("robots.txt")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
-        [ApiExplorerSettings(IgnoreApi = true)]
-        public ActionResult GetRobotsTxt()
-        {
-            return GetWebClientResource("robots.txt");
-        }
-
-        /// <summary>
-        /// Gets a resource from the web client.
-        /// </summary>
-        /// <param name="resourceName">The resource name.</param>
-        /// <response code="200">Web client returned.</response>
-        /// <response code="404">Server does not host a web client.</response>
-        /// <returns>The resource.</returns>
-        [HttpGet("web/{*resourceName}")]
-        [ApiExplorerSettings(IgnoreApi = true)]
-        [ProducesResponseType(StatusCodes.Status200OK)]
-        [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult GetWebClientResource([FromRoute] string resourceName)
-        {
-            if (!_appConfig.HostWebClient() || WebClientUiPath == null)
-            {
-                return NotFound("Server does not host a web client.");
-            }
-
-            var path = resourceName;
-            var basePath = WebClientUiPath;
-
-            var requestPathAndQuery = Request.GetEncodedPathAndQuery();
-            // Bounce them to the startup wizard if it hasn't been completed yet
-            if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted
-                && !requestPathAndQuery.Contains("wizard", StringComparison.OrdinalIgnoreCase)
-                && requestPathAndQuery.Contains("index", StringComparison.OrdinalIgnoreCase))
-            {
-                return Redirect("index.html?start=wizard#!/wizardstart.html");
-            }
-
-            return PhysicalFile(_resourceFileManager.GetResourcePath(basePath, path), MimeTypes.GetMimeType(path));
-        }
-
-        /// <summary>
-        /// Gets the favicon.
-        /// </summary>
-        /// <response code="200">Favicon.ico returned.</response>
-        /// <returns>The favicon.</returns>
-        [HttpGet("favicon.ico")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
-        [ApiExplorerSettings(IgnoreApi = true)]
-        public ActionResult GetFavIcon()
-        {
-            return GetWebClientResource("favicon.ico");
-        }
-
-        /// <summary>
-        /// Gets the path of the directory containing the static web interface content.
-        /// </summary>
-        /// <param name="appConfig">The app configuration.</param>
-        /// <param name="serverConfigManager">The server configuration manager.</param>
-        /// <returns>The directory path, or null if the server is not hosting the web client.</returns>
-        public static string? GetWebClientUiPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager)
-        {
-            if (!appConfig.HostWebClient())
-            {
-                return null;
-            }
-
-            if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath))
-            {
-                return serverConfigManager.Configuration.DashboardSourcePath;
-            }
-
-            return serverConfigManager.ApplicationPaths.WebPath;
-        }
-
         private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin)
         {
             return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1));

+ 2 - 1
Jellyfin.Api/Controllers/LiveTvController.cs

@@ -17,6 +17,7 @@ using Jellyfin.Api.Models.LiveTvDtos;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
@@ -1071,7 +1072,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesFile(MediaTypeNames.Application.Json)]
         public async Task<ActionResult> GetSchedulesDirectCountries()
         {
-            var client = _httpClientFactory.CreateClient();
+            var client = _httpClientFactory.CreateClient(NamedClient.Default);
             // https://json.schedulesdirect.org/20141201/available/countries
             // Can't dispose the response as it's required up the call chain.
             var response = await client.GetAsync("https://json.schedulesdirect.org/20141201/available/countries")

+ 2 - 1
Jellyfin.Api/Controllers/RemoteImageController.cs

@@ -10,6 +10,7 @@ using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
@@ -246,7 +247,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Task.</returns>
         private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
         {
-            var httpClient = _httpClientFactory.CreateClient();
+            var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
             using var response = await httpClient.GetAsync(url).ConfigureAwait(false);
             var ext = response.Content.Headers.ContentType.MediaType.Split('/').Last();
             var fullCachePath = GetFullCachePath(urlHash + "." + ext);

+ 2 - 1
Jellyfin.Api/Controllers/VideosController.cs

@@ -12,6 +12,7 @@ using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.StreamingDtos;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
@@ -475,7 +476,7 @@ namespace Jellyfin.Api.Controllers
             {
                 StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
 
-                var httpClient = _httpClientFactory.CreateClient();
+                var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
                 return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, HttpContext).ConfigureAwait(false);
             }
 

+ 2 - 1
Jellyfin.Api/Helpers/AudioHelper.cs

@@ -3,6 +3,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Models.StreamingDtos;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
@@ -138,7 +139,7 @@ namespace Jellyfin.Api.Helpers
             {
                 StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
 
-                var httpClient = _httpClientFactory.CreateClient();
+                var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
                 return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false);
             }
 

+ 0 - 56
Jellyfin.Api/MvcRoutePrefix.cs

@@ -1,56 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ApplicationModels;
-
-namespace Jellyfin.Api
-{
-    /// <summary>
-    /// Route prefixing for ASP.NET MVC.
-    /// </summary>
-    public static class MvcRoutePrefix
-    {
-        /// <summary>
-        /// Adds route prefixes to the MVC conventions.
-        /// </summary>
-        /// <param name="opts">The MVC options.</param>
-        /// <param name="prefixes">The list of prefixes.</param>
-        public static void UseGeneralRoutePrefix(this MvcOptions opts, params string[] prefixes)
-        {
-            opts.Conventions.Insert(0, new RoutePrefixConvention(prefixes));
-        }
-
-        private class RoutePrefixConvention : IApplicationModelConvention
-        {
-            private readonly AttributeRouteModel[] _routePrefixes;
-
-            public RoutePrefixConvention(IEnumerable<string> prefixes)
-            {
-                _routePrefixes = prefixes.Select(p => new AttributeRouteModel(new RouteAttribute(p))).ToArray();
-            }
-
-            public void Apply(ApplicationModel application)
-            {
-                foreach (var controller in application.Controllers)
-                {
-                    if (controller.Selectors == null)
-                    {
-                        continue;
-                    }
-
-                    var newSelectors = new List<SelectorModel>();
-                    foreach (var selector in controller.Selectors)
-                    {
-                        newSelectors.AddRange(_routePrefixes.Select(routePrefix => new SelectorModel(selector)
-                        {
-                            AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(routePrefix, selector.AttributeRouteModel)
-                        }));
-                    }
-
-                    controller.Selectors.Clear();
-                    newSelectors.ForEach(selector => controller.Selectors.Add(selector));
-                }
-            }
-        }
-    }
-}

+ 10 - 3
Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs

@@ -1,6 +1,8 @@
+using System.Collections.Generic;
 using Jellyfin.Server.Middleware;
 using MediaBrowser.Controller.Configuration;
 using Microsoft.AspNetCore.Builder;
+using Microsoft.OpenApi.Models;
 
 namespace Jellyfin.Server.Extensions
 {
@@ -23,6 +25,7 @@ namespace Jellyfin.Server.Extensions
             // specifying the Swagger JSON endpoint.
 
             var baseUrl = serverConfigurationManager.Configuration.BaseUrl.Trim('/');
+            var apiDocBaseUrl = serverConfigurationManager.Configuration.BaseUrl;
             if (!string.IsNullOrEmpty(baseUrl))
             {
                 baseUrl += '/';
@@ -32,21 +35,25 @@ namespace Jellyfin.Server.Extensions
                 .UseSwagger(c =>
                 {
                     // Custom path requires {documentName}, SwaggerDoc documentName is 'api-docs'
-                    c.RouteTemplate = $"/{baseUrl}{{documentName}}/openapi.json";
+                    c.RouteTemplate = "{documentName}/openapi.json";
+                    c.PreSerializeFilters.Add((swagger, httpReq) =>
+                    {
+                        swagger.Servers = new List<OpenApiServer> { new OpenApiServer { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}{apiDocBaseUrl}" } };
+                    });
                 })
                 .UseSwaggerUI(c =>
                 {
                     c.DocumentTitle = "Jellyfin API";
                     c.SwaggerEndpoint($"/{baseUrl}api-docs/openapi.json", "Jellyfin API");
-                    c.RoutePrefix = $"{baseUrl}api-docs/swagger";
                     c.InjectStylesheet($"/{baseUrl}api-docs/swagger/custom.css");
+                    c.RoutePrefix = "api-docs/swagger";
                 })
                 .UseReDoc(c =>
                 {
                     c.DocumentTitle = "Jellyfin API";
                     c.SpecUrl($"/{baseUrl}api-docs/openapi.json");
-                    c.RoutePrefix = $"{baseUrl}api-docs/redoc";
                     c.InjectStylesheet($"/{baseUrl}api-docs/redoc/custom.css");
+                    c.RoutePrefix = "api-docs/redoc";
                 });
         }
 

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

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Reflection;
-using Jellyfin.Api;
 using Jellyfin.Api.Auth;
 using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
 using Jellyfin.Api.Auth.DownloadPolicy;
@@ -19,7 +18,6 @@ using Jellyfin.Api.Controllers;
 using Jellyfin.Server.Filters;
 using Jellyfin.Server.Formatters;
 using Jellyfin.Server.Models;
-using MediaBrowser.Common;
 using MediaBrowser.Common.Json;
 using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Authentication;
@@ -136,10 +134,9 @@ namespace Jellyfin.Server.Extensions
         /// Extension method for adding the jellyfin API to the service collection.
         /// </summary>
         /// <param name="serviceCollection">The service collection.</param>
-        /// <param name="baseUrl">The base url for the API.</param>
-        /// <param name="pluginAssemblies">An IEnumberable containing all plugin assemblies with API controllers.</param>
+        /// <param name="pluginAssemblies">An IEnumerable containing all plugin assemblies with API controllers.</param>
         /// <returns>The MVC builder.</returns>
-        public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl, IEnumerable<Assembly> pluginAssemblies)
+        public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable<Assembly> pluginAssemblies)
         {
             IMvcBuilder mvcBuilder = serviceCollection
                 .AddCors(options =>
@@ -152,7 +149,9 @@ namespace Jellyfin.Server.Extensions
                 })
                 .AddMvc(opts =>
                 {
-                    opts.UseGeneralRoutePrefix(baseUrl);
+                    // Allow requester to change between camelCase and PascalCase
+                    opts.RespectBrowserAcceptHeader = true;
+
                     opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter());
                     opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter());
 

+ 1 - 1
Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs

@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Formatters
         public CamelCaseJsonProfileFormatter() : base(JsonDefaults.GetCamelCaseOptions())
         {
             SupportedMediaTypes.Clear();
-            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\""));
+            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.CamelCaseMediaType));
         }
     }
 }

+ 3 - 2
Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs

@@ -1,3 +1,4 @@
+using System.Net.Mime;
 using MediaBrowser.Common.Json;
 using Microsoft.AspNetCore.Mvc.Formatters;
 using Microsoft.Net.Http.Headers;
@@ -16,8 +17,8 @@ namespace Jellyfin.Server.Formatters
         {
             SupportedMediaTypes.Clear();
             // Add application/json for default formatter
-            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
-            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"PascalCase\""));
+            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json));
+            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.PascalCaseMediaType));
         }
     }
 }

+ 2 - 1
Jellyfin.Server/Formatters/XmlOutputFormatter.cs

@@ -16,8 +16,9 @@ namespace Jellyfin.Server.Formatters
         /// </summary>
         public XmlOutputFormatter()
         {
+            SupportedMediaTypes.Clear();
             SupportedMediaTypes.Add(MediaTypeNames.Text.Xml);
-            SupportedMediaTypes.Add("text/xml;charset=UTF-8");
+
             SupportedEncodings.Add(Encoding.UTF8);
             SupportedEncodings.Add(Encoding.Unicode);
         }

+ 0 - 36
Jellyfin.Server/HealthChecks/JellyfinDbHealthCheck.cs

@@ -1,36 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Server.Implementations;
-using Microsoft.Extensions.Diagnostics.HealthChecks;
-
-namespace Jellyfin.Server.HealthChecks
-{
-    /// <summary>
-    /// Checks connectivity to the database.
-    /// </summary>
-    public class JellyfinDbHealthCheck : IHealthCheck
-    {
-        private readonly JellyfinDbProvider _dbProvider;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="JellyfinDbHealthCheck"/> class.
-        /// </summary>
-        /// <param name="dbProvider">The jellyfin db provider.</param>
-        public JellyfinDbHealthCheck(JellyfinDbProvider dbProvider)
-        {
-            _dbProvider = dbProvider;
-        }
-
-        /// <inheritdoc />
-        public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
-        {
-            await using var jellyfinDb = _dbProvider.CreateContext();
-            if (await jellyfinDb.Database.CanConnectAsync(cancellationToken).ConfigureAwait(false))
-            {
-                return HealthCheckResult.Healthy("Database connection successful.");
-            }
-
-            return HealthCheckResult.Unhealthy("Unable to connect to the database.");
-        }
-    }
-}

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

@@ -44,6 +44,7 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.7" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.7" />
     <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.7" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.7" />
     <PackageReference Include="prometheus-net" Version="3.6.0" />
     <PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
     <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />

+ 1 - 5
Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs

@@ -44,11 +44,7 @@ namespace Jellyfin.Server.Middleware
             var localPath = httpContext.Request.Path.ToString();
             var baseUrlPrefix = serverConfigurationManager.Configuration.BaseUrl;
 
-            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))
+            if (!localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
             {
                 // Always redirect back to the default path if the base prefix is invalid or missing
                 _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);

+ 1 - 1
Jellyfin.Server/Program.cs

@@ -169,7 +169,7 @@ namespace Jellyfin.Server
                 // If hosting the web client, validate the client content path
                 if (startupConfig.HostWebClient())
                 {
-                    string? webContentPath = DashboardController.GetWebClientUiPath(startupConfig, appHost.ServerConfigurationManager);
+                    string? webContentPath = appHost.ServerConfigurationManager.ApplicationPaths.WebPath;
                     if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0)
                     {
                         throw new InvalidOperationException(

+ 60 - 38
Jellyfin.Server/Startup.cs

@@ -3,15 +3,18 @@ using System.ComponentModel;
 using System.Net.Http.Headers;
 using Jellyfin.Api.TypeConverters;
 using Jellyfin.Server.Extensions;
-using Jellyfin.Server.HealthChecks;
+using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Middleware;
 using Jellyfin.Server.Models;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Extensions;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.FileProviders;
 using Microsoft.Extensions.Hosting;
 using Prometheus;
 
@@ -50,9 +53,7 @@ namespace Jellyfin.Server
             {
                 options.HttpsPort = _serverApplicationHost.HttpsPort;
             });
-            services.AddJellyfinApi(
-                _serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'),
-                _serverApplicationHost.GetApiPluginAssemblies());
+            services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies());
 
             services.AddJellyfinApiSwagger();
 
@@ -79,7 +80,7 @@ namespace Jellyfin.Server
                 .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
 
             services.AddHealthChecks()
-                .AddCheck<JellyfinDbHealthCheck>("JellyfinDb");
+                .AddDbContextCheck<JellyfinDb>();
         }
 
         /// <summary>
@@ -87,57 +88,78 @@ namespace Jellyfin.Server
         /// </summary>
         /// <param name="app">The application builder.</param>
         /// <param name="env">The webhost environment.</param>
+        /// <param name="appConfig">The application config.</param>
         public void Configure(
             IApplicationBuilder app,
-            IWebHostEnvironment env)
+            IWebHostEnvironment env,
+            IConfiguration appConfig)
         {
-            if (env.IsDevelopment())
+            // Only add base url redirection if a base url is set.
+            if (!string.IsNullOrEmpty(_serverConfigurationManager.Configuration.BaseUrl))
             {
-                app.UseDeveloperExceptionPage();
+                app.UseBaseUrlRedirection();
             }
 
-            app.UseMiddleware<ExceptionMiddleware>();
+            // Wrap rest of configuration so everything only listens on BaseUrl.
+            app.Map(_serverConfigurationManager.Configuration.BaseUrl, mainApp =>
+            {
+                if (env.IsDevelopment())
+                {
+                    mainApp.UseDeveloperExceptionPage();
+                }
 
-            app.UseMiddleware<ResponseTimeMiddleware>();
+                mainApp.UseMiddleware<ExceptionMiddleware>();
 
-            app.UseWebSockets();
+                mainApp.UseMiddleware<ResponseTimeMiddleware>();
 
-            app.UseResponseCompression();
+                mainApp.UseWebSockets();
 
-            app.UseCors(ServerCorsPolicy.DefaultPolicyName);
+                mainApp.UseResponseCompression();
 
-            if (_serverConfigurationManager.Configuration.RequireHttps
-                && _serverApplicationHost.ListenWithHttps)
-            {
-                app.UseHttpsRedirection();
-            }
+                mainApp.UseCors(ServerCorsPolicy.DefaultPolicyName);
 
-            app.UseStaticFiles();
-            app.UseAuthentication();
-            app.UseJellyfinApiSwagger(_serverConfigurationManager);
-            app.UseRouting();
-            app.UseAuthorization();
-            if (_serverConfigurationManager.Configuration.EnableMetrics)
-            {
-                // Must be registered after any middleware that could chagne HTTP response codes or the data will be bad
-                app.UseHttpMetrics();
-            }
+                if (_serverConfigurationManager.Configuration.RequireHttps
+                    && _serverApplicationHost.ListenWithHttps)
+                {
+                    mainApp.UseHttpsRedirection();
+                }
 
-            app.UseLanFiltering();
-            app.UseIpBasedAccessValidation();
-            app.UseBaseUrlRedirection();
-            app.UseWebSocketHandler();
-            app.UseServerStartupMessage();
+                mainApp.UseStaticFiles();
+                if (appConfig.HostWebClient())
+                {
+                    mainApp.UseStaticFiles(new StaticFileOptions
+                    {
+                        FileProvider = new PhysicalFileProvider(_serverConfigurationManager.ApplicationPaths.WebPath),
+                        RequestPath = "/web"
+                    });
+                }
+
+                mainApp.UseAuthentication();
+                mainApp.UseJellyfinApiSwagger(_serverConfigurationManager);
+                mainApp.UseRouting();
+                mainApp.UseAuthorization();
+
+                mainApp.UseLanFiltering();
+                mainApp.UseIpBasedAccessValidation();
+                mainApp.UseWebSocketHandler();
+                mainApp.UseServerStartupMessage();
 
-            app.UseEndpoints(endpoints =>
-            {
-                endpoints.MapControllers();
                 if (_serverConfigurationManager.Configuration.EnableMetrics)
                 {
-                    endpoints.MapMetrics(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/') + "/metrics");
+                    // Must be registered after any middleware that could change HTTP response codes or the data will be bad
+                    mainApp.UseHttpMetrics();
                 }
 
-                endpoints.MapHealthChecks(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/') + "/health");
+                mainApp.UseEndpoints(endpoints =>
+                {
+                    endpoints.MapControllers();
+                    if (_serverConfigurationManager.Configuration.EnableMetrics)
+                    {
+                        endpoints.MapMetrics("/metrics");
+                    }
+
+                    endpoints.MapHealthChecks("/health");
+                });
             });
 
             // Add type descriptor for legacy datetime parsing.

+ 1 - 4
MediaBrowser.Common/Configuration/IApplicationPaths.cs

@@ -1,5 +1,3 @@
-using MediaBrowser.Model.Configuration;
-
 namespace MediaBrowser.Common.Configuration
 {
     /// <summary>
@@ -17,8 +15,7 @@ namespace MediaBrowser.Common.Configuration
         /// Gets the path to the web UI resources folder.
         /// </summary>
         /// <remarks>
-        /// This value is not relevant if the server is configured to not host any static web content. Additionally,
-        /// the value for <see cref="ServerConfiguration.DashboardSourcePath"/> takes precedence over this one.
+        /// This value is not relevant if the server is configured to not host any static web content.
         /// </remarks>
         string WebPath { get; }
 

+ 10 - 0
MediaBrowser.Common/Json/JsonDefaults.cs

@@ -9,6 +9,16 @@ namespace MediaBrowser.Common.Json
     /// </summary>
     public static class JsonDefaults
     {
+        /// <summary>
+        /// Pascal case json profile media type.
+        /// </summary>
+        public const string PascalCaseMediaType = "application/json; profile=\"PascalCase\"";
+
+        /// <summary>
+        /// Camel case json profile media type.
+        /// </summary>
+        public const string CamelCaseMediaType = "application/json; profile=\"CamelCase\"";
+
         /// <summary>
         /// Gets the default <see cref="JsonSerializerOptions" /> options.
         /// </summary>

+ 0 - 11
MediaBrowser.Common/Net/CacheMode.cs

@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1602
-
-namespace MediaBrowser.Common.Net
-{
-    public enum CacheMode
-    {
-        None = 0,
-        Unconditional = 1
-    }
-}

+ 0 - 15
MediaBrowser.Common/Net/CompressionMethods.cs

@@ -1,15 +0,0 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1602
-
-using System;
-
-namespace MediaBrowser.Common.Net
-{
-    [Flags]
-    public enum CompressionMethods
-    {
-        None = 0b00000001,
-        Deflate = 0b00000010,
-        Gzip = 0b00000100
-    }
-}

+ 0 - 105
MediaBrowser.Common/Net/HttpRequestOptions.cs

@@ -1,105 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using Microsoft.Net.Http.Headers;
-
-namespace MediaBrowser.Common.Net
-{
-    /// <summary>
-    /// Class HttpRequestOptions.
-    /// </summary>
-    public class HttpRequestOptions
-    {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="HttpRequestOptions"/> class.
-        /// </summary>
-        public HttpRequestOptions()
-        {
-            RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
-            CacheMode = CacheMode.None;
-            DecompressionMethod = CompressionMethods.Deflate;
-        }
-
-        /// <summary>
-        /// Gets or sets the URL.
-        /// </summary>
-        /// <value>The URL.</value>
-        public string Url { get; set; }
-
-        public CompressionMethods DecompressionMethod { get; set; }
-
-        /// <summary>
-        /// Gets or sets the accept header.
-        /// </summary>
-        /// <value>The accept header.</value>
-        public string AcceptHeader
-        {
-            get => GetHeaderValue(HeaderNames.Accept);
-            set => RequestHeaders[HeaderNames.Accept] = value;
-        }
-
-        /// <summary>
-        /// Gets or sets the cancellation token.
-        /// </summary>
-        /// <value>The cancellation token.</value>
-        public CancellationToken CancellationToken { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user agent.
-        /// </summary>
-        /// <value>The user agent.</value>
-        public string UserAgent
-        {
-            get => GetHeaderValue(HeaderNames.UserAgent);
-            set => RequestHeaders[HeaderNames.UserAgent] = value;
-        }
-
-        /// <summary>
-        /// Gets or sets the referrer.
-        /// </summary>
-        /// <value>The referrer.</value>
-        public string Referer
-        {
-            get => GetHeaderValue(HeaderNames.Referer);
-            set => RequestHeaders[HeaderNames.Referer] = value;
-        }
-
-        /// <summary>
-        /// Gets or sets the host.
-        /// </summary>
-        /// <value>The host.</value>
-        public string Host
-        {
-            get => GetHeaderValue(HeaderNames.Host);
-            set => RequestHeaders[HeaderNames.Host] = value;
-        }
-
-        public Dictionary<string, string> RequestHeaders { get; private set; }
-
-        public string RequestContentType { get; set; }
-
-        public string RequestContent { get; set; }
-
-        public bool BufferContent { get; set; }
-
-        public bool LogErrorResponseBody { get; set; }
-
-        public bool EnableKeepAlive { get; set; }
-
-        public CacheMode CacheMode { get; set; }
-
-        public TimeSpan CacheLength { get; set; }
-
-        public bool EnableDefaultUserAgent { get; set; }
-
-        private string GetHeaderValue(string name)
-        {
-            RequestHeaders.TryGetValue(name, out var value);
-
-            return value;
-        }
-    }
-}

+ 0 - 80
MediaBrowser.Common/Net/HttpResponseInfo.cs

@@ -1,80 +0,0 @@
-using System;
-using System.IO;
-using System.Net;
-using System.Net.Http.Headers;
-
-namespace MediaBrowser.Common.Net
-{
-    /// <summary>
-    /// Class HttpResponseInfo.
-    /// </summary>
-    public sealed class HttpResponseInfo : IDisposable
-    {
-#pragma warning disable CS1591
-        public HttpResponseInfo()
-        {
-        }
-
-        public HttpResponseInfo(HttpResponseHeaders headers, HttpContentHeaders contentHeader)
-        {
-            Headers = headers;
-            ContentHeaders = contentHeader;
-        }
-
-#pragma warning restore CS1591
-
-        /// <summary>
-        /// Gets or sets the type of the content.
-        /// </summary>
-        /// <value>The type of the content.</value>
-        public string ContentType { get; set; }
-
-        /// <summary>
-        /// Gets or sets the response URL.
-        /// </summary>
-        /// <value>The response URL.</value>
-        public string ResponseUrl { get; set; }
-
-        /// <summary>
-        /// Gets or sets the content.
-        /// </summary>
-        /// <value>The content.</value>
-        public Stream Content { get; set; }
-
-        /// <summary>
-        /// Gets or sets the status code.
-        /// </summary>
-        /// <value>The status code.</value>
-        public HttpStatusCode StatusCode { get; set; }
-
-        /// <summary>
-        /// Gets or sets the temp file path.
-        /// </summary>
-        /// <value>The temp file path.</value>
-        public string TempFilePath { get; set; }
-
-        /// <summary>
-        /// Gets or sets the length of the content.
-        /// </summary>
-        /// <value>The length of the content.</value>
-        public long? ContentLength { get; set; }
-
-        /// <summary>
-        /// Gets or sets the headers.
-        /// </summary>
-        /// <value>The headers.</value>
-        public HttpResponseHeaders Headers { get; set; }
-
-        /// <summary>
-        /// Gets or sets the content headers.
-        /// </summary>
-        /// <value>The content headers.</value>
-        public HttpContentHeaders ContentHeaders { get; set; }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            // backwards compatibility
-        }
-    }
-}

+ 0 - 53
MediaBrowser.Common/Net/IHttpClient.cs

@@ -1,53 +0,0 @@
-using System;
-using System.IO;
-using System.Net.Http;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Common.Net
-{
-    /// <summary>
-    /// Interface IHttpClient.
-    /// </summary>
-    public interface IHttpClient
-    {
-        /// <summary>
-        /// Gets the response.
-        /// </summary>
-        /// <param name="options">The options.</param>
-        /// <returns>Task{HttpResponseInfo}.</returns>
-        Task<HttpResponseInfo> GetResponse(HttpRequestOptions options);
-
-        /// <summary>
-        /// Gets the specified options.
-        /// </summary>
-        /// <param name="options">The options.</param>
-        /// <returns>Task{Stream}.</returns>
-        Task<Stream> Get(HttpRequestOptions options);
-
-        /// <summary>
-        /// Warning: Deprecated function,
-        /// use 'Task{HttpResponseInfo} SendAsync(HttpRequestOptions options, HttpMethod httpMethod);' instead
-        /// Sends the asynchronous.
-        /// </summary>
-        /// <param name="options">The options.</param>
-        /// <param name="httpMethod">The HTTP method.</param>
-        /// <returns>Task{HttpResponseInfo}.</returns>
-        [Obsolete("Use 'Task{HttpResponseInfo} SendAsync(HttpRequestOptions options, HttpMethod httpMethod);' instead")]
-        Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod);
-
-        /// <summary>
-        /// Sends the asynchronous.
-        /// </summary>
-        /// <param name="options">The options.</param>
-        /// <param name="httpMethod">The HTTP method.</param>
-        /// <returns>Task{HttpResponseInfo}.</returns>
-        Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, HttpMethod httpMethod);
-
-        /// <summary>
-        /// Posts the specified options.
-        /// </summary>
-        /// <param name="options">The options.</param>
-        /// <returns>Task{HttpResponseInfo}.</returns>
-        Task<HttpResponseInfo> Post(HttpRequestOptions options);
-    }
-}

+ 51 - 8
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -60,8 +60,6 @@ namespace MediaBrowser.Controller.Entities
 
         protected BaseItem()
         {
-            ThemeSongIds = Array.Empty<Guid>();
-            ThemeVideoIds = Array.Empty<Guid>();
             Tags = Array.Empty<string>();
             Genres = Array.Empty<string>();
             Studios = Array.Empty<string>();
@@ -100,12 +98,52 @@ namespace MediaBrowser.Controller.Entities
         };
 
         [JsonIgnore]
-        public Guid[] ThemeSongIds { get; set; }
+        public Guid[] ThemeSongIds
+        {
+            get
+            {
+                if (_themeSongIds == null)
+                {
+                    _themeSongIds = GetExtras()
+                        .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeSong)
+                        .Select(song => song.Id)
+                        .ToArray();
+                }
+
+                return _themeSongIds;
+            }
+
+            private set
+            {
+                _themeSongIds = value;
+            }
+        }
+
         [JsonIgnore]
-        public Guid[] ThemeVideoIds { get; set; }
+        public Guid[] ThemeVideoIds
+        {
+            get
+            {
+                if (_themeVideoIds == null)
+                {
+                    _themeVideoIds = GetExtras()
+                        .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeVideo)
+                        .Select(song => song.Id)
+                        .ToArray();
+                }
+
+                return _themeVideoIds;
+            }
+
+            private set
+            {
+                _themeVideoIds = value;
+            }
+        }
 
         [JsonIgnore]
         public string PreferredMetadataCountryCode { get; set; }
+
         [JsonIgnore]
         public string PreferredMetadataLanguage { get; set; }
 
@@ -635,6 +673,9 @@ namespace MediaBrowser.Controller.Entities
         }
 
         private string _sortName;
+        private Guid[] _themeSongIds;
+        private Guid[] _themeVideoIds;
+
         /// <summary>
         /// Gets the name of the sort.
         /// </summary>
@@ -1582,7 +1623,8 @@ namespace MediaBrowser.Controller.Entities
 
             await Task.WhenAll(tasks).ConfigureAwait(false);
 
-            item.ThemeVideoIds = newThemeVideoIds;
+            // They are expected to be sorted by SortName
+            item.ThemeVideoIds = newThemeVideos.OrderBy(i => i.SortName).Select(i => i.Id).ToArray();
 
             return themeVideosChanged;
         }
@@ -1619,7 +1661,8 @@ namespace MediaBrowser.Controller.Entities
 
             await Task.WhenAll(tasks).ConfigureAwait(false);
 
-            item.ThemeSongIds = newThemeSongIds;
+            // They are expected to be sorted by SortName
+            item.ThemeSongIds = newThemeSongs.OrderBy(i => i.SortName).Select(i => i.Id).ToArray();
 
             return themeSongsChanged;
         }
@@ -2910,12 +2953,12 @@ namespace MediaBrowser.Controller.Entities
 
         public IEnumerable<BaseItem> GetThemeSongs()
         {
-            return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeSong)).OrderBy(i => i.SortName);
+            return ThemeSongIds.Select(LibraryManager.GetItemById);
         }
 
         public IEnumerable<BaseItem> GetThemeVideos()
         {
-            return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeVideo)).OrderBy(i => i.SortName);
+            return ThemeVideoIds.Select(LibraryManager.GetItemById);
         }
 
         /// <summary>

+ 123 - 56
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -456,6 +456,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
             var isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
             var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
+            var isNvencHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
             var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
             var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
             var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
@@ -515,6 +516,24 @@ namespace MediaBrowser.Controller.MediaEncoding
                     }
                 }
 
+                if (state.IsVideoRequest
+                    && string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
+                {
+                    var isColorDepth10 = IsColorDepth10(state);
+
+                    if (isNvencHevcDecoder && isColorDepth10
+                        && _mediaEncoder.SupportsHwaccel("opencl")
+                        && encodingOptions.EnableTonemapping
+                        && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
+                        && state.VideoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
+                    {
+                        arg.Append("-init_hw_device opencl=ocl:")
+                            .Append(encodingOptions.OpenclDevice)
+                            .Append(' ')
+                            .Append("-filter_hw_device ocl ");
+                    }
+                }
+
                 if (state.IsVideoRequest
                     && string.Equals(encodingOptions.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
                 {
@@ -1003,11 +1022,33 @@ namespace MediaBrowser.Controller.MediaEncoding
             if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
                 && !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
                 && !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
+                && !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
                 && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
             {
                 param = "-pix_fmt yuv420p " + param;
             }
 
+            if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
+            {
+                var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
+                var videoStream = state.VideoStream;
+                var isColorDepth10 = IsColorDepth10(state);
+
+                if (videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1
+                    && isColorDepth10
+                    && _mediaEncoder.SupportsHwaccel("opencl")
+                    && encodingOptions.EnableTonemapping
+                    && !string.IsNullOrEmpty(videoStream.VideoRange)
+                    && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
+                {
+                    param = "-pix_fmt nv12 " + param;
+                }
+                else
+                {
+                    param = "-pix_fmt yuv420p " + param;
+                }
+            }
+
             if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
             {
                 param = "-pix_fmt nv21 " + param;
@@ -1611,64 +1652,45 @@ namespace MediaBrowser.Controller.MediaEncoding
             var outputSizeParam = ReadOnlySpan<char>.Empty;
             var request = state.BaseRequest;
 
-            // Add resolution params, if specified
-            if (request.Width.HasValue
-                || request.Height.HasValue
-                || request.MaxHeight.HasValue
-                || request.MaxWidth.HasValue)
+            outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"');
+
+            // All possible beginning of video filters
+            // Don't break the order
+            string[] beginOfOutputSizeParam = new[]
             {
-                outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"');
+                // for tonemap_opencl
+                "hwupload,tonemap_opencl",
 
                 // hwupload=extra_hw_frames=64,vpp_qsv (for overlay_qsv on linux)
-                var index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase);
+                "hwupload=extra_hw_frames",
+
+                // vpp_qsv
+                "vpp",
+
+                // hwdownload,format=p010le (hardware decode + software encode for vaapi)
+                "hwdownload",
+
+                // format=nv12|vaapi,hwupload,scale_vaapi
+                "format",
+
+                // bwdif,scale=expr
+                "bwdif",
+
+                // yadif,scale=expr
+                "yadif",
+
+                // scale=expr
+                "scale"
+            };
+
+            var index = -1;
+            foreach (var param in beginOfOutputSizeParam)
+            {
+                index = outputSizeParam.IndexOf(param, StringComparison.OrdinalIgnoreCase);
                 if (index != -1)
                 {
                     outputSizeParam = outputSizeParam.Slice(index);
-                }
-                else
-                {
-                    // vpp_qsv
-                    index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase);
-                    if (index != -1)
-                    {
-                        outputSizeParam = outputSizeParam.Slice(index);
-                    }
-                    else
-                    {
-                        // hwdownload,format=p010le (hardware decode + software encode for vaapi)
-                        index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase);
-                        if (index != -1)
-                        {
-                            outputSizeParam = outputSizeParam.Slice(index);
-                        }
-                        else
-                        {
-                            // format=nv12|vaapi,hwupload,scale_vaapi
-                            index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
-                            if (index != -1)
-                            {
-                                outputSizeParam = outputSizeParam.Slice(index);
-                            }
-                            else
-                            {
-                                // yadif,scale=expr
-                                index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
-                                if (index != -1)
-                                {
-                                    outputSizeParam = outputSizeParam.Slice(index);
-                                }
-                                else
-                                {
-                                    // scale=expr
-                                    index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
-                                    if (index != -1)
-                                    {
-                                        outputSizeParam = outputSizeParam.Slice(index);
-                                    }
-                                }
-                            }
-                        }
-                    }
+                    break;
                 }
             }
 
@@ -1747,9 +1769,9 @@ namespace MediaBrowser.Controller.MediaEncoding
                 */
                 if (isLinux)
                 {
-                    retStr = !outputSizeParam.IsEmpty ?
-                        " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\"" :
-                        " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\"";
+                    retStr = !outputSizeParam.IsEmpty
+                        ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\""
+                        : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\"";
                 }
             }
 
@@ -2084,8 +2106,10 @@ namespace MediaBrowser.Controller.MediaEncoding
             var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
             var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1;
             var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
+            var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
             var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
             var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+            var isColorDepth10 = IsColorDepth10(state);
 
             var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
             var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
@@ -2093,6 +2117,50 @@ namespace MediaBrowser.Controller.MediaEncoding
             // If double rate deinterlacing is enabled and the input framerate is 30fps or below, otherwise the output framerate will be too high for many devices
             var doubleRateDeinterlace = options.DeinterlaceDoubleRate && (videoStream?.RealFrameRate ?? 60) <= 30;
 
+            // Currently only with the use of NVENC decoder can we get a decent performance.
+            // Currently only the HEVC/H265 format is supported.
+            // NVIDIA Pascal and Turing or higher are recommended.
+            if (isNvdecHevcDecoder && isColorDepth10
+                && _mediaEncoder.SupportsHwaccel("opencl")
+                && options.EnableTonemapping
+                && !string.IsNullOrEmpty(videoStream.VideoRange)
+                && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
+            {
+                var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}";
+
+                if (options.TonemappingParam != 0)
+                {
+                    parameters += ":param={4}";
+                }
+
+                if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
+                {
+                    parameters += ":range={5}";
+                }
+
+                // Upload the HDR10 or HLG data to the OpenCL device,
+                // use tonemap_opencl filter for tone mapping,
+                // and then download the SDR data to memory.
+                filters.Add("hwupload");
+                filters.Add(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        parameters,
+                        options.TonemappingAlgorithm,
+                        options.TonemappingDesat,
+                        options.TonemappingThreshold,
+                        options.TonemappingPeak,
+                        options.TonemappingParam,
+                        options.TonemappingRange));
+                filters.Add("hwdownload");
+
+                if (hasGraphicalSubs || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true)
+                    || string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
+                {
+                    filters.Add("format=nv12");
+                }
+            }
+
             // When the input may or may not be hardware VAAPI decodable
             if (isVaapiH264Encoder)
             {
@@ -2110,7 +2178,6 @@ namespace MediaBrowser.Controller.MediaEncoding
             else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder)
             {
                 var codec = videoStream.Codec.ToLowerInvariant();
-                var isColorDepth10 = IsColorDepth10(state);
 
                 // Assert 10-bit hardware VAAPI decodable
                 if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)

+ 1 - 0
MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj

@@ -24,6 +24,7 @@
 
   <ItemGroup>
     <PackageReference Include="BDInfo" Version="0.7.6.1" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
     <PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.1" />
     <PackageReference Include="UTF.Unknown" Version="2.3.0" />
   </ItemGroup>

+ 14 - 0
MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs

@@ -282,6 +282,20 @@ namespace MediaBrowser.MediaEncoding.Probing
         [JsonPropertyName("disposition")]
         public IReadOnlyDictionary<string, int> Disposition { get; set; }
 
+        /// <summary>
+        /// Gets or sets the color range.
+        /// </summary>
+        /// <value>The color range.</value>
+        [JsonPropertyName("color_range")]
+        public string ColorRange { get; set; }
+
+        /// <summary>
+        /// Gets or sets the color space.
+        /// </summary>
+        /// <value>The color space.</value>
+        [JsonPropertyName("color_space")]
+        public string ColorSpace { get; set; }
+
         /// <summary>
         /// Gets or sets the color transfer.
         /// </summary>

+ 10 - 0
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -714,6 +714,16 @@ namespace MediaBrowser.MediaEncoding.Probing
                     stream.RefFrames = streamInfo.Refs;
                 }
 
+                if (!string.IsNullOrEmpty(streamInfo.ColorRange))
+                {
+                    stream.ColorRange = streamInfo.ColorRange;
+                }
+
+                if (!string.IsNullOrEmpty(streamInfo.ColorSpace))
+                {
+                    stream.ColorSpace = streamInfo.ColorSpace;
+                }
+
                 if (!string.IsNullOrEmpty(streamInfo.ColorTransfer))
                 {
                     stream.ColorTransfer = streamInfo.ColorTransfer;

+ 12 - 15
MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

@@ -6,6 +6,7 @@ using System.Diagnostics;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Net.Http;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
@@ -31,7 +32,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         private readonly IApplicationPaths _appPaths;
         private readonly IFileSystem _fileSystem;
         private readonly IMediaEncoder _mediaEncoder;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IMediaSourceManager _mediaSourceManager;
 
         /// <summary>
@@ -46,7 +47,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             IApplicationPaths appPaths,
             IFileSystem fileSystem,
             IMediaEncoder mediaEncoder,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IMediaSourceManager mediaSourceManager)
         {
             _libraryManager = libraryManager;
@@ -54,7 +55,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             _appPaths = appPaths;
             _fileSystem = fileSystem;
             _mediaEncoder = mediaEncoder;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _mediaSourceManager = mediaSourceManager;
         }
 
@@ -750,24 +751,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             }
         }
 
-        private Task<Stream> GetStream(string path, MediaProtocol protocol, CancellationToken cancellationToken)
+        private async Task<Stream> GetStream(string path, MediaProtocol protocol, CancellationToken cancellationToken)
         {
             switch (protocol)
             {
                 case MediaProtocol.Http:
-                    var opts = new HttpRequestOptions()
-                    {
-                        Url = path,
-                        CancellationToken = cancellationToken,
-
-                        // Needed for seeking
-                        BufferContent = true
-                    };
-
-                    return _httpClient.Get(opts);
+                {
+                    using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                        .GetAsync(path, cancellationToken)
+                        .ConfigureAwait(false);
+                    return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                }
 
                 case MediaProtocol.File:
-                    return Task.FromResult<Stream>(File.OpenRead(path));
+                    return File.OpenRead(path);
                 default:
                     throw new ArgumentOutOfRangeException(nameof(protocol));
             }

+ 28 - 1
MediaBrowser.Model/Configuration/EncodingOptions.cs

@@ -31,6 +31,22 @@ namespace MediaBrowser.Model.Configuration
 
         public string VaapiDevice { get; set; }
 
+        public string OpenclDevice { get; set; }
+
+        public bool EnableTonemapping { get; set; }
+
+        public string TonemappingAlgorithm { get; set; }
+
+        public string TonemappingRange { get; set; }
+
+        public double TonemappingDesat { get; set; }
+
+        public double TonemappingThreshold { get; set; }
+
+        public double TonemappingPeak { get; set; }
+
+        public double TonemappingParam { get; set; }
+
         public int H264Crf { get; set; }
 
         public int H265Crf { get; set; }
@@ -58,8 +74,19 @@ namespace MediaBrowser.Model.Configuration
             EnableThrottling = false;
             ThrottleDelaySeconds = 180;
             EncodingThreadCount = -1;
-            // This is a DRM device that is almost guaranteed to be there on every intel platform, plus it's the default one in ffmpeg if you don't specify anything
+            // This is a DRM device that is almost guaranteed to be there on every intel platform,
+            // plus it's the default one in ffmpeg if you don't specify anything
             VaapiDevice = "/dev/dri/renderD128";
+            // This is the OpenCL device that is used for tonemapping.
+            // The left side of the dot is the platform number, and the right side is the device number on the platform.
+            OpenclDevice = "0.0";
+            EnableTonemapping = false;
+            TonemappingAlgorithm = "reinhard";
+            TonemappingRange = "auto";
+            TonemappingDesat = 0;
+            TonemappingThreshold = 0.8;
+            TonemappingPeak = 0;
+            TonemappingParam = 0;
             H264Crf = 23;
             H265Crf = 28;
             DeinterlaceDoubleRate = false;

+ 0 - 6
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -166,12 +166,6 @@ namespace MediaBrowser.Model.Configuration
         /// <value><c>true</c> if [enable dashboard response caching]; otherwise, <c>false</c>.</value>
         public bool EnableDashboardResponseCaching { get; set; }
 
-        /// <summary>
-        /// Gets or sets a custom path to serve the dashboard from.
-        /// </summary>
-        /// <value>The dashboard source path, or null if the default path should be used.</value>
-        public string DashboardSourcePath { get; set; }
-
         /// <summary>
         /// Gets or sets the image saving convention.
         /// </summary>

+ 12 - 6
MediaBrowser.Model/Entities/MediaStream.cs

@@ -35,6 +35,18 @@ namespace MediaBrowser.Model.Entities
         /// <value>The language.</value>
         public string Language { get; set; }
 
+        /// <summary>
+        /// Gets or sets the color range.
+        /// </summary>
+        /// <value>The color range.</value>
+        public string ColorRange { get; set; }
+
+        /// <summary>
+        /// Gets or sets the color space.
+        /// </summary>
+        /// <value>The color space.</value>
+        public string ColorSpace { get; set; }
+
         /// <summary>
         /// Gets or sets the color transfer.
         /// </summary>
@@ -47,12 +59,6 @@ namespace MediaBrowser.Model.Entities
         /// <value>The color primaries.</value>
         public string ColorPrimaries { get; set; }
 
-        /// <summary>
-        /// Gets or sets the color space.
-        /// </summary>
-        /// <value>The color space.</value>
-        public string ColorSpace { get; set; }
-
         /// <summary>
         /// Gets or sets the comment.
         /// </summary>

+ 1 - 1
MediaBrowser.Model/Net/MimeTypes.cs

@@ -125,7 +125,7 @@ namespace MediaBrowser.Model.Net
             { ".wmv", "video/x-ms-wmv" },
 
             // Type audio
-            { ".aac", "audio/mp4" },
+            { ".aac", "audio/aac" },
             { ".ac3", "audio/ac3" },
             { ".ape", "audio/x-ape" },
             { ".dsf", "audio/dsf" },

+ 1 - 1
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -156,7 +156,7 @@ namespace MediaBrowser.Providers.Manager
         /// <inheritdoc/>
         public async Task SaveImage(BaseItem item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken)
         {
-            var httpClient = _httpClientFactory.CreateClient();
+            var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
             using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
 
             var contentType = response.Content.Headers.ContentType.MediaType;

+ 2 - 1
MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs

@@ -4,6 +4,7 @@ using System.Collections.Generic;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -96,7 +97,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
         /// <inheritdoc />
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            var httpClient = _httpClientFactory.CreateClient();
+            var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
             return httpClient.GetAsync(url, cancellationToken);
         }
 

+ 2 - 1
MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs

@@ -10,6 +10,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
@@ -173,7 +174,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
 
             Directory.CreateDirectory(Path.GetDirectoryName(path));
 
-            using var response = await _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
             await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
             await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);

+ 2 - 1
MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs

@@ -4,6 +4,7 @@ using System.Collections.Generic;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -137,7 +138,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            var httpClient = _httpClientFactory.CreateClient();
+            var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
             return httpClient.GetAsync(url, cancellationToken);
         }
 

+ 2 - 1
MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs

@@ -9,6 +9,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
@@ -154,7 +155,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
 
             var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId);
 
-            using var response = await _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
 
             Directory.CreateDirectory(Path.GetDirectoryName(path));

+ 2 - 1
MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs

@@ -14,6 +14,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Xml;
 using MediaBrowser.Common;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
@@ -765,7 +766,7 @@ namespace MediaBrowser.Providers.Music
                 _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds);
                 _stopWatchMusicBrainz.Restart();
 
-                response = await _httpClientFactory.CreateClient().SendAsync(options).ConfigureAwait(false);
+                response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options).ConfigureAwait(false);
 
                 // We retry a finite number of times, and only whilst MB is indicating 503 (throttling)
             }

+ 2 - 1
MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs

@@ -6,6 +6,7 @@ using System.Globalization;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
@@ -82,7 +83,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
 
         public string Name => "The Open Movie Database";

+ 3 - 2
MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs

@@ -9,6 +9,7 @@ using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
@@ -129,7 +130,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
 
             var url = OmdbProvider.GetOmdbUrl(urlQuery);
 
-            using var response = await OmdbProvider.GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false);
+            using var response = await OmdbProvider.GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
             var resultList = new List<SearchResult>();
 
@@ -274,7 +275,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
 
         class SearchResult

+ 3 - 2
MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs

@@ -10,6 +10,7 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
@@ -296,7 +297,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
                     "i={0}&plot=short&tomatoes=true&r=json",
                     imdbParam));
 
-            using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false);
+            using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
             var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(stream).ConfigureAwait(false);
             Directory.CreateDirectory(Path.GetDirectoryName(path));
@@ -334,7 +335,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
                     imdbParam,
                     seasonId));
 
-            using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false);
+            using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
             var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<SeasonRootObject>(stream).ConfigureAwait(false);
             Directory.CreateDirectory(Path.GetDirectoryName(path));

+ 2 - 1
MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs

@@ -6,6 +6,7 @@ using System.Net.Http;
 using System.Globalization;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
@@ -116,7 +117,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
     }
 }

+ 2 - 1
MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs

@@ -5,6 +5,7 @@ using System.Collections.Generic;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
@@ -244,7 +245,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
 
         public int Order => 0;

+ 2 - 1
MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
@@ -106,7 +107,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
         /// <inheritdoc />
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
     }
 }

+ 2 - 1
MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
@@ -148,7 +149,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
     }
 }

+ 2 - 1
MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
@@ -146,7 +147,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
     }
 }

+ 2 - 1
MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs

@@ -8,6 +8,7 @@ using System.Text;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
@@ -410,7 +411,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
     }
 }

+ 2 - 1
MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Providers;
@@ -155,7 +156,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
     }
 }

+ 2 - 1
MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs

@@ -10,6 +10,7 @@ using System.Net.Http.Headers;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
@@ -271,7 +272,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
     }
 }

+ 2 - 1
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs

@@ -7,6 +7,7 @@ using System.Linq;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Providers;
@@ -204,7 +205,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
     }
 }

+ 3 - 2
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs

@@ -11,6 +11,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
@@ -383,7 +384,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
         internal Task<HttpResponseMessage> GetMovieDbResponse(HttpRequestMessage message)
         {
             message.Headers.UserAgent.ParseAdd(_appHost.ApplicationUserAgent);
-            return _httpClientFactory.CreateClient().SendAsync(message);
+            return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(message);
         }
 
         /// <inheritdoc />
@@ -392,7 +393,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
         /// <inheritdoc />
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
     }
 }

+ 2 - 1
MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
@@ -131,7 +132,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
     }
 }

+ 2 - 1
MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs

@@ -12,6 +12,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
@@ -267,7 +268,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
     }
 }

+ 2 - 1
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs

@@ -7,6 +7,7 @@ using System.Net.Http;
 using System.Net.Http.Headers;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
@@ -138,7 +139,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
         protected Task<HttpResponseMessage> GetResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
     }
 }

+ 2 - 1
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs

@@ -7,6 +7,7 @@ using System.Linq;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
@@ -38,7 +39,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
 
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)

+ 2 - 1
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs

@@ -9,6 +9,7 @@ using System.Net.Http;
 using System.Net.Http.Headers;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
@@ -124,7 +125,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
 
         private async Task<SeasonResult> GetSeasonInfo(string seriesTmdbId, int season, string preferredMetadataLanguage,

+ 2 - 1
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
@@ -182,7 +183,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
     }
 }

+ 2 - 1
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs

@@ -10,6 +10,7 @@ using System.Net.Http.Headers;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
@@ -551,7 +552,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
     }
 }

+ 2 - 1
MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs

@@ -4,6 +4,7 @@ using System.Collections.Generic;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Providers;
@@ -36,7 +37,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
     }
 }

+ 3 - 2
MediaBrowser.Providers/Studios/StudiosImageProvider.cs

@@ -8,6 +8,7 @@ using System.Linq;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
@@ -122,7 +123,7 @@ namespace MediaBrowser.Providers.Studios
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            var httpClient = _httpClientFactory.CreateClient();
+            var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
             return httpClient.GetAsync(url, cancellationToken);
         }
 
@@ -140,7 +141,7 @@ namespace MediaBrowser.Providers.Studios
 
             if (!fileInfo.Exists || (DateTime.UtcNow - fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays > 1)
             {
-                var httpClient = _httpClientFactory.CreateClient();
+                var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
 
                 Directory.CreateDirectory(Path.GetDirectoryName(file));
                 await using var response = await httpClient.GetStreamAsync(url).ConfigureAwait(false);

+ 1 - 1
debian/rules

@@ -40,7 +40,7 @@ override_dh_clistrip:
 
 override_dh_auto_build:
 	dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \
-		"-p:GenerateDocumentationFile=true;DebugSymbols=false;DebugType=none" Jellyfin.Server
+		"-p:DebugSymbols=false;DebugType=none" Jellyfin.Server
 
 override_dh_auto_clean:
 	dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true

+ 1 - 1
deployment/Dockerfile.docker.amd64

@@ -12,4 +12,4 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 
 # because of changes in docker and systemd we need to not build in parallel at the moment
 # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=true;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"

+ 1 - 1
deployment/Dockerfile.docker.arm64

@@ -12,4 +12,4 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 
 # because of changes in docker and systemd we need to not build in parallel at the moment
 # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=true;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"

+ 1 - 1
deployment/Dockerfile.docker.armhf

@@ -12,4 +12,4 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 
 # because of changes in docker and systemd we need to not build in parallel at the moment
 # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=true;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"

+ 1 - 1
deployment/build.linux.amd64

@@ -16,7 +16,7 @@ else
 fi
 
 # Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=true;DebugSymbols=false;DebugType=none;UseAppHost=true"
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
 tar -czf jellyfin-server_${version}_linux-amd64.tar.gz -C dist jellyfin-server_${version}
 rm -rf dist/jellyfin-server_${version}
 

+ 1 - 1
deployment/build.macos

@@ -16,7 +16,7 @@ else
 fi
 
 # Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=true;DebugSymbols=false;DebugType=none;UseAppHost=true"
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
 tar -czf jellyfin-server_${version}_macos-amd64.tar.gz -C dist jellyfin-server_${version}
 rm -rf dist/jellyfin-server_${version}
 

+ 1 - 1
deployment/build.portable

@@ -16,7 +16,7 @@ else
 fi
 
 # Build archives
-dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=true;DebugSymbols=false;DebugType=none;UseAppHost=true"
+dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
 tar -czf jellyfin-server_${version}_portable.tar.gz -C dist jellyfin-server_${version}
 rm -rf dist/jellyfin-server_${version}
 

+ 1 - 1
deployment/build.windows.amd64

@@ -23,7 +23,7 @@ fi
 output_dir="dist/jellyfin-server_${version}"
 
 # Build binary
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output ${output_dir}/ "-p:GenerateDocumentationFile=true;DebugSymbols=false;DebugType=none;UseAppHost=true"
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output ${output_dir}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
 
 # Prepare addins
 addin_build_dir="$( mktemp -d )"

+ 1 - 1
fedora/jellyfin.spec

@@ -54,7 +54,7 @@ The Jellyfin media server backend.
 export DOTNET_CLI_TELEMETRY_OPTOUT=1
 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
 dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \
-    "-p:GenerateDocumentationFile=true;DebugSymbols=false;DebugType=none" Jellyfin.Server
+    "-p:DebugSymbols=false;DebugType=none" Jellyfin.Server
 %{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/jellyfin/LICENSE
 %{__install} -D -m 0644 %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf
 %{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json