瀏覽代碼

Merge remote-tracking branch 'upstream/master' into dynamic-cors

crobibero 4 年之前
父節點
當前提交
8a08111adc
共有 100 個文件被更改,包括 834 次插入1586 次删除
  1. 4 4
      .ci/azure-pipelines-main.yml
  2. 7 2
      .ci/azure-pipelines-test.yml
  3. 1 1
      .vscode/tasks.json
  4. 2 0
      CONTRIBUTORS.md
  5. 1 1
      Dockerfile
  6. 1 1
      Dockerfile.arm
  7. 1 1
      Dockerfile.arm64
  8. 3 3
      Emby.Dlna/ConnectionManager/ConnectionManagerService.cs
  9. 2 2
      Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
  10. 1 1
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  11. 2 2
      Emby.Dlna/Didl/DidlBuilder.cs
  12. 1 0
      Emby.Dlna/Emby.Dlna.csproj
  13. 12 18
      Emby.Dlna/Eventing/DlnaEventManager.cs
  14. 8 7
      Emby.Dlna/Main/DlnaEntryPoint.cs
  15. 3 3
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs
  16. 21 20
      Emby.Dlna/PlayTo/Device.cs
  17. 5 4
      Emby.Dlna/PlayTo/PlayToManager.cs
  18. 45 71
      Emby.Dlna/PlayTo/SsdpHttpClient.cs
  19. 3 7
      Emby.Dlna/Service/BaseService.cs
  20. 2 2
      Emby.Naming/Common/NamingOptions.cs
  21. 1 1
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  22. 21 26
      Emby.Server.Implementations/ApplicationHost.cs
  23. 1 1
      Emby.Server.Implementations/Channels/ChannelManager.cs
  24. 1 1
      Emby.Server.Implementations/Dto/DtoService.cs
  25. 4 4
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  26. 0 335
      Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
  27. 4 3
      Emby.Server.Implementations/IO/FileRefresher.cs
  28. 1 1
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  29. 0 32
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  30. 1 31
      Emby.Server.Implementations/IO/StreamHelper.cs
  31. 21 30
      Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
  32. 6 25
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  33. 10 33
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  34. 148 245
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  35. 7 24
      Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
  36. 40 58
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  37. 7 6
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  38. 6 10
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  39. 17 30
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  40. 2 2
      Emby.Server.Implementations/Localization/Core/nb.json
  41. 60 3
      Emby.Server.Implementations/Localization/Core/nn.json
  42. 16 16
      Emby.Server.Implementations/Localization/Core/ta.json
  43. 28 31
      Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
  44. 21 21
      Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
  45. 6 6
      Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
  46. 7 7
      Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
  47. 6 9
      Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
  48. 7 10
      Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
  49. 36 52
      Emby.Server.Implementations/Updates/InstallationManager.cs
  50. 5 1
      Jellyfin.Api/BaseJellyfinApiController.cs
  51. 2 100
      Jellyfin.Api/Controllers/DashboardController.cs
  52. 1 3
      Jellyfin.Api/Controllers/DisplayPreferencesController.cs
  53. 11 9
      Jellyfin.Api/Controllers/DlnaServerController.cs
  54. 6 1
      Jellyfin.Api/Controllers/DynamicHlsController.cs
  55. 2 1
      Jellyfin.Api/Controllers/LiveTvController.cs
  56. 2 1
      Jellyfin.Api/Controllers/RemoteImageController.cs
  57. 13 9
      Jellyfin.Api/Controllers/VideosController.cs
  58. 2 1
      Jellyfin.Api/Helpers/AudioHelper.cs
  59. 1 1
      Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
  60. 2 2
      Jellyfin.Api/Jellyfin.Api.csproj
  61. 0 56
      Jellyfin.Api/MvcRoutePrefix.cs
  62. 31 47
      Jellyfin.Data/Entities/ActivityLog.cs
  63. 3 1
      Jellyfin.Data/Entities/DisplayPreferences.cs
  64. 16 45
      Jellyfin.Data/Entities/Group.cs
  65. 39 6
      Jellyfin.Data/Entities/ImageInfo.cs
  66. 4 3
      Jellyfin.Data/Entities/ItemDisplayPreferences.cs
  67. 2 0
      Jellyfin.Data/Entities/Libraries/Artwork.cs
  68. 2 0
      Jellyfin.Data/Entities/Libraries/Book.cs
  69. 3 1
      Jellyfin.Data/Entities/Libraries/BookMetadata.cs
  70. 2 0
      Jellyfin.Data/Entities/Libraries/Chapter.cs
  71. 2 0
      Jellyfin.Data/Entities/Libraries/Collection.cs
  72. 2 2
      Jellyfin.Data/Entities/Libraries/CollectionItem.cs
  73. 2 0
      Jellyfin.Data/Entities/Libraries/Company.cs
  74. 1 1
      Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs
  75. 2 0
      Jellyfin.Data/Entities/Libraries/CustomItem.cs
  76. 1 1
      Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs
  77. 2 0
      Jellyfin.Data/Entities/Libraries/Episode.cs
  78. 1 1
      Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs
  79. 5 5
      Jellyfin.Data/Entities/Libraries/Genre.cs
  80. 7 5
      Jellyfin.Data/Entities/Libraries/ItemMetadata.cs
  81. 2 0
      Jellyfin.Data/Entities/Libraries/MediaFile.cs
  82. 5 5
      Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs
  83. 2 0
      Jellyfin.Data/Entities/Libraries/Movie.cs
  84. 3 1
      Jellyfin.Data/Entities/Libraries/MovieMetadata.cs
  85. 2 0
      Jellyfin.Data/Entities/Libraries/MusicAlbum.cs
  86. 3 1
      Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs
  87. 2 0
      Jellyfin.Data/Entities/Libraries/Person.cs
  88. 7 5
      Jellyfin.Data/Entities/Libraries/PersonRole.cs
  89. 2 0
      Jellyfin.Data/Entities/Libraries/Photo.cs
  90. 1 1
      Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs
  91. 5 5
      Jellyfin.Data/Entities/Libraries/Rating.cs
  92. 2 0
      Jellyfin.Data/Entities/Libraries/Release.cs
  93. 2 0
      Jellyfin.Data/Entities/Libraries/Season.cs
  94. 1 1
      Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs
  95. 2 0
      Jellyfin.Data/Entities/Libraries/Series.cs
  96. 3 1
      Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs
  97. 2 0
      Jellyfin.Data/Entities/Libraries/Track.cs
  98. 1 1
      Jellyfin.Data/Entities/Libraries/TrackMetadata.cs
  99. 2 34
      Jellyfin.Data/Entities/Permission.cs
  100. 1 25
      Jellyfin.Data/Entities/Preference.cs

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

@@ -64,28 +64,28 @@ jobs:
           arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
           zipAfterPublish: false
 
-      - task: PublishPipelineArtifact@0
+      - task: PublishPipelineArtifact@1
         displayName: 'Publish Artifact Naming'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:
           targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll'
           artifactName: 'Jellyfin.Naming'
 
-      - task: PublishPipelineArtifact@0
+      - task: PublishPipelineArtifact@1
         displayName: 'Publish Artifact Controller'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:
           targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
           artifactName: 'Jellyfin.Controller'
 
-      - task: PublishPipelineArtifact@0
+      - task: PublishPipelineArtifact@1
         displayName: 'Publish Artifact Model'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:
           targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
           artifactName: 'Jellyfin.Model'
 
-      - task: PublishPipelineArtifact@0
+      - task: PublishPipelineArtifact@1
         displayName: 'Publish Artifact Common'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:

+ 7 - 2
.ci/azure-pipelines-test.yml

@@ -74,7 +74,6 @@ jobs:
       - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
         displayName: 'Run ReportGenerator'
-        enabled: false
         inputs:
           reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
           targetdir: "$(Agent.TempDirectory)/merged/"
@@ -84,10 +83,16 @@ jobs:
       - task: PublishCodeCoverageResults@1
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
         displayName: 'Publish Code Coverage'
-        enabled: false
         inputs:
           codeCoverageTool: "cobertura"
           #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
           summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml"
           pathToSources: $(Build.SourcesDirectory)
           failIfCoverageEmpty: true
+
+      - task: PublishPipelineArtifact@1
+        displayName: 'Publish OpenAPI Artifact'
+        condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
+        inputs:
+          targetPath: "tests/Jellyfin.Api.Tests/bin/Release/netcoreapp3.1/openapi.json"
+          artifactName: 'OpenAPI Spec'

+ 1 - 1
.vscode/tasks.json

@@ -17,7 +17,7 @@
             "type": "process",
             "args": [
                 "test",
-                "${workspaceFolder}/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj"
+                "${workspaceFolder}/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj"
             ],
             "problemMatcher": "$msCompile"
         }

+ 2 - 0
CONTRIBUTORS.md

@@ -57,6 +57,7 @@
  - [Larvitar](https://github.com/Larvitar)
  - [LeoVerto](https://github.com/LeoVerto)
  - [Liggy](https://github.com/Liggy)
+ - [lmaonator](https://github.com/lmaonator)
  - [LogicalPhallacy](https://github.com/LogicalPhallacy)
  - [loli10K](https://github.com/loli10K)
  - [lostmypillow](https://github.com/lostmypillow)
@@ -78,6 +79,7 @@
  - [nvllsvm](https://github.com/nvllsvm)
  - [nyanmisaka](https://github.com/nyanmisaka)
  - [oddstr13](https://github.com/oddstr13)
+ - [orryverducci](https://github.com/orryverducci)
  - [petermcneil](https://github.com/petermcneil)
  - [Phlogi](https://github.com/Phlogi)
  - [pjeanjean](https://github.com/pjeanjean)

+ 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=false;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=false;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=false;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 - 1
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -1363,7 +1363,7 @@ namespace Emby.Dlna.ContentDirectory
                 };
             }
 
-            Logger.LogError("Error parsing item Id: {id}. Returning user root folder.", id);
+            Logger.LogError("Error parsing item Id: {Id}. Returning user root folder.", id);
 
             return new ServerItem(_libraryManager.GetUserRootFolder());
         }

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

@@ -948,7 +948,7 @@ namespace Emby.Dlna.Didl
             }
             catch (XmlException ex)
             {
-                _logger.LogError(ex, "Error adding xml value: {value}", name);
+                _logger.LogError(ex, "Error adding xml value: {Value}", name);
             }
         }
 
@@ -960,7 +960,7 @@ namespace Emby.Dlna.Didl
             }
             catch (XmlException ex)
             {
-                _logger.LogError(ex, "Error adding xml value: {value}", value);
+                _logger.LogError(ex, "Error adding xml value: {Value}", value);
             }
         }
 

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

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

@@ -136,8 +136,8 @@ namespace Emby.Naming.Common
 
             CleanDateTimes = new[]
             {
-                @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
-                @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
+                @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
+                @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
             };
 
             CleanStrings = new[]

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

@@ -308,7 +308,7 @@ namespace Emby.Server.Implementations.AppBase
             }
             catch (Exception ex)
             {
-                Logger.LogError(ex, "Error loading configuration file: {path}", path);
+                Logger.LogError(ex, "Error loading configuration file: {Path}", path);
 
                 return Activator.CreateInstance(configurationType);
             }

+ 21 - 26
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;
 
@@ -279,6 +280,10 @@ namespace Emby.Server.Implementations
                 Password = ServerConfigurationManager.Configuration.CertificatePassword
             };
             Certificate = GetCertificate(CertificateInfo);
+
+            ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
+            ApplicationVersionString = ApplicationVersion.ToString(3);
+            ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
         }
 
         public string ExpandVirtualPath(string path)
@@ -308,16 +313,16 @@ namespace Emby.Server.Implementations
         }
 
         /// <inheritdoc />
-        public Version ApplicationVersion { get; } = typeof(ApplicationHost).Assembly.GetName().Version;
+        public Version ApplicationVersion { get; }
 
         /// <inheritdoc />
-        public string ApplicationVersionString { get; } = typeof(ApplicationHost).Assembly.GetName().Version.ToString(3);
+        public string ApplicationVersionString { get; }
 
         /// <summary>
         /// Gets the current application user agent.
         /// </summary>
         /// <value>The application user agent.</value>
-        public string ApplicationUserAgent => Name.Replace(' ', '-') + "/" + ApplicationVersionString;
+        public string ApplicationUserAgent { get; }
 
         /// <summary>
         /// Gets the email address for use within a comment section of a user agent field.
@@ -522,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>();
@@ -650,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();
 
@@ -1296,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)
             {
@@ -1401,7 +1396,7 @@ namespace Emby.Server.Implementations
 
             foreach (var assembly in assemblies)
             {
-                Logger.LogDebug("Found API endpoints in plugin {name}", assembly.FullName);
+                Logger.LogDebug("Found API endpoints in plugin {Name}", assembly.FullName);
                 yield return assembly;
             }
         }

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

@@ -890,7 +890,7 @@ namespace Emby.Server.Implementations.Channels
             }
             catch (Exception ex)
             {
-                _logger.LogError(ex, "Error writing to channel cache file: {path}", path);
+                _logger.LogError(ex, "Error writing to channel cache file: {Path}", path);
             }
         }
 

+ 1 - 1
Emby.Server.Implementations/Dto/DtoService.cs

@@ -197,7 +197,7 @@ namespace Emby.Server.Implementations.Dto
                 catch (Exception ex)
                 {
                     // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
-                    _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {itemName}", item.Name);
+                    _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {ItemName}", item.Name);
                 }
             }
 

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

@@ -32,10 +32,10 @@
     <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
-    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.7" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.7" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
+    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.7" />
     <PackageReference Include="Mono.Nat" Version="2.0.2" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />

+ 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
-            };
-        }
-    }
-}

+ 4 - 3
Emby.Server.Implementations/IO/FileRefresher.cs

@@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.IO
                     continue;
                 }
 
-                _logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path);
+                _logger.LogInformation("{Name} ({Path}) will be refreshed.", item.Name, item.Path);
 
                 try
                 {
@@ -160,11 +160,11 @@ namespace Emby.Server.Implementations.IO
                     // For now swallow and log.
                     // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
                     // Should we remove it from it's parent?
-                    _logger.LogError(ex, "Error refreshing {name}", item.Name);
+                    _logger.LogError(ex, "Error refreshing {Name}", item.Name);
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error refreshing {name}", item.Name);
+                    _logger.LogError(ex, "Error refreshing {Name}", item.Name);
                 }
             }
         }
@@ -214,6 +214,7 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
             _disposed = true;

+ 1 - 1
Emby.Server.Implementations/IO/LibraryMonitor.cs

@@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.IO
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error in ReportFileSystemChanged for {path}", path);
+                    _logger.LogError(ex, "Error in ReportFileSystemChanged for {Path}", path);
                 }
             }
         }

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

@@ -398,30 +398,6 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
-        public virtual void SetReadOnly(string path, bool isReadOnly)
-        {
-            if (OperatingSystem.Id != OperatingSystemId.Windows)
-            {
-                return;
-            }
-
-            var info = GetExtendedFileSystemInfo(path);
-
-            if (info.Exists && info.IsReadOnly != isReadOnly)
-            {
-                if (isReadOnly)
-                {
-                    File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.ReadOnly);
-                }
-                else
-                {
-                    var attributes = File.GetAttributes(path);
-                    attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
-                    File.SetAttributes(path, attributes);
-                }
-            }
-        }
-
         public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly)
         {
             if (OperatingSystem.Id != OperatingSystemId.Windows)
@@ -707,14 +683,6 @@ namespace Emby.Server.Implementations.IO
             return Directory.EnumerateFileSystemEntries(path, "*", searchOption);
         }
 
-        public virtual void SetExecutable(string path)
-        {
-            if (OperatingSystem.Id == OperatingSystemId.Darwin)
-            {
-                RunProcess("chmod", "+x \"" + path + "\"", Path.GetDirectoryName(path));
-            }
-        }
-
         private static void RunProcess(string path, string args, string workingDirectory)
         {
             using (var process = Process.Start(new ProcessStartInfo

+ 1 - 31
Emby.Server.Implementations/IO/StreamHelper.cs

@@ -11,8 +11,6 @@ namespace Emby.Server.Implementations.IO
 {
     public class StreamHelper : IStreamHelper
     {
-        private const int StreamCopyToBufferSize = 81920;
-
         public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken)
         {
             byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
@@ -83,37 +81,9 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
-        public async Task<int> CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken)
-        {
-            byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
-            try
-            {
-                int totalBytesRead = 0;
-
-                int bytesRead;
-                while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
-                {
-                    var bytesToWrite = bytesRead;
-
-                    if (bytesToWrite > 0)
-                    {
-                        await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
-
-                        totalBytesRead += bytesRead;
-                    }
-                }
-
-                return totalBytesRead;
-            }
-            finally
-            {
-                ArrayPool<byte>.Shared.Return(buffer);
-            }
-        }
-
         public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
         {
-            byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
+            byte[] buffer = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
             try
             {
                 int bytesRead;

+ 21 - 30
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;
         }
 
@@ -52,10 +52,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 _logger.LogInformation("Copying recording stream to file {0}", targetFile);
 
                 // The media source is infinite so we need to handle stopping ourselves
-                var durationToken = new CancellationTokenSource(duration);
-                cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+                using var durationToken = new CancellationTokenSource(duration);
+                using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
 
-                await directStreamProvider.CopyToAsync(output, cancellationToken).ConfigureAwait(false);
+                await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false);
             }
 
             _logger.LogInformation("Recording completed to file {0}", targetFile);
@@ -63,37 +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
-            };
+            _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
-                    var durationToken = new CancellationTokenSource(duration);
-                    cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).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, 81920, cancellationToken).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);
         }

+ 6 - 25
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;
@@ -604,11 +605,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             return Task.CompletedTask;
         }
 
-        public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
-        {
-            return Task.CompletedTask;
-        }
-
         public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
         {
             throw new NotImplementedException();
@@ -808,11 +804,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             return null;
         }
 
-        public IEnumerable<ActiveRecordingInfo> GetAllActiveRecordings()
-        {
-            return _activeRecordings.Values.Where(i => i.Timer.Status == RecordingStatus.InProgress && !i.CancellationTokenSource.IsCancellationRequested);
-        }
-
         public ActiveRecordingInfo GetActiveRecordingInfo(string path)
         {
             if (string.IsNullOrWhiteSpace(path))
@@ -1015,16 +1006,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             throw new Exception("Tuner not found.");
         }
 
-        private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing)
-        {
-            var json = _jsonSerializer.SerializeToString(mediaSource);
-            mediaSource = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
-
-            mediaSource.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + "_" + mediaSource.Id;
-
-            return mediaSource;
-        }
-
         public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
         {
             if (string.IsNullOrWhiteSpace(channelId))
@@ -1654,10 +1635,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
             if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
             {
-                return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config);
+                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)

+ 10 - 33
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -8,12 +8,9 @@ using System.IO;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
@@ -26,26 +23,24 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         private readonly ILogger _logger;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IServerApplicationPaths _appPaths;
+        private readonly IJsonSerializer _json;
+        private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
+
         private bool _hasExited;
         private Stream _logFileStream;
         private string _targetPath;
         private Process _process;
-        private readonly IJsonSerializer _json;
-        private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
-        private readonly IServerConfigurationManager _config;
 
         public EncodedRecorder(
             ILogger logger,
             IMediaEncoder mediaEncoder,
             IServerApplicationPaths appPaths,
-            IJsonSerializer json,
-            IServerConfigurationManager config)
+            IJsonSerializer json)
         {
             _logger = logger;
             _mediaEncoder = mediaEncoder;
             _appPaths = appPaths;
             _json = json;
-            _config = config;
         }
 
         private static bool CopySubtitles => false;
@@ -58,19 +53,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         public async Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
         {
             // The media source is infinite so we need to handle stopping ourselves
-            var durationToken = new CancellationTokenSource(duration);
-            cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+            using var durationToken = new CancellationTokenSource(duration);
+            using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
 
-            await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationToken).ConfigureAwait(false);
+            await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationTokenSource.Token).ConfigureAwait(false);
 
             _logger.LogInformation("Recording completed to file {0}", targetFile);
         }
 
-        private EncodingOptions GetEncodingOptions()
-        {
-            return _config.GetConfiguration<EncodingOptions>("encoding");
-        }
-
         private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
         {
             _targetPath = targetFile;
@@ -108,7 +98,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 StartInfo = processStartInfo,
                 EnableRaisingEvents = true
             };
-            _process.Exited += (sender, args) => OnFfMpegProcessExited(_process, inputFile);
+            _process.Exited += (sender, args) => OnFfMpegProcessExited(_process);
 
             _process.Start();
 
@@ -221,20 +211,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         }
 
         protected string GetOutputSizeParam()
-        {
-            var filters = new List<string>();
-
-            filters.Add("yadif=0:-1:0");
-
-            var output = string.Empty;
-
-            if (filters.Count > 0)
-            {
-                output += string.Format(CultureInfo.InvariantCulture, " -vf \"{0}\"", string.Join(",", filters.ToArray()));
-            }
-
-            return output;
-        }
+            => "-vf \"yadif=0:-1:0\"";
 
         private void Stop()
         {
@@ -291,7 +268,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         /// <summary>
         /// Processes the exited.
         /// </summary>
-        private void OnFfMpegProcessExited(Process process, string inputFile)
+        private void OnFfMpegProcessExited(Process process)
         {
             using (process)
             {

+ 148 - 245
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;
@@ -24,23 +26,23 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 {
     public class SchedulesDirect : IListingsProvider
     {
+        private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
+
         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;
 
-        private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
-
         public SchedulesDirect(
             ILogger<SchedulesDirect> logger,
             IJsonSerializer jsonSerializer,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IApplicationHost appHost)
         {
             _logger = logger;
             _jsonSerializer = jsonSerializer;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
         }
 
@@ -61,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             while (start <= end)
             {
-                dates.Add(start.ToString("yyyy-MM-dd"));
+                dates.Add(start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
                 start = start.AddDays(1);
             }
 
@@ -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 programIdsWithImages =
-                        programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
-                        .ToList();
+            var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
 
-                    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)
@@ -367,13 +352,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             if (!string.IsNullOrWhiteSpace(details.originalAirDate))
             {
-                info.OriginalAirDate = DateTime.Parse(details.originalAirDate);
+                info.OriginalAirDate = DateTime.Parse(details.originalAirDate, CultureInfo.InvariantCulture);
                 info.ProductionYear = info.OriginalAirDate.Value.Year;
             }
 
             if (details.movie != null)
             {
-                if (!string.IsNullOrEmpty(details.movie.year) && int.TryParse(details.movie.year, out int year))
+                if (!string.IsNullOrEmpty(details.movie.year)
+                    && int.TryParse(details.movie.year, out int year))
                 {
                     info.ProductionYear = year;
                 }
@@ -482,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)
             {
@@ -518,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);
 
-                    if (root != null)
+                var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(response).ConfigureAwait(false);
+
+                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)
@@ -587,7 +558,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 return null;
             }
 
-            NameValuePair savedToken = null;
+            NameValuePair savedToken;
             if (!_tokens.TryGetValue(username, out savedToken))
             {
                 savedToken = new NameValuePair();
@@ -633,16 +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)
             {
@@ -659,65 +630,28 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 }
             }
 
-            options.RequestHeaders["token"] = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false);
-            return await Post(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<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);
-        }
-
-        private async Task<string> GetTokenInternal(string username, string password,
+        private async Task<string> GetTokenInternal(
+            string username,
+            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)
@@ -736,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)
@@ -768,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)
             {
@@ -851,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)
                 {

+ 2 - 2
Emby.Server.Implementations/Localization/Core/nb.json

@@ -71,7 +71,7 @@
     "ScheduledTaskFailedWithName": "{0} mislykkes",
     "ScheduledTaskStartedWithName": "{0} startet",
     "ServerNameNeedsToBeRestarted": "{0} må startes på nytt",
-    "Shows": "Programmer",
+    "Shows": "Program",
     "Songs": "Sanger",
     "StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.",
     "SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for  {0}",
@@ -88,7 +88,7 @@
     "UserOnlineFromDevice": "{0} er tilkoblet fra {1}",
     "UserPasswordChangedWithName": "Passordet for {0} er oppdatert",
     "UserPolicyUpdatedWithName": "Brukerpolicyen har blitt oppdatert for {0}",
-    "UserStartedPlayingItemWithValues": "{0} har startet avspilling {1}",
+    "UserStartedPlayingItemWithValues": "{0} har startet avspilling {1} på {2}",
     "UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling  {1}",
     "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt",
     "ValueSpecialEpisodeName": "Spesialepisode - {0}",

+ 60 - 3
Emby.Server.Implementations/Localization/Core/nn.json

@@ -35,7 +35,7 @@
     "AuthenticationSucceededWithUserName": "{0} Har logga inn",
     "Artists": "Artistar",
     "Application": "Program",
-    "AppDeviceValues": "App: {0}, Einheit: {1}",
+    "AppDeviceValues": "App: {0}, Eining: {1}",
     "Albums": "Album",
     "NotificationOptionServerRestartRequired": "Tenaren krev omstart",
     "NotificationOptionPluginUpdateInstalled": "Tilleggsprogram-oppdatering vart installert",
@@ -43,7 +43,7 @@
     "NotificationOptionPluginInstalled": "Tilleggsprogram installert",
     "NotificationOptionPluginError": "Tilleggsprogram feila",
     "NotificationOptionNewLibraryContent": "Nytt innhald er lagt til",
-    "NotificationOptionInstallationFailed": "Installasjonen feila",
+    "NotificationOptionInstallationFailed": "Installasjonsfeil",
     "NotificationOptionCameraImageUploaded": "Kamerabilde vart lasta opp",
     "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppa",
     "NotificationOptionAudioPlayback": "Lydavspilling påbyrja",
@@ -56,5 +56,62 @@
     "MusicVideos": "Musikkvideoar",
     "Music": "Musikk",
     "Movies": "Filmar",
-    "MixedContent": "Blanda innhald"
+    "MixedContent": "Blanda innhald",
+    "Sync": "Synkronisera",
+    "TaskDownloadMissingSubtitlesDescription": "Søk Internettet for manglande undertekstar basert på metadatainnstillingar.",
+    "TaskDownloadMissingSubtitles": "Last ned manglande undertekstar",
+    "TaskRefreshChannelsDescription": "Oppdater internettkanalinformasjon.",
+    "TaskRefreshChannels": "Oppdater kanalar",
+    "TaskCleanTranscodeDescription": "Slett transkodefiler som er meir enn ein dag gamal.",
+    "TaskCleanTranscode": "Reins transkodemappe",
+    "TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringar for programtillegg som er sette opp til å oppdaterast automatisk.",
+    "TaskUpdatePlugins": "Oppdaterer programtillegg",
+    "TaskRefreshPeopleDescription": "Oppdaterer metadata for skodespelarar og regissørar i mediebiblioteket ditt.",
+    "TaskRefreshPeople": "Oppdater personar",
+    "TaskCleanLogsDescription": "Slett loggfiler som er meir enn {0} dagar gamle.",
+    "TaskCleanLogs": "Reins loggmappe",
+    "TaskRefreshLibraryDescription": "Skannar mediebiblioteket ditt for nye filer og oppdaterer metadata.",
+    "TaskRefreshLibrary": "Skann mediebibliotek",
+    "TaskRefreshChapterImagesDescription": "Lager miniatyrbilete for videoar som har kapittel.",
+    "TaskRefreshChapterImages": "Trekk ut kapittelbilete",
+    "TaskCleanCacheDescription": "Slettar mellomlagra filer som ikkje lengre trengst av systemet.",
+    "TaskCleanCache": "Rens mappe for hurtiglager",
+    "TasksChannelsCategory": "Internettkanalar",
+    "TasksApplicationCategory": "Applikasjon",
+    "TasksLibraryCategory": "Bibliotek",
+    "TasksMaintenanceCategory": "Vedlikehald",
+    "VersionNumber": "Versjon {0}",
+    "ValueSpecialEpisodeName": "Spesialepisode - {0}",
+    "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt",
+    "UserStoppedPlayingItemWithValues": "{0} har fullført avspeling {1} på {2}",
+    "UserStartedPlayingItemWithValues": "{0} spelar {1} på {2}",
+    "UserPolicyUpdatedWithName": "Brukarreglar har blitt oppdatert for {0}",
+    "UserPasswordChangedWithName": "Passordet for {0} er oppdatert",
+    "UserOnlineFromDevice": "{0} er direktekopla frå {1}",
+    "UserOfflineFromDevice": "{0} har kopla frå {1}",
+    "UserLockedOutWithName": "Brukar {0} har blitt utestengd",
+    "UserDownloadingItemWithValues": "{0} lastar ned {1}",
+    "UserDeletedWithName": "Brukar {0} er sletta",
+    "UserCreatedWithName": "Brukar {0} er oppretta",
+    "User": "Brukar",
+    "TvShows": "TV-seriar",
+    "System": "System",
+    "SubtitleDownloadFailureFromForItem": "Feila å laste ned undertekstar frå {0} for {1}",
+    "StartupEmbyServerIsLoading": "Jellyfintenaren laster. Prøv igjen om litt.",
+    "Songs": "Songar",
+    "Shows": "Program",
+    "ServerNameNeedsToBeRestarted": "{0} må omstartast",
+    "ScheduledTaskStartedWithName": "{0} starta",
+    "ScheduledTaskFailedWithName": "{0} feila",
+    "ProviderValue": "Leverandør: {0}",
+    "PluginUpdatedWithName": "{0} blei oppdatert",
+    "PluginUninstalledWithName": "{0} blei avinstallert",
+    "PluginInstalledWithName": "{0} blei installert",
+    "Plugin": "Programtillegg",
+    "Playlists": "Speleliste",
+    "Photos": "Foto",
+    "NotificationOptionVideoPlaybackStopped": "Videoavspeling stoppa",
+    "NotificationOptionVideoPlayback": "Videoavspeling starta",
+    "NotificationOptionUserLockedOut": "Brukar er utestengd",
+    "NotificationOptionTaskFailed": "Planlagt oppgåve feila"
 }

+ 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} ஐ இயக்குகிறது"
 }

+ 28 - 31
Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs

@@ -5,10 +5,10 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
-using MediaBrowser.Model.Globalization;
 
 namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 {
@@ -21,10 +21,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         /// Gets or sets the application paths.
         /// </summary>
         /// <value>The application paths.</value>
-        private IApplicationPaths ApplicationPaths { get; set; }
-
+        private readonly IApplicationPaths _applicationPaths;
         private readonly ILogger<DeleteCacheFileTask> _logger;
-
         private readonly IFileSystem _fileSystem;
         private readonly ILocalizationManager _localization;
 
@@ -37,20 +35,41 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             IFileSystem fileSystem,
             ILocalizationManager localization)
         {
-            ApplicationPaths = appPaths;
+            _applicationPaths = appPaths;
             _logger = logger;
             _fileSystem = fileSystem;
             _localization = localization;
         }
 
+        /// <inheritdoc />
+        public string Name => _localization.GetLocalizedString("TaskCleanCache");
+
+        /// <inheritdoc />
+        public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription");
+
+        /// <inheritdoc />
+        public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+        /// <inheritdoc />
+        public string Key => "DeleteCacheFiles";
+
+        /// <inheritdoc />
+        public bool IsHidden => false;
+
+        /// <inheritdoc />
+        public bool IsEnabled => true;
+
+        /// <inheritdoc />
+        public bool IsLogged => true;
+
         /// <summary>
         /// Creates the triggers that define when the task will run.
         /// </summary>
         /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
         public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
         {
-            return new[] {
-
+            return new[]
+            {
                 // Every so often
                 new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
             };
@@ -68,7 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 
             try
             {
-                DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.CachePath, minDateModified, progress);
+                DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.CachePath, minDateModified, progress);
             }
             catch (DirectoryNotFoundException)
             {
@@ -81,7 +100,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 
             try
             {
-                DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, minDateModified, progress);
+                DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.TempDirectory, minDateModified, progress);
             }
             catch (DirectoryNotFoundException)
             {
@@ -91,7 +110,6 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             return Task.CompletedTask;
         }
 
-
         /// <summary>
         /// Deletes the cache files from directory with a last write time less than a given date.
         /// </summary>
@@ -164,26 +182,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
                 _logger.LogError(ex, "Error deleting file {path}", path);
             }
         }
-
-        /// <inheritdoc />
-        public string Name => _localization.GetLocalizedString("TaskCleanCache");
-
-        /// <inheritdoc />
-        public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription");
-
-        /// <inheritdoc />
-        public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
-
-        /// <inheritdoc />
-        public string Key => "DeleteCacheFiles";
-
-        /// <inheritdoc />
-        public bool IsHidden => false;
-
-        /// <inheritdoc />
-        public bool IsEnabled => true;
-
-        /// <inheritdoc />
-        public bool IsLogged => true;
     }
 }

+ 21 - 21
Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs

@@ -34,6 +34,27 @@ namespace Emby.Server.Implementations.ScheduledTasks
             _localization = localization;
         }
 
+        /// <inheritdoc />
+        public string Name => _localization.GetLocalizedString("TaskUpdatePlugins");
+
+        /// <inheritdoc />
+        public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription");
+
+        /// <inheritdoc />
+        public string Category => _localization.GetLocalizedString("TasksApplicationCategory");
+
+        /// <inheritdoc />
+        public string Key => "PluginUpdates";
+
+        /// <inheritdoc />
+        public bool IsHidden => false;
+
+        /// <inheritdoc />
+        public bool IsEnabled => true;
+
+        /// <inheritdoc />
+        public bool IsLogged => true;
+
         /// <summary>
         /// Creates the triggers that define when the task will run.
         /// </summary>
@@ -98,26 +119,5 @@ namespace Emby.Server.Implementations.ScheduledTasks
 
             progress.Report(100);
         }
-
-        /// <inheritdoc />
-        public string Name => _localization.GetLocalizedString("TaskUpdatePlugins");
-
-        /// <inheritdoc />
-        public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription");
-
-        /// <inheritdoc />
-        public string Category => _localization.GetLocalizedString("TasksApplicationCategory");
-
-        /// <inheritdoc />
-        public string Key => "PluginUpdates";
-
-        /// <inheritdoc />
-        public bool IsHidden => false;
-
-        /// <inheritdoc />
-        public bool IsEnabled => true;
-
-        /// <inheritdoc />
-        public bool IsLogged => true;
     }
 }

+ 6 - 6
Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs

@@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
     public class DailyTrigger : ITaskTrigger
     {
         /// <summary>
-        /// Get the time of day to trigger the task to run.
+        /// Occurs when [triggered].
+        /// </summary>
+        public event EventHandler<EventArgs> Triggered;
+
+        /// <summary>
+        /// Gets or sets the time of day to trigger the task to run.
         /// </summary>
         /// <value>The time of day.</value>
         public TimeSpan TimeOfDay { get; set; }
@@ -69,11 +74,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
             }
         }
 
-        /// <summary>
-        /// Occurs when [triggered].
-        /// </summary>
-        public event EventHandler<EventArgs> Triggered;
-
         /// <summary>
         /// Called when [triggered].
         /// </summary>

+ 7 - 7
Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs

@@ -11,6 +11,13 @@ namespace Emby.Server.Implementations.ScheduledTasks
     /// </summary>
     public class IntervalTrigger : ITaskTrigger
     {
+        private DateTime _lastStartDate;
+
+        /// <summary>
+        /// Occurs when [triggered].
+        /// </summary>
+        public event EventHandler<EventArgs> Triggered;
+
         /// <summary>
         /// Gets or sets the interval.
         /// </summary>
@@ -28,8 +35,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// <value>The timer.</value>
         private Timer Timer { get; set; }
 
-        private DateTime _lastStartDate;
-
         /// <summary>
         /// Stars waiting for the trigger action.
         /// </summary>
@@ -88,11 +93,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
             }
         }
 
-        /// <summary>
-        /// Occurs when [triggered].
-        /// </summary>
-        public event EventHandler<EventArgs> Triggered;
-
         /// <summary>
         /// Called when [triggered].
         /// </summary>

+ 6 - 9
Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs

@@ -12,6 +12,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
     /// </summary>
     public class StartupTrigger : ITaskTrigger
     {
+        /// <summary>
+        /// Occurs when [triggered].
+        /// </summary>
+        public event EventHandler<EventArgs> Triggered;
+
         public int DelayMs { get; set; }
 
         /// <summary>
@@ -48,20 +53,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
         {
         }
 
-        /// <summary>
-        /// Occurs when [triggered].
-        /// </summary>
-        public event EventHandler<EventArgs> Triggered;
-
         /// <summary>
         /// Called when [triggered].
         /// </summary>
         private void OnTriggered()
         {
-            if (Triggered != null)
-            {
-                Triggered(this, EventArgs.Empty);
-            }
+            Triggered?.Invoke(this, EventArgs.Empty);
         }
     }
 }

+ 7 - 10
Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs

@@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
     public class WeeklyTrigger : ITaskTrigger
     {
         /// <summary>
-        /// Get the time of day to trigger the task to run.
+        /// Occurs when [triggered].
+        /// </summary>
+        public event EventHandler<EventArgs> Triggered;
+
+        /// <summary>
+        /// Gets or sets the time of day to trigger the task to run.
         /// </summary>
         /// <value>The time of day.</value>
         public TimeSpan TimeOfDay { get; set; }
@@ -95,20 +100,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
             }
         }
 
-        /// <summary>
-        /// Occurs when [triggered].
-        /// </summary>
-        public event EventHandler<EventArgs> Triggered;
-
         /// <summary>
         /// Called when [triggered].
         /// </summary>
         private void OnTriggered()
         {
-            if (Triggered != null)
-            {
-                Triggered(this, EventArgs.Empty);
-            }
+            Triggered?.Invoke(this, EventArgs.Empty);
         }
     }
 }

+ 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 - 100
Jellyfin.Api/Controllers/DashboardController.cs

@@ -15,6 +15,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
 {
@@ -26,38 +27,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>
@@ -169,87 +152,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");
-            }
-
-            var stream = new FileStream(_resourceFileManager.GetResourcePath(basePath, path), FileMode.Open, FileAccess.Read);
-            return File(stream, 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));

+ 1 - 3
Jellyfin.Api/Controllers/DisplayPreferencesController.cs

@@ -153,7 +153,6 @@ namespace Jellyfin.Api.Controllers
             {
                 var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Parse(key.Substring("landing-".Length)), existingDisplayPreferences.Client);
                 itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
-                _displayPreferencesManager.SaveChanges(itemPreferences);
             }
 
             var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Empty, existingDisplayPreferences.Client);
@@ -167,8 +166,7 @@ namespace Jellyfin.Api.Controllers
                 itemPrefs.ViewType = viewType;
             }
 
-            _displayPreferencesManager.SaveChanges(existingDisplayPreferences);
-            _displayPreferencesManager.SaveChanges(itemPrefs);
+            _displayPreferencesManager.SaveChanges();
 
             return NoContent();
         }

+ 11 - 9
Jellyfin.Api/Controllers/DlnaServerController.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
+using System.Net.Mime;
 using System.Threading.Tasks;
 using Emby.Dlna;
 using Emby.Dlna.Main;
@@ -17,8 +18,6 @@ namespace Jellyfin.Api.Controllers
     [Route("Dlna")]
     public class DlnaServerController : BaseJellyfinApiController
     {
-        private const string XMLContentType = "text/xml; charset=UTF-8";
-
         private readonly IDlnaManager _dlnaManager;
         private readonly IContentDirectory _contentDirectory;
         private readonly IConnectionManager _connectionManager;
@@ -44,7 +43,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
         [HttpGet("{serverId}/description")]
         [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
-        [Produces(XMLContentType)]
+        [Produces(MediaTypeNames.Text.Xml)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult GetDescriptionXml([FromRoute] string serverId)
         {
@@ -61,8 +60,9 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Dlna content directory returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
         [HttpGet("{serverId}/ContentDirectory")]
-        [HttpGet("{serverId}/ContentDirectory.xml", Name = "GetContentDirectory_2")]
-        [Produces(XMLContentType)]
+        [HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
+        [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
+        [Produces(MediaTypeNames.Text.Xml)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
         public ActionResult GetContentDirectory([FromRoute] string serverId)
@@ -76,8 +76,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="serverId">Server UUID.</param>
         /// <returns>Dlna media receiver registrar xml.</returns>
         [HttpGet("{serverId}/MediaReceiverRegistrar")]
-        [HttpGet("{serverId}/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")]
-        [Produces(XMLContentType)]
+        [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
+        [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")]
+        [Produces(MediaTypeNames.Text.Xml)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
         public ActionResult GetMediaReceiverRegistrar([FromRoute] string serverId)
@@ -91,8 +92,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="serverId">Server UUID.</param>
         /// <returns>Dlna media receiver registrar xml.</returns>
         [HttpGet("{serverId}/ConnectionManager")]
-        [HttpGet("{serverId}/ConnectionManager.xml", Name = "GetConnectionManager_2")]
-        [Produces(XMLContentType)]
+        [HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
+        [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")]
+        [Produces(MediaTypeNames.Text.Xml)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
         public ActionResult GetConnectionManager([FromRoute] string serverId)

+ 6 - 1
Jellyfin.Api/Controllers/DynamicHlsController.cs

@@ -1354,15 +1354,20 @@ namespace Jellyfin.Api.Controllers
                 segmentFormat = "mpegts";
             }
 
+            var maxMuxingQueueSize = encodingOptions.MaxMuxingQueueSize > 128
+                ? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
+                : "128";
+
             return string.Format(
                 CultureInfo.InvariantCulture,
-                "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size 2048 -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"",
+                "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -individual_header_trailer 0 -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"",
                 inputModifier,
                 _encodingHelper.GetInputArgument(state, encodingOptions),
                 threads,
                 mapArgs,
                 GetVideoArguments(state, encodingOptions, startNumber),
                 GetAudioArguments(state, encodingOptions),
+                maxMuxingQueueSize,
                 state.SegmentLength.ToString(CultureInfo.InvariantCulture),
                 segmentFormat,
                 startNumberParam,

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

@@ -16,6 +16,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;
@@ -1069,7 +1070,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         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

@@ -9,6 +9,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
@@ -244,7 +245,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);

+ 13 - 9
Jellyfin.Api/Controllers/VideosController.cs

@@ -11,6 +11,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;
@@ -233,7 +234,7 @@ namespace Jellyfin.Api.Controllers
                     .First();
             }
 
-            var list = primaryVersion.LinkedAlternateVersions.ToList();
+            var alternateVersionsOfPrimary = primaryVersion.LinkedAlternateVersions.ToList();
 
             foreach (var item in items.Where(i => i.Id != primaryVersion.Id))
             {
@@ -241,17 +242,20 @@ namespace Jellyfin.Api.Controllers
 
                 await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
 
-                list.Add(new LinkedChild
+                if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase)))
                 {
-                    Path = item.Path,
-                    ItemId = item.Id
-                });
+                    alternateVersionsOfPrimary.Add(new LinkedChild
+                    {
+                        Path = item.Path,
+                        ItemId = item.Id
+                    });
+                }
 
                 foreach (var linkedItem in item.LinkedAlternateVersions)
                 {
-                    if (!list.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
+                    if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
                     {
-                        list.Add(linkedItem);
+                        alternateVersionsOfPrimary.Add(linkedItem);
                     }
                 }
 
@@ -262,7 +266,7 @@ namespace Jellyfin.Api.Controllers
                 }
             }
 
-            primaryVersion.LinkedAlternateVersions = list.ToArray();
+            primaryVersion.LinkedAlternateVersions = alternateVersionsOfPrimary.ToArray();
             await primaryVersion.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
             return NoContent();
         }
@@ -470,7 +474,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);
             }
 

+ 1 - 1
Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs

@@ -72,7 +72,7 @@ namespace Jellyfin.Api.Helpers
                 return new NoContentResult();
             }
 
-            return new PhysicalFileResult(path, contentType);
+            return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true };
         }
 
         /// <summary>

+ 2 - 2
Jellyfin.Api/Jellyfin.Api.csproj

@@ -14,9 +14,9 @@
 
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
-    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.6" />
+    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.7" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
-    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.5.1" />
   </ItemGroup>

+ 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));
-                }
-            }
-        }
-    }
-}

+ 31 - 47
Jellyfin.Data/Entities/ActivityLog.cs

@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
 using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
@@ -11,7 +9,7 @@ namespace Jellyfin.Data.Entities
     /// <summary>
     /// An entity referencing an activity log entry.
     /// </summary>
-    public partial class ActivityLog : IHasConcurrencyToken
+    public class ActivityLog : IHasConcurrencyToken
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="ActivityLog"/> class.
@@ -32,13 +30,11 @@ namespace Jellyfin.Data.Entities
                 throw new ArgumentNullException(nameof(type));
             }
 
-            this.Name = name;
-            this.Type = type;
-            this.UserId = userId;
-            this.DateCreated = DateTime.UtcNow;
-            this.LogSeverity = LogLevel.Trace;
-
-            Init();
+            Name = name;
+            Type = type;
+            UserId = userId;
+            DateCreated = DateTime.UtcNow;
+            LogSeverity = LogLevel.Trace;
         }
 
         /// <summary>
@@ -47,38 +43,21 @@ namespace Jellyfin.Data.Entities
         /// </summary>
         protected ActivityLog()
         {
-            Init();
-        }
-
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="name">The name.</param>
-        /// <param name="type">The type.</param>
-        /// <param name="userId">The user's id.</param>
-        /// <returns>The new <see cref="ActivityLog"/> instance.</returns>
-        public static ActivityLog Create(string name, string type, Guid userId)
-        {
-            return new ActivityLog(name, type, userId);
         }
 
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
         /// <summary>
         /// Gets or sets the identity of this instance.
         /// This is the key in the backing database.
         /// </summary>
-        [Key]
-        [Required]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         public int Id { get; protected set; }
 
         /// <summary>
         /// Gets or sets the name.
-        /// Required, Max length = 512.
         /// </summary>
+        /// <remarks>
+        /// Required, Max length = 512.
+        /// </remarks>
         [Required]
         [MaxLength(512)]
         [StringLength(512)]
@@ -86,24 +65,30 @@ namespace Jellyfin.Data.Entities
 
         /// <summary>
         /// Gets or sets the overview.
-        /// Max length = 512.
         /// </summary>
+        /// <remarks>
+        /// Max length = 512.
+        /// </remarks>
         [MaxLength(512)]
         [StringLength(512)]
         public string Overview { get; set; }
 
         /// <summary>
         /// Gets or sets the short overview.
-        /// Max length = 512.
         /// </summary>
+        /// <remarks>
+        /// Max length = 512.
+        /// </remarks>
         [MaxLength(512)]
         [StringLength(512)]
         public string ShortOverview { get; set; }
 
         /// <summary>
         /// Gets or sets the type.
-        /// Required, Max length = 256.
         /// </summary>
+        /// <remarks>
+        /// Required, Max length = 256.
+        /// </remarks>
         [Required]
         [MaxLength(256)]
         [StringLength(256)]
@@ -111,43 +96,42 @@ namespace Jellyfin.Data.Entities
 
         /// <summary>
         /// Gets or sets the user id.
-        /// Required.
         /// </summary>
-        [Required]
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         public Guid UserId { get; set; }
 
         /// <summary>
         /// Gets or sets the item id.
-        /// Max length = 256.
         /// </summary>
+        /// <remarks>
+        /// Max length = 256.
+        /// </remarks>
         [MaxLength(256)]
         [StringLength(256)]
         public string ItemId { get; set; }
 
         /// <summary>
         /// Gets or sets the date created. This should be in UTC.
-        /// Required.
         /// </summary>
-        [Required]
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         public DateTime DateCreated { get; set; }
 
         /// <summary>
         /// Gets or sets the log severity. Default is <see cref="LogLevel.Trace"/>.
-        /// Required.
         /// </summary>
-        [Required]
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         public LogLevel LogSeverity { get; set; }
 
-        /// <summary>
-        /// Gets or sets the row version.
-        /// Required, ConcurrencyToken.
-        /// </summary>
+        /// <inheritdoc />
         [ConcurrencyCheck]
-        [Required]
         public uint RowVersion { get; set; }
 
-        partial void Init();
-
         /// <inheritdoc />
         public void OnSavingChanges()
         {

+ 3 - 1
Jellyfin.Data/Entities/DisplayPreferences.cs

@@ -1,4 +1,6 @@
-using System;
+#pragma warning disable CA2227
+
+using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;

+ 16 - 45
Jellyfin.Data/Entities/Group.cs

@@ -1,9 +1,8 @@
-#pragma warning disable CS1591
+#pragma warning disable CA2227
 
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
 using System.Linq;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Interfaces;
@@ -13,11 +12,10 @@ namespace Jellyfin.Data.Entities
     /// <summary>
     /// An entity representing a group.
     /// </summary>
-    public partial class Group : IHasPermissions, IHasConcurrencyToken
+    public class Group : IHasPermissions, IHasConcurrencyToken
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="Group"/> class.
-        /// Public constructor with required data.
         /// </summary>
         /// <param name="name">The name of the group.</param>
         public Group(string name)
@@ -31,33 +29,25 @@ namespace Jellyfin.Data.Entities
             Id = Guid.NewGuid();
 
             Permissions = new HashSet<Permission>();
-            ProviderMappings = new HashSet<ProviderMapping>();
             Preferences = new HashSet<Preference>();
-
-            Init();
         }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="Group"/> class.
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
         /// </summary>
+        /// <remarks>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </remarks>
         protected Group()
         {
-            Init();
         }
 
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
         /// <summary>
         /// Gets or sets the id of this group.
         /// </summary>
         /// <remarks>
         /// Identity, Indexed, Required.
         /// </remarks>
-        [Key]
-        [Required]
         public Guid Id { get; protected set; }
 
         /// <summary>
@@ -71,42 +61,19 @@ namespace Jellyfin.Data.Entities
         [StringLength(255)]
         public string Name { get; set; }
 
-        /// <summary>
-        /// Gets or sets the row version.
-        /// </summary>
-        /// <remarks>
-        /// Required, Concurrency Token.
-        /// </remarks>
+        /// <inheritdoc />
         [ConcurrencyCheck]
-        [Required]
         public uint RowVersion { get; set; }
 
-        public void OnSavingChanges()
-        {
-            RowVersion++;
-        }
-
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-
-        [ForeignKey("Permission_GroupPermissions_Id")]
+        /// <summary>
+        /// Gets or sets a collection containing the group's permissions.
+        /// </summary>
         public virtual ICollection<Permission> Permissions { get; protected set; }
 
-        [ForeignKey("ProviderMapping_ProviderMappings_Id")]
-        public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; }
-
-        [ForeignKey("Preference_Preferences_Id")]
-        public virtual ICollection<Preference> Preferences { get; protected set; }
-
         /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
+        /// Gets or sets a collection containing the group's preferences.
         /// </summary>
-        /// <param name="name">The name of this group.</param>
-        public static Group Create(string name)
-        {
-            return new Group(name);
-        }
+        public virtual ICollection<Preference> Preferences { get; protected set; }
 
         /// <inheritdoc/>
         public bool HasPermission(PermissionKind kind)
@@ -120,6 +87,10 @@ namespace Jellyfin.Data.Entities
             Permissions.First(p => p.Kind == kind).Value = value;
         }
 
-        partial void Init();
+        /// <inheritdoc />
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
     }
 }

+ 39 - 6
Jellyfin.Data/Entities/ImageInfo.cs

@@ -1,32 +1,65 @@
-#pragma warning disable CS1591
-
-using System;
+using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 
 namespace Jellyfin.Data.Entities
 {
+    /// <summary>
+    /// An entity representing an image.
+    /// </summary>
     public class ImageInfo
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ImageInfo"/> class.
+        /// </summary>
+        /// <param name="path">The path.</param>
         public ImageInfo(string path)
         {
             Path = path;
             LastModified = DateTime.UtcNow;
         }
 
-        [Key]
-        [Required]
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ImageInfo"/> class.
+        /// </summary>
+        /// <remarks>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </remarks>
+        protected ImageInfo()
+        {
+        }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <remarks>
+        /// Identity, Indexed, Required.
+        /// </remarks>
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         public int Id { get; protected set; }
 
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
         public Guid? UserId { get; protected set; }
 
+        /// <summary>
+        /// Gets or sets the path of the image.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
         [MaxLength(512)]
         [StringLength(512)]
         public string Path { get; set; }
 
-        [Required]
+        /// <summary>
+        /// Gets or sets the date last modified.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         public DateTime LastModified { get; set; }
     }
 }

+ 4 - 3
Jellyfin.Data/Entities/ItemDisplayPreferences.cs

@@ -1,12 +1,13 @@
-#pragma warning disable CS1591
-
-using System;
+using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 using Jellyfin.Data.Enums;
 
 namespace Jellyfin.Data.Entities
 {
+    /// <summary>
+    /// An entity that represents a user's display preferences for a specific item.
+    /// </summary>
     public class ItemDisplayPreferences
     {
         /// <summary>

+ 2 - 0
Jellyfin.Data/Entities/Libraries/Artwork.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;

+ 2 - 0
Jellyfin.Data/Entities/Libraries/Book.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System.Collections.Generic;
 using Jellyfin.Data.Interfaces;
 

+ 3 - 1
Jellyfin.Data/Entities/Libraries/BookMetadata.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations.Schema;
@@ -8,7 +10,7 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// An entity containing metadata for a book.
     /// </summary>
-    public class BookMetadata : Metadata, IHasCompanies
+    public class BookMetadata : ItemMetadata, IHasCompanies
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="BookMetadata"/> class.

+ 2 - 0
Jellyfin.Data/Entities/Libraries/Chapter.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;

+ 2 - 0
Jellyfin.Data/Entities/Libraries/Collection.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;

+ 2 - 2
Jellyfin.Data/Entities/Libraries/CollectionItem.cs

@@ -73,7 +73,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Gets or sets the next item in the collection.
         /// </summary>
         /// <remarks>
-        /// TODO check if this properly updated dependant and has the proper principal relationship
+        /// TODO check if this properly updated dependant and has the proper principal relationship.
         /// </remarks>
         public virtual CollectionItem Next { get; set; }
 
@@ -81,7 +81,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Gets or sets the previous item in the collection.
         /// </summary>
         /// <remarks>
-        /// TODO check if this properly updated dependant and has the proper principal relationship
+        /// TODO check if this properly updated dependant and has the proper principal relationship.
         /// </remarks>
         public virtual CollectionItem Previous { get; set; }
 

+ 2 - 0
Jellyfin.Data/Entities/Libraries/Company.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;

+ 1 - 1
Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs

@@ -6,7 +6,7 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// An entity holding metadata for a <see cref="Company"/>.
     /// </summary>
-    public class CompanyMetadata : Metadata
+    public class CompanyMetadata : ItemMetadata
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="CompanyMetadata"/> class.

+ 2 - 0
Jellyfin.Data/Entities/Libraries/CustomItem.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System.Collections.Generic;
 using Jellyfin.Data.Interfaces;
 

+ 1 - 1
Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs

@@ -5,7 +5,7 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// An entity containing metadata for a custom item.
     /// </summary>
-    public class CustomItemMetadata : Metadata
+    public class CustomItemMetadata : ItemMetadata
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="CustomItemMetadata"/> class.

+ 2 - 0
Jellyfin.Data/Entities/Libraries/Episode.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System.Collections.Generic;
 using Jellyfin.Data.Interfaces;

+ 1 - 1
Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs

@@ -6,7 +6,7 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// An entity containing metadata for an <see cref="Episode"/>.
     /// </summary>
-    public class EpisodeMetadata : Metadata
+    public class EpisodeMetadata : ItemMetadata
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="EpisodeMetadata"/> class.

+ 5 - 5
Jellyfin.Data/Entities/Libraries/Genre.cs

@@ -14,8 +14,8 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Initializes a new instance of the <see cref="Genre"/> class.
         /// </summary>
         /// <param name="name">The name.</param>
-        /// <param name="metadata">The metadata.</param>
-        public Genre(string name, Metadata metadata)
+        /// <param name="itemMetadata">The metadata.</param>
+        public Genre(string name, ItemMetadata itemMetadata)
         {
             if (string.IsNullOrEmpty(name))
             {
@@ -24,12 +24,12 @@ namespace Jellyfin.Data.Entities.Libraries
 
             Name = name;
 
-            if (metadata == null)
+            if (itemMetadata == null)
             {
-                throw new ArgumentNullException(nameof(metadata));
+                throw new ArgumentNullException(nameof(itemMetadata));
             }
 
-            metadata.Genres.Add(this);
+            itemMetadata.Genres.Add(this);
         }
 
         /// <summary>

+ 7 - 5
Jellyfin.Data/Entities/Libraries/Metadata.cs → Jellyfin.Data/Entities/Libraries/ItemMetadata.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
@@ -9,14 +11,14 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// An abstract class that holds metadata.
     /// </summary>
-    public abstract class Metadata : IHasArtwork, IHasConcurrencyToken
+    public abstract class ItemMetadata : IHasArtwork, IHasConcurrencyToken
     {
         /// <summary>
-        /// Initializes a new instance of the <see cref="Metadata"/> class.
+        /// Initializes a new instance of the <see cref="ItemMetadata"/> class.
         /// </summary>
         /// <param name="title">The title or name of the object.</param>
         /// <param name="language">ISO-639-3 3-character language codes.</param>
-        protected Metadata(string title, string language)
+        protected ItemMetadata(string title, string language)
         {
             if (string.IsNullOrEmpty(title))
             {
@@ -41,12 +43,12 @@ namespace Jellyfin.Data.Entities.Libraries
         }
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="Metadata"/> class.
+        /// Initializes a new instance of the <see cref="ItemMetadata"/> class.
         /// </summary>
         /// <remarks>
         /// Default constructor. Protected due to being abstract.
         /// </remarks>
-        protected Metadata()
+        protected ItemMetadata()
         {
         }
 

+ 2 - 0
Jellyfin.Data/Entities/Libraries/MediaFile.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;

+ 5 - 5
Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs

@@ -14,8 +14,8 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Initializes a new instance of the <see cref="MetadataProviderId"/> class.
         /// </summary>
         /// <param name="providerId">The provider id.</param>
-        /// <param name="metadata">The metadata entity.</param>
-        public MetadataProviderId(string providerId, Metadata metadata)
+        /// <param name="itemMetadata">The metadata entity.</param>
+        public MetadataProviderId(string providerId, ItemMetadata itemMetadata)
         {
             if (string.IsNullOrEmpty(providerId))
             {
@@ -24,12 +24,12 @@ namespace Jellyfin.Data.Entities.Libraries
 
             ProviderId = providerId;
 
-            if (metadata == null)
+            if (itemMetadata == null)
             {
-                throw new ArgumentNullException(nameof(metadata));
+                throw new ArgumentNullException(nameof(itemMetadata));
             }
 
-            metadata.Sources.Add(this);
+            itemMetadata.Sources.Add(this);
         }
 
         /// <summary>

+ 2 - 0
Jellyfin.Data/Entities/Libraries/Movie.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System.Collections.Generic;
 using Jellyfin.Data.Interfaces;
 

+ 3 - 1
Jellyfin.Data/Entities/Libraries/MovieMetadata.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
@@ -8,7 +10,7 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// An entity holding the metadata for a movie.
     /// </summary>
-    public class MovieMetadata : Metadata, IHasCompanies
+    public class MovieMetadata : ItemMetadata, IHasCompanies
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="MovieMetadata"/> class.

+ 2 - 0
Jellyfin.Data/Entities/Libraries/MusicAlbum.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System.Collections.Generic;
 
 namespace Jellyfin.Data.Entities.Libraries

+ 3 - 1
Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 
@@ -6,7 +8,7 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// An entity holding the metadata for a music album.
     /// </summary>
-    public class MusicAlbumMetadata : Metadata
+    public class MusicAlbumMetadata : ItemMetadata
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="MusicAlbumMetadata"/> class.

+ 2 - 0
Jellyfin.Data/Entities/Libraries/Person.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;

+ 7 - 5
Jellyfin.Data/Entities/Libraries/PersonRole.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
@@ -16,17 +18,17 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Initializes a new instance of the <see cref="PersonRole"/> class.
         /// </summary>
         /// <param name="type">The role type.</param>
-        /// <param name="metadata">The metadata.</param>
-        public PersonRole(PersonRoleType type, Metadata metadata)
+        /// <param name="itemMetadata">The metadata.</param>
+        public PersonRole(PersonRoleType type, ItemMetadata itemMetadata)
         {
             Type = type;
 
-            if (metadata == null)
+            if (itemMetadata == null)
             {
-                throw new ArgumentNullException(nameof(metadata));
+                throw new ArgumentNullException(nameof(itemMetadata));
             }
 
-            metadata.PersonRoles.Add(this);
+            itemMetadata.PersonRoles.Add(this);
 
             Sources = new HashSet<MetadataProviderId>();
         }

+ 2 - 0
Jellyfin.Data/Entities/Libraries/Photo.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System.Collections.Generic;
 using Jellyfin.Data.Interfaces;
 

+ 1 - 1
Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs

@@ -5,7 +5,7 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// An entity that holds metadata for a photo.
     /// </summary>
-    public class PhotoMetadata : Metadata
+    public class PhotoMetadata : ItemMetadata
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="PhotoMetadata"/> class.

+ 5 - 5
Jellyfin.Data/Entities/Libraries/Rating.cs

@@ -14,17 +14,17 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Initializes a new instance of the <see cref="Rating"/> class.
         /// </summary>
         /// <param name="value">The value.</param>
-        /// <param name="metadata">The metadata.</param>
-        public Rating(double value, Metadata metadata)
+        /// <param name="itemMetadata">The metadata.</param>
+        public Rating(double value, ItemMetadata itemMetadata)
         {
             Value = value;
 
-            if (metadata == null)
+            if (itemMetadata == null)
             {
-                throw new ArgumentNullException(nameof(metadata));
+                throw new ArgumentNullException(nameof(itemMetadata));
             }
 
-            metadata.Ratings.Add(this);
+            itemMetadata.Ratings.Add(this);
         }
 
         /// <summary>

+ 2 - 0
Jellyfin.Data/Entities/Libraries/Release.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;

+ 2 - 0
Jellyfin.Data/Entities/Libraries/Season.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System.Collections.Generic;
 

+ 1 - 1
Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs

@@ -6,7 +6,7 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// An entity that holds metadata for seasons.
     /// </summary>
-    public class SeasonMetadata : Metadata
+    public class SeasonMetadata : ItemMetadata
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="SeasonMetadata"/> class.

+ 2 - 0
Jellyfin.Data/Entities/Libraries/Series.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System.Collections.Generic;
 

+ 3 - 1
Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
@@ -9,7 +11,7 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// An entity representing series metadata.
     /// </summary>
-    public class SeriesMetadata : Metadata, IHasCompanies
+    public class SeriesMetadata : ItemMetadata, IHasCompanies
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="SeriesMetadata"/> class.

+ 2 - 0
Jellyfin.Data/Entities/Libraries/Track.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System.Collections.Generic;
 using Jellyfin.Data.Interfaces;

+ 1 - 1
Jellyfin.Data/Entities/Libraries/TrackMetadata.cs

@@ -5,7 +5,7 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// An entity holding metadata for a track.
     /// </summary>
-    public class TrackMetadata : Metadata
+    public class TrackMetadata : ItemMetadata
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="TrackMetadata"/> class.

+ 2 - 34
Jellyfin.Data/Entities/Permission.cs

@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 using Jellyfin.Data.Enums;
@@ -10,7 +8,7 @@ namespace Jellyfin.Data.Entities
     /// <summary>
     /// An entity representing whether the associated user has a specific permission.
     /// </summary>
-    public partial class Permission : IHasConcurrencyToken
+    public class Permission : IHasConcurrencyToken
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="Permission"/> class.
@@ -22,8 +20,6 @@ namespace Jellyfin.Data.Entities
         {
             Kind = kind;
             Value = value;
-
-            Init();
         }
 
         /// <summary>
@@ -32,21 +28,14 @@ namespace Jellyfin.Data.Entities
         /// </summary>
         protected Permission()
         {
-            Init();
         }
 
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
         /// <summary>
         /// Gets or sets the id of this permission.
         /// </summary>
         /// <remarks>
         /// Identity, Indexed, Required.
         /// </remarks>
-        [Key]
-        [Required]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         public int Id { get; protected set; }
 
@@ -56,7 +45,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         public PermissionKind Kind { get; protected set; }
 
         /// <summary>
@@ -65,36 +53,16 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         public bool Value { get; set; }
 
-        /// <summary>
-        /// Gets or sets the row version.
-        /// </summary>
-        /// <remarks>
-        /// Required, ConcurrencyToken.
-        /// </remarks>
+        /// <inheritdoc />
         [ConcurrencyCheck]
-        [Required]
         public uint RowVersion { get; set; }
 
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="kind">The permission kind.</param>
-        /// <param name="value">The value of this permission.</param>
-        /// <returns>The newly created instance.</returns>
-        public static Permission Create(PermissionKind kind, bool value)
-        {
-            return new Permission(kind, value);
-        }
-
         /// <inheritdoc/>
         public void OnSavingChanges()
         {
             RowVersion++;
         }
-
-        partial void Init();
     }
 }

+ 1 - 25
Jellyfin.Data/Entities/Preference.cs

@@ -31,18 +31,12 @@ namespace Jellyfin.Data.Entities
         {
         }
 
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
         /// <summary>
         /// Gets or sets the id of this preference.
         /// </summary>
         /// <remarks>
         /// Identity, Indexed, Required.
         /// </remarks>
-        [Key]
-        [Required]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         public int Id { get; protected set; }
 
@@ -52,7 +46,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         public PreferenceKind Kind { get; protected set; }
 
         /// <summary>
@@ -66,27 +59,10 @@ namespace Jellyfin.Data.Entities
         [StringLength(65535)]
         public string Value { get; set; }
 
-        /// <summary>
-        /// Gets or sets the row version.
-        /// </summary>
-        /// <remarks>
-        /// Required, ConcurrencyToken.
-        /// </remarks>
+        /// <inheritdoc/>
         [ConcurrencyCheck]
-        [Required]
         public uint RowVersion { get; set; }
 
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="kind">The preference kind.</param>
-        /// <param name="value">The value.</param>
-        /// <returns>The new instance.</returns>
-        public static Preference Create(PreferenceKind kind, string value)
-        {
-            return new Preference(kind, value);
-        }
-
         /// <inheritdoc/>
         public void OnSavingChanges()
         {

部分文件因文件數量過多而無法顯示