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

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

crobibero 4 жил өмнө
parent
commit
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)'
           arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
           zipAfterPublish: false
           zipAfterPublish: false
 
 
-      - task: PublishPipelineArtifact@0
+      - task: PublishPipelineArtifact@1
         displayName: 'Publish Artifact Naming'
         displayName: 'Publish Artifact Naming'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:
         inputs:
           targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll'
           targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll'
           artifactName: 'Jellyfin.Naming'
           artifactName: 'Jellyfin.Naming'
 
 
-      - task: PublishPipelineArtifact@0
+      - task: PublishPipelineArtifact@1
         displayName: 'Publish Artifact Controller'
         displayName: 'Publish Artifact Controller'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:
         inputs:
           targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
           targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
           artifactName: 'Jellyfin.Controller'
           artifactName: 'Jellyfin.Controller'
 
 
-      - task: PublishPipelineArtifact@0
+      - task: PublishPipelineArtifact@1
         displayName: 'Publish Artifact Model'
         displayName: 'Publish Artifact Model'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:
         inputs:
           targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
           targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
           artifactName: 'Jellyfin.Model'
           artifactName: 'Jellyfin.Model'
 
 
-      - task: PublishPipelineArtifact@0
+      - task: PublishPipelineArtifact@1
         displayName: 'Publish Artifact Common'
         displayName: 'Publish Artifact Common'
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
         inputs:
         inputs:

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

@@ -74,7 +74,6 @@ jobs:
       - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
       - 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
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
         displayName: 'Run ReportGenerator'
         displayName: 'Run ReportGenerator'
-        enabled: false
         inputs:
         inputs:
           reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
           reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
           targetdir: "$(Agent.TempDirectory)/merged/"
           targetdir: "$(Agent.TempDirectory)/merged/"
@@ -84,10 +83,16 @@ jobs:
       - task: PublishCodeCoverageResults@1
       - task: PublishCodeCoverageResults@1
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
         displayName: 'Publish Code Coverage'
         displayName: 'Publish Code Coverage'
-        enabled: false
         inputs:
         inputs:
           codeCoverageTool: "cobertura"
           codeCoverageTool: "cobertura"
           #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
           #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
           summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml"
           summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml"
           pathToSources: $(Build.SourcesDirectory)
           pathToSources: $(Build.SourcesDirectory)
           failIfCoverageEmpty: true
           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",
             "type": "process",
             "args": [
             "args": [
                 "test",
                 "test",
-                "${workspaceFolder}/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj"
+                "${workspaceFolder}/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj"
             ],
             ],
             "problemMatcher": "$msCompile"
             "problemMatcher": "$msCompile"
         }
         }

+ 2 - 0
CONTRIBUTORS.md

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

+ 1 - 1
Dockerfile

@@ -14,7 +14,7 @@ COPY . .
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 # because of changes in docker and systemd we need to not build in parallel at the moment
 # 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
 # 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
 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
 # Discard objs - may cause failures if exists
 RUN find . -type d -name obj | xargs -r rm -r
 RUN find . -type d -name obj | xargs -r rm -r
 # Build
 # 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
 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
 # Discard objs - may cause failures if exists
 RUN find . -type d -name obj | xargs -r rm -r
 RUN find . -type d -name obj | xargs -r rm -r
 # Build
 # 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 multiarch/qemu-user-static:x86_64-aarch64 as qemu
 FROM arm64v8/debian:buster-slim
 FROM arm64v8/debian:buster-slim

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

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

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

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

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

@@ -948,7 +948,7 @@ namespace Emby.Dlna.Didl
             }
             }
             catch (XmlException ex)
             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)
             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>
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
     <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
     <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
   </ItemGroup>
   </ItemGroup>
 
 
 </Project>
 </Project>

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

@@ -6,6 +6,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Net.Http;
 using System.Net.Http;
+using System.Net.Mime;
 using System.Text;
 using System.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
@@ -20,13 +21,13 @@ namespace Emby.Dlna.Eventing
             new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
             new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
 
 
         private readonly ILogger _logger;
         private readonly ILogger _logger;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
 
 
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         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;
             _logger = logger;
         }
         }
 
 
@@ -167,24 +168,17 @@ namespace Emby.Dlna.Eventing
 
 
             builder.Append("</e:propertyset>");
             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
             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)
             catch (OperationCanceledException)
             {
             {

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

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

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

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

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

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

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

@@ -4,6 +4,7 @@ using System;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Net;
 using System.Net;
+using System.Net.Http;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Data.Events;
 using Jellyfin.Data.Events;
@@ -33,7 +34,7 @@ namespace Emby.Dlna.PlayTo
         private readonly IDlnaManager _dlnaManager;
         private readonly IDlnaManager _dlnaManager;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
         private readonly IImageProcessor _imageProcessor;
         private readonly IImageProcessor _imageProcessor;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly IUserDataManager _userDataManager;
         private readonly IUserDataManager _userDataManager;
         private readonly ILocalizationManager _localization;
         private readonly ILocalizationManager _localization;
@@ -46,7 +47,7 @@ namespace Emby.Dlna.PlayTo
         private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
         private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
         private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
         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;
             _logger = logger;
             _sessionManager = sessionManager;
             _sessionManager = sessionManager;
@@ -56,7 +57,7 @@ namespace Emby.Dlna.PlayTo
             _appHost = appHost;
             _appHost = appHost;
             _imageProcessor = imageProcessor;
             _imageProcessor = imageProcessor;
             _deviceDiscovery = deviceDiscovery;
             _deviceDiscovery = deviceDiscovery;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _config = config;
             _config = config;
             _userDataManager = userDataManager;
             _userDataManager = userDataManager;
             _localization = localization;
             _localization = localization;
@@ -174,7 +175,7 @@ namespace Emby.Dlna.PlayTo
 
 
             if (controller == null)
             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;
                 string deviceName = device.Properties.Name;
 
 

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

@@ -4,6 +4,8 @@ using System;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Net.Http;
 using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
 using System.Text;
 using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -20,11 +22,11 @@ namespace Emby.Dlna.PlayTo
 
 
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         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(
         public async Task<XDocument> SendCommandAsync(
@@ -36,20 +38,18 @@ namespace Emby.Dlna.PlayTo
             CancellationToken cancellationToken = default)
             CancellationToken cancellationToken = default)
         {
         {
             var url = NormalizeServiceUrl(baseUrl, service.ControlUrl);
             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)
         private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
@@ -76,49 +76,32 @@ namespace Emby.Dlna.PlayTo
             int eventport,
             int eventport,
             int timeOut = 3600)
             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)
         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 url,
             string soapAction,
             string soapAction,
             string postData,
             string postData,
@@ -130,29 +113,20 @@ namespace Emby.Dlna.PlayTo
                 soapAction = $"\"{soapAction}\"";
                 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))
             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
 #pragma warning disable CS1591
 
 
+using System.Net.Http;
 using Emby.Dlna.Eventing;
 using Emby.Dlna.Eventing;
-using MediaBrowser.Common.Net;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Dlna.Service
 namespace Emby.Dlna.Service
 {
 {
     public class BaseService : IDlnaEventManager
     public class BaseService : IDlnaEventManager
     {
     {
-        protected BaseService(ILogger<BaseService> logger, IHttpClient httpClient)
+        protected BaseService(ILogger<BaseService> logger, IHttpClientFactory httpClientFactory)
         {
         {
             Logger = logger;
             Logger = logger;
-            HttpClient = httpClient;
-
-            EventManager = new DlnaEventManager(logger, HttpClient);
+            EventManager = new DlnaEventManager(logger, httpClientFactory);
         }
         }
 
 
         protected IDlnaEventManager EventManager { get; }
         protected IDlnaEventManager EventManager { get; }
 
 
-        protected IHttpClient HttpClient { get; }
-
         protected ILogger Logger { get; }
         protected ILogger Logger { get; }
 
 
         public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)
         public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)

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

@@ -136,8 +136,8 @@ namespace Emby.Naming.Common
 
 
             CleanDateTimes = new[]
             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[]
             CleanStrings = new[]

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

@@ -308,7 +308,7 @@ namespace Emby.Server.Implementations.AppBase
             }
             }
             catch (Exception ex)
             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);
                 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;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Json;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Common.Updates;
@@ -122,8 +123,8 @@ namespace Emby.Server.Implementations
 
 
         private IMediaEncoder _mediaEncoder;
         private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
         private ISessionManager _sessionManager;
+        private IHttpClientFactory _httpClientFactory;
         private IWebSocketManager _webSocketManager;
         private IWebSocketManager _webSocketManager;
-        private IHttpClient _httpClient;
 
 
         private string[] _urlPrefixes;
         private string[] _urlPrefixes;
 
 
@@ -279,6 +280,10 @@ namespace Emby.Server.Implementations
                 Password = ServerConfigurationManager.Configuration.CertificatePassword
                 Password = ServerConfigurationManager.Configuration.CertificatePassword
             };
             };
             Certificate = GetCertificate(CertificateInfo);
             Certificate = GetCertificate(CertificateInfo);
+
+            ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
+            ApplicationVersionString = ApplicationVersion.ToString(3);
+            ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
         }
         }
 
 
         public string ExpandVirtualPath(string path)
         public string ExpandVirtualPath(string path)
@@ -308,16 +313,16 @@ namespace Emby.Server.Implementations
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public Version ApplicationVersion { get; } = typeof(ApplicationHost).Assembly.GetName().Version;
+        public Version ApplicationVersion { get; }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public string ApplicationVersionString { get; } = typeof(ApplicationHost).Assembly.GetName().Version.ToString(3);
+        public string ApplicationVersionString { get; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the current application user agent.
         /// Gets the current application user agent.
         /// </summary>
         /// </summary>
         /// <value>The application user agent.</value>
         /// <value>The application user agent.</value>
-        public string ApplicationUserAgent => Name.Replace(' ', '-') + "/" + ApplicationVersionString;
+        public string ApplicationUserAgent { get; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the email address for use within a comment section of a user agent field.
         /// 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(_fileSystemManager);
             ServiceCollection.AddSingleton<TvdbClientManager>();
             ServiceCollection.AddSingleton<TvdbClientManager>();
 
 
-            ServiceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
-
             ServiceCollection.AddSingleton(_networkManager);
             ServiceCollection.AddSingleton(_networkManager);
 
 
             ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
             ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
@@ -650,8 +653,8 @@ namespace Emby.Server.Implementations
 
 
             _mediaEncoder = Resolve<IMediaEncoder>();
             _mediaEncoder = Resolve<IMediaEncoder>();
             _sessionManager = Resolve<ISessionManager>();
             _sessionManager = Resolve<ISessionManager>();
+            _httpClientFactory = Resolve<IHttpClientFactory>();
             _webSocketManager = Resolve<IWebSocketManager>();
             _webSocketManager = Resolve<IWebSocketManager>();
-            _httpClient = Resolve<IHttpClient>();
 
 
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
 
 
@@ -1296,25 +1299,17 @@ namespace Emby.Server.Implementations
 
 
             try
             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)
             catch (OperationCanceledException)
             {
             {
@@ -1401,7 +1396,7 @@ namespace Emby.Server.Implementations
 
 
             foreach (var assembly in assemblies)
             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;
                 yield return assembly;
             }
             }
         }
         }

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

@@ -890,7 +890,7 @@ namespace Emby.Server.Implementations.Channels
             }
             }
             catch (Exception ex)
             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)
                 catch (Exception ex)
                 {
                 {
                     // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
                     // 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.ResponseCompression" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.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="Mono.Nat" Version="2.0.2" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
     <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;
                     continue;
                 }
                 }
 
 
-                _logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path);
+                _logger.LogInformation("{Name} ({Path}) will be refreshed.", item.Name, item.Path);
 
 
                 try
                 try
                 {
                 {
@@ -160,11 +160,11 @@ namespace Emby.Server.Implementations.IO
                     // For now swallow and log.
                     // For now swallow and log.
                     // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
                     // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
                     // Should we remove it from it's parent?
                     // 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)
                 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()
         public void Dispose()
         {
         {
             _disposed = true;
             _disposed = true;

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

@@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.IO
                 }
                 }
                 catch (Exception ex)
                 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)
         public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly)
         {
         {
             if (OperatingSystem.Id != OperatingSystemId.Windows)
             if (OperatingSystem.Id != OperatingSystemId.Windows)
@@ -707,14 +683,6 @@ namespace Emby.Server.Implementations.IO
             return Directory.EnumerateFileSystemEntries(path, "*", searchOption);
             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)
         private static void RunProcess(string path, string args, string workingDirectory)
         {
         {
             using (var process = Process.Start(new ProcessStartInfo
             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
     public class StreamHelper : IStreamHelper
     {
     {
-        private const int StreamCopyToBufferSize = 81920;
-
         public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken)
         public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken)
         {
         {
             byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
             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)
         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
             try
             {
             {
                 int bytesRead;
                 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
     public class DirectRecorder : IRecorder
     {
     {
         private readonly ILogger _logger;
         private readonly ILogger _logger;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IStreamHelper _streamHelper;
         private readonly IStreamHelper _streamHelper;
 
 
-        public DirectRecorder(ILogger logger, IHttpClient httpClient, IStreamHelper streamHelper)
+        public DirectRecorder(ILogger logger, IHttpClientFactory httpClientFactory, IStreamHelper streamHelper)
         {
         {
             _logger = logger;
             _logger = logger;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _streamHelper = streamHelper;
             _streamHelper = streamHelper;
         }
         }
 
 
@@ -52,10 +52,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 _logger.LogInformation("Copying recording stream to file {0}", targetFile);
                 _logger.LogInformation("Copying recording stream to file {0}", targetFile);
 
 
                 // The media source is infinite so we need to handle stopping ourselves
                 // 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);
             _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)
         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);
             _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.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using System.Net.Http;
 using System.Text;
 using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -48,7 +49,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
         private readonly ILogger<EmbyTV> _logger;
         private readonly ILogger<EmbyTV> _logger;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
 
 
@@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             IMediaSourceManager mediaSourceManager,
             IMediaSourceManager mediaSourceManager,
             ILogger<EmbyTV> logger,
             ILogger<EmbyTV> logger,
             IJsonSerializer jsonSerializer,
             IJsonSerializer jsonSerializer,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IServerConfigurationManager config,
             IServerConfigurationManager config,
             ILiveTvManager liveTvManager,
             ILiveTvManager liveTvManager,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
@@ -94,7 +95,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
             _appHost = appHost;
             _appHost = appHost;
             _logger = logger;
             _logger = logger;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _config = config;
             _config = config;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
@@ -604,11 +605,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             return Task.CompletedTask;
             return Task.CompletedTask;
         }
         }
 
 
-        public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
-        {
-            return Task.CompletedTask;
-        }
-
         public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
         public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
         {
         {
             throw new NotImplementedException();
             throw new NotImplementedException();
@@ -808,11 +804,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             return null;
             return null;
         }
         }
 
 
-        public IEnumerable<ActiveRecordingInfo> GetAllActiveRecordings()
-        {
-            return _activeRecordings.Values.Where(i => i.Timer.Status == RecordingStatus.InProgress && !i.CancellationTokenSource.IsCancellationRequested);
-        }
-
         public ActiveRecordingInfo GetActiveRecordingInfo(string path)
         public ActiveRecordingInfo GetActiveRecordingInfo(string path)
         {
         {
             if (string.IsNullOrWhiteSpace(path))
             if (string.IsNullOrWhiteSpace(path))
@@ -1015,16 +1006,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             throw new Exception("Tuner not found.");
             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)
         public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
         {
         {
             if (string.IsNullOrWhiteSpace(channelId))
             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))
             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)
         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.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
@@ -26,26 +23,24 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IServerApplicationPaths _appPaths;
         private readonly IServerApplicationPaths _appPaths;
+        private readonly IJsonSerializer _json;
+        private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
+
         private bool _hasExited;
         private bool _hasExited;
         private Stream _logFileStream;
         private Stream _logFileStream;
         private string _targetPath;
         private string _targetPath;
         private Process _process;
         private Process _process;
-        private readonly IJsonSerializer _json;
-        private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
-        private readonly IServerConfigurationManager _config;
 
 
         public EncodedRecorder(
         public EncodedRecorder(
             ILogger logger,
             ILogger logger,
             IMediaEncoder mediaEncoder,
             IMediaEncoder mediaEncoder,
             IServerApplicationPaths appPaths,
             IServerApplicationPaths appPaths,
-            IJsonSerializer json,
-            IServerConfigurationManager config)
+            IJsonSerializer json)
         {
         {
             _logger = logger;
             _logger = logger;
             _mediaEncoder = mediaEncoder;
             _mediaEncoder = mediaEncoder;
             _appPaths = appPaths;
             _appPaths = appPaths;
             _json = json;
             _json = json;
-            _config = config;
         }
         }
 
 
         private static bool CopySubtitles => false;
         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)
         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
             // 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);
             _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)
         private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
         {
         {
             _targetPath = targetFile;
             _targetPath = targetFile;
@@ -108,7 +98,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 StartInfo = processStartInfo,
                 StartInfo = processStartInfo,
                 EnableRaisingEvents = true
                 EnableRaisingEvents = true
             };
             };
-            _process.Exited += (sender, args) => OnFfMpegProcessExited(_process, inputFile);
+            _process.Exited += (sender, args) => OnFfMpegProcessExited(_process);
 
 
             _process.Start();
             _process.Start();
 
 
@@ -221,20 +211,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         }
         }
 
 
         protected string GetOutputSizeParam()
         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()
         private void Stop()
         {
         {
@@ -291,7 +268,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         /// <summary>
         /// <summary>
         /// Processes the exited.
         /// Processes the exited.
         /// </summary>
         /// </summary>
-        private void OnFfMpegProcessExited(Process process, string inputFile)
+        private void OnFfMpegProcessExited(Process process)
         {
         {
             using (process)
             using (process)
             {
             {

+ 148 - 245
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -8,6 +8,8 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Net;
 using System.Net;
 using System.Net.Http;
 using System.Net.Http;
+using System.Net.Mime;
+using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
@@ -24,23 +26,23 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 {
 {
     public class SchedulesDirect : IListingsProvider
     public class SchedulesDirect : IListingsProvider
     {
     {
+        private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
+
         private readonly ILogger<SchedulesDirect> _logger;
         private readonly ILogger<SchedulesDirect> _logger;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
         private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
         private readonly IApplicationHost _appHost;
         private readonly IApplicationHost _appHost;
 
 
-        private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
-
         public SchedulesDirect(
         public SchedulesDirect(
             ILogger<SchedulesDirect> logger,
             ILogger<SchedulesDirect> logger,
             IJsonSerializer jsonSerializer,
             IJsonSerializer jsonSerializer,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IApplicationHost appHost)
             IApplicationHost appHost)
         {
         {
             _logger = logger;
             _logger = logger;
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
             _appHost = appHost;
         }
         }
 
 
@@ -61,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
 
             while (start <= end)
             while (start <= end)
             {
             {
-                dates.Add(start.ToString("yyyy-MM-dd"));
+                dates.Add(start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
                 start = start.AddDays(1);
                 start = start.AddDays(1);
             }
             }
 
 
@@ -102,95 +104,78 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             var requestString = _jsonSerializer.SerializeToString(requestList);
             var requestString = _jsonSerializer.SerializeToString(requestList);
             _logger.LogDebug("Request string for schedules is: {RequestString}", requestString);
             _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)
         private static int GetSizeOrder(ScheduleDirect.ImageData image)
@@ -367,13 +352,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
 
             if (!string.IsNullOrWhiteSpace(details.originalAirDate))
             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;
                 info.ProductionYear = info.OriginalAirDate.Value.Year;
             }
             }
 
 
             if (details.movie != null)
             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;
                     info.ProductionYear = year;
                 }
                 }
@@ -482,22 +468,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
 
             imageIdString = imageIdString.TrimEnd(',') + "]";
             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
             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)
             catch (Exception ex)
             {
             {
@@ -518,41 +497,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 return lineups;
                 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
             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)
             catch (Exception ex)
@@ -587,7 +558,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 return null;
                 return null;
             }
             }
 
 
-            NameValuePair savedToken = null;
+            NameValuePair savedToken;
             if (!_tokens.TryGetValue(username, out savedToken))
             if (!_tokens.TryGetValue(username, out savedToken))
             {
             {
                 savedToken = new NameValuePair();
                 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,
             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
             try
             {
             {
-                return await _httpClient.Post(options).ConfigureAwait(false);
+                return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
             }
             }
             catch (HttpException ex)
             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)
             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)
         private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken)
@@ -736,20 +670,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
 
             _logger.LogInformation("Adding new LineUp ");
             _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)
         private async Task<bool> HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken)
@@ -768,25 +691,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
 
             _logger.LogInformation("Headends on account ");
             _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
             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)
             catch (HttpException ex)
             {
             {
@@ -851,55 +766,43 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 throw new Exception("token required");
                 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>();
             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;
             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
     public class XmlTvListingsProvider : IListingsProvider
     {
     {
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILogger<XmlTvListingsProvider> _logger;
         private readonly ILogger<XmlTvListingsProvider> _logger;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IZipClient _zipClient;
         private readonly IZipClient _zipClient;
 
 
         public XmlTvListingsProvider(
         public XmlTvListingsProvider(
             IServerConfigurationManager config,
             IServerConfigurationManager config,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILogger<XmlTvListingsProvider> logger,
             ILogger<XmlTvListingsProvider> logger,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             IZipClient zipClient)
             IZipClient zipClient)
         {
         {
             _config = config;
             _config = config;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _logger = logger;
             _logger = logger;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _zipClient = zipClient;
             _zipClient = zipClient;
@@ -78,28 +78,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
 
             Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
             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);
             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
     public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
     {
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
         private readonly ISocketFactory _socketFactory;
         private readonly ISocketFactory _socketFactory;
         private readonly INetworkManager _networkManager;
         private readonly INetworkManager _networkManager;
@@ -43,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             IServerConfigurationManager config,
             IServerConfigurationManager config,
             ILogger<HdHomerunHost> logger,
             ILogger<HdHomerunHost> logger,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IServerApplicationHost appHost,
             IServerApplicationHost appHost,
             ISocketFactory socketFactory,
             ISocketFactory socketFactory,
             INetworkManager networkManager,
             INetworkManager networkManager,
@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             IMemoryCache memoryCache)
             IMemoryCache memoryCache)
             : base(config, logger, fileSystem, memoryCache)
             : base(config, logger, fileSystem, memoryCache)
         {
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
             _appHost = appHost;
             _socketFactory = socketFactory;
             _socketFactory = socketFactory;
             _networkManager = networkManager;
             _networkManager = networkManager;
@@ -71,15 +71,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         {
         {
             var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
             var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
 
 
-            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)
             var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken)
                 .ConfigureAwait(false) ?? new List<Channels>();
                 .ConfigureAwait(false) ?? new List<Channels>();
 
 
@@ -133,14 +126,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
 
             try
             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)
                 var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken)
                     .ConfigureAwait(false);
                     .ConfigureAwait(false);
 
 
@@ -183,48 +172,41 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         {
         {
             var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
             var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
 
 
-            using (var response = await _httpClient.SendAsync(
-                new HttpRequestOptions()
-                {
-                    Url = string.Format(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)
         private static string StripXML(string source)
@@ -634,7 +616,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                     info,
                     info,
                     streamId,
                     streamId,
                     FileSystem,
                     FileSystem,
-                    _httpClient,
+                    _httpClientFactory,
                     Logger,
                     Logger,
                     Config,
                     Config,
                     _appHost,
                     _appHost,

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

@@ -5,6 +5,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using System.Net.Http;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
@@ -26,7 +27,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 {
 {
     public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
     public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
     {
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
         private readonly INetworkManager _networkManager;
         private readonly INetworkManager _networkManager;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaSourceManager _mediaSourceManager;
@@ -37,14 +38,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             IMediaSourceManager mediaSourceManager,
             IMediaSourceManager mediaSourceManager,
             ILogger<M3UTunerHost> logger,
             ILogger<M3UTunerHost> logger,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IServerApplicationHost appHost,
             IServerApplicationHost appHost,
             INetworkManager networkManager,
             INetworkManager networkManager,
             IStreamHelper streamHelper,
             IStreamHelper streamHelper,
             IMemoryCache memoryCache)
             IMemoryCache memoryCache)
             : base(config, logger, fileSystem, memoryCache)
             : base(config, logger, fileSystem, memoryCache)
         {
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
             _appHost = appHost;
             _networkManager = networkManager;
             _networkManager = networkManager;
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
@@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         {
         {
             var channelIdPrefix = GetFullChannelIdPrefix(info);
             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)
         public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
@@ -116,7 +117,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
 
                 if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
                 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)
         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.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using System.Net.Http;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -19,13 +20,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
     public class M3uParser
     public class M3uParser
     {
     {
         private readonly ILogger _logger;
         private readonly ILogger _logger;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
 
 
-        public M3uParser(ILogger logger, IHttpClient httpClient, IServerApplicationHost appHost)
+        public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory, IServerApplicationHost appHost)
         {
         {
             _logger = logger;
             _logger = logger;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
             _appHost = appHost;
         }
         }
 
 
@@ -51,13 +52,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         {
         {
             if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
             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));
             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
     public class SharedHttpStream : LiveStream, IDirectStreamProvider
     {
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
 
 
         public SharedHttpStream(
         public SharedHttpStream(
@@ -29,14 +29,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             TunerHostInfo tunerHostInfo,
             TunerHostInfo tunerHostInfo,
             string originalStreamId,
             string originalStreamId,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILogger logger,
             ILogger logger,
             IConfigurationManager configurationManager,
             IConfigurationManager configurationManager,
             IServerApplicationHost appHost,
             IServerApplicationHost appHost,
             IStreamHelper streamHelper)
             IStreamHelper streamHelper)
             : base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper)
             : base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper)
         {
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
             _appHost = appHost;
             OriginalStreamId = originalStreamId;
             OriginalStreamId = originalStreamId;
             EnableStreamSharing = true;
             EnableStreamSharing = true;
@@ -55,25 +55,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             var typeName = GetType().Name;
             var typeName = GetType().Name;
             Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
             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 extension = "ts";
             var requiresRemux = false;
             var requiresRemux = false;
 
 
-            var contentType = response.ContentType ?? string.Empty;
+            var contentType = response.Content.Headers.ContentType.ToString();
             if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1)
             if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1)
             {
             {
                 requiresRemux = true;
                 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 () =>
             return Task.Run(async () =>
             {
             {
                 try
                 try
                 {
                 {
                     Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
                     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)
                 catch (OperationCanceledException ex)
                 {
                 {

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

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

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

@@ -35,7 +35,7 @@
     "AuthenticationSucceededWithUserName": "{0} Har logga inn",
     "AuthenticationSucceededWithUserName": "{0} Har logga inn",
     "Artists": "Artistar",
     "Artists": "Artistar",
     "Application": "Program",
     "Application": "Program",
-    "AppDeviceValues": "App: {0}, Einheit: {1}",
+    "AppDeviceValues": "App: {0}, Eining: {1}",
     "Albums": "Album",
     "Albums": "Album",
     "NotificationOptionServerRestartRequired": "Tenaren krev omstart",
     "NotificationOptionServerRestartRequired": "Tenaren krev omstart",
     "NotificationOptionPluginUpdateInstalled": "Tilleggsprogram-oppdatering vart installert",
     "NotificationOptionPluginUpdateInstalled": "Tilleggsprogram-oppdatering vart installert",
@@ -43,7 +43,7 @@
     "NotificationOptionPluginInstalled": "Tilleggsprogram installert",
     "NotificationOptionPluginInstalled": "Tilleggsprogram installert",
     "NotificationOptionPluginError": "Tilleggsprogram feila",
     "NotificationOptionPluginError": "Tilleggsprogram feila",
     "NotificationOptionNewLibraryContent": "Nytt innhald er lagt til",
     "NotificationOptionNewLibraryContent": "Nytt innhald er lagt til",
-    "NotificationOptionInstallationFailed": "Installasjonen feila",
+    "NotificationOptionInstallationFailed": "Installasjonsfeil",
     "NotificationOptionCameraImageUploaded": "Kamerabilde vart lasta opp",
     "NotificationOptionCameraImageUploaded": "Kamerabilde vart lasta opp",
     "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppa",
     "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppa",
     "NotificationOptionAudioPlayback": "Lydavspilling påbyrja",
     "NotificationOptionAudioPlayback": "Lydavspilling påbyrja",
@@ -56,5 +56,62 @@
     "MusicVideos": "Musikkvideoar",
     "MusicVideos": "Musikkvideoar",
     "Music": "Musikk",
     "Music": "Musikk",
     "Movies": "Filmar",
     "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": "சேவையக அமைப்புகள் புதுப்பிக்கப்பட்டன",
     "MessageServerConfigurationUpdated": "சேவையக அமைப்புகள் புதுப்பிக்கப்பட்டன",
     "MessageApplicationUpdatedTo": "ஜெல்லிஃபின் சேவையகம் {0} இற்கு புதுப்பிக்கப்பட்டது",
     "MessageApplicationUpdatedTo": "ஜெல்லிஃபின் சேவையகம் {0} இற்கு புதுப்பிக்கப்பட்டது",
     "MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது",
     "MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது",
-    "Inherit": "மரபரிமையாகப் பெறு",
+    "Inherit": "மரபரிமையாகப் பெறு",
     "HeaderRecordingGroups": "பதிவு குழுக்கள்",
     "HeaderRecordingGroups": "பதிவு குழுக்கள்",
     "HeaderCameraUploads": "புகைப்பட பதிவேற்றங்கள்",
     "HeaderCameraUploads": "புகைப்பட பதிவேற்றங்கள்",
     "Folders": "கோப்புறைகள்",
     "Folders": "கோப்புறைகள்",
@@ -31,7 +31,7 @@
     "TaskDownloadMissingSubtitles": "விடுபட்டுபோன வசன வரிகளைப் பதிவிறக்கு",
     "TaskDownloadMissingSubtitles": "விடுபட்டுபோன வசன வரிகளைப் பதிவிறக்கு",
     "TaskRefreshChannels": "சேனல்களை புதுப்பி",
     "TaskRefreshChannels": "சேனல்களை புதுப்பி",
     "TaskUpdatePlugins": "உட்செருகிகளை புதுப்பி",
     "TaskUpdatePlugins": "உட்செருகிகளை புதுப்பி",
-    "TaskRefreshLibrary": "மீடியா நூலகத்தை ஆராய்",
+    "TaskRefreshLibrary": "ஊடக நூலகத்தை ஆராய்",
     "TasksChannelsCategory": "இணைய சேனல்கள்",
     "TasksChannelsCategory": "இணைய சேனல்கள்",
     "TasksApplicationCategory": "செயலி",
     "TasksApplicationCategory": "செயலி",
     "TasksLibraryCategory": "நூலகம்",
     "TasksLibraryCategory": "நூலகம்",
@@ -46,7 +46,7 @@
     "Sync": "ஒத்திசைவு",
     "Sync": "ஒத்திசைவு",
     "StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
     "StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
     "Songs": "பாடல்கள்",
     "Songs": "பாடல்கள்",
-    "Shows": "தொடர்கள்",
+    "Shows": "நிகழ்ச்சிகள்",
     "ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
     "ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
     "ScheduledTaskStartedWithName": "{0} துவங்கியது",
     "ScheduledTaskStartedWithName": "{0} துவங்கியது",
     "ScheduledTaskFailedWithName": "{0} தோல்வியடைந்தது",
     "ScheduledTaskFailedWithName": "{0} தோல்வியடைந்தது",
@@ -67,20 +67,20 @@
     "NotificationOptionAudioPlayback": "ஒலி இசைக்கத் துவங்கியுள்ளது",
     "NotificationOptionAudioPlayback": "ஒலி இசைக்கத் துவங்கியுள்ளது",
     "NotificationOptionApplicationUpdateInstalled": "செயலி புதுப்பிக்கப்பட்டது",
     "NotificationOptionApplicationUpdateInstalled": "செயலி புதுப்பிக்கப்பட்டது",
     "NotificationOptionApplicationUpdateAvailable": "செயலியினை புதுப்பிக்கலாம்",
     "NotificationOptionApplicationUpdateAvailable": "செயலியினை புதுப்பிக்கலாம்",
-    "NameSeasonUnknown": "பருவம் அறியப்படாதவை",
+    "NameSeasonUnknown": "அறியப்படாத பருவம்",
     "NameSeasonNumber": "பருவம் {0}",
     "NameSeasonNumber": "பருவம் {0}",
     "NameInstallFailed": "{0} நிறுவல் தோல்வியடைந்தது",
     "NameInstallFailed": "{0} நிறுவல் தோல்வியடைந்தது",
     "MusicVideos": "இசைப்படங்கள்",
     "MusicVideos": "இசைப்படங்கள்",
     "Music": "இசை",
     "Music": "இசை",
     "Movies": "திரைப்படங்கள்",
     "Movies": "திரைப்படங்கள்",
-    "Latest": "புதிய",
+    "Latest": "புதியவை",
     "LabelRunningTimeValue": "ஓடும் நேரம்: {0}",
     "LabelRunningTimeValue": "ஓடும் நேரம்: {0}",
     "LabelIpAddressValue": "ஐபி முகவரி: {0}",
     "LabelIpAddressValue": "ஐபி முகவரி: {0}",
     "ItemRemovedWithName": "{0} நூலகத்திலிருந்து அகற்றப்பட்டது",
     "ItemRemovedWithName": "{0} நூலகத்திலிருந்து அகற்றப்பட்டது",
     "ItemAddedWithName": "{0} நூலகத்தில் சேர்க்கப்பட்டது",
     "ItemAddedWithName": "{0} நூலகத்தில் சேர்க்கப்பட்டது",
-    "HeaderNextUp": "அடுத்ததாக",
+    "HeaderNextUp": "அடுத்தத",
     "HeaderLiveTV": "நேரடித் தொலைக்காட்சி",
     "HeaderLiveTV": "நேரடித் தொலைக்காட்சி",
-    "HeaderFavoriteSongs": "பிடித்த பாட்டுகள்",
+    "HeaderFavoriteSongs": "பிடித்த பாட்கள்",
     "HeaderFavoriteShows": "பிடித்த தொடர்கள்",
     "HeaderFavoriteShows": "பிடித்த தொடர்கள்",
     "HeaderFavoriteEpisodes": "பிடித்த அத்தியாயங்கள்",
     "HeaderFavoriteEpisodes": "பிடித்த அத்தியாயங்கள்",
     "HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்",
     "HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்",
@@ -93,25 +93,25 @@
     "Channels": "சேனல்கள்",
     "Channels": "சேனல்கள்",
     "Books": "புத்தகங்கள்",
     "Books": "புத்தகங்கள்",
     "AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது",
     "AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது",
-    "Artists": "கலைஞர்",
+    "Artists": "கலைஞர்கள்",
     "Application": "செயலி",
     "Application": "செயலி",
     "Albums": "ஆல்பங்கள்",
     "Albums": "ஆல்பங்கள்",
     "NewVersionIsAvailable": "ஜெல்லிஃபின் சேவையகத்தின் புதிய பதிப்பு பதிவிறக்கத்திற்கு கிடைக்கிறது.",
     "NewVersionIsAvailable": "ஜெல்லிஃபின் சேவையகத்தின் புதிய பதிப்பு பதிவிறக்கத்திற்கு கிடைக்கிறது.",
-    "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0 புதுப்பிக்கப்பட்டது",
+    "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0} புதுப்பிக்கப்பட்டது",
     "TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.",
     "TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.",
     "UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
     "UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
-    "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0 } இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
-    "TaskDownloadMissingSubtitlesDescription": "மெட்டாடேட்டா உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
+    "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
+    "TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
     "TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.",
     "TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.",
-    "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட செருகுநிரல்களுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
-    "TaskRefreshPeopleDescription": "உங்கள் மீடியா நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மெட்டாடேட்டாவை புதுப்பிக்கும்.",
+    "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
+    "TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.",
     "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
     "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
-    "TaskCleanLogs": "பதிவு அடைவ சுத்தம் செய்யுங்கள்",
-    "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் மீடியா நூலகத்தை ஸ்கேன் செய்து மீத்தரவை புதுப்பிக்கும்.",
+    "TaskCleanLogs": "பதிவு அடைவ சுத்தம் செய்யுங்கள்",
+    "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் ஊடக நூலகத்தை ஆராய்ந்து மீத்தரவை புதுப்பிக்கும்.",
     "TaskRefreshChapterImagesDescription": "அத்தியாயங்களைக் கொண்ட வீடியோக்களுக்கான சிறு உருவங்களை உருவாக்குகிறது.",
     "TaskRefreshChapterImagesDescription": "அத்தியாயங்களைக் கொண்ட வீடியோக்களுக்கான சிறு உருவங்களை உருவாக்குகிறது.",
     "ValueHasBeenAddedToLibrary": "உங்கள் மீடியா நூலகத்தில் {0} சேர்க்கப்பட்டது",
     "ValueHasBeenAddedToLibrary": "உங்கள் மீடியா நூலகத்தில் {0} சேர்க்கப்பட்டது",
     "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
     "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
     "HomeVideos": "முகப்பு வீடியோக்கள்",
     "HomeVideos": "முகப்பு வீடியோக்கள்",
-    "UserStoppedPlayingItemWithValues": "{2} இல் {1} முடித்துவிட்டது",
+    "UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது",
     "UserStartedPlayingItemWithValues": "{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;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
-using MediaBrowser.Model.Globalization;
 
 
 namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 {
 {
@@ -21,10 +21,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         /// Gets or sets the application paths.
         /// Gets or sets the application paths.
         /// </summary>
         /// </summary>
         /// <value>The application paths.</value>
         /// <value>The application paths.</value>
-        private IApplicationPaths ApplicationPaths { get; set; }
-
+        private readonly IApplicationPaths _applicationPaths;
         private readonly ILogger<DeleteCacheFileTask> _logger;
         private readonly ILogger<DeleteCacheFileTask> _logger;
-
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly ILocalizationManager _localization;
         private readonly ILocalizationManager _localization;
 
 
@@ -37,20 +35,41 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             ILocalizationManager localization)
             ILocalizationManager localization)
         {
         {
-            ApplicationPaths = appPaths;
+            _applicationPaths = appPaths;
             _logger = logger;
             _logger = logger;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _localization = localization;
             _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>
         /// <summary>
         /// Creates the triggers that define when the task will run.
         /// Creates the triggers that define when the task will run.
         /// </summary>
         /// </summary>
         /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
         /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
         public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
         public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
         {
         {
-            return new[] {
-
+            return new[]
+            {
                 // Every so often
                 // Every so often
                 new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
                 new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
             };
             };
@@ -68,7 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 
 
             try
             try
             {
             {
-                DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.CachePath, minDateModified, progress);
+                DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.CachePath, minDateModified, progress);
             }
             }
             catch (DirectoryNotFoundException)
             catch (DirectoryNotFoundException)
             {
             {
@@ -81,7 +100,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 
 
             try
             try
             {
             {
-                DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, minDateModified, progress);
+                DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.TempDirectory, minDateModified, progress);
             }
             }
             catch (DirectoryNotFoundException)
             catch (DirectoryNotFoundException)
             {
             {
@@ -91,7 +110,6 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             return Task.CompletedTask;
             return Task.CompletedTask;
         }
         }
 
 
-
         /// <summary>
         /// <summary>
         /// Deletes the cache files from directory with a last write time less than a given date.
         /// Deletes the cache files from directory with a last write time less than a given date.
         /// </summary>
         /// </summary>
@@ -164,26 +182,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
                 _logger.LogError(ex, "Error deleting file {path}", path);
                 _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;
             _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>
         /// <summary>
         /// Creates the triggers that define when the task will run.
         /// Creates the triggers that define when the task will run.
         /// </summary>
         /// </summary>
@@ -98,26 +119,5 @@ namespace Emby.Server.Implementations.ScheduledTasks
 
 
             progress.Report(100);
             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
     public class DailyTrigger : ITaskTrigger
     {
     {
         /// <summary>
         /// <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>
         /// </summary>
         /// <value>The time of day.</value>
         /// <value>The time of day.</value>
         public TimeSpan TimeOfDay { get; set; }
         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>
         /// <summary>
         /// Called when [triggered].
         /// Called when [triggered].
         /// </summary>
         /// </summary>

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

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

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

@@ -12,6 +12,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
     /// </summary>
     /// </summary>
     public class StartupTrigger : ITaskTrigger
     public class StartupTrigger : ITaskTrigger
     {
     {
+        /// <summary>
+        /// Occurs when [triggered].
+        /// </summary>
+        public event EventHandler<EventArgs> Triggered;
+
         public int DelayMs { get; set; }
         public int DelayMs { get; set; }
 
 
         /// <summary>
         /// <summary>
@@ -48,20 +53,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
         {
         {
         }
         }
 
 
-        /// <summary>
-        /// Occurs when [triggered].
-        /// </summary>
-        public event EventHandler<EventArgs> Triggered;
-
         /// <summary>
         /// <summary>
         /// Called when [triggered].
         /// Called when [triggered].
         /// </summary>
         /// </summary>
         private void OnTriggered()
         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
     public class WeeklyTrigger : ITaskTrigger
     {
     {
         /// <summary>
         /// <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>
         /// </summary>
         /// <value>The time of day.</value>
         /// <value>The time of day.</value>
         public TimeSpan TimeOfDay { get; set; }
         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>
         /// <summary>
         /// Called when [triggered].
         /// Called when [triggered].
         /// </summary>
         /// </summary>
         private void OnTriggered()
         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>
         /// </summary>
         private readonly ILogger<InstallationManager> _logger;
         private readonly ILogger<InstallationManager> _logger;
         private readonly IApplicationPaths _appPaths;
         private readonly IApplicationPaths _appPaths;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
@@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Updates
             ILogger<InstallationManager> logger,
             ILogger<InstallationManager> logger,
             IApplicationHost appHost,
             IApplicationHost appHost,
             IApplicationPaths appPaths,
             IApplicationPaths appPaths,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IJsonSerializer jsonSerializer,
             IJsonSerializer jsonSerializer,
             IServerConfigurationManager config,
             IServerConfigurationManager config,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
@@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Updates
             _logger = logger;
             _logger = logger;
             _applicationHost = appHost;
             _applicationHost = appHost;
             _appPaths = appPaths;
             _appPaths = appPaths;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
             _config = config;
             _config = config;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
@@ -116,26 +116,18 @@ namespace Emby.Server.Implementations.Updates
         {
         {
             try
             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)
             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
             // Always override the passed-in target (which is a file) and figure it out again
             string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
             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
             // CA5351: Do Not Use Broken Cryptographic Algorithms
 #pragma warning disable CA5351
 #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
 #pragma warning restore CA5351
         }
         }
 
 

+ 5 - 1
Jellyfin.Api/BaseJellyfinApiController.cs

@@ -1,4 +1,5 @@
 using System.Net.Mime;
 using System.Net.Mime;
+using MediaBrowser.Common.Json;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc;
 
 
 namespace Jellyfin.Api
 namespace Jellyfin.Api
@@ -8,7 +9,10 @@ namespace Jellyfin.Api
     /// </summary>
     /// </summary>
     [ApiController]
     [ApiController]
     [Route("[controller]")]
     [Route("[controller]")]
-    [Produces(MediaTypeNames.Application.Json)]
+    [Produces(
+        MediaTypeNames.Application.Json,
+        JsonDefaults.CamelCaseMediaType,
+        JsonDefaults.PascalCaseMediaType)]
     public class BaseJellyfinApiController : ControllerBase
     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.AspNetCore.Mvc;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
 
 
 namespace Jellyfin.Api.Controllers
 namespace Jellyfin.Api.Controllers
 {
 {
@@ -26,38 +27,20 @@ namespace Jellyfin.Api.Controllers
     {
     {
         private readonly ILogger<DashboardController> _logger;
         private readonly ILogger<DashboardController> _logger;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
-        private readonly IConfiguration _appConfig;
-        private readonly IServerConfigurationManager _serverConfigurationManager;
-        private readonly IResourceFileManager _resourceFileManager;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="DashboardController"/> class.
         /// Initializes a new instance of the <see cref="DashboardController"/> class.
         /// </summary>
         /// </summary>
         /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
         /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
         /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> 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(
         public DashboardController(
             ILogger<DashboardController> logger,
             ILogger<DashboardController> logger,
-            IServerApplicationHost appHost,
-            IConfiguration appConfig,
-            IResourceFileManager resourceFileManager,
-            IServerConfigurationManager serverConfigurationManager)
+            IServerApplicationHost appHost)
         {
         {
             _logger = logger;
             _logger = logger;
             _appHost = appHost;
             _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>
         /// <summary>
         /// Gets the configuration pages.
         /// Gets the configuration pages.
         /// </summary>
         /// </summary>
@@ -169,87 +152,6 @@ namespace Jellyfin.Api.Controllers
             return NotFound();
             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)
         private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin)
         {
         {
             return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1));
             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);
                 var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Parse(key.Substring("landing-".Length)), existingDisplayPreferences.Client);
                 itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
                 itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
-                _displayPreferencesManager.SaveChanges(itemPreferences);
             }
             }
 
 
             var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Empty, existingDisplayPreferences.Client);
             var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Empty, existingDisplayPreferences.Client);
@@ -167,8 +166,7 @@ namespace Jellyfin.Api.Controllers
                 itemPrefs.ViewType = viewType;
                 itemPrefs.ViewType = viewType;
             }
             }
 
 
-            _displayPreferencesManager.SaveChanges(existingDisplayPreferences);
-            _displayPreferencesManager.SaveChanges(itemPrefs);
+            _displayPreferencesManager.SaveChanges();
 
 
             return NoContent();
             return NoContent();
         }
         }

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

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

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

@@ -1354,15 +1354,20 @@ namespace Jellyfin.Api.Controllers
                 segmentFormat = "mpegts";
                 segmentFormat = "mpegts";
             }
             }
 
 
+            var maxMuxingQueueSize = encodingOptions.MaxMuxingQueueSize > 128
+                ? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
+                : "128";
+
             return string.Format(
             return string.Format(
                 CultureInfo.InvariantCulture,
                 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,
                 inputModifier,
                 _encodingHelper.GetInputArgument(state, encodingOptions),
                 _encodingHelper.GetInputArgument(state, encodingOptions),
                 threads,
                 threads,
                 mapArgs,
                 mapArgs,
                 GetVideoArguments(state, encodingOptions, startNumber),
                 GetVideoArguments(state, encodingOptions, startNumber),
                 GetAudioArguments(state, encodingOptions),
                 GetAudioArguments(state, encodingOptions),
+                maxMuxingQueueSize,
                 state.SegmentLength.ToString(CultureInfo.InvariantCulture),
                 state.SegmentLength.ToString(CultureInfo.InvariantCulture),
                 segmentFormat,
                 segmentFormat,
                 startNumberParam,
                 startNumberParam,

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

@@ -16,6 +16,7 @@ using Jellyfin.Api.Models.LiveTvDtos;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
@@ -1069,7 +1070,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetSchedulesDirectCountries()
         public async Task<ActionResult> GetSchedulesDirectCountries()
         {
         {
-            var client = _httpClientFactory.CreateClient();
+            var client = _httpClientFactory.CreateClient(NamedClient.Default);
             // https://json.schedulesdirect.org/20141201/available/countries
             // https://json.schedulesdirect.org/20141201/available/countries
             // Can't dispose the response as it's required up the call chain.
             // 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")
             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 System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
@@ -244,7 +245,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
         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);
             using var response = await httpClient.GetAsync(url).ConfigureAwait(false);
             var ext = response.Content.Headers.ContentType.MediaType.Split('/').Last();
             var ext = response.Content.Headers.ContentType.MediaType.Split('/').Last();
             var fullCachePath = GetFullCachePath(urlHash + "." + ext);
             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.Helpers;
 using Jellyfin.Api.Models.StreamingDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dlna;
@@ -233,7 +234,7 @@ namespace Jellyfin.Api.Controllers
                     .First();
                     .First();
             }
             }
 
 
-            var list = primaryVersion.LinkedAlternateVersions.ToList();
+            var alternateVersionsOfPrimary = primaryVersion.LinkedAlternateVersions.ToList();
 
 
             foreach (var item in items.Where(i => i.Id != primaryVersion.Id))
             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);
                 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)
                 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);
             await primaryVersion.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
             return NoContent();
             return NoContent();
         }
         }
@@ -470,7 +474,7 @@ namespace Jellyfin.Api.Controllers
             {
             {
                 StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
                 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);
                 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 System.Threading.Tasks;
 using Jellyfin.Api.Models.StreamingDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 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);
                 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);
                 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 NoContentResult();
             }
             }
 
 
-            return new PhysicalFileResult(path, contentType);
+            return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true };
         }
         }
 
 
         /// <summary>
         /// <summary>

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

@@ -14,9 +14,9 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
     <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.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" Version="5.5.1" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.5.1" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.5.1" />
   </ItemGroup>
   </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;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 using System.ComponentModel.DataAnnotations.Schema;
@@ -11,7 +9,7 @@ namespace Jellyfin.Data.Entities
     /// <summary>
     /// <summary>
     /// An entity referencing an activity log entry.
     /// An entity referencing an activity log entry.
     /// </summary>
     /// </summary>
-    public partial class ActivityLog : IHasConcurrencyToken
+    public class ActivityLog : IHasConcurrencyToken
     {
     {
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ActivityLog"/> class.
         /// Initializes a new instance of the <see cref="ActivityLog"/> class.
@@ -32,13 +30,11 @@ namespace Jellyfin.Data.Entities
                 throw new ArgumentNullException(nameof(type));
                 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>
         /// <summary>
@@ -47,38 +43,21 @@ namespace Jellyfin.Data.Entities
         /// </summary>
         /// </summary>
         protected ActivityLog()
         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>
         /// <summary>
         /// Gets or sets the identity of this instance.
         /// Gets or sets the identity of this instance.
         /// This is the key in the backing database.
         /// This is the key in the backing database.
         /// </summary>
         /// </summary>
-        [Key]
-        [Required]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         public int Id { get; protected set; }
         public int Id { get; protected set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the name.
         /// Gets or sets the name.
-        /// Required, Max length = 512.
         /// </summary>
         /// </summary>
+        /// <remarks>
+        /// Required, Max length = 512.
+        /// </remarks>
         [Required]
         [Required]
         [MaxLength(512)]
         [MaxLength(512)]
         [StringLength(512)]
         [StringLength(512)]
@@ -86,24 +65,30 @@ namespace Jellyfin.Data.Entities
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the overview.
         /// Gets or sets the overview.
-        /// Max length = 512.
         /// </summary>
         /// </summary>
+        /// <remarks>
+        /// Max length = 512.
+        /// </remarks>
         [MaxLength(512)]
         [MaxLength(512)]
         [StringLength(512)]
         [StringLength(512)]
         public string Overview { get; set; }
         public string Overview { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the short overview.
         /// Gets or sets the short overview.
-        /// Max length = 512.
         /// </summary>
         /// </summary>
+        /// <remarks>
+        /// Max length = 512.
+        /// </remarks>
         [MaxLength(512)]
         [MaxLength(512)]
         [StringLength(512)]
         [StringLength(512)]
         public string ShortOverview { get; set; }
         public string ShortOverview { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the type.
         /// Gets or sets the type.
-        /// Required, Max length = 256.
         /// </summary>
         /// </summary>
+        /// <remarks>
+        /// Required, Max length = 256.
+        /// </remarks>
         [Required]
         [Required]
         [MaxLength(256)]
         [MaxLength(256)]
         [StringLength(256)]
         [StringLength(256)]
@@ -111,43 +96,42 @@ namespace Jellyfin.Data.Entities
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the user id.
         /// Gets or sets the user id.
-        /// Required.
         /// </summary>
         /// </summary>
-        [Required]
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         public Guid UserId { get; set; }
         public Guid UserId { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the item id.
         /// Gets or sets the item id.
-        /// Max length = 256.
         /// </summary>
         /// </summary>
+        /// <remarks>
+        /// Max length = 256.
+        /// </remarks>
         [MaxLength(256)]
         [MaxLength(256)]
         [StringLength(256)]
         [StringLength(256)]
         public string ItemId { get; set; }
         public string ItemId { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the date created. This should be in UTC.
         /// Gets or sets the date created. This should be in UTC.
-        /// Required.
         /// </summary>
         /// </summary>
-        [Required]
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         public DateTime DateCreated { get; set; }
         public DateTime DateCreated { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the log severity. Default is <see cref="LogLevel.Trace"/>.
         /// Gets or sets the log severity. Default is <see cref="LogLevel.Trace"/>.
-        /// Required.
         /// </summary>
         /// </summary>
-        [Required]
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         public LogLevel LogSeverity { get; set; }
         public LogLevel LogSeverity { get; set; }
 
 
-        /// <summary>
-        /// Gets or sets the row version.
-        /// Required, ConcurrencyToken.
-        /// </summary>
+        /// <inheritdoc />
         [ConcurrencyCheck]
         [ConcurrencyCheck]
-        [Required]
         public uint RowVersion { get; set; }
         public uint RowVersion { get; set; }
 
 
-        partial void Init();
-
         /// <inheritdoc />
         /// <inheritdoc />
         public void OnSavingChanges()
         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.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 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;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Interfaces;
 using Jellyfin.Data.Interfaces;
@@ -13,11 +12,10 @@ namespace Jellyfin.Data.Entities
     /// <summary>
     /// <summary>
     /// An entity representing a group.
     /// An entity representing a group.
     /// </summary>
     /// </summary>
-    public partial class Group : IHasPermissions, IHasConcurrencyToken
+    public class Group : IHasPermissions, IHasConcurrencyToken
     {
     {
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="Group"/> class.
         /// Initializes a new instance of the <see cref="Group"/> class.
-        /// Public constructor with required data.
         /// </summary>
         /// </summary>
         /// <param name="name">The name of the group.</param>
         /// <param name="name">The name of the group.</param>
         public Group(string name)
         public Group(string name)
@@ -31,33 +29,25 @@ namespace Jellyfin.Data.Entities
             Id = Guid.NewGuid();
             Id = Guid.NewGuid();
 
 
             Permissions = new HashSet<Permission>();
             Permissions = new HashSet<Permission>();
-            ProviderMappings = new HashSet<ProviderMapping>();
             Preferences = new HashSet<Preference>();
             Preferences = new HashSet<Preference>();
-
-            Init();
         }
         }
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="Group"/> class.
         /// Initializes a new instance of the <see cref="Group"/> class.
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
         /// </summary>
         /// </summary>
+        /// <remarks>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </remarks>
         protected Group()
         protected Group()
         {
         {
-            Init();
         }
         }
 
 
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
         /// <summary>
         /// <summary>
         /// Gets or sets the id of this group.
         /// Gets or sets the id of this group.
         /// </summary>
         /// </summary>
         /// <remarks>
         /// <remarks>
         /// Identity, Indexed, Required.
         /// Identity, Indexed, Required.
         /// </remarks>
         /// </remarks>
-        [Key]
-        [Required]
         public Guid Id { get; protected set; }
         public Guid Id { get; protected set; }
 
 
         /// <summary>
         /// <summary>
@@ -71,42 +61,19 @@ namespace Jellyfin.Data.Entities
         [StringLength(255)]
         [StringLength(255)]
         public string Name { get; set; }
         public string Name { get; set; }
 
 
-        /// <summary>
-        /// Gets or sets the row version.
-        /// </summary>
-        /// <remarks>
-        /// Required, Concurrency Token.
-        /// </remarks>
+        /// <inheritdoc />
         [ConcurrencyCheck]
         [ConcurrencyCheck]
-        [Required]
         public uint RowVersion { get; set; }
         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; }
         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>
         /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
+        /// Gets or sets a collection containing the group's preferences.
         /// </summary>
         /// </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/>
         /// <inheritdoc/>
         public bool HasPermission(PermissionKind kind)
         public bool HasPermission(PermissionKind kind)
@@ -120,6 +87,10 @@ namespace Jellyfin.Data.Entities
             Permissions.First(p => p.Kind == kind).Value = value;
             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;
 using System.ComponentModel.DataAnnotations.Schema;
 using System.ComponentModel.DataAnnotations.Schema;
 
 
 namespace Jellyfin.Data.Entities
 namespace Jellyfin.Data.Entities
 {
 {
+    /// <summary>
+    /// An entity representing an image.
+    /// </summary>
     public class ImageInfo
     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)
         public ImageInfo(string path)
         {
         {
             Path = path;
             Path = path;
             LastModified = DateTime.UtcNow;
             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)]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         public int Id { get; protected set; }
         public int Id { get; protected set; }
 
 
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
         public Guid? UserId { get; protected set; }
         public Guid? UserId { get; protected set; }
 
 
+        /// <summary>
+        /// Gets or sets the path of the image.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
         [Required]
         [MaxLength(512)]
         [MaxLength(512)]
         [StringLength(512)]
         [StringLength(512)]
         public string Path { get; set; }
         public string Path { get; set; }
 
 
-        [Required]
+        /// <summary>
+        /// Gets or sets the date last modified.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         public DateTime LastModified { get; set; }
         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;
 using System.ComponentModel.DataAnnotations.Schema;
 using System.ComponentModel.DataAnnotations.Schema;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 
 
 namespace Jellyfin.Data.Entities
 namespace Jellyfin.Data.Entities
 {
 {
+    /// <summary>
+    /// An entity that represents a user's display preferences for a specific item.
+    /// </summary>
     public class ItemDisplayPreferences
     public class ItemDisplayPreferences
     {
     {
         /// <summary>
         /// <summary>

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

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 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 System.Collections.Generic;
 using Jellyfin.Data.Interfaces;
 using Jellyfin.Data.Interfaces;
 
 

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

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations.Schema;
 using System.ComponentModel.DataAnnotations.Schema;
@@ -8,7 +10,7 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// <summary>
     /// An entity containing metadata for a book.
     /// An entity containing metadata for a book.
     /// </summary>
     /// </summary>
-    public class BookMetadata : Metadata, IHasCompanies
+    public class BookMetadata : ItemMetadata, IHasCompanies
     {
     {
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="BookMetadata"/> class.
         /// 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;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 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.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 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.
         /// Gets or sets the next item in the collection.
         /// </summary>
         /// </summary>
         /// <remarks>
         /// <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>
         /// </remarks>
         public virtual CollectionItem Next { get; set; }
         public virtual CollectionItem Next { get; set; }
 
 
@@ -81,7 +81,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Gets or sets the previous item in the collection.
         /// Gets or sets the previous item in the collection.
         /// </summary>
         /// </summary>
         /// <remarks>
         /// <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>
         /// </remarks>
         public virtual CollectionItem Previous { get; set; }
         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.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 using System.ComponentModel.DataAnnotations.Schema;

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

@@ -6,7 +6,7 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// <summary>
     /// An entity holding metadata for a <see cref="Company"/>.
     /// An entity holding metadata for a <see cref="Company"/>.
     /// </summary>
     /// </summary>
-    public class CompanyMetadata : Metadata
+    public class CompanyMetadata : ItemMetadata
     {
     {
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="CompanyMetadata"/> class.
         /// 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 System.Collections.Generic;
 using Jellyfin.Data.Interfaces;
 using Jellyfin.Data.Interfaces;
 
 

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

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

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

@@ -6,7 +6,7 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// <summary>
     /// An entity containing metadata for an <see cref="Episode"/>.
     /// An entity containing metadata for an <see cref="Episode"/>.
     /// </summary>
     /// </summary>
-    public class EpisodeMetadata : Metadata
+    public class EpisodeMetadata : ItemMetadata
     {
     {
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="EpisodeMetadata"/> class.
         /// 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.
         /// Initializes a new instance of the <see cref="Genre"/> class.
         /// </summary>
         /// </summary>
         /// <param name="name">The name.</param>
         /// <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))
             if (string.IsNullOrEmpty(name))
             {
             {
@@ -24,12 +24,12 @@ namespace Jellyfin.Data.Entities.Libraries
 
 
             Name = name;
             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>
         /// <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;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
@@ -9,14 +11,14 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// <summary>
     /// An abstract class that holds metadata.
     /// An abstract class that holds metadata.
     /// </summary>
     /// </summary>
-    public abstract class Metadata : IHasArtwork, IHasConcurrencyToken
+    public abstract class ItemMetadata : IHasArtwork, IHasConcurrencyToken
     {
     {
         /// <summary>
         /// <summary>
-        /// Initializes a new instance of the <see cref="Metadata"/> class.
+        /// Initializes a new instance of the <see cref="ItemMetadata"/> class.
         /// </summary>
         /// </summary>
         /// <param name="title">The title or name of the object.</param>
         /// <param name="title">The title or name of the object.</param>
         /// <param name="language">ISO-639-3 3-character language codes.</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))
             if (string.IsNullOrEmpty(title))
             {
             {
@@ -41,12 +43,12 @@ namespace Jellyfin.Data.Entities.Libraries
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Initializes a new instance of the <see cref="Metadata"/> class.
+        /// Initializes a new instance of the <see cref="ItemMetadata"/> class.
         /// </summary>
         /// </summary>
         /// <remarks>
         /// <remarks>
         /// Default constructor. Protected due to being abstract.
         /// Default constructor. Protected due to being abstract.
         /// </remarks>
         /// </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;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 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.
         /// Initializes a new instance of the <see cref="MetadataProviderId"/> class.
         /// </summary>
         /// </summary>
         /// <param name="providerId">The provider id.</param>
         /// <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))
             if (string.IsNullOrEmpty(providerId))
             {
             {
@@ -24,12 +24,12 @@ namespace Jellyfin.Data.Entities.Libraries
 
 
             ProviderId = providerId;
             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>
         /// <summary>

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

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

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

@@ -1,3 +1,5 @@
+#pragma warning disable CA2227
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
@@ -16,17 +18,17 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Initializes a new instance of the <see cref="PersonRole"/> class.
         /// Initializes a new instance of the <see cref="PersonRole"/> class.
         /// </summary>
         /// </summary>
         /// <param name="type">The role type.</param>
         /// <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;
             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>();
             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 System.Collections.Generic;
 using Jellyfin.Data.Interfaces;
 using Jellyfin.Data.Interfaces;
 
 

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

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

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

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

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

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

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

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

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

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

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

@@ -5,7 +5,7 @@ namespace Jellyfin.Data.Entities.Libraries
     /// <summary>
     /// <summary>
     /// An entity holding metadata for a track.
     /// An entity holding metadata for a track.
     /// </summary>
     /// </summary>
-    public class TrackMetadata : Metadata
+    public class TrackMetadata : ItemMetadata
     {
     {
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="TrackMetadata"/> class.
         /// 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;
 using System.ComponentModel.DataAnnotations.Schema;
 using System.ComponentModel.DataAnnotations.Schema;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
@@ -10,7 +8,7 @@ namespace Jellyfin.Data.Entities
     /// <summary>
     /// <summary>
     /// An entity representing whether the associated user has a specific permission.
     /// An entity representing whether the associated user has a specific permission.
     /// </summary>
     /// </summary>
-    public partial class Permission : IHasConcurrencyToken
+    public class Permission : IHasConcurrencyToken
     {
     {
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="Permission"/> class.
         /// Initializes a new instance of the <see cref="Permission"/> class.
@@ -22,8 +20,6 @@ namespace Jellyfin.Data.Entities
         {
         {
             Kind = kind;
             Kind = kind;
             Value = value;
             Value = value;
-
-            Init();
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -32,21 +28,14 @@ namespace Jellyfin.Data.Entities
         /// </summary>
         /// </summary>
         protected Permission()
         protected Permission()
         {
         {
-            Init();
         }
         }
 
 
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
         /// <summary>
         /// <summary>
         /// Gets or sets the id of this permission.
         /// Gets or sets the id of this permission.
         /// </summary>
         /// </summary>
         /// <remarks>
         /// <remarks>
         /// Identity, Indexed, Required.
         /// Identity, Indexed, Required.
         /// </remarks>
         /// </remarks>
-        [Key]
-        [Required]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         public int Id { get; protected set; }
         public int Id { get; protected set; }
 
 
@@ -56,7 +45,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// <remarks>
         /// Required.
         /// Required.
         /// </remarks>
         /// </remarks>
-        [Required]
         public PermissionKind Kind { get; protected set; }
         public PermissionKind Kind { get; protected set; }
 
 
         /// <summary>
         /// <summary>
@@ -65,36 +53,16 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// <remarks>
         /// Required.
         /// Required.
         /// </remarks>
         /// </remarks>
-        [Required]
         public bool Value { get; set; }
         public bool Value { get; set; }
 
 
-        /// <summary>
-        /// Gets or sets the row version.
-        /// </summary>
-        /// <remarks>
-        /// Required, ConcurrencyToken.
-        /// </remarks>
+        /// <inheritdoc />
         [ConcurrencyCheck]
         [ConcurrencyCheck]
-        [Required]
         public uint RowVersion { get; set; }
         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/>
         /// <inheritdoc/>
         public void OnSavingChanges()
         public void OnSavingChanges()
         {
         {
             RowVersion++;
             RowVersion++;
         }
         }
-
-        partial void Init();
     }
     }
 }
 }

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

@@ -31,18 +31,12 @@ namespace Jellyfin.Data.Entities
         {
         {
         }
         }
 
 
-        /*************************************************************************
-         * Properties
-         *************************************************************************/
-
         /// <summary>
         /// <summary>
         /// Gets or sets the id of this preference.
         /// Gets or sets the id of this preference.
         /// </summary>
         /// </summary>
         /// <remarks>
         /// <remarks>
         /// Identity, Indexed, Required.
         /// Identity, Indexed, Required.
         /// </remarks>
         /// </remarks>
-        [Key]
-        [Required]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         public int Id { get; protected set; }
         public int Id { get; protected set; }
 
 
@@ -52,7 +46,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// <remarks>
         /// Required.
         /// Required.
         /// </remarks>
         /// </remarks>
-        [Required]
         public PreferenceKind Kind { get; protected set; }
         public PreferenceKind Kind { get; protected set; }
 
 
         /// <summary>
         /// <summary>
@@ -66,27 +59,10 @@ namespace Jellyfin.Data.Entities
         [StringLength(65535)]
         [StringLength(65535)]
         public string Value { get; set; }
         public string Value { get; set; }
 
 
-        /// <summary>
-        /// Gets or sets the row version.
-        /// </summary>
-        /// <remarks>
-        /// Required, ConcurrencyToken.
-        /// </remarks>
+        /// <inheritdoc/>
         [ConcurrencyCheck]
         [ConcurrencyCheck]
-        [Required]
         public uint RowVersion { get; set; }
         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/>
         /// <inheritdoc/>
         public void OnSavingChanges()
         public void OnSavingChanges()
         {
         {

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