diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml index 2a1c0e6f22..7617f0f5a2 100644 --- a/.ci/azure-pipelines-main.yml +++ b/.ci/azure-pipelines-main.yml @@ -64,28 +64,28 @@ jobs: arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)' zipAfterPublish: false - - task: PublishPipelineArtifact@0 + - task: PublishPipelineArtifact@1 displayName: 'Publish Artifact Naming' condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) inputs: targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll' artifactName: 'Jellyfin.Naming' - - task: PublishPipelineArtifact@0 + - task: PublishPipelineArtifact@1 displayName: 'Publish Artifact Controller' condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) inputs: targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll' artifactName: 'Jellyfin.Controller' - - task: PublishPipelineArtifact@0 + - task: PublishPipelineArtifact@1 displayName: 'Publish Artifact Model' condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) inputs: targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll' artifactName: 'Jellyfin.Model' - - task: PublishPipelineArtifact@0 + - task: PublishPipelineArtifact@1 displayName: 'Publish Artifact Common' condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) inputs: diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml index a3c7f8526c..eca8aa90f9 100644 --- a/.ci/azure-pipelines-test.yml +++ b/.ci/azure-pipelines-test.yml @@ -74,7 +74,6 @@ jobs: - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging displayName: 'Run ReportGenerator' - enabled: false inputs: reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml" targetdir: "$(Agent.TempDirectory)/merged/" @@ -84,10 +83,16 @@ jobs: - task: PublishCodeCoverageResults@1 condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging displayName: 'Publish Code Coverage' - enabled: false inputs: codeCoverageTool: "cobertura" #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2 summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml" pathToSources: $(Build.SourcesDirectory) failIfCoverageEmpty: true + + - task: PublishPipelineArtifact@1 + displayName: 'Publish OpenAPI Artifact' + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + inputs: + targetPath: "tests/Jellyfin.Api.Tests/bin/Release/netcoreapp3.1/openapi.json" + artifactName: 'OpenAPI Spec' diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7ddc49d5ce..09c523eb28 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -17,7 +17,7 @@ "type": "process", "args": [ "test", - "${workspaceFolder}/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj" + "${workspaceFolder}/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj" ], "problemMatcher": "$msCompile" } diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 1b4fdc8c46..f0724b4129 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -57,6 +57,7 @@ - [Larvitar](https://github.com/Larvitar) - [LeoVerto](https://github.com/LeoVerto) - [Liggy](https://github.com/Liggy) + - [lmaonator](https://github.com/lmaonator) - [LogicalPhallacy](https://github.com/LogicalPhallacy) - [loli10K](https://github.com/loli10K) - [lostmypillow](https://github.com/lostmypillow) @@ -78,6 +79,7 @@ - [nvllsvm](https://github.com/nvllsvm) - [nyanmisaka](https://github.com/nyanmisaka) - [oddstr13](https://github.com/oddstr13) + - [orryverducci](https://github.com/orryverducci) - [petermcneil](https://github.com/petermcneil) - [Phlogi](https://github.com/Phlogi) - [pjeanjean](https://github.com/pjeanjean) diff --git a/Dockerfile b/Dockerfile index d3fb138a81..69af9b77b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 # because of changes in docker and systemd we need to not build in parallel at the moment # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting -RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" +RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none" FROM debian:buster-slim diff --git a/Dockerfile.arm b/Dockerfile.arm index 59b8a8c982..efeed25dff 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -21,7 +21,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 # Discard objs - may cause failures if exists RUN find . -type d -name obj | xargs -r rm -r # Build -RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" +RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none" FROM multiarch/qemu-user-static:x86_64-arm as qemu diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 1a691b5727..1f2c2ec36d 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -21,7 +21,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 # Discard objs - may cause failures if exists RUN find . -type d -name obj | xargs -r rm -r # Build -RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" +RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none" FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu FROM arm64v8/debian:buster-slim diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs index 12338e2b4b..f5a7eca720 100644 --- a/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs +++ b/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs @@ -1,8 +1,8 @@ #pragma warning disable CS1591 +using System.Net.Http; using System.Threading.Tasks; using Emby.Dlna.Service; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; using Microsoft.Extensions.Logging; @@ -18,8 +18,8 @@ namespace Emby.Dlna.ConnectionManager IDlnaManager dlna, IServerConfigurationManager config, ILogger logger, - IHttpClient httpClient) - : base(logger, httpClient) + IHttpClientFactory httpClientFactory) + : base(logger, httpClientFactory) { _dlna = dlna; _config = config; diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs index 72732823ac..5760f260cf 100644 --- a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs @@ -2,11 +2,11 @@ using System; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using Emby.Dlna.Service; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; @@ -41,7 +41,7 @@ namespace Emby.Dlna.ContentDirectory IServerConfigurationManager config, IUserManager userManager, ILogger logger, - IHttpClient httpClient, + IHttpClientFactory httpClient, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IUserViewManager userViewManager, diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index be1ed7872e..4b108b89ea 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/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()); } diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index bd09a80511..5b8a89d8f3 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -948,7 +948,7 @@ namespace Emby.Dlna.Didl } catch (XmlException ex) { - _logger.LogError(ex, "Error adding xml value: {value}", name); + _logger.LogError(ex, "Error adding xml value: {Value}", name); } } @@ -960,7 +960,7 @@ namespace Emby.Dlna.Didl } catch (XmlException ex) { - _logger.LogError(ex, "Error adding xml value: {value}", value); + _logger.LogError(ex, "Error adding xml value: {Value}", value); } } diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index 8538f580ca..6ed49944c0 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -80,6 +80,7 @@ + diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs index b66e966dff..7d8da86ef9 100644 --- a/Emby.Dlna/Eventing/DlnaEventManager.cs +++ b/Emby.Dlna/Eventing/DlnaEventManager.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; +using System.Net.Mime; using System.Text; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; @@ -20,13 +21,13 @@ namespace Emby.Dlna.Eventing new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); private readonly ILogger _logger; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public DlnaEventManager(ILogger logger, IHttpClient httpClient) + public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _logger = logger; } @@ -167,24 +168,17 @@ namespace Emby.Dlna.Eventing builder.Append(""); - var options = new HttpRequestOptions - { - RequestContent = builder.ToString(), - RequestContentType = "text/xml", - Url = subscription.CallbackUrl, - BufferContent = false - }; - - options.RequestHeaders.Add("NT", subscription.NotificationType); - options.RequestHeaders.Add("NTS", "upnp:propchange"); - options.RequestHeaders.Add("SID", subscription.Id); - options.RequestHeaders.Add("SEQ", subscription.TriggerCount.ToString(_usCulture)); + using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl); + options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml); + options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType); + options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange"); + options.Headers.TryAddWithoutValidation("SID", subscription.Id); + options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture)); try { - using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false)) - { - } + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); } catch (OperationCanceledException) { diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 0ad82022dd..40c2cc0e0a 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -2,6 +2,7 @@ using System; using System.Globalization; +using System.Net.Http; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -36,7 +37,7 @@ namespace Emby.Dlna.Main private readonly ILogger _logger; private readonly IServerApplicationHost _appHost; private readonly ISessionManager _sessionManager; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IDlnaManager _dlnaManager; @@ -61,7 +62,7 @@ namespace Emby.Dlna.Main ILoggerFactory loggerFactory, IServerApplicationHost appHost, ISessionManager sessionManager, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, @@ -79,7 +80,7 @@ namespace Emby.Dlna.Main _config = config; _appHost = appHost; _sessionManager = sessionManager; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _libraryManager = libraryManager; _userManager = userManager; _dlnaManager = dlnaManager; @@ -101,7 +102,7 @@ namespace Emby.Dlna.Main config, userManager, loggerFactory.CreateLogger(), - httpClient, + httpClientFactory, localizationManager, mediaSourceManager, userViewManager, @@ -112,11 +113,11 @@ namespace Emby.Dlna.Main dlnaManager, config, loggerFactory.CreateLogger(), - httpClient); + httpClientFactory); MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService( loggerFactory.CreateLogger(), - httpClient, + httpClientFactory, config); Current = this; } @@ -364,7 +365,7 @@ namespace Emby.Dlna.Main _appHost, _imageProcessor, _deviceDiscovery, - _httpClient, + _httpClientFactory, _config, _userDataManager, _localization, diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs index 28de2fef52..e6d845e1e4 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs @@ -1,8 +1,8 @@ #pragma warning disable CS1591 +using System.Net.Http; using System.Threading.Tasks; using Emby.Dlna.Service; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.Extensions.Logging; @@ -14,9 +14,9 @@ namespace Emby.Dlna.MediaReceiverRegistrar public MediaReceiverRegistrarService( ILogger logger, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IServerConfigurationManager config) - : base(logger, httpClient) + : base(logger, httpClientFactory) { _config = config; } diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index 5462e7abc5..c97acdb026 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Net.Http; using System.Security; using System.Threading; using System.Threading.Tasks; @@ -21,7 +22,7 @@ namespace Emby.Dlna.PlayTo { private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; @@ -34,10 +35,10 @@ namespace Emby.Dlna.PlayTo private int _connectFailureCount; private bool _disposed; - public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger) + public Device(DeviceInfo deviceProperties, IHttpClientFactory httpClientFactory, ILogger logger) { Properties = deviceProperties; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _logger = logger; } @@ -236,7 +237,7 @@ namespace Emby.Dlna.PlayTo _logger.LogDebug("Setting mute"); var value = mute ? 1 : 0; - await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value)) + await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value)) .ConfigureAwait(false); IsMuted = mute; @@ -271,7 +272,7 @@ namespace Emby.Dlna.PlayTo // Remote control will perform better Volume = value; - await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value)) + await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value)) .ConfigureAwait(false); } @@ -292,7 +293,7 @@ namespace Emby.Dlna.PlayTo throw new InvalidOperationException("Unable to find service"); } - await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME")) + await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME")) .ConfigureAwait(false); RestartTimer(true); @@ -326,7 +327,7 @@ namespace Emby.Dlna.PlayTo } var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); - await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header) + await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header) .ConfigureAwait(false); await Task.Delay(50).ConfigureAwait(false); @@ -368,7 +369,7 @@ namespace Emby.Dlna.PlayTo throw new InvalidOperationException("Unable to find service"); } - return new SsdpHttpClient(_httpClient).SendCommandAsync( + return new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, @@ -397,7 +398,7 @@ namespace Emby.Dlna.PlayTo var service = GetAvTransportService(); - await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)) + await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)) .ConfigureAwait(false); RestartTimer(true); @@ -415,7 +416,7 @@ namespace Emby.Dlna.PlayTo var service = GetAvTransportService(); - await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)) + await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)) .ConfigureAwait(false); TransportState = TransportState.Paused; @@ -542,7 +543,7 @@ namespace Emby.Dlna.PlayTo return; } - var result = await new SsdpHttpClient(_httpClient).SendCommandAsync( + var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, @@ -592,7 +593,7 @@ namespace Emby.Dlna.PlayTo return; } - var result = await new SsdpHttpClient(_httpClient).SendCommandAsync( + var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, @@ -625,7 +626,7 @@ namespace Emby.Dlna.PlayTo return null; } - var result = await new SsdpHttpClient(_httpClient).SendCommandAsync( + var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, @@ -667,7 +668,7 @@ namespace Emby.Dlna.PlayTo var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var result = await new SsdpHttpClient(_httpClient).SendCommandAsync( + var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, @@ -734,7 +735,7 @@ namespace Emby.Dlna.PlayTo var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var result = await new SsdpHttpClient(_httpClient).SendCommandAsync( + var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, @@ -912,7 +913,7 @@ namespace Emby.Dlna.PlayTo string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); - var httpClient = new SsdpHttpClient(_httpClient); + var httpClient = new SsdpHttpClient(_httpClientFactory); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); @@ -940,7 +941,7 @@ namespace Emby.Dlna.PlayTo string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); - var httpClient = new SsdpHttpClient(_httpClient); + var httpClient = new SsdpHttpClient(_httpClientFactory); _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync"); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); @@ -969,9 +970,9 @@ namespace Emby.Dlna.PlayTo return baseUrl + url; } - public static async Task CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, ILogger logger, CancellationToken cancellationToken) + public static async Task CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken) { - var ssdpHttpClient = new SsdpHttpClient(httpClient); + var ssdpHttpClient = new SsdpHttpClient(httpClientFactory); var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false); @@ -1079,7 +1080,7 @@ namespace Emby.Dlna.PlayTo } } - return new Device(deviceProperties, httpClient, logger); + return new Device(deviceProperties, httpClientFactory, logger); } private static DeviceIcon CreateIcon(XElement element) diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index ff801f2630..3d1dd3e73f 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -4,6 +4,7 @@ using System; using System.Globalization; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Events; @@ -33,7 +34,7 @@ namespace Emby.Dlna.PlayTo private readonly IDlnaManager _dlnaManager; private readonly IServerApplicationHost _appHost; private readonly IImageProcessor _imageProcessor; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerConfigurationManager _config; private readonly IUserDataManager _userDataManager; private readonly ILocalizationManager _localization; @@ -46,7 +47,7 @@ namespace Emby.Dlna.PlayTo private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1); private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); - public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder) + public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder) { _logger = logger; _sessionManager = sessionManager; @@ -56,7 +57,7 @@ namespace Emby.Dlna.PlayTo _appHost = appHost; _imageProcessor = imageProcessor; _deviceDiscovery = deviceDiscovery; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _config = config; _userDataManager = userDataManager; _localization = localization; @@ -174,7 +175,7 @@ namespace Emby.Dlna.PlayTo if (controller == null) { - var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _logger, cancellationToken).ConfigureAwait(false); + var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false); string deviceName = device.Properties.Name; diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs index ab262bebfc..8683c89971 100644 --- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs +++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs @@ -4,6 +4,8 @@ using System; using System.Globalization; using System.IO; using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -20,11 +22,11 @@ namespace Emby.Dlna.PlayTo private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; - public SsdpHttpClient(IHttpClient httpClient) + public SsdpHttpClient(IHttpClientFactory httpClientFactory) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; } public async Task SendCommandAsync( @@ -36,20 +38,18 @@ namespace Emby.Dlna.PlayTo CancellationToken cancellationToken = default) { var url = NormalizeServiceUrl(baseUrl, service.ControlUrl); - using (var response = await PostSoapDataAsync( - url, - $"\"{service.ServiceType}#{command}\"", - postData, - header, - cancellationToken) - .ConfigureAwait(false)) - using (var stream = response.Content) - using (var reader = new StreamReader(stream, Encoding.UTF8)) - { - return XDocument.Parse( - await reader.ReadToEndAsync().ConfigureAwait(false), - LoadOptions.PreserveWhitespace); - } + using var response = await PostSoapDataAsync( + url, + $"\"{service.ServiceType}#{command}\"", + postData, + header, + cancellationToken) + .ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + using var reader = new StreamReader(stream, Encoding.UTF8); + return XDocument.Parse( + await reader.ReadToEndAsync().ConfigureAwait(false), + LoadOptions.PreserveWhitespace); } private static string NormalizeServiceUrl(string baseUrl, string serviceUrl) @@ -76,49 +76,32 @@ namespace Emby.Dlna.PlayTo int eventport, int timeOut = 3600) { - var options = new HttpRequestOptions - { - Url = url, - UserAgent = USERAGENT, - LogErrorResponseBody = true, - BufferContent = false, - }; - - options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture); - options.RequestHeaders["CALLBACK"] = "<" + localIp + ":" + eventport.ToString(_usCulture) + ">"; - options.RequestHeaders["NT"] = "upnp:event"; - options.RequestHeaders["TIMEOUT"] = "Second-" + timeOut.ToString(_usCulture); - - using (await _httpClient.SendAsync(options, new HttpMethod("SUBSCRIBE")).ConfigureAwait(false)) - { - } + using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url); + options.Headers.UserAgent.ParseAdd(USERAGENT); + options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(_usCulture)); + options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">"); + options.Headers.TryAddWithoutValidation("NT", "upnp:event"); + options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture)); + + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(options, HttpCompletionOption.ResponseHeadersRead) + .ConfigureAwait(false); } public async Task 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 PostSoapDataAsync( + private Task PostSoapDataAsync( string url, string soapAction, string postData, @@ -130,29 +113,20 @@ namespace Emby.Dlna.PlayTo soapAction = $"\"{soapAction}\""; } - var options = new HttpRequestOptions - { - Url = url, - UserAgent = USERAGENT, - LogErrorResponseBody = true, - BufferContent = false, - - CancellationToken = cancellationToken - }; - - options.RequestHeaders["SOAPAction"] = soapAction; - options.RequestHeaders["Pragma"] = "no-cache"; - options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName; + using var options = new HttpRequestMessage(HttpMethod.Post, url); + options.Headers.UserAgent.ParseAdd(USERAGENT); + options.Headers.TryAddWithoutValidation("SOAPACTION", soapAction); + options.Headers.TryAddWithoutValidation("Pragma", "no-cache"); + options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName); if (!string.IsNullOrEmpty(header)) { - options.RequestHeaders["contentFeatures.dlna.org"] = header; + options.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header); } - options.RequestContentType = "text/xml"; - options.RequestContent = postData; + options.Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml); - return _httpClient.Post(options); + return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken); } } } diff --git a/Emby.Dlna/Service/BaseService.cs b/Emby.Dlna/Service/BaseService.cs index 40d069e7cd..a97c4d63a6 100644 --- a/Emby.Dlna/Service/BaseService.cs +++ b/Emby.Dlna/Service/BaseService.cs @@ -1,25 +1,21 @@ #pragma warning disable CS1591 +using System.Net.Http; using Emby.Dlna.Eventing; -using MediaBrowser.Common.Net; using Microsoft.Extensions.Logging; namespace Emby.Dlna.Service { public class BaseService : IDlnaEventManager { - protected BaseService(ILogger logger, IHttpClient httpClient) + protected BaseService(ILogger logger, IHttpClientFactory httpClientFactory) { Logger = logger; - HttpClient = httpClient; - - EventManager = new DlnaEventManager(logger, HttpClient); + EventManager = new DlnaEventManager(logger, httpClientFactory); } protected IDlnaEventManager EventManager { get; } - protected IHttpClient HttpClient { get; } - protected ILogger Logger { get; } public EventSubscriptionResponse CancelEventSubscription(string subscriptionId) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index d1e17f4169..fd4244f64d 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -136,8 +136,8 @@ namespace Emby.Naming.Common CleanDateTimes = new[] { - @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*", - @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*" + @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*", + @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*" }; CleanStrings = new[] diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index d4a8268b97..4ab0a2a3f2 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -308,7 +308,7 @@ namespace Emby.Server.Implementations.AppBase } catch (Exception ex) { - Logger.LogError(ex, "Error loading configuration file: {path}", path); + Logger.LogError(ex, "Error loading configuration file: {Path}", path); return Activator.CreateInstance(configurationType); } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8e9a581eae..c37e87d969 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -49,6 +49,7 @@ using Jellyfin.Api.Helpers; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; +using MediaBrowser.Common.Json; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; @@ -122,8 +123,8 @@ namespace Emby.Server.Implementations private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; + private IHttpClientFactory _httpClientFactory; private IWebSocketManager _webSocketManager; - private IHttpClient _httpClient; private string[] _urlPrefixes; @@ -279,6 +280,10 @@ namespace Emby.Server.Implementations Password = ServerConfigurationManager.Configuration.CertificatePassword }; Certificate = GetCertificate(CertificateInfo); + + ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; + ApplicationVersionString = ApplicationVersion.ToString(3); + ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; } public string ExpandVirtualPath(string path) @@ -308,16 +313,16 @@ namespace Emby.Server.Implementations } /// - public Version ApplicationVersion { get; } = typeof(ApplicationHost).Assembly.GetName().Version; + public Version ApplicationVersion { get; } /// - public string ApplicationVersionString { get; } = typeof(ApplicationHost).Assembly.GetName().Version.ToString(3); + public string ApplicationVersionString { get; } /// /// Gets the current application user agent. /// /// The application user agent. - public string ApplicationUserAgent => Name.Replace(' ', '-') + "/" + ApplicationVersionString; + public string ApplicationUserAgent { get; } /// /// 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(); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(_networkManager); ServiceCollection.AddSingleton(); @@ -650,8 +653,8 @@ namespace Emby.Server.Implementations _mediaEncoder = Resolve(); _sessionManager = Resolve(); + _httpClientFactory = Resolve(); _webSocketManager = Resolve(); - _httpClient = Resolve(); ((AuthenticationRepository)Resolve()).Initialize(); @@ -1296,25 +1299,17 @@ namespace Emby.Server.Implementations try { - using (var response = await _httpClient.SendAsync( - new HttpRequestOptions - { - Url = apiUrl, - LogErrorResponseBody = false, - BufferContent = false, - CancellationToken = cancellationToken - }, HttpMethod.Post).ConfigureAwait(false)) - { - using (var reader = new StreamReader(response.Content)) - { - var result = await reader.ReadToEndAsync().ConfigureAwait(false); - var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase); + using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); - Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid); - return valid; - } - } + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var result = await System.Text.Json.JsonSerializer.DeserializeAsync(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false); + var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase); + + _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); + Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid); + return valid; } catch (OperationCanceledException) { @@ -1401,7 +1396,7 @@ namespace Emby.Server.Implementations foreach (var assembly in assemblies) { - Logger.LogDebug("Found API endpoints in plugin {name}", assembly.FullName); + Logger.LogDebug("Found API endpoints in plugin {Name}", assembly.FullName); yield return assembly; } } diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 26fc1bee44..fb1bb65a09 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -890,7 +890,7 @@ namespace Emby.Server.Implementations.Channels } catch (Exception ex) { - _logger.LogError(ex, "Error writing to channel cache file: {path}", path); + _logger.LogError(ex, "Error writing to channel cache file: {Path}", path); } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index f2c7118fe0..57c1398e90 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -197,7 +197,7 @@ namespace Emby.Server.Implementations.Dto catch (Exception ex) { // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions - _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {itemName}", item.Name); + _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {ItemName}", item.Name); } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 56fc573270..0a348f0d00 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -32,10 +32,10 @@ - - - - + + + + diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs deleted file mode 100644 index 25adc58126..0000000000 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ /dev/null @@ -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 -{ - /// - /// Class HttpClientManager. - /// - public class HttpClientManager : IHttpClient - { - private readonly ILogger _logger; - private readonly IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - private readonly IApplicationHost _appHost; - - /// - /// 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. - /// - /// The HTTP clients. - private readonly ConcurrentDictionary _httpClients = new ConcurrentDictionary(); - - /// - /// Initializes a new instance of the class. - /// - public HttpClientManager( - IApplicationPaths appPaths, - ILogger logger, - IFileSystem fileSystem, - IApplicationHost appHost) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _fileSystem = fileSystem; - _appPaths = appPaths ?? throw new ArgumentNullException(nameof(appPaths)); - _appHost = appHost; - } - - /// - /// Gets the correct http client for the given url. - /// - /// The url. - /// HttpClient. - 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; - } - - /// - /// Gets the response internal. - /// - /// The options. - /// Task{HttpResponseInfo}. - public Task GetResponse(HttpRequestOptions options) - => SendAsync(options, HttpMethod.Get); - - /// - /// Performs a GET request and returns the resulting stream. - /// - /// The options. - /// Task{Stream}. - public async Task Get(HttpRequestOptions options) - { - var response = await GetResponse(options).ConfigureAwait(false); - return response.Content; - } - - /// - /// send as an asynchronous operation. - /// - /// The options. - /// The HTTP method. - /// Task{HttpResponseInfo}. - public Task SendAsync(HttpRequestOptions options, string httpMethod) - => SendAsync(options, new HttpMethod(httpMethod)); - - /// - /// send as an asynchronous operation. - /// - /// The options. - /// The HTTP method. - /// Task{HttpResponseInfo}. - public async Task 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 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()); - } - } - - 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() - }; - } - - /// - public Task Post(HttpRequestOptions options) - => SendAsync(options, HttpMethod.Post); - - private void ValidateParams(HttpRequestOptions options) - { - if (string.IsNullOrEmpty(options.Url)) - { - throw new ArgumentNullException(nameof(options)); - } - } - - /// - /// Gets the host from URL. - /// - /// The URL. - /// System.String. - 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 - }; - } - } -} diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index fe74f1de73..7435e9d0bf 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.IO continue; } - _logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path); + _logger.LogInformation("{Name} ({Path}) will be refreshed.", item.Name, item.Path); try { @@ -160,11 +160,11 @@ namespace Emby.Server.Implementations.IO // For now swallow and log. // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable) // Should we remove it from it's parent? - _logger.LogError(ex, "Error refreshing {name}", item.Name); + _logger.LogError(ex, "Error refreshing {Name}", item.Name); } catch (Exception ex) { - _logger.LogError(ex, "Error refreshing {name}", item.Name); + _logger.LogError(ex, "Error refreshing {Name}", item.Name); } } } @@ -214,6 +214,7 @@ namespace Emby.Server.Implementations.IO } } + /// public void Dispose() { _disposed = true; diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 9290dfcd0e..3353fae9d8 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.IO } catch (Exception ex) { - _logger.LogError(ex, "Error in ReportFileSystemChanged for {path}", path); + _logger.LogError(ex, "Error in ReportFileSystemChanged for {Path}", path); } } } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index ab6483bf9f..3cb025111d 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -398,30 +398,6 @@ namespace Emby.Server.Implementations.IO } } - public virtual void SetReadOnly(string path, bool isReadOnly) - { - if (OperatingSystem.Id != OperatingSystemId.Windows) - { - return; - } - - var info = GetExtendedFileSystemInfo(path); - - if (info.Exists && info.IsReadOnly != isReadOnly) - { - if (isReadOnly) - { - File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.ReadOnly); - } - else - { - var attributes = File.GetAttributes(path); - attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly); - File.SetAttributes(path, attributes); - } - } - } - public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly) { if (OperatingSystem.Id != OperatingSystemId.Windows) @@ -707,14 +683,6 @@ namespace Emby.Server.Implementations.IO return Directory.EnumerateFileSystemEntries(path, "*", searchOption); } - public virtual void SetExecutable(string path) - { - if (OperatingSystem.Id == OperatingSystemId.Darwin) - { - RunProcess("chmod", "+x \"" + path + "\"", Path.GetDirectoryName(path)); - } - } - private static void RunProcess(string path, string args, string workingDirectory) { using (var process = Process.Start(new ProcessStartInfo diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs index 40b397edc2..c16ebd61b7 100644 --- a/Emby.Server.Implementations/IO/StreamHelper.cs +++ b/Emby.Server.Implementations/IO/StreamHelper.cs @@ -11,8 +11,6 @@ namespace Emby.Server.Implementations.IO { public class StreamHelper : IStreamHelper { - private const int StreamCopyToBufferSize = 81920; - public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken) { byte[] buffer = ArrayPool.Shared.Rent(bufferSize); @@ -83,37 +81,9 @@ namespace Emby.Server.Implementations.IO } } - public async Task CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken) - { - byte[] buffer = ArrayPool.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.Shared.Return(buffer); - } - } - public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) { - byte[] buffer = ArrayPool.Shared.Rent(StreamCopyToBufferSize); + byte[] buffer = ArrayPool.Shared.Rent(IODefaults.CopyToBufferSize); try { int bytesRead; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index 2e13a3bb37..44560d1e21 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -16,13 +16,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public class DirectRecorder : IRecorder { private readonly ILogger _logger; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IStreamHelper _streamHelper; - public DirectRecorder(ILogger logger, IHttpClient httpClient, IStreamHelper streamHelper) + public DirectRecorder(ILogger logger, IHttpClientFactory httpClientFactory, IStreamHelper streamHelper) { _logger = logger; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _streamHelper = streamHelper; } @@ -52,10 +52,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _logger.LogInformation("Copying recording stream to file {0}", targetFile); // The media source is infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + using var durationToken = new CancellationTokenSource(duration); + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); - await directStreamProvider.CopyToAsync(output, cancellationToken).ConfigureAwait(false); + await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false); } _logger.LogInformation("Recording completed to file {0}", targetFile); @@ -63,37 +63,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { - var httpRequestOptions = new HttpRequestOptions - { - Url = mediaSource.Path, - BufferContent = false, - - // Some remote urls will expect a user agent to be supplied - UserAgent = "Emby/3.0", + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(mediaSource.Path, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - // Shouldn't matter but may cause issues - DecompressionMethod = CompressionMethods.None - }; + _logger.LogInformation("Opened recording stream from tuner provider"); - using (var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false)) - { - _logger.LogInformation("Opened recording stream from tuner provider"); + Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); - Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); + await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read); - using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - onStarted(); + onStarted(); - _logger.LogInformation("Copying recording stream to file {0}", targetFile); + _logger.LogInformation("Copying recording stream to file {0}", targetFile); - // The media source if infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + // The media source if infinite so we need to handle stopping ourselves + var durationToken = new CancellationTokenSource(duration); + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - await _streamHelper.CopyUntilCancelled(response.Content, output, 81920, cancellationToken).ConfigureAwait(false); - } - } + await _streamHelper.CopyUntilCancelled( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), + output, + IODefaults.CopyToBufferSize, + cancellationToken).ConfigureAwait(false); _logger.LogInformation("Recording completed to file {0}", targetFile); } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 09c52d95bb..ca60c33664 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -48,7 +49,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly IServerApplicationHost _appHost; private readonly ILogger _logger; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerConfigurationManager _config; private readonly IJsonSerializer _jsonSerializer; @@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV IMediaSourceManager mediaSourceManager, ILogger logger, IJsonSerializer jsonSerializer, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, @@ -94,7 +95,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _appHost = appHost; _logger = logger; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _config = config; _fileSystem = fileSystem; _libraryManager = libraryManager; @@ -604,11 +605,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return Task.CompletedTask; } - public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -808,11 +804,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return null; } - public IEnumerable GetAllActiveRecordings() - { - return _activeRecordings.Values.Where(i => i.Timer.Status == RecordingStatus.InProgress && !i.CancellationTokenSource.IsCancellationRequested); - } - public ActiveRecordingInfo GetActiveRecordingInfo(string path) { if (string.IsNullOrWhiteSpace(path)) @@ -1015,16 +1006,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new Exception("Tuner not found."); } - private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing) - { - var json = _jsonSerializer.SerializeToString(mediaSource); - mediaSource = _jsonSerializer.DeserializeFromString(json); - - mediaSource.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + "_" + mediaSource.Id; - - return mediaSource; - } - public async Task> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(channelId)) @@ -1654,10 +1635,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http)) { - return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config); + return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer); } - return new DirectRecorder(_logger, _httpClient, _streamHelper); + return new DirectRecorder(_logger, _httpClientFactory, _streamHelper); } private void OnSuccessfulRecording(TimerInfo timer, string path) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 612dc5238b..3e5457dbd4 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -8,12 +8,9 @@ using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; @@ -26,26 +23,24 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly ILogger _logger; private readonly IMediaEncoder _mediaEncoder; private readonly IServerApplicationPaths _appPaths; + private readonly IJsonSerializer _json; + private readonly TaskCompletionSource _taskCompletionSource = new TaskCompletionSource(); + private bool _hasExited; private Stream _logFileStream; private string _targetPath; private Process _process; - private readonly IJsonSerializer _json; - private readonly TaskCompletionSource _taskCompletionSource = new TaskCompletionSource(); - private readonly IServerConfigurationManager _config; public EncodedRecorder( ILogger logger, IMediaEncoder mediaEncoder, IServerApplicationPaths appPaths, - IJsonSerializer json, - IServerConfigurationManager config) + IJsonSerializer json) { _logger = logger; _mediaEncoder = mediaEncoder; _appPaths = appPaths; _json = json; - _config = config; } private static bool CopySubtitles => false; @@ -58,19 +53,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public async Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { // The media source is infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + using var durationToken = new CancellationTokenSource(duration); + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); - await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationToken).ConfigureAwait(false); + await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationTokenSource.Token).ConfigureAwait(false); _logger.LogInformation("Recording completed to file {0}", targetFile); } - private EncodingOptions GetEncodingOptions() - { - return _config.GetConfiguration("encoding"); - } - private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { _targetPath = targetFile; @@ -108,7 +98,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV StartInfo = processStartInfo, EnableRaisingEvents = true }; - _process.Exited += (sender, args) => OnFfMpegProcessExited(_process, inputFile); + _process.Exited += (sender, args) => OnFfMpegProcessExited(_process); _process.Start(); @@ -221,20 +211,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } protected string GetOutputSizeParam() - { - var filters = new List(); - - filters.Add("yadif=0:-1:0"); - - var output = string.Empty; - - if (filters.Count > 0) - { - output += string.Format(CultureInfo.InvariantCulture, " -vf \"{0}\"", string.Join(",", filters.ToArray())); - } - - return output; - } + => "-vf \"yadif=0:-1:0\""; private void Stop() { @@ -291,7 +268,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV /// /// Processes the exited. /// - private void OnFfMpegProcessExited(Process process, string inputFile) + private void OnFfMpegProcessExited(Process process) { using (process) { diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index c4d5cc58aa..f9ae55af85 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -8,6 +8,8 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Mime; +using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; @@ -24,23 +26,23 @@ namespace Emby.Server.Implementations.LiveTv.Listings { public class SchedulesDirect : IListingsProvider { + private const string ApiUrl = "https://json.schedulesdirect.org/20141201"; + private readonly ILogger _logger; private readonly IJsonSerializer _jsonSerializer; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1); private readonly IApplicationHost _appHost; - private const string ApiUrl = "https://json.schedulesdirect.org/20141201"; - public SchedulesDirect( ILogger logger, IJsonSerializer jsonSerializer, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IApplicationHost appHost) { _logger = logger; _jsonSerializer = jsonSerializer; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; } @@ -61,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings while (start <= end) { - dates.Add(start.ToString("yyyy-MM-dd")); + dates.Add(start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); start = start.AddDays(1); } @@ -102,95 +104,78 @@ namespace Emby.Server.Implementations.LiveTv.Listings var requestString = _jsonSerializer.SerializeToString(requestList); _logger.LogDebug("Request string for schedules is: {RequestString}", requestString); - var httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/schedules", - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true, - RequestContent = requestString - }; + using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules"); + options.Content = new StringContent(requestString, Encoding.UTF8, MediaTypeNames.Application.Json); + options.Headers.TryAddWithoutValidation("token", token); + using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false); + await using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync>(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>(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>(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>(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(); + 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(); - 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(); - 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(); + var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase)); + var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase)); - const double DesiredAspect = 2.0 / 3; + const double DesiredAspect = 2.0 / 3; - programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ?? - GetProgramImage(ApiUrl, allImages, true, DesiredAspect); + programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ?? + GetProgramImage(ApiUrl, allImages, true, DesiredAspect); - const double WideAspect = 16.0 / 9; + const double WideAspect = 16.0 / 9; - programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect); + programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect); - // Don't supply the same image twice - if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal)) - { - programEntry.thumbImage = null; - } - - programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect); - - // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? - // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? - // GetProgramImage(ApiUrl, data, "Banner-LO", false) ?? - // GetProgramImage(ApiUrl, data, "Banner-LOT", false); - } + // Don't supply the same image twice + if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal)) + { + programEntry.thumbImage = null; } - programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID])); - } + programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect); - return programsInfo; + // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? + // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? + // GetProgramImage(ApiUrl, data, "Banner-LO", false) ?? + // GetProgramImage(ApiUrl, data, "Banner-LOT", false); + } } + + programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID])); } + + return programsInfo; } private static int GetSizeOrder(ScheduleDirect.ImageData image) @@ -367,13 +352,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings if (!string.IsNullOrWhiteSpace(details.originalAirDate)) { - info.OriginalAirDate = DateTime.Parse(details.originalAirDate); + info.OriginalAirDate = DateTime.Parse(details.originalAirDate, CultureInfo.InvariantCulture); info.ProductionYear = info.OriginalAirDate.Value.Year; } if (details.movie != null) { - if (!string.IsNullOrEmpty(details.movie.year) && int.TryParse(details.movie.year, out int year)) + if (!string.IsNullOrEmpty(details.movie.year) + && int.TryParse(details.movie.year, out int year)) { info.ProductionYear = year; } @@ -482,22 +468,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings imageIdString = imageIdString.TrimEnd(',') + "]"; - var httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/metadata/programs", - UserAgent = UserAgent, - CancellationToken = cancellationToken, - RequestContent = imageIdString, - LogErrorResponseBody = true, - }; + using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs"); + message.Content = new StringContent(imageIdString, Encoding.UTF8, MediaTypeNames.Application.Json); try { - using (var innerResponse2 = await Post(httpOptions, true, info).ConfigureAwait(false)) - { - return await _jsonSerializer.DeserializeFromStreamAsync>( - 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>( + response).ConfigureAwait(false); } catch (Exception ex) { @@ -518,41 +497,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings return lineups; } - var options = new HttpRequestOptions() - { - Url = ApiUrl + "/headends?country=" + country + "&postalcode=" + location, - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true - }; - - options.RequestHeaders["token"] = token; + using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/headends?country=" + country + "&postalcode=" + location); + options.Headers.TryAddWithoutValidation("token", token); try { - using (var httpResponse = await Get(options, false, info).ConfigureAwait(false)) - using (Stream responce = httpResponse.Content) - { - var root = await _jsonSerializer.DeserializeFromStreamAsync>(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>(response).ConfigureAwait(false); + + if (root != null) + { + foreach (ScheduleDirect.Headends headend in root) { - foreach (ScheduleDirect.Headends headend in root) + foreach (ScheduleDirect.Lineup lineup in headend.lineups) { - foreach (ScheduleDirect.Lineup lineup in headend.lineups) + lineups.Add(new NameIdPair { - lineups.Add(new NameIdPair - { - Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name, - Id = lineup.uri.Substring(18) - }); - } + Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name, + Id = lineup.uri.Substring(18) + }); } } - else - { - _logger.LogInformation("No lineups available"); - } + } + else + { + _logger.LogInformation("No lineups available"); } } catch (Exception ex) @@ -587,7 +558,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings return null; } - NameValuePair savedToken = null; + NameValuePair savedToken; if (!_tokens.TryGetValue(username, out savedToken)) { savedToken = new NameValuePair(); @@ -633,16 +604,16 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - private async Task Post(HttpRequestOptions options, + private async Task Send( + HttpRequestMessage options, bool enableRetry, - ListingsProviderInfo providerInfo) + ListingsProviderInfo providerInfo, + CancellationToken cancellationToken, + HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - // Schedules direct requires that the client support compression and will return a 400 response without it - options.DecompressionMethod = CompressionMethods.Deflate; - try { - return await _httpClient.Post(options).ConfigureAwait(false); + return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false); } catch (HttpException ex) { @@ -659,65 +630,28 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - options.RequestHeaders["token"] = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false); - return await Post(options, false, providerInfo).ConfigureAwait(false); + options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false)); + return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false); } - private async Task 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 GetTokenInternal(string username, string password, + private async Task GetTokenInternal( + string username, + string password, CancellationToken cancellationToken) { - var httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/token", - UserAgent = UserAgent, - RequestContent = "{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", - CancellationToken = cancellationToken, - LogErrorResponseBody = true - }; - // _logger.LogInformation("Obtaining token from Schedules Direct from addres: " + httpOptions.Url + " with body " + - // httpOptions.RequestContent); + using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token"); + options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json); - using (var response = await Post(httpOptions, false, null).ConfigureAwait(false)) + using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var root = await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false); + if (root.message == "OK") { - var root = await _jsonSerializer.DeserializeFromStreamAsync(response.Content).ConfigureAwait(false); - if (root.message == "OK") - { - _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token); - return root.token; - } - - throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message); + _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token); + return root.token; } + + throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message); } private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken) @@ -736,20 +670,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings _logger.LogInformation("Adding new LineUp "); - var httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/lineups/" + info.ListingsId, - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true, - BufferContent = false - }; - - httpOptions.RequestHeaders["token"] = token; - - using (await _httpClient.SendAsync(httpOptions, HttpMethod.Put).ConfigureAwait(false)) - { - } + using var options = new HttpRequestMessage(HttpMethod.Put, ApiUrl + "/lineups/" + info.ListingsId); + options.Headers.TryAddWithoutValidation("token", token); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); } private async Task HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken) @@ -768,25 +691,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings _logger.LogInformation("Headends on account "); - var options = new HttpRequestOptions() - { - Url = ApiUrl + "/lineups", - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true - }; - - options.RequestHeaders["token"] = token; + using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups"); + options.Headers.TryAddWithoutValidation("token", token); try { - using (var httpResponse = await Get(options, false, null).ConfigureAwait(false)) - using (var response = httpResponse.Content) - { - var root = await _jsonSerializer.DeserializeFromStreamAsync(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(stream).ConfigureAwait(false); - return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase)); - } + return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase)); } catch (HttpException ex) { @@ -851,55 +766,43 @@ namespace Emby.Server.Implementations.LiveTv.Listings throw new Exception("token required"); } - var httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/lineups/" + listingsId, - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true, - }; - - httpOptions.RequestHeaders["token"] = token; + using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId); + options.Headers.TryAddWithoutValidation("token", token); var list = new List(); - using (var httpResponse = await Get(httpOptions, true, info).ConfigureAwait(false)) - using (var response = httpResponse.Content) - { - var root = await _jsonSerializer.DeserializeFromStreamAsync(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(); + 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(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(); - 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; diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index f33d071747..2d6f453bd4 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -25,20 +25,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings public class XmlTvListingsProvider : IListingsProvider { private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly IZipClient _zipClient; public XmlTvListingsProvider( IServerConfigurationManager config, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, ILogger logger, IFileSystem fileSystem, IZipClient zipClient) { _config = config; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _logger = logger; _fileSystem = fileSystem; _zipClient = zipClient; @@ -78,28 +78,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)); - using (var res = await _httpClient.SendAsync( - new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = path, - DecompressionMethod = CompressionMethods.Gzip, - }, - HttpMethod.Get).ConfigureAwait(false)) - using (var stream = res.Content) - using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew)) + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew)) { - if (res.ContentHeaders.ContentEncoding.Contains("gzip")) - { - using (var gzStream = new GZipStream(stream, CompressionMode.Decompress)) - { - await gzStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); - } - } - else - { - await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); - } + await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); } return UnzipIfNeeded(path, cacheFile); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 2b5f69d41a..28e30fac8b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerApplicationHost _appHost; private readonly ISocketFactory _socketFactory; private readonly INetworkManager _networkManager; @@ -43,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun IServerConfigurationManager config, ILogger logger, IFileSystem fileSystem, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IServerApplicationHost appHost, ISocketFactory socketFactory, INetworkManager networkManager, @@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun IMemoryCache memoryCache) : base(config, logger, fileSystem, memoryCache) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; _socketFactory = socketFactory; _networkManager = networkManager; @@ -71,15 +71,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - var options = new HttpRequestOptions - { - Url = model.LineupURL, - CancellationToken = cancellationToken, - BufferContent = false - }; - - using var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false); - await using var stream = response.Content; + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var lineup = await JsonSerializer.DeserializeAsync>(stream, cancellationToken: cancellationToken) .ConfigureAwait(false) ?? new List(); @@ -133,14 +126,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun try { - using var response = await _httpClient.SendAsync( - new HttpRequestOptions - { - Url = string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), - CancellationToken = cancellationToken, - BufferContent = false - }, HttpMethod.Get).ConfigureAwait(false); - await using var stream = response.Content; + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var discoverResponse = await JsonSerializer.DeserializeAsync(stream, cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -183,48 +172,41 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - using (var response = await _httpClient.SendAsync( - new HttpRequestOptions() - { - Url = string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), - CancellationToken = cancellationToken, - BufferContent = false - }, - HttpMethod.Get).ConfigureAwait(false)) - using (var stream = response.Content) - using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8)) - { - var tuners = new List(); - 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(); + while (!sr.EndOfStream) + { + string line = StripXML(sr.ReadLine()); + if (line.Contains("Channel", StringComparison.Ordinal)) { - string line = StripXML(sr.ReadLine()); - if (line.Contains("Channel", StringComparison.Ordinal)) + LiveTvTunerStatus status; + var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); + var name = line.Substring(0, index - 1); + var currentChannel = line.Substring(index + 7); + if (currentChannel != "none") { - LiveTvTunerStatus status; - var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); - var name = line.Substring(0, index - 1); - var currentChannel = line.Substring(index + 7); - if (currentChannel != "none") - { - status = LiveTvTunerStatus.LiveTv; - } - else - { - status = LiveTvTunerStatus.Available; - } - - tuners.Add(new LiveTvTunerInfo - { - Name = name, - SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, - ProgramName = currentChannel, - Status = status - }); + status = LiveTvTunerStatus.LiveTv; + } + else + { + status = LiveTvTunerStatus.Available; } - } - return tuners; + tuners.Add(new LiveTvTunerInfo + { + Name = name, + SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, + ProgramName = currentChannel, + Status = status + }); + } } + + return tuners; } private static string StripXML(string source) @@ -634,7 +616,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun info, streamId, FileSystem, - _httpClient, + _httpClientFactory, Logger, Config, _appHost, diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 8fc29fb4a2..8107bc427b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; @@ -26,7 +27,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerApplicationHost _appHost; private readonly INetworkManager _networkManager; private readonly IMediaSourceManager _mediaSourceManager; @@ -37,14 +38,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts IMediaSourceManager mediaSourceManager, ILogger logger, IFileSystem fileSystem, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IServerApplicationHost appHost, INetworkManager networkManager, IStreamHelper streamHelper, IMemoryCache memoryCache) : base(config, logger, fileSystem, memoryCache) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; _networkManager = networkManager; _mediaSourceManager = mediaSourceManager; @@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var channelIdPrefix = GetFullChannelIdPrefix(info); - return await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false); + return await new M3uParser(Logger, _httpClientFactory, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false); } public Task> GetTunerInfos(CancellationToken cancellationToken) @@ -116,7 +117,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { - return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config, _appHost, _streamHelper); + return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); } } @@ -125,7 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public async Task Validate(TunerHostInfo info) { - using (var stream = await new M3uParser(Logger, _httpClient, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false)) + using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false)) { } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 875977219a..f066a749e3 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -19,13 +20,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public class M3uParser { private readonly ILogger _logger; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerApplicationHost _appHost; - public M3uParser(ILogger logger, IHttpClient httpClient, IServerApplicationHost appHost) + public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory, IServerApplicationHost appHost) { _logger = logger; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; } @@ -51,13 +52,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - return _httpClient.Get(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - // Some data providers will require a user agent - UserAgent = _appHost.ApplicationUserAgent - }); + return _httpClientFactory.CreateClient(NamedClient.Default) + .GetStreamAsync(url); } return Task.FromResult((Stream)File.OpenRead(url)); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index bc4dcd8946..6c10fca8c6 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public class SharedHttpStream : LiveStream, IDirectStreamProvider { - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerApplicationHost _appHost; public SharedHttpStream( @@ -29,14 +29,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts TunerHostInfo tunerHostInfo, string originalStreamId, IFileSystem fileSystem, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, ILogger logger, IConfigurationManager configurationManager, IServerApplicationHost appHost, IStreamHelper streamHelper) : base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; OriginalStreamId = originalStreamId; EnableStreamSharing = true; @@ -55,25 +55,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var typeName = GetType().Name; Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url); - var httpRequestOptions = new HttpRequestOptions - { - Url = url, - CancellationToken = CancellationToken.None, - BufferContent = false, - DecompressionMethod = CompressionMethods.None - }; - - foreach (var header in mediaSource.RequiredHttpHeaders) - { - httpRequestOptions.RequestHeaders[header.Key] = header.Value; - } - - var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); var extension = "ts"; var requiresRemux = false; - var contentType = response.ContentType ?? string.Empty; + var contentType = response.Content.Headers.ContentType.ToString(); if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1) { requiresRemux = true; @@ -132,24 +121,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } } - private Task StartStreaming(HttpResponseInfo response, TaskCompletionSource openTaskCompletionSource, CancellationToken cancellationToken) + private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource openTaskCompletionSource, CancellationToken cancellationToken) { return Task.Run(async () => { try { Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath); - using (response) - using (var stream = response.Content) - using (var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - await StreamHelper.CopyToAsync( - stream, - fileStream, - IODefaults.CopyToBufferSize, - () => Resolve(openTaskCompletionSource), - cancellationToken).ConfigureAwait(false); - } + using var message = response; + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read); + await StreamHelper.CopyToAsync( + stream, + fileStream, + IODefaults.CopyToBufferSize, + () => Resolve(openTaskCompletionSource), + cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException ex) { diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index d4341f2e8b..a97c2e17ad 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -71,7 +71,7 @@ "ScheduledTaskFailedWithName": "{0} mislykkes", "ScheduledTaskStartedWithName": "{0} startet", "ServerNameNeedsToBeRestarted": "{0} må startes på nytt", - "Shows": "Programmer", + "Shows": "Program", "Songs": "Sanger", "StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.", "SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for {0}", @@ -88,7 +88,7 @@ "UserOnlineFromDevice": "{0} er tilkoblet fra {1}", "UserPasswordChangedWithName": "Passordet for {0} er oppdatert", "UserPolicyUpdatedWithName": "Brukerpolicyen har blitt oppdatert for {0}", - "UserStartedPlayingItemWithValues": "{0} har startet avspilling {1}", + "UserStartedPlayingItemWithValues": "{0} har startet avspilling {1} på {2}", "UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling {1}", "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt", "ValueSpecialEpisodeName": "Spesialepisode - {0}", diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json index 281cadac5b..fb6e81beb3 100644 --- a/Emby.Server.Implementations/Localization/Core/nn.json +++ b/Emby.Server.Implementations/Localization/Core/nn.json @@ -35,7 +35,7 @@ "AuthenticationSucceededWithUserName": "{0} Har logga inn", "Artists": "Artistar", "Application": "Program", - "AppDeviceValues": "App: {0}, Einheit: {1}", + "AppDeviceValues": "App: {0}, Eining: {1}", "Albums": "Album", "NotificationOptionServerRestartRequired": "Tenaren krev omstart", "NotificationOptionPluginUpdateInstalled": "Tilleggsprogram-oppdatering vart installert", @@ -43,7 +43,7 @@ "NotificationOptionPluginInstalled": "Tilleggsprogram installert", "NotificationOptionPluginError": "Tilleggsprogram feila", "NotificationOptionNewLibraryContent": "Nytt innhald er lagt til", - "NotificationOptionInstallationFailed": "Installasjonen feila", + "NotificationOptionInstallationFailed": "Installasjonsfeil", "NotificationOptionCameraImageUploaded": "Kamerabilde vart lasta opp", "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppa", "NotificationOptionAudioPlayback": "Lydavspilling påbyrja", @@ -56,5 +56,62 @@ "MusicVideos": "Musikkvideoar", "Music": "Musikk", "Movies": "Filmar", - "MixedContent": "Blanda innhald" + "MixedContent": "Blanda innhald", + "Sync": "Synkronisera", + "TaskDownloadMissingSubtitlesDescription": "Søk Internettet for manglande undertekstar basert på metadatainnstillingar.", + "TaskDownloadMissingSubtitles": "Last ned manglande undertekstar", + "TaskRefreshChannelsDescription": "Oppdater internettkanalinformasjon.", + "TaskRefreshChannels": "Oppdater kanalar", + "TaskCleanTranscodeDescription": "Slett transkodefiler som er meir enn ein dag gamal.", + "TaskCleanTranscode": "Reins transkodemappe", + "TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringar for programtillegg som er sette opp til å oppdaterast automatisk.", + "TaskUpdatePlugins": "Oppdaterer programtillegg", + "TaskRefreshPeopleDescription": "Oppdaterer metadata for skodespelarar og regissørar i mediebiblioteket ditt.", + "TaskRefreshPeople": "Oppdater personar", + "TaskCleanLogsDescription": "Slett loggfiler som er meir enn {0} dagar gamle.", + "TaskCleanLogs": "Reins loggmappe", + "TaskRefreshLibraryDescription": "Skannar mediebiblioteket ditt for nye filer og oppdaterer metadata.", + "TaskRefreshLibrary": "Skann mediebibliotek", + "TaskRefreshChapterImagesDescription": "Lager miniatyrbilete for videoar som har kapittel.", + "TaskRefreshChapterImages": "Trekk ut kapittelbilete", + "TaskCleanCacheDescription": "Slettar mellomlagra filer som ikkje lengre trengst av systemet.", + "TaskCleanCache": "Rens mappe for hurtiglager", + "TasksChannelsCategory": "Internettkanalar", + "TasksApplicationCategory": "Applikasjon", + "TasksLibraryCategory": "Bibliotek", + "TasksMaintenanceCategory": "Vedlikehald", + "VersionNumber": "Versjon {0}", + "ValueSpecialEpisodeName": "Spesialepisode - {0}", + "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt", + "UserStoppedPlayingItemWithValues": "{0} har fullført avspeling {1} på {2}", + "UserStartedPlayingItemWithValues": "{0} spelar {1} på {2}", + "UserPolicyUpdatedWithName": "Brukarreglar har blitt oppdatert for {0}", + "UserPasswordChangedWithName": "Passordet for {0} er oppdatert", + "UserOnlineFromDevice": "{0} er direktekopla frå {1}", + "UserOfflineFromDevice": "{0} har kopla frå {1}", + "UserLockedOutWithName": "Brukar {0} har blitt utestengd", + "UserDownloadingItemWithValues": "{0} lastar ned {1}", + "UserDeletedWithName": "Brukar {0} er sletta", + "UserCreatedWithName": "Brukar {0} er oppretta", + "User": "Brukar", + "TvShows": "TV-seriar", + "System": "System", + "SubtitleDownloadFailureFromForItem": "Feila å laste ned undertekstar frå {0} for {1}", + "StartupEmbyServerIsLoading": "Jellyfintenaren laster. Prøv igjen om litt.", + "Songs": "Songar", + "Shows": "Program", + "ServerNameNeedsToBeRestarted": "{0} må omstartast", + "ScheduledTaskStartedWithName": "{0} starta", + "ScheduledTaskFailedWithName": "{0} feila", + "ProviderValue": "Leverandør: {0}", + "PluginUpdatedWithName": "{0} blei oppdatert", + "PluginUninstalledWithName": "{0} blei avinstallert", + "PluginInstalledWithName": "{0} blei installert", + "Plugin": "Programtillegg", + "Playlists": "Speleliste", + "Photos": "Foto", + "NotificationOptionVideoPlaybackStopped": "Videoavspeling stoppa", + "NotificationOptionVideoPlayback": "Videoavspeling starta", + "NotificationOptionUserLockedOut": "Brukar er utestengd", + "NotificationOptionTaskFailed": "Planlagt oppgåve feila" } diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json index d6be86da3e..ed6877f7d7 100644 --- a/Emby.Server.Implementations/Localization/Core/ta.json +++ b/Emby.Server.Implementations/Localization/Core/ta.json @@ -18,7 +18,7 @@ "MessageServerConfigurationUpdated": "சேவையக அமைப்புகள் புதுப்பிக்கப்பட்டன", "MessageApplicationUpdatedTo": "ஜெல்லிஃபின் சேவையகம் {0} இற்கு புதுப்பிக்கப்பட்டது", "MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது", - "Inherit": "மரபரிமையாகப் பெறு", + "Inherit": "மரபுரிமையாகப் பெறு", "HeaderRecordingGroups": "பதிவு குழுக்கள்", "HeaderCameraUploads": "புகைப்பட பதிவேற்றங்கள்", "Folders": "கோப்புறைகள்", @@ -31,7 +31,7 @@ "TaskDownloadMissingSubtitles": "விடுபட்டுபோன வசன வரிகளைப் பதிவிறக்கு", "TaskRefreshChannels": "சேனல்களை புதுப்பி", "TaskUpdatePlugins": "உட்செருகிகளை புதுப்பி", - "TaskRefreshLibrary": "மீடியா நூலகத்தை ஆராய்", + "TaskRefreshLibrary": "ஊடக நூலகத்தை ஆராய்", "TasksChannelsCategory": "இணைய சேனல்கள்", "TasksApplicationCategory": "செயலி", "TasksLibraryCategory": "நூலகம்", @@ -46,7 +46,7 @@ "Sync": "ஒத்திசைவு", "StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.", "Songs": "பாடல்கள்", - "Shows": "தொடர்கள்", + "Shows": "நிகழ்ச்சிகள்", "ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்", "ScheduledTaskStartedWithName": "{0} துவங்கியது", "ScheduledTaskFailedWithName": "{0} தோல்வியடைந்தது", @@ -67,20 +67,20 @@ "NotificationOptionAudioPlayback": "ஒலி இசைக்கத் துவங்கியுள்ளது", "NotificationOptionApplicationUpdateInstalled": "செயலி புதுப்பிக்கப்பட்டது", "NotificationOptionApplicationUpdateAvailable": "செயலியினை புதுப்பிக்கலாம்", - "NameSeasonUnknown": "பருவம் அறியப்படாதவை", + "NameSeasonUnknown": "அறியப்படாத பருவம்", "NameSeasonNumber": "பருவம் {0}", "NameInstallFailed": "{0} நிறுவல் தோல்வியடைந்தது", "MusicVideos": "இசைப்படங்கள்", "Music": "இசை", "Movies": "திரைப்படங்கள்", - "Latest": "புதியன", + "Latest": "புதியவை", "LabelRunningTimeValue": "ஓடும் நேரம்: {0}", "LabelIpAddressValue": "ஐபி முகவரி: {0}", "ItemRemovedWithName": "{0} நூலகத்திலிருந்து அகற்றப்பட்டது", "ItemAddedWithName": "{0} நூலகத்தில் சேர்க்கப்பட்டது", - "HeaderNextUp": "அடுத்ததாக", + "HeaderNextUp": "அடுத்தது", "HeaderLiveTV": "நேரடித் தொலைக்காட்சி", - "HeaderFavoriteSongs": "பிடித்த பாட்டுகள்", + "HeaderFavoriteSongs": "பிடித்த பாடல்கள்", "HeaderFavoriteShows": "பிடித்த தொடர்கள்", "HeaderFavoriteEpisodes": "பிடித்த அத்தியாயங்கள்", "HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்", @@ -93,25 +93,25 @@ "Channels": "சேனல்கள்", "Books": "புத்தகங்கள்", "AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது", - "Artists": "கலைஞர்", + "Artists": "கலைஞர்கள்", "Application": "செயலி", "Albums": "ஆல்பங்கள்", "NewVersionIsAvailable": "ஜெல்லிஃபின் சேவையகத்தின் புதிய பதிப்பு பதிவிறக்கத்திற்கு கிடைக்கிறது.", - "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0 புதுப்பிக்கப்பட்டது", + "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0} புதுப்பிக்கப்பட்டது", "TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.", "UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது", - "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0 } இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன", - "TaskDownloadMissingSubtitlesDescription": "மெட்டாடேட்டா உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.", + "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன", + "TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.", "TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.", - "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட செருகுநிரல்களுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.", - "TaskRefreshPeopleDescription": "உங்கள் மீடியா நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மெட்டாடேட்டாவை புதுப்பிக்கும்.", + "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.", + "TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.", "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.", - "TaskCleanLogs": "பதிவு அடைவு சுத்தம் செய்யுங்கள்", - "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் மீடியா நூலகத்தை ஸ்கேன் செய்து மீத்தரவை புதுப்பிக்கும்.", + "TaskCleanLogs": "பதிவு அடைவை சுத்தம் செய்யுங்கள்", + "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் ஊடக நூலகத்தை ஆராய்ந்து மீத்தரவை புதுப்பிக்கும்.", "TaskRefreshChapterImagesDescription": "அத்தியாயங்களைக் கொண்ட வீடியோக்களுக்கான சிறு உருவங்களை உருவாக்குகிறது.", "ValueHasBeenAddedToLibrary": "உங்கள் மீடியா நூலகத்தில் {0} சேர்க்கப்பட்டது", "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்", "HomeVideos": "முகப்பு வீடியோக்கள்", - "UserStoppedPlayingItemWithValues": "{2} இல் {1} முடித்துவிட்டது", + "UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது", "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது" } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index e29fcfb5f1..5adcefc1fe 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -5,10 +5,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks.Tasks { @@ -21,10 +21,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// Gets or sets the application paths. /// /// The application paths. - private IApplicationPaths ApplicationPaths { get; set; } - + private readonly IApplicationPaths _applicationPaths; private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; @@ -37,20 +35,41 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks IFileSystem fileSystem, ILocalizationManager localization) { - ApplicationPaths = appPaths; + _applicationPaths = appPaths; _logger = logger; _fileSystem = fileSystem; _localization = localization; } + /// + public string Name => _localization.GetLocalizedString("TaskCleanCache"); + + /// + public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription"); + + /// + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// + public string Key => "DeleteCacheFiles"; + + /// + public bool IsHidden => false; + + /// + public bool IsEnabled => true; + + /// + public bool IsLogged => true; + /// /// Creates the triggers that define when the task will run. /// /// IEnumerable{BaseTaskTrigger}. public IEnumerable GetDefaultTriggers() { - return new[] { - + return new[] + { // Every so often new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} }; @@ -68,7 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks try { - DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.CachePath, minDateModified, progress); + DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.CachePath, minDateModified, progress); } catch (DirectoryNotFoundException) { @@ -81,7 +100,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks try { - DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, minDateModified, progress); + DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.TempDirectory, minDateModified, progress); } catch (DirectoryNotFoundException) { @@ -91,7 +110,6 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks return Task.CompletedTask; } - /// /// Deletes the cache files from directory with a last write time less than a given date. /// @@ -164,26 +182,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks _logger.LogError(ex, "Error deleting file {path}", path); } } - - /// - public string Name => _localization.GetLocalizedString("TaskCleanCache"); - - /// - public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription"); - - /// - public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); - - /// - public string Key => "DeleteCacheFiles"; - - /// - public bool IsHidden => false; - - /// - public bool IsEnabled => true; - - /// - public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs index 7388086fb0..c5af68bcec 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs @@ -34,6 +34,27 @@ namespace Emby.Server.Implementations.ScheduledTasks _localization = localization; } + /// + public string Name => _localization.GetLocalizedString("TaskUpdatePlugins"); + + /// + public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription"); + + /// + public string Category => _localization.GetLocalizedString("TasksApplicationCategory"); + + /// + public string Key => "PluginUpdates"; + + /// + public bool IsHidden => false; + + /// + public bool IsEnabled => true; + + /// + public bool IsLogged => true; + /// /// Creates the triggers that define when the task will run. /// @@ -98,26 +119,5 @@ namespace Emby.Server.Implementations.ScheduledTasks progress.Report(100); } - - /// - public string Name => _localization.GetLocalizedString("TaskUpdatePlugins"); - - /// - public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription"); - - /// - public string Category => _localization.GetLocalizedString("TasksApplicationCategory"); - - /// - public string Key => "PluginUpdates"; - - /// - public bool IsHidden => false; - - /// - public bool IsEnabled => true; - - /// - public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs index eb628ec5fe..8b67d37d7a 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs @@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks public class DailyTrigger : ITaskTrigger { /// - /// Get the time of day to trigger the task to run. + /// Occurs when [triggered]. + /// + public event EventHandler Triggered; + + /// + /// Gets or sets the time of day to trigger the task to run. /// /// The time of day. public TimeSpan TimeOfDay { get; set; } @@ -69,11 +74,6 @@ namespace Emby.Server.Implementations.ScheduledTasks } } - /// - /// Occurs when [triggered]. - /// - public event EventHandler Triggered; - /// /// Called when [triggered]. /// diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs index 247a6785ad..b04fd7c7e2 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs @@ -11,6 +11,13 @@ namespace Emby.Server.Implementations.ScheduledTasks /// public class IntervalTrigger : ITaskTrigger { + private DateTime _lastStartDate; + + /// + /// Occurs when [triggered]. + /// + public event EventHandler Triggered; + /// /// Gets or sets the interval. /// @@ -28,8 +35,6 @@ namespace Emby.Server.Implementations.ScheduledTasks /// The timer. private Timer Timer { get; set; } - private DateTime _lastStartDate; - /// /// Stars waiting for the trigger action. /// @@ -88,11 +93,6 @@ namespace Emby.Server.Implementations.ScheduledTasks } } - /// - /// Occurs when [triggered]. - /// - public event EventHandler Triggered; - /// /// Called when [triggered]. /// diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs index 96e5d88970..7cd5493da8 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs @@ -12,6 +12,11 @@ namespace Emby.Server.Implementations.ScheduledTasks /// public class StartupTrigger : ITaskTrigger { + /// + /// Occurs when [triggered]. + /// + public event EventHandler Triggered; + public int DelayMs { get; set; } /// @@ -48,20 +53,12 @@ namespace Emby.Server.Implementations.ScheduledTasks { } - /// - /// Occurs when [triggered]. - /// - public event EventHandler Triggered; - /// /// Called when [triggered]. /// private void OnTriggered() { - if (Triggered != null) - { - Triggered(this, EventArgs.Empty); - } + Triggered?.Invoke(this, EventArgs.Empty); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs index 4f1bf5c19a..0c0ebec082 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs @@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks public class WeeklyTrigger : ITaskTrigger { /// - /// Get the time of day to trigger the task to run. + /// Occurs when [triggered]. + /// + public event EventHandler Triggered; + + /// + /// Gets or sets the time of day to trigger the task to run. /// /// The time of day. public TimeSpan TimeOfDay { get; set; } @@ -95,20 +100,12 @@ namespace Emby.Server.Implementations.ScheduledTasks } } - /// - /// Occurs when [triggered]. - /// - public event EventHandler Triggered; - /// /// Called when [triggered]. /// private void OnTriggered() { - if (Triggered != null) - { - Triggered(this, EventArgs.Empty); - } + Triggered?.Invoke(this, EventArgs.Empty); } } } diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 4f54c06dd2..f121a34938 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Updates /// private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IJsonSerializer _jsonSerializer; private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; @@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Updates ILogger logger, IApplicationHost appHost, IApplicationPaths appPaths, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IJsonSerializer jsonSerializer, IServerConfigurationManager config, IFileSystem fileSystem, @@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Updates _logger = logger; _applicationHost = appHost; _appPaths = appPaths; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _jsonSerializer = jsonSerializer; _config = config; _fileSystem = fileSystem; @@ -116,26 +116,18 @@ namespace Emby.Server.Implementations.Updates { try { - using (var response = await _httpClient.SendAsync( - new HttpRequestOptions - { - Url = manifest, - CancellationToken = cancellationToken, - CacheMode = CacheMode.Unconditional, - CacheLength = TimeSpan.FromMinutes(3) - }, - HttpMethod.Get).ConfigureAwait(false)) - using (Stream stream = response.Content) + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(manifest, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + + try { - try - { - return await _jsonSerializer.DeserializeFromStreamAsync>(stream).ConfigureAwait(false); - } - catch (SerializationException ex) - { - _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest); - return Array.Empty(); - } + return await _jsonSerializer.DeserializeFromStreamAsync>(stream).ConfigureAwait(false); + } + catch (SerializationException ex) + { + _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest); + return Array.Empty(); } } catch (UriFormatException ex) @@ -360,42 +352,34 @@ namespace Emby.Server.Implementations.Updates // Always override the passed-in target (which is a file) and figure it out again string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(package.SourceUrl, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + // CA5351: Do Not Use Broken Cryptographic Algorithms #pragma warning disable CA5351 - using (var res = await _httpClient.SendAsync( - new HttpRequestOptions - { - Url = package.SourceUrl, - CancellationToken = cancellationToken, - // We need it to be buffered for setting the position - BufferContent = true - }, - HttpMethod.Get).ConfigureAwait(false)) - using (var stream = res.Content) - using (var md5 = MD5.Create()) - { - cancellationToken.ThrowIfCancellationRequested(); + using var md5 = MD5.Create(); + cancellationToken.ThrowIfCancellationRequested(); - var hash = Hex.Encode(md5.ComputeHash(stream)); - if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase)) - { - _logger.LogError( - "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}", - package.Name, - package.Checksum, - hash); - throw new InvalidDataException("The checksum of the received data doesn't match."); - } - - if (Directory.Exists(targetDir)) - { - Directory.Delete(targetDir, true); - } + var hash = Hex.Encode(md5.ComputeHash(stream)); + if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogError( + "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}", + package.Name, + package.Checksum, + hash); + throw new InvalidDataException("The checksum of the received data doesn't match."); + } - stream.Position = 0; - _zipClient.ExtractAllFromZip(stream, targetDir, true); + if (Directory.Exists(targetDir)) + { + Directory.Delete(targetDir, true); } + stream.Position = 0; + _zipClient.ExtractAllFromZip(stream, targetDir, true); + #pragma warning restore CA5351 } diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs index a34f9eb62f..1c1fc71d79 100644 --- a/Jellyfin.Api/BaseJellyfinApiController.cs +++ b/Jellyfin.Api/BaseJellyfinApiController.cs @@ -1,4 +1,5 @@ using System.Net.Mime; +using MediaBrowser.Common.Json; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api @@ -8,7 +9,10 @@ namespace Jellyfin.Api /// [ApiController] [Route("[controller]")] - [Produces(MediaTypeNames.Application.Json)] + [Produces( + MediaTypeNames.Application.Json, + JsonDefaults.CamelCaseMediaType, + JsonDefaults.PascalCaseMediaType)] public class BaseJellyfinApiController : ControllerBase { } diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index 33abe3ccdc..3f0fc2e913 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Controllers { @@ -26,38 +27,20 @@ namespace Jellyfin.Api.Controllers { private readonly ILogger _logger; private readonly IServerApplicationHost _appHost; - private readonly IConfiguration _appConfig; - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IResourceFileManager _resourceFileManager; /// /// Initializes a new instance of the class. /// /// Instance of interface. /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. public DashboardController( ILogger logger, - IServerApplicationHost appHost, - IConfiguration appConfig, - IResourceFileManager resourceFileManager, - IServerConfigurationManager serverConfigurationManager) + IServerApplicationHost appHost) { _logger = logger; _appHost = appHost; - _appConfig = appConfig; - _resourceFileManager = resourceFileManager; - _serverConfigurationManager = serverConfigurationManager; } - /// - /// Gets the path of the directory containing the static web interface content, or null if the server is not - /// hosting the web client. - /// - private string? WebClientUiPath => GetWebClientUiPath(_appConfig, _serverConfigurationManager); - /// /// Gets the configuration pages. /// @@ -169,87 +152,6 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - /// - /// Gets the robots.txt. - /// - /// Robots.txt returned. - /// The robots.txt. - [HttpGet("robots.txt")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ApiExplorerSettings(IgnoreApi = true)] - public ActionResult GetRobotsTxt() - { - return GetWebClientResource("robots.txt"); - } - - /// - /// Gets a resource from the web client. - /// - /// The resource name. - /// Web client returned. - /// Server does not host a web client. - /// The resource. - [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)); - } - - /// - /// Gets the favicon. - /// - /// Favicon.ico returned. - /// The favicon. - [HttpGet("favicon.ico")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ApiExplorerSettings(IgnoreApi = true)] - public ActionResult GetFavIcon() - { - return GetWebClientResource("favicon.ico"); - } - - /// - /// Gets the path of the directory containing the static web interface content. - /// - /// The app configuration. - /// The server configuration manager. - /// The directory path, or null if the server is not hosting the web client. - 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 GetConfigPages(IPlugin plugin) { return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1)); diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index c547d0cde3..c3b67eec3f 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -153,7 +153,6 @@ namespace Jellyfin.Api.Controllers { var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Parse(key.Substring("landing-".Length)), existingDisplayPreferences.Client); itemPreferences.ViewType = Enum.Parse(displayPreferences.ViewType); - _displayPreferencesManager.SaveChanges(itemPreferences); } var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Empty, existingDisplayPreferences.Client); @@ -167,8 +166,7 @@ namespace Jellyfin.Api.Controllers itemPrefs.ViewType = viewType; } - _displayPreferencesManager.SaveChanges(existingDisplayPreferences); - _displayPreferencesManager.SaveChanges(itemPrefs); + _displayPreferencesManager.SaveChanges(); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index ee87d917ba..9ebd89819b 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Net.Mime; using System.Threading.Tasks; using Emby.Dlna; using Emby.Dlna.Main; @@ -17,8 +18,6 @@ namespace Jellyfin.Api.Controllers [Route("Dlna")] public class DlnaServerController : BaseJellyfinApiController { - private const string XMLContentType = "text/xml; charset=UTF-8"; - private readonly IDlnaManager _dlnaManager; private readonly IContentDirectory _contentDirectory; private readonly IConnectionManager _connectionManager; @@ -44,7 +43,7 @@ namespace Jellyfin.Api.Controllers /// An containing the description xml. [HttpGet("{serverId}/description")] [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")] - [Produces(XMLContentType)] + [Produces(MediaTypeNames.Text.Xml)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetDescriptionXml([FromRoute] string serverId) { @@ -61,8 +60,9 @@ namespace Jellyfin.Api.Controllers /// Dlna content directory returned. /// An containing the dlna content directory xml. [HttpGet("{serverId}/ContentDirectory")] - [HttpGet("{serverId}/ContentDirectory.xml", Name = "GetContentDirectory_2")] - [Produces(XMLContentType)] + [HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")] + [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")] + [Produces(MediaTypeNames.Text.Xml)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetContentDirectory([FromRoute] string serverId) @@ -76,8 +76,9 @@ namespace Jellyfin.Api.Controllers /// Server UUID. /// Dlna media receiver registrar xml. [HttpGet("{serverId}/MediaReceiverRegistrar")] - [HttpGet("{serverId}/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")] - [Produces(XMLContentType)] + [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")] + [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")] + [Produces(MediaTypeNames.Text.Xml)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetMediaReceiverRegistrar([FromRoute] string serverId) @@ -91,8 +92,9 @@ namespace Jellyfin.Api.Controllers /// Server UUID. /// Dlna media receiver registrar xml. [HttpGet("{serverId}/ConnectionManager")] - [HttpGet("{serverId}/ConnectionManager.xml", Name = "GetConnectionManager_2")] - [Produces(XMLContentType)] + [HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")] + [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")] + [Produces(MediaTypeNames.Text.Xml)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetConnectionManager([FromRoute] string serverId) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index b115ac6cd0..0c884d58d5 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1354,15 +1354,20 @@ namespace Jellyfin.Api.Controllers segmentFormat = "mpegts"; } + var maxMuxingQueueSize = encodingOptions.MaxMuxingQueueSize > 128 + ? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture) + : "128"; + return string.Format( CultureInfo.InvariantCulture, - "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size 2048 -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"", + "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -individual_header_trailer 0 -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"", inputModifier, _encodingHelper.GetInputArgument(state, encodingOptions), threads, mapArgs, GetVideoArguments(state, encodingOptions, startNumber), GetAudioArguments(state, encodingOptions), + maxMuxingQueueSize, state.SegmentLength.ToString(CultureInfo.InvariantCulture), segmentFormat, startNumberParam, diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 3ccfc826db..eace0f911c 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -16,6 +16,7 @@ using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Data.Enums; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -1069,7 +1070,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetSchedulesDirectCountries() { - var client = _httpClientFactory.CreateClient(); + var client = _httpClientFactory.CreateClient(NamedClient.Default); // https://json.schedulesdirect.org/20141201/available/countries // Can't dispose the response as it's required up the call chain. var response = await client.GetAsync("https://json.schedulesdirect.org/20141201/available/countries") diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 30a4f73fc0..81aefd15cc 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -244,7 +245,7 @@ namespace Jellyfin.Api.Controllers /// Task. private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath) { - var httpClient = _httpClientFactory.CreateClient(); + var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); using var response = await httpClient.GetAsync(url).ConfigureAwait(false); var ext = response.Content.Headers.ContentType.MediaType.Split('/').Last(); var fullCachePath = GetFullCachePath(urlHash + "." + ext); diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index f42810c945..83b03f965d 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -11,6 +11,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; @@ -233,7 +234,7 @@ namespace Jellyfin.Api.Controllers .First(); } - var list = primaryVersion.LinkedAlternateVersions.ToList(); + var alternateVersionsOfPrimary = primaryVersion.LinkedAlternateVersions.ToList(); foreach (var item in items.Where(i => i.Id != primaryVersion.Id)) { @@ -241,17 +242,20 @@ namespace Jellyfin.Api.Controllers await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - list.Add(new LinkedChild + if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase))) { - Path = item.Path, - ItemId = item.Id - }); + alternateVersionsOfPrimary.Add(new LinkedChild + { + Path = item.Path, + ItemId = item.Id + }); + } foreach (var linkedItem in item.LinkedAlternateVersions) { - if (!list.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase))) + if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase))) { - list.Add(linkedItem); + alternateVersionsOfPrimary.Add(linkedItem); } } @@ -262,7 +266,7 @@ namespace Jellyfin.Api.Controllers } } - primaryVersion.LinkedAlternateVersions = list.ToArray(); + primaryVersion.LinkedAlternateVersions = alternateVersionsOfPrimary.ToArray(); await primaryVersion.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -470,7 +474,7 @@ namespace Jellyfin.Api.Controllers { StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); - var httpClient = _httpClientFactory.CreateClient(); + var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, HttpContext).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs index eae2be05ba..a3f2d88ce5 100644 --- a/Jellyfin.Api/Helpers/AudioHelper.cs +++ b/Jellyfin.Api/Helpers/AudioHelper.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; @@ -138,7 +139,7 @@ namespace Jellyfin.Api.Helpers { StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); - var httpClient = _httpClientFactory.CreateClient(); + var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index deb54dbe61..884bfbe447 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -72,7 +72,7 @@ namespace Jellyfin.Api.Helpers return new NoContentResult(); } - return new PhysicalFileResult(path, contentType); + return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true }; } /// diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 24bc07b666..ca0542b036 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -14,9 +14,9 @@ - + - + diff --git a/Jellyfin.Api/MvcRoutePrefix.cs b/Jellyfin.Api/MvcRoutePrefix.cs deleted file mode 100644 index e009730947..0000000000 --- a/Jellyfin.Api/MvcRoutePrefix.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ApplicationModels; - -namespace Jellyfin.Api -{ - /// - /// Route prefixing for ASP.NET MVC. - /// - public static class MvcRoutePrefix - { - /// - /// Adds route prefixes to the MVC conventions. - /// - /// The MVC options. - /// The list of prefixes. - 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 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(); - 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)); - } - } - } - } -} diff --git a/Jellyfin.Data/Entities/ActivityLog.cs b/Jellyfin.Data/Entities/ActivityLog.cs index 3858916d15..620e828306 100644 --- a/Jellyfin.Data/Entities/ActivityLog.cs +++ b/Jellyfin.Data/Entities/ActivityLog.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -11,7 +9,7 @@ namespace Jellyfin.Data.Entities /// /// An entity referencing an activity log entry. /// - public partial class ActivityLog : IHasConcurrencyToken + public class ActivityLog : IHasConcurrencyToken { /// /// Initializes a new instance of the class. @@ -32,13 +30,11 @@ namespace Jellyfin.Data.Entities throw new ArgumentNullException(nameof(type)); } - this.Name = name; - this.Type = type; - this.UserId = userId; - this.DateCreated = DateTime.UtcNow; - this.LogSeverity = LogLevel.Trace; - - Init(); + Name = name; + Type = type; + UserId = userId; + DateCreated = DateTime.UtcNow; + LogSeverity = LogLevel.Trace; } /// @@ -47,38 +43,21 @@ namespace Jellyfin.Data.Entities /// protected ActivityLog() { - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// The name. - /// The type. - /// The user's id. - /// The new instance. - public static ActivityLog Create(string name, string type, Guid userId) - { - return new ActivityLog(name, type, userId); } - /************************************************************************* - * Properties - *************************************************************************/ - /// /// Gets or sets the identity of this instance. /// This is the key in the backing database. /// - [Key] - [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; protected set; } /// /// Gets or sets the name. - /// Required, Max length = 512. /// + /// + /// Required, Max length = 512. + /// [Required] [MaxLength(512)] [StringLength(512)] @@ -86,24 +65,30 @@ namespace Jellyfin.Data.Entities /// /// Gets or sets the overview. - /// Max length = 512. /// + /// + /// Max length = 512. + /// [MaxLength(512)] [StringLength(512)] public string Overview { get; set; } /// /// Gets or sets the short overview. - /// Max length = 512. /// + /// + /// Max length = 512. + /// [MaxLength(512)] [StringLength(512)] public string ShortOverview { get; set; } /// /// Gets or sets the type. - /// Required, Max length = 256. /// + /// + /// Required, Max length = 256. + /// [Required] [MaxLength(256)] [StringLength(256)] @@ -111,43 +96,42 @@ namespace Jellyfin.Data.Entities /// /// Gets or sets the user id. - /// Required. /// - [Required] + /// + /// Required. + /// public Guid UserId { get; set; } /// /// Gets or sets the item id. - /// Max length = 256. /// + /// + /// Max length = 256. + /// [MaxLength(256)] [StringLength(256)] public string ItemId { get; set; } /// /// Gets or sets the date created. This should be in UTC. - /// Required. /// - [Required] + /// + /// Required. + /// public DateTime DateCreated { get; set; } /// /// Gets or sets the log severity. Default is . - /// Required. /// - [Required] + /// + /// Required. + /// public LogLevel LogSeverity { get; set; } - /// - /// Gets or sets the row version. - /// Required, ConcurrencyToken. - /// + /// [ConcurrencyCheck] - [Required] public uint RowVersion { get; set; } - partial void Init(); - /// public void OnSavingChanges() { diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Data/Entities/DisplayPreferences.cs index cda83f6bb7..701e4df004 100644 --- a/Jellyfin.Data/Entities/DisplayPreferences.cs +++ b/Jellyfin.Data/Entities/DisplayPreferences.cs @@ -1,4 +1,6 @@ -using System; +#pragma warning disable CA2227 + +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; diff --git a/Jellyfin.Data/Entities/Group.cs b/Jellyfin.Data/Entities/Group.cs index ca12ba4213..878811e59c 100644 --- a/Jellyfin.Data/Entities/Group.cs +++ b/Jellyfin.Data/Entities/Group.cs @@ -1,9 +1,8 @@ -#pragma warning disable CS1591 +#pragma warning disable CA2227 using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Jellyfin.Data.Enums; using Jellyfin.Data.Interfaces; @@ -13,11 +12,10 @@ namespace Jellyfin.Data.Entities /// /// An entity representing a group. /// - public partial class Group : IHasPermissions, IHasConcurrencyToken + public class Group : IHasPermissions, IHasConcurrencyToken { /// /// Initializes a new instance of the class. - /// Public constructor with required data. /// /// The name of the group. public Group(string name) @@ -31,33 +29,25 @@ namespace Jellyfin.Data.Entities Id = Guid.NewGuid(); Permissions = new HashSet(); - ProviderMappings = new HashSet(); Preferences = new HashSet(); - - Init(); } /// /// Initializes a new instance of the class. - /// Default constructor. Protected due to required properties, but present because EF needs it. /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// protected Group() { - Init(); } - /************************************************************************* - * Properties - *************************************************************************/ - /// /// Gets or sets the id of this group. /// /// /// Identity, Indexed, Required. /// - [Key] - [Required] public Guid Id { get; protected set; } /// @@ -71,42 +61,19 @@ namespace Jellyfin.Data.Entities [StringLength(255)] public string Name { get; set; } - /// - /// Gets or sets the row version. - /// - /// - /// Required, Concurrency Token. - /// + /// [ConcurrencyCheck] - [Required] public uint RowVersion { get; set; } - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - [ForeignKey("Permission_GroupPermissions_Id")] + /// + /// Gets or sets a collection containing the group's permissions. + /// public virtual ICollection Permissions { get; protected set; } - [ForeignKey("ProviderMapping_ProviderMappings_Id")] - public virtual ICollection ProviderMappings { get; protected set; } - - [ForeignKey("Preference_Preferences_Id")] - public virtual ICollection Preferences { get; protected set; } - /// - /// Static create function (for use in LINQ queries, etc.) + /// Gets or sets a collection containing the group's preferences. /// - /// The name of this group. - public static Group Create(string name) - { - return new Group(name); - } + public virtual ICollection Preferences { get; protected set; } /// public bool HasPermission(PermissionKind kind) @@ -120,6 +87,10 @@ namespace Jellyfin.Data.Entities Permissions.First(p => p.Kind == kind).Value = value; } - partial void Init(); + /// + public void OnSavingChanges() + { + RowVersion++; + } } } diff --git a/Jellyfin.Data/Entities/ImageInfo.cs b/Jellyfin.Data/Entities/ImageInfo.cs index cf0895ad43..ab8452e62c 100644 --- a/Jellyfin.Data/Entities/ImageInfo.cs +++ b/Jellyfin.Data/Entities/ImageInfo.cs @@ -1,32 +1,65 @@ -#pragma warning disable CS1591 - -using System; +using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities { + /// + /// An entity representing an image. + /// public class ImageInfo { + /// + /// Initializes a new instance of the class. + /// + /// The path. public ImageInfo(string path) { Path = path; LastModified = DateTime.UtcNow; } - [Key] - [Required] + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected ImageInfo() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; protected set; } + /// + /// Gets or sets the user id. + /// public Guid? UserId { get; protected set; } + /// + /// Gets or sets the path of the image. + /// + /// + /// Required. + /// [Required] [MaxLength(512)] [StringLength(512)] public string Path { get; set; } - [Required] + /// + /// Gets or sets the date last modified. + /// + /// + /// Required. + /// public DateTime LastModified { get; set; } } } diff --git a/Jellyfin.Data/Entities/ItemDisplayPreferences.cs b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs index 023cdc7400..d81e4a31c4 100644 --- a/Jellyfin.Data/Entities/ItemDisplayPreferences.cs +++ b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs @@ -1,12 +1,13 @@ -#pragma warning disable CS1591 - -using System; +using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Enums; namespace Jellyfin.Data.Entities { + /// + /// An entity that represents a user's display preferences for a specific item. + /// public class ItemDisplayPreferences { /// diff --git a/Jellyfin.Data/Entities/Libraries/Artwork.cs b/Jellyfin.Data/Entities/Libraries/Artwork.cs index 7be22af254..06cd333301 100644 --- a/Jellyfin.Data/Entities/Libraries/Artwork.cs +++ b/Jellyfin.Data/Entities/Libraries/Artwork.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; diff --git a/Jellyfin.Data/Entities/Libraries/Book.cs b/Jellyfin.Data/Entities/Libraries/Book.cs index 8337788ddd..2e63f75bd9 100644 --- a/Jellyfin.Data/Entities/Libraries/Book.cs +++ b/Jellyfin.Data/Entities/Libraries/Book.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System.Collections.Generic; using Jellyfin.Data.Interfaces; diff --git a/Jellyfin.Data/Entities/Libraries/BookMetadata.cs b/Jellyfin.Data/Entities/Libraries/BookMetadata.cs index bd716712b1..4a3d290f01 100644 --- a/Jellyfin.Data/Entities/Libraries/BookMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/BookMetadata.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; @@ -8,7 +10,7 @@ namespace Jellyfin.Data.Entities.Libraries /// /// An entity containing metadata for a book. /// - public class BookMetadata : Metadata, IHasCompanies + public class BookMetadata : ItemMetadata, IHasCompanies { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Data/Entities/Libraries/Chapter.cs b/Jellyfin.Data/Entities/Libraries/Chapter.cs index d9293c3cc4..f503de3794 100644 --- a/Jellyfin.Data/Entities/Libraries/Chapter.cs +++ b/Jellyfin.Data/Entities/Libraries/Chapter.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; diff --git a/Jellyfin.Data/Entities/Libraries/Collection.cs b/Jellyfin.Data/Entities/Libraries/Collection.cs index 2e1bbcfb60..39eded752d 100644 --- a/Jellyfin.Data/Entities/Libraries/Collection.cs +++ b/Jellyfin.Data/Entities/Libraries/Collection.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; diff --git a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs index c9306f630a..4467c9bbdc 100644 --- a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs +++ b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs @@ -73,7 +73,7 @@ namespace Jellyfin.Data.Entities.Libraries /// Gets or sets the next item in the collection. /// /// - /// 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. /// public virtual CollectionItem Next { get; set; } @@ -81,7 +81,7 @@ namespace Jellyfin.Data.Entities.Libraries /// Gets or sets the previous item in the collection. /// /// - /// 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. /// public virtual CollectionItem Previous { get; set; } diff --git a/Jellyfin.Data/Entities/Libraries/Company.cs b/Jellyfin.Data/Entities/Libraries/Company.cs index 02da26bc22..3b6ed3309a 100644 --- a/Jellyfin.Data/Entities/Libraries/Company.cs +++ b/Jellyfin.Data/Entities/Libraries/Company.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; diff --git a/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs b/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs index 60cc96a340..8aa0486afa 100644 --- a/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs @@ -6,7 +6,7 @@ namespace Jellyfin.Data.Entities.Libraries /// /// An entity holding metadata for a . /// - public class CompanyMetadata : Metadata + public class CompanyMetadata : ItemMetadata { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Data/Entities/Libraries/CustomItem.cs b/Jellyfin.Data/Entities/Libraries/CustomItem.cs index 6a4f0a5378..115489c786 100644 --- a/Jellyfin.Data/Entities/Libraries/CustomItem.cs +++ b/Jellyfin.Data/Entities/Libraries/CustomItem.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System.Collections.Generic; using Jellyfin.Data.Interfaces; diff --git a/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs b/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs index bc18355281..f0daedfbe8 100644 --- a/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs @@ -5,7 +5,7 @@ namespace Jellyfin.Data.Entities.Libraries /// /// An entity containing metadata for a custom item. /// - public class CustomItemMetadata : Metadata + public class CustomItemMetadata : ItemMetadata { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Data/Entities/Libraries/Episode.cs b/Jellyfin.Data/Entities/Libraries/Episode.cs index 430a11e3c8..0bdc2d7642 100644 --- a/Jellyfin.Data/Entities/Libraries/Episode.cs +++ b/Jellyfin.Data/Entities/Libraries/Episode.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System; using System.Collections.Generic; using Jellyfin.Data.Interfaces; diff --git a/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs b/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs index 348100cb4c..7efb840f0b 100644 --- a/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs @@ -6,7 +6,7 @@ namespace Jellyfin.Data.Entities.Libraries /// /// An entity containing metadata for an . /// - public class EpisodeMetadata : Metadata + public class EpisodeMetadata : ItemMetadata { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Data/Entities/Libraries/Genre.cs b/Jellyfin.Data/Entities/Libraries/Genre.cs index aeedd7bfde..2a2dbd1a5d 100644 --- a/Jellyfin.Data/Entities/Libraries/Genre.cs +++ b/Jellyfin.Data/Entities/Libraries/Genre.cs @@ -14,8 +14,8 @@ namespace Jellyfin.Data.Entities.Libraries /// Initializes a new instance of the class. /// /// The name. - /// The metadata. - public Genre(string name, Metadata metadata) + /// The metadata. + public Genre(string name, ItemMetadata itemMetadata) { if (string.IsNullOrEmpty(name)) { @@ -24,12 +24,12 @@ namespace Jellyfin.Data.Entities.Libraries Name = name; - if (metadata == null) + if (itemMetadata == null) { - throw new ArgumentNullException(nameof(metadata)); + throw new ArgumentNullException(nameof(itemMetadata)); } - metadata.Genres.Add(this); + itemMetadata.Genres.Add(this); } /// diff --git a/Jellyfin.Data/Entities/Libraries/Metadata.cs b/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs similarity index 92% rename from Jellyfin.Data/Entities/Libraries/Metadata.cs rename to Jellyfin.Data/Entities/Libraries/ItemMetadata.cs index 877bb5fbdc..1d2dc0f669 100644 --- a/Jellyfin.Data/Entities/Libraries/Metadata.cs +++ b/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -9,14 +11,14 @@ namespace Jellyfin.Data.Entities.Libraries /// /// An abstract class that holds metadata. /// - public abstract class Metadata : IHasArtwork, IHasConcurrencyToken + public abstract class ItemMetadata : IHasArtwork, IHasConcurrencyToken { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The title or name of the object. /// ISO-639-3 3-character language codes. - protected Metadata(string title, string language) + protected ItemMetadata(string title, string language) { if (string.IsNullOrEmpty(title)) { @@ -41,12 +43,12 @@ namespace Jellyfin.Data.Entities.Libraries } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// Default constructor. Protected due to being abstract. /// - protected Metadata() + protected ItemMetadata() { } diff --git a/Jellyfin.Data/Entities/Libraries/MediaFile.cs b/Jellyfin.Data/Entities/Libraries/MediaFile.cs index 8bc649c98f..9924d5728d 100644 --- a/Jellyfin.Data/Entities/Libraries/MediaFile.cs +++ b/Jellyfin.Data/Entities/Libraries/MediaFile.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; diff --git a/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs b/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs index 6e6de598e8..fcfb35bfac 100644 --- a/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs +++ b/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs @@ -14,8 +14,8 @@ namespace Jellyfin.Data.Entities.Libraries /// Initializes a new instance of the class. /// /// The provider id. - /// The metadata entity. - public MetadataProviderId(string providerId, Metadata metadata) + /// The metadata entity. + public MetadataProviderId(string providerId, ItemMetadata itemMetadata) { if (string.IsNullOrEmpty(providerId)) { @@ -24,12 +24,12 @@ namespace Jellyfin.Data.Entities.Libraries ProviderId = providerId; - if (metadata == null) + if (itemMetadata == null) { - throw new ArgumentNullException(nameof(metadata)); + throw new ArgumentNullException(nameof(itemMetadata)); } - metadata.Sources.Add(this); + itemMetadata.Sources.Add(this); } /// diff --git a/Jellyfin.Data/Entities/Libraries/Movie.cs b/Jellyfin.Data/Entities/Libraries/Movie.cs index 0a8cc83dda..08db904fa8 100644 --- a/Jellyfin.Data/Entities/Libraries/Movie.cs +++ b/Jellyfin.Data/Entities/Libraries/Movie.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System.Collections.Generic; using Jellyfin.Data.Interfaces; diff --git a/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs b/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs index 31102bf130..aa1501a5cc 100644 --- a/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -8,7 +10,7 @@ namespace Jellyfin.Data.Entities.Libraries /// /// An entity holding the metadata for a movie. /// - public class MovieMetadata : Metadata, IHasCompanies + public class MovieMetadata : ItemMetadata, IHasCompanies { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs b/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs index 2ed1f78c56..06aff6f457 100644 --- a/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs +++ b/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System.Collections.Generic; namespace Jellyfin.Data.Entities.Libraries diff --git a/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs b/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs index cc5919bfe3..05c0b0374b 100644 --- a/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -6,7 +8,7 @@ namespace Jellyfin.Data.Entities.Libraries /// /// An entity holding the metadata for a music album. /// - public class MusicAlbumMetadata : Metadata + public class MusicAlbumMetadata : ItemMetadata { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Data/Entities/Libraries/Person.cs b/Jellyfin.Data/Entities/Libraries/Person.cs index 8beb3dd084..af4c87b73c 100644 --- a/Jellyfin.Data/Entities/Libraries/Person.cs +++ b/Jellyfin.Data/Entities/Libraries/Person.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; diff --git a/Jellyfin.Data/Entities/Libraries/PersonRole.cs b/Jellyfin.Data/Entities/Libraries/PersonRole.cs index 5290228d6e..cd38ee83d0 100644 --- a/Jellyfin.Data/Entities/Libraries/PersonRole.cs +++ b/Jellyfin.Data/Entities/Libraries/PersonRole.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -16,17 +18,17 @@ namespace Jellyfin.Data.Entities.Libraries /// Initializes a new instance of the class. /// /// The role type. - /// The metadata. - public PersonRole(PersonRoleType type, Metadata metadata) + /// The metadata. + public PersonRole(PersonRoleType type, ItemMetadata itemMetadata) { Type = type; - if (metadata == null) + if (itemMetadata == null) { - throw new ArgumentNullException(nameof(metadata)); + throw new ArgumentNullException(nameof(itemMetadata)); } - metadata.PersonRoles.Add(this); + itemMetadata.PersonRoles.Add(this); Sources = new HashSet(); } diff --git a/Jellyfin.Data/Entities/Libraries/Photo.cs b/Jellyfin.Data/Entities/Libraries/Photo.cs index 44338a4cea..25562ec96f 100644 --- a/Jellyfin.Data/Entities/Libraries/Photo.cs +++ b/Jellyfin.Data/Entities/Libraries/Photo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System.Collections.Generic; using Jellyfin.Data.Interfaces; diff --git a/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs b/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs index 1ef9dd5f9c..ffc790b574 100644 --- a/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs @@ -5,7 +5,7 @@ namespace Jellyfin.Data.Entities.Libraries /// /// An entity that holds metadata for a photo. /// - public class PhotoMetadata : Metadata + public class PhotoMetadata : ItemMetadata { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Data/Entities/Libraries/Rating.cs b/Jellyfin.Data/Entities/Libraries/Rating.cs index ba054a39e0..98226cd802 100644 --- a/Jellyfin.Data/Entities/Libraries/Rating.cs +++ b/Jellyfin.Data/Entities/Libraries/Rating.cs @@ -14,17 +14,17 @@ namespace Jellyfin.Data.Entities.Libraries /// Initializes a new instance of the class. /// /// The value. - /// The metadata. - public Rating(double value, Metadata metadata) + /// The metadata. + public Rating(double value, ItemMetadata itemMetadata) { Value = value; - if (metadata == null) + if (itemMetadata == null) { - throw new ArgumentNullException(nameof(metadata)); + throw new ArgumentNullException(nameof(itemMetadata)); } - metadata.Ratings.Add(this); + itemMetadata.Ratings.Add(this); } /// diff --git a/Jellyfin.Data/Entities/Libraries/Release.cs b/Jellyfin.Data/Entities/Libraries/Release.cs index 43c7080d79..b633e08fb3 100644 --- a/Jellyfin.Data/Entities/Libraries/Release.cs +++ b/Jellyfin.Data/Entities/Libraries/Release.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; diff --git a/Jellyfin.Data/Entities/Libraries/Season.cs b/Jellyfin.Data/Entities/Libraries/Season.cs index eef788bad2..eb6674dbc3 100644 --- a/Jellyfin.Data/Entities/Libraries/Season.cs +++ b/Jellyfin.Data/Entities/Libraries/Season.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System; using System.Collections.Generic; diff --git a/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs b/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs index eedeb089e8..7ce79756b2 100644 --- a/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs @@ -6,7 +6,7 @@ namespace Jellyfin.Data.Entities.Libraries /// /// An entity that holds metadata for seasons. /// - public class SeasonMetadata : Metadata + public class SeasonMetadata : ItemMetadata { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Data/Entities/Libraries/Series.cs b/Jellyfin.Data/Entities/Libraries/Series.cs index e959c1fe00..8c8317d14b 100644 --- a/Jellyfin.Data/Entities/Libraries/Series.cs +++ b/Jellyfin.Data/Entities/Libraries/Series.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System; using System.Collections.Generic; diff --git a/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs b/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs index 898f3006dc..877dbfc69c 100644 --- a/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -9,7 +11,7 @@ namespace Jellyfin.Data.Entities.Libraries /// /// An entity representing series metadata. /// - public class SeriesMetadata : Metadata, IHasCompanies + public class SeriesMetadata : ItemMetadata, IHasCompanies { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Data/Entities/Libraries/Track.cs b/Jellyfin.Data/Entities/Libraries/Track.cs index 09ce82a9b4..782bfb5ce7 100644 --- a/Jellyfin.Data/Entities/Libraries/Track.cs +++ b/Jellyfin.Data/Entities/Libraries/Track.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA2227 + using System; using System.Collections.Generic; using Jellyfin.Data.Interfaces; diff --git a/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs b/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs index 048068a1a3..321f93bf2e 100644 --- a/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs @@ -5,7 +5,7 @@ namespace Jellyfin.Data.Entities.Libraries /// /// An entity holding metadata for a track. /// - public class TrackMetadata : Metadata + public class TrackMetadata : ItemMetadata { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Data/Entities/Permission.cs b/Jellyfin.Data/Entities/Permission.cs index c0f67f8363..d92e5d9d25 100644 --- a/Jellyfin.Data/Entities/Permission.cs +++ b/Jellyfin.Data/Entities/Permission.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Enums; @@ -10,7 +8,7 @@ namespace Jellyfin.Data.Entities /// /// An entity representing whether the associated user has a specific permission. /// - public partial class Permission : IHasConcurrencyToken + public class Permission : IHasConcurrencyToken { /// /// Initializes a new instance of the class. @@ -22,8 +20,6 @@ namespace Jellyfin.Data.Entities { Kind = kind; Value = value; - - Init(); } /// @@ -32,21 +28,14 @@ namespace Jellyfin.Data.Entities /// protected Permission() { - Init(); } - /************************************************************************* - * Properties - *************************************************************************/ - /// /// Gets or sets the id of this permission. /// /// /// Identity, Indexed, Required. /// - [Key] - [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; protected set; } @@ -56,7 +45,6 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public PermissionKind Kind { get; protected set; } /// @@ -65,36 +53,16 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public bool Value { get; set; } - /// - /// Gets or sets the row version. - /// - /// - /// Required, ConcurrencyToken. - /// + /// [ConcurrencyCheck] - [Required] public uint RowVersion { get; set; } - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// The permission kind. - /// The value of this permission. - /// The newly created instance. - public static Permission Create(PermissionKind kind, bool value) - { - return new Permission(kind, value); - } - /// public void OnSavingChanges() { RowVersion++; } - - partial void Init(); } } diff --git a/Jellyfin.Data/Entities/Preference.cs b/Jellyfin.Data/Entities/Preference.cs index 1797f0a401..4efddf2a41 100644 --- a/Jellyfin.Data/Entities/Preference.cs +++ b/Jellyfin.Data/Entities/Preference.cs @@ -31,18 +31,12 @@ namespace Jellyfin.Data.Entities { } - /************************************************************************* - * Properties - *************************************************************************/ - /// /// Gets or sets the id of this preference. /// /// /// Identity, Indexed, Required. /// - [Key] - [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; protected set; } @@ -52,7 +46,6 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public PreferenceKind Kind { get; protected set; } /// @@ -66,27 +59,10 @@ namespace Jellyfin.Data.Entities [StringLength(65535)] public string Value { get; set; } - /// - /// Gets or sets the row version. - /// - /// - /// Required, ConcurrencyToken. - /// + /// [ConcurrencyCheck] - [Required] public uint RowVersion { get; set; } - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// The preference kind. - /// The value. - /// The new instance. - public static Preference Create(PreferenceKind kind, string value) - { - return new Preference(kind, value); - } - /// public void OnSavingChanges() { diff --git a/Jellyfin.Data/Entities/ProviderMapping.cs b/Jellyfin.Data/Entities/ProviderMapping.cs deleted file mode 100644 index 44ebfba76d..0000000000 --- a/Jellyfin.Data/Entities/ProviderMapping.cs +++ /dev/null @@ -1,129 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class ProviderMapping - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected ProviderMapping() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static ProviderMapping CreateProviderMappingUnsafe() - { - return new ProviderMapping(); - } - - /// - /// Public constructor with required data. - /// - /// - /// - /// - /// - /// - public ProviderMapping(string providername, string providersecrets, string providerdata, User _user0, Group _group1) - { - if (string.IsNullOrEmpty(providername)) - { - throw new ArgumentNullException(nameof(providername)); - } - - this.ProviderName = providername; - - if (string.IsNullOrEmpty(providersecrets)) - { - throw new ArgumentNullException(nameof(providersecrets)); - } - - this.ProviderSecrets = providersecrets; - - if (string.IsNullOrEmpty(providerdata)) - { - throw new ArgumentNullException(nameof(providerdata)); - } - - this.ProviderData = providerdata; - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// - /// - /// - /// - /// - public static ProviderMapping Create(string providername, string providersecrets, string providerdata, User _user0, Group _group1) - { - return new ProviderMapping(providername, providersecrets, providerdata, _user0, _group1); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } - - /// - /// Required, Max length = 255 - /// - [Required] - [MaxLength(255)] - [StringLength(255)] - public string ProviderName { get; set; } - - /// - /// Required, Max length = 65535 - /// - [Required] - [MaxLength(65535)] - [StringLength(65535)] - public string ProviderSecrets { get; set; } - - /// - /// Required, Max length = 65535 - /// - [Required] - [MaxLength(65535)] - [StringLength(65535)] - public string ProviderData { get; set; } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 7ea1f44986..f7ab57a1b1 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CA2227 using System; using System.Collections.Generic; @@ -15,7 +15,7 @@ namespace Jellyfin.Data.Entities /// /// An entity representing a user. /// - public partial class User : IHasPermissions, IHasConcurrencyToken + public class User : IHasPermissions, IHasConcurrencyToken { /// /// The values being delimited here are Guids, so commas work as they do not appear in Guids. @@ -75,7 +75,6 @@ namespace Jellyfin.Data.Entities AddDefaultPermissions(); AddDefaultPreferences(); - Init(); } /// @@ -84,21 +83,14 @@ namespace Jellyfin.Data.Entities /// protected User() { - Init(); } - /************************************************************************* - * Properties - *************************************************************************/ - /// /// Gets or sets the Id of the user. /// /// /// Identity, Indexed, Required. /// - [Key] - [Required] [JsonIgnore] public Guid Id { get; set; } @@ -139,7 +131,6 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public bool MustUpdatePassword { get; set; } /// @@ -180,7 +171,6 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public int InvalidLoginAttemptCount { get; set; } /// @@ -204,7 +194,6 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public SubtitlePlaybackMode SubtitleMode { get; set; } /// @@ -213,7 +202,6 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public bool PlayDefaultAudioTrack { get; set; } /// @@ -232,7 +220,6 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public bool DisplayMissingEpisodes { get; set; } /// @@ -241,7 +228,6 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public bool DisplayCollectionsView { get; set; } /// @@ -250,7 +236,6 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public bool EnableLocalPassword { get; set; } /// @@ -259,7 +244,6 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public bool HidePlayedInLatest { get; set; } /// @@ -268,7 +252,6 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public bool RememberAudioSelections { get; set; } /// @@ -277,7 +260,6 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public bool RememberSubtitleSelections { get; set; } /// @@ -286,7 +268,6 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public bool EnableNextEpisodeAutoPlay { get; set; } /// @@ -295,7 +276,6 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public bool EnableAutoLogin { get; set; } /// @@ -304,7 +284,6 @@ namespace Jellyfin.Data.Entities /// /// Required. /// - [Required] public bool EnableUserPreferenceAccess { get; set; } /// @@ -322,7 +301,6 @@ namespace Jellyfin.Data.Entities /// This is a temporary stopgap for until the library db is migrated. /// This corresponds to the value of the index of this user in the library db. /// - [Required] public long InternalId { get; set; } /// @@ -340,7 +318,9 @@ namespace Jellyfin.Data.Entities [Required] public virtual DisplayPreferences DisplayPreferences { get; set; } - [Required] + /// + /// Gets or sets the level of sync play permissions this user has. + /// public SyncPlayAccess SyncPlayAccess { get; set; } /// @@ -350,13 +330,8 @@ namespace Jellyfin.Data.Entities /// Required, Concurrency Token. /// [ConcurrencyCheck] - [Required] public uint RowVersion { get; set; } - /************************************************************************* - * Navigation properties - *************************************************************************/ - /// /// Gets or sets the list of access schedules this user has. /// @@ -395,18 +370,6 @@ namespace Jellyfin.Data.Entities [ForeignKey("Preference_Preferences_Guid")] public virtual ICollection Preferences { get; protected set; } - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// The username for the created user. - /// The Id of the user's authentication provider. - /// The Id of the user's password reset provider. - /// The created instance. - public static User Create(string username, string authenticationProviderId, string passwordResetProviderId) - { - return new User(username, authenticationProviderId, passwordResetProviderId); - } - /// public void OnSavingChanges() { @@ -519,7 +482,5 @@ namespace Jellyfin.Data.Entities Preferences.Add(new Preference(val, string.Empty)); } } - - partial void Init(); } } diff --git a/Jellyfin.Data/Enums/ArtKind.cs b/Jellyfin.Data/Enums/ArtKind.cs index 71b4db6f26..f7a73848c8 100644 --- a/Jellyfin.Data/Enums/ArtKind.cs +++ b/Jellyfin.Data/Enums/ArtKind.cs @@ -1,13 +1,33 @@ -#pragma warning disable CS1591 - namespace Jellyfin.Data.Enums { + /// + /// An enum representing types of art. + /// public enum ArtKind { - Other, - Poster, - Banner, - Thumbnail, - Logo + /// + /// Another type of art, not covered by the other members. + /// + Other = 0, + + /// + /// A poster. + /// + Poster = 1, + + /// + /// A banner. + /// + Banner = 2, + + /// + /// A thumbnail. + /// + Thumbnail = 3, + + /// + /// A logo. + /// + Logo = 4 } } diff --git a/Jellyfin.Data/Enums/DynamicDayOfWeek.cs b/Jellyfin.Data/Enums/DynamicDayOfWeek.cs index a33cd9d1cd..d3d8dd8227 100644 --- a/Jellyfin.Data/Enums/DynamicDayOfWeek.cs +++ b/Jellyfin.Data/Enums/DynamicDayOfWeek.cs @@ -1,18 +1,58 @@ -#pragma warning disable CS1591 - namespace Jellyfin.Data.Enums { + /// + /// An enum that represents a day of the week, weekdays, weekends, or all days. + /// public enum DynamicDayOfWeek { + /// + /// Sunday. + /// Sunday = 0, + + /// + /// Monday. + /// Monday = 1, + + /// + /// Tuesday. + /// Tuesday = 2, + + /// + /// Wednesday. + /// Wednesday = 3, + + /// + /// Thursday. + /// Thursday = 4, + + /// + /// Friday. + /// Friday = 5, + + /// + /// Saturday. + /// Saturday = 6, + + /// + /// All days of the week. + /// Everyday = 7, + + /// + /// A week day, or Monday-Friday. + /// Weekday = 8, + + /// + /// Saturday and Sunday. + /// Weekend = 9 } } diff --git a/Jellyfin.Data/Enums/IndexingKind.cs b/Jellyfin.Data/Enums/IndexingKind.cs index fafe47e0c1..c0df077143 100644 --- a/Jellyfin.Data/Enums/IndexingKind.cs +++ b/Jellyfin.Data/Enums/IndexingKind.cs @@ -1,7 +1,8 @@ -#pragma warning disable CS1591 - -namespace Jellyfin.Data.Enums +namespace Jellyfin.Data.Enums { + /// + /// An enum representing a type of indexing in a user's display preferences. + /// public enum IndexingKind { /// diff --git a/Jellyfin.Data/Enums/MediaFileKind.cs b/Jellyfin.Data/Enums/MediaFileKind.cs index b03591fb80..797c26ec27 100644 --- a/Jellyfin.Data/Enums/MediaFileKind.cs +++ b/Jellyfin.Data/Enums/MediaFileKind.cs @@ -1,13 +1,33 @@ -#pragma warning disable CS1591 - namespace Jellyfin.Data.Enums { + /// + /// An enum representing the type of media file. + /// public enum MediaFileKind { - Main, - Sidecar, - AdditionalPart, - AlternativeFormat, - AdditionalStream + /// + /// The main file. + /// + Main = 0, + + /// + /// A sidecar file. + /// + Sidecar = 1, + + /// + /// An additional part to the main file. + /// + AdditionalPart = 2, + + /// + /// An alternative format to the main file. + /// + AlternativeFormat = 3, + + /// + /// An additional stream for the main file. + /// + AdditionalStream = 4 } } diff --git a/Jellyfin.Data/Enums/PersonRoleType.cs b/Jellyfin.Data/Enums/PersonRoleType.cs index 2d80eaa4ca..1e619f5eef 100644 --- a/Jellyfin.Data/Enums/PersonRoleType.cs +++ b/Jellyfin.Data/Enums/PersonRoleType.cs @@ -1,20 +1,68 @@ -#pragma warning disable CS1591 - namespace Jellyfin.Data.Enums { + /// + /// An enum representing a person's role in a specific media item. + /// public enum PersonRoleType { - Other, - Director, - Artist, - OriginalArtist, - Actor, - VoiceActor, - Producer, - Remixer, - Conductor, - Composer, - Author, - Editor + /// + /// Another role, not covered by the other types. + /// + Other = 0, + + /// + /// The director of the media. + /// + Director = 1, + + /// + /// An artist. + /// + Artist = 2, + + /// + /// The original artist. + /// + OriginalArtist = 3, + + /// + /// An actor. + /// + Actor = 4, + + /// + /// A voice actor. + /// + VoiceActor = 5, + + /// + /// A producer. + /// + Producer = 6, + + /// + /// A remixer. + /// + Remixer = 7, + + /// + /// A conductor. + /// + Conductor = 8, + + /// + /// A composer. + /// + Composer = 9, + + /// + /// An author. + /// + Author = 10, + + /// + /// An editor. + /// + Editor = 11 } } diff --git a/Jellyfin.Data/Enums/SubtitlePlaybackMode.cs b/Jellyfin.Data/Enums/SubtitlePlaybackMode.cs index c8fc211593..ca41300edf 100644 --- a/Jellyfin.Data/Enums/SubtitlePlaybackMode.cs +++ b/Jellyfin.Data/Enums/SubtitlePlaybackMode.cs @@ -1,13 +1,33 @@ -#pragma warning disable CS1591 - -namespace Jellyfin.Data.Enums +namespace Jellyfin.Data.Enums { + /// + /// An enum representing a subtitle playback mode. + /// public enum SubtitlePlaybackMode { + /// + /// The default subtitle playback mode. + /// Default = 0, + + /// + /// Always show subtitles. + /// Always = 1, + + /// + /// Only show forced subtitles. + /// OnlyForced = 2, + + /// + /// Don't show subtitles. + /// None = 3, + + /// + /// Only show subtitles when the current audio stream is in a different language. + /// Smart = 4 } } diff --git a/Jellyfin.Data/Enums/UnratedItem.cs b/Jellyfin.Data/Enums/UnratedItem.cs index 5259e77394..871794086d 100644 --- a/Jellyfin.Data/Enums/UnratedItem.cs +++ b/Jellyfin.Data/Enums/UnratedItem.cs @@ -1,17 +1,53 @@ -#pragma warning disable CS1591 - namespace Jellyfin.Data.Enums { + /// + /// An enum representing an unrated item. + /// public enum UnratedItem { - Movie, - Trailer, - Series, - Music, - Book, - LiveTvChannel, - LiveTvProgram, - ChannelContent, - Other + /// + /// A movie. + /// + Movie = 0, + + /// + /// A trailer. + /// + Trailer = 1, + + /// + /// A series. + /// + Series = 2, + + /// + /// Music. + /// + Music = 3, + + /// + /// A book. + /// + Book = 4, + + /// + /// A live TV channel + /// + LiveTvChannel = 5, + + /// + /// A live TV program. + /// + LiveTvProgram = 6, + + /// + /// Channel content. + /// + ChannelContent = 7, + + /// + /// Another type, not covered by the other fields. + /// + Other = 8 } } diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 1e01521add..95343f91b5 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -4,7 +4,7 @@ netstandard2.0;netstandard2.1 false true - true + true true true true @@ -41,8 +41,12 @@ - - + + + + + + diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 21748ca192..30ed3e6af3 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -24,11 +24,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs index 7c5c5a3ec5..46f1c618f2 100644 --- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs +++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs @@ -14,22 +14,21 @@ namespace Jellyfin.Server.Implementations.Users /// public class DisplayPreferencesManager : IDisplayPreferencesManager { - private readonly JellyfinDbProvider _dbProvider; + private readonly JellyfinDb _dbContext; /// /// Initializes a new instance of the class. /// - /// The Jellyfin db provider. - public DisplayPreferencesManager(JellyfinDbProvider dbProvider) + /// The database context. + public DisplayPreferencesManager(JellyfinDb dbContext) { - _dbProvider = dbProvider; + _dbContext = dbContext; } /// public DisplayPreferences GetDisplayPreferences(Guid userId, string client) { - using var dbContext = _dbProvider.CreateContext(); - var prefs = dbContext.DisplayPreferences + var prefs = _dbContext.DisplayPreferences .Include(pref => pref.HomeSections) .FirstOrDefault(pref => pref.UserId == userId && string.Equals(pref.Client, client)); @@ -37,7 +36,7 @@ namespace Jellyfin.Server.Implementations.Users if (prefs == null) { prefs = new DisplayPreferences(userId, client); - dbContext.DisplayPreferences.Add(prefs); + _dbContext.DisplayPreferences.Add(prefs); } return prefs; @@ -46,14 +45,13 @@ namespace Jellyfin.Server.Implementations.Users /// public ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client) { - using var dbContext = _dbProvider.CreateContext(); - var prefs = dbContext.ItemDisplayPreferences + var prefs = _dbContext.ItemDisplayPreferences .FirstOrDefault(pref => pref.UserId == userId && pref.ItemId == itemId && string.Equals(pref.Client, client)); if (prefs == null) { prefs = new ItemDisplayPreferences(userId, Guid.Empty, client); - dbContext.ItemDisplayPreferences.Add(prefs); + _dbContext.ItemDisplayPreferences.Add(prefs); } return prefs; @@ -62,27 +60,15 @@ namespace Jellyfin.Server.Implementations.Users /// public IList ListItemDisplayPreferences(Guid userId, string client) { - using var dbContext = _dbProvider.CreateContext(); - - return dbContext.ItemDisplayPreferences + return _dbContext.ItemDisplayPreferences .Where(prefs => prefs.UserId == userId && prefs.ItemId != Guid.Empty && string.Equals(prefs.Client, client)) .ToList(); } /// - public void SaveChanges(DisplayPreferences preferences) - { - using var dbContext = _dbProvider.CreateContext(); - dbContext.Update(preferences); - dbContext.SaveChanges(); - } - - /// - public void SaveChanges(ItemDisplayPreferences preferences) + public void SaveChanges() { - using var dbContext = _dbProvider.CreateContext(); - dbContext.Update(preferences); - dbContext.SaveChanges(); + _dbContext.SaveChanges(); } } } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 755844dd9e..8d569a779a 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Reflection; using Emby.Drawing; using Emby.Server.Implementations; @@ -15,6 +16,7 @@ using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; using MediaBrowser.Model.IO; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -67,12 +69,8 @@ namespace Jellyfin.Server Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}."); } - // TODO: Set up scoping and use AddDbContextPool, - // can't register as Transient since tracking transient in GC is funky - // serviceCollection.AddDbContext( - // options => options - // .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"), - // ServiceLifetime.Transient); + ServiceCollection.AddDbContextPool( + options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}")); ServiceCollection.AddEventServices(); ServiceCollection.AddSingleton(); diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 71c66a310a..c7fbfa4d02 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; using Jellyfin.Server.Middleware; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; +using Microsoft.OpenApi.Models; namespace Jellyfin.Server.Extensions { @@ -23,6 +25,7 @@ namespace Jellyfin.Server.Extensions // specifying the Swagger JSON endpoint. var baseUrl = serverConfigurationManager.Configuration.BaseUrl.Trim('/'); + var apiDocBaseUrl = serverConfigurationManager.Configuration.BaseUrl; if (!string.IsNullOrEmpty(baseUrl)) { baseUrl += '/'; @@ -32,19 +35,25 @@ namespace Jellyfin.Server.Extensions .UseSwagger(c => { // Custom path requires {documentName}, SwaggerDoc documentName is 'api-docs' - c.RouteTemplate = $"/{baseUrl}{{documentName}}/openapi.json"; + c.RouteTemplate = "{documentName}/openapi.json"; + c.PreSerializeFilters.Add((swagger, httpReq) => + { + swagger.Servers = new List { new OpenApiServer { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}{apiDocBaseUrl}" } }; + }); }) .UseSwaggerUI(c => { c.DocumentTitle = "Jellyfin API"; c.SwaggerEndpoint($"/{baseUrl}api-docs/openapi.json", "Jellyfin API"); - c.RoutePrefix = $"{baseUrl}api-docs/swagger"; + c.InjectStylesheet($"/{baseUrl}api-docs/swagger/custom.css"); + c.RoutePrefix = "api-docs/swagger"; }) .UseReDoc(c => { c.DocumentTitle = "Jellyfin API"; c.SpecUrl($"/{baseUrl}api-docs/openapi.json"); - c.RoutePrefix = $"{baseUrl}api-docs/redoc"; + c.InjectStylesheet($"/{baseUrl}api-docs/redoc/custom.css"); + c.RoutePrefix = "api-docs/redoc"; }); } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 896f5f0b85..8dcce93a45 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using Jellyfin.Api; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Auth.DownloadPolicy; @@ -134,13 +133,11 @@ namespace Jellyfin.Server.Extensions /// Extension method for adding the jellyfin API to the service collection. /// /// The service collection. - /// The base url for the API. - /// An IEnumberable containing all plugin assemblies with API controllers. + /// An IEnumerable containing all plugin assemblies with API controllers. /// /// The configured cors hosts. /// The MVC builder. public static IMvcBuilder AddJellyfinApi( this IServiceCollection serviceCollection, - string baseUrl, IEnumerable pluginAssemblies, string[] corsHosts) { @@ -155,7 +152,9 @@ namespace Jellyfin.Server.Extensions }) .AddMvc(opts => { - opts.UseGeneralRoutePrefix(baseUrl); + // Allow requester to change between camelCase and PascalCase + opts.RespectBrowserAcceptHeader = true; + opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter()); opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter()); diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs index 9b347ae2c2..8043989b1e 100644 --- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Formatters public CamelCaseJsonProfileFormatter() : base(JsonDefaults.GetCamelCaseOptions()) { SupportedMediaTypes.Clear(); - SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\"")); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.CamelCaseMediaType)); } } } diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs index 0024708bad..d0110b125c 100644 --- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs @@ -1,3 +1,4 @@ +using System.Net.Mime; using MediaBrowser.Common.Json; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; @@ -16,8 +17,8 @@ namespace Jellyfin.Server.Formatters { SupportedMediaTypes.Clear(); // Add application/json for default formatter - SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json")); - SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"PascalCase\"")); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json)); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.PascalCaseMediaType)); } } } diff --git a/Jellyfin.Server/Formatters/XmlOutputFormatter.cs b/Jellyfin.Server/Formatters/XmlOutputFormatter.cs index 58319657d6..01d99d7c87 100644 --- a/Jellyfin.Server/Formatters/XmlOutputFormatter.cs +++ b/Jellyfin.Server/Formatters/XmlOutputFormatter.cs @@ -16,8 +16,9 @@ namespace Jellyfin.Server.Formatters /// public XmlOutputFormatter() { + SupportedMediaTypes.Clear(); SupportedMediaTypes.Add(MediaTypeNames.Text.Xml); - SupportedMediaTypes.Add("text/xml;charset=UTF-8"); + SupportedEncodings.Add(Encoding.UTF8); SupportedEncodings.Add(Encoding.Unicode); } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 7541707d9f..c3bec1c71c 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -41,8 +41,10 @@ - - + + + + @@ -63,4 +65,16 @@ + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs index 9316737bdf..ae3a3a1c54 100644 --- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs +++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs @@ -44,11 +44,7 @@ namespace Jellyfin.Server.Middleware var localPath = httpContext.Request.Path.ToString(); var baseUrlPrefix = serverConfigurationManager.Configuration.BaseUrl; - if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) - || string.IsNullOrEmpty(localPath) - || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) + if (!localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) { // Always redirect back to the default path if the base prefix is invalid or missing _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath); diff --git a/Jellyfin.Server/Middleware/CorsPolicyProvider.cs b/Jellyfin.Server/Middleware/CorsPolicyProvider.cs new file mode 100644 index 0000000000..7c2b28ed8f --- /dev/null +++ b/Jellyfin.Server/Middleware/CorsPolicyProvider.cs @@ -0,0 +1,7 @@ +namespace Jellyfin.Server.Middleware +{ + public class CorsPolicyProvider + { + + } +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index b9a90f9dbf..45959aec2d 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -169,7 +169,7 @@ namespace Jellyfin.Server // If hosting the web client, validate the client content path if (startupConfig.HostWebClient()) { - string? webContentPath = DashboardController.GetWebClientUiPath(startupConfig, appHost.ServerConfigurationManager); + string? webContentPath = appHost.ServerConfigurationManager.ApplicationPaths.WebPath; if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0) { throw new InvalidOperationException( diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index c3ca863833..5601915a33 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -3,14 +3,18 @@ using System.ComponentModel; using System.Net.Http.Headers; using Jellyfin.Api.TypeConverters; using Jellyfin.Server.Extensions; +using Jellyfin.Server.Implementations; using Jellyfin.Server.Middleware; using Jellyfin.Server.Models; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Prometheus; @@ -50,7 +54,6 @@ namespace Jellyfin.Server options.HttpsPort = _serverApplicationHost.HttpsPort; }); services.AddJellyfinApi( - _serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'), _serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.Configuration.CorsHosts); @@ -77,6 +80,9 @@ namespace Jellyfin.Server c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_serverApplicationHost.ApplicationUserAgentAddress})")); }) .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler()); + + services.AddHealthChecks() + .AddDbContextCheck(); } /// @@ -84,54 +90,78 @@ namespace Jellyfin.Server /// /// The application builder. /// The webhost environment. + /// The application config. public void Configure( IApplicationBuilder app, - IWebHostEnvironment env) + IWebHostEnvironment env, + IConfiguration appConfig) { - if (env.IsDevelopment()) + // Only add base url redirection if a base url is set. + if (!string.IsNullOrEmpty(_serverConfigurationManager.Configuration.BaseUrl)) { - app.UseDeveloperExceptionPage(); + app.UseBaseUrlRedirection(); } - app.UseMiddleware(); + // Wrap rest of configuration so everything only listens on BaseUrl. + app.Map(_serverConfigurationManager.Configuration.BaseUrl, mainApp => + { + if (env.IsDevelopment()) + { + mainApp.UseDeveloperExceptionPage(); + } - app.UseMiddleware(); + mainApp.UseMiddleware(); - app.UseWebSockets(); + mainApp.UseMiddleware(); - app.UseResponseCompression(); + mainApp.UseWebSockets(); - app.UseCors(ServerCorsPolicy.DefaultPolicyName); + mainApp.UseResponseCompression(); - if (_serverConfigurationManager.Configuration.RequireHttps - && _serverApplicationHost.ListenWithHttps) - { - app.UseHttpsRedirection(); - } + mainApp.UseCors(ServerCorsPolicy.DefaultPolicyName); - app.UseAuthentication(); - app.UseJellyfinApiSwagger(_serverConfigurationManager); - app.UseRouting(); - app.UseAuthorization(); - if (_serverConfigurationManager.Configuration.EnableMetrics) - { - // Must be registered after any middleware that could change HTTP response codes or the data will be bad - app.UseHttpMetrics(); - } + if (_serverConfigurationManager.Configuration.RequireHttps + && _serverApplicationHost.ListenWithHttps) + { + mainApp.UseHttpsRedirection(); + } - app.UseLanFiltering(); - app.UseIpBasedAccessValidation(); - app.UseBaseUrlRedirection(); - app.UseWebSocketHandler(); - app.UseServerStartupMessage(); + mainApp.UseStaticFiles(); + if (appConfig.HostWebClient()) + { + mainApp.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(_serverConfigurationManager.ApplicationPaths.WebPath), + RequestPath = "/web" + }); + } + + mainApp.UseAuthentication(); + mainApp.UseJellyfinApiSwagger(_serverConfigurationManager); + mainApp.UseRouting(); + mainApp.UseAuthorization(); + + mainApp.UseLanFiltering(); + mainApp.UseIpBasedAccessValidation(); + mainApp.UseWebSocketHandler(); + mainApp.UseServerStartupMessage(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); if (_serverConfigurationManager.Configuration.EnableMetrics) { - endpoints.MapMetrics(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/') + "/metrics"); + // Must be registered after any middleware that could change HTTP response codes or the data will be bad + mainApp.UseHttpMetrics(); } + + mainApp.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + if (_serverConfigurationManager.Configuration.EnableMetrics) + { + endpoints.MapMetrics("/metrics"); + } + + endpoints.MapHealthChecks("/health"); + }); }); // Add type descriptor for legacy datetime parsing. diff --git a/Jellyfin.Server/wwwroot/api-docs/banner-dark.svg b/Jellyfin.Server/wwwroot/api-docs/banner-dark.svg new file mode 100644 index 0000000000..b62b7545c7 --- /dev/null +++ b/Jellyfin.Server/wwwroot/api-docs/banner-dark.svg @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/Jellyfin.Server/wwwroot/api-docs/redoc/custom.css b/Jellyfin.Server/wwwroot/api-docs/redoc/custom.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Jellyfin.Server/wwwroot/api-docs/swagger/custom.css b/Jellyfin.Server/wwwroot/api-docs/swagger/custom.css new file mode 100644 index 0000000000..acb59888e0 --- /dev/null +++ b/Jellyfin.Server/wwwroot/api-docs/swagger/custom.css @@ -0,0 +1,15 @@ +/* logo */ +.topbar-wrapper img[alt="Swagger UI"], .topbar-wrapper span { + visibility: collapse; +} + +.topbar-wrapper .link:after { + content: url(../banner-dark.svg); + display: block; + -moz-box-sizing: border-box; + box-sizing: border-box; + max-width: 100%; + max-height: 100%; + width: 150px; +} +/* end logo */ diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index 4cea616826..57c6546675 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -1,5 +1,3 @@ -using MediaBrowser.Model.Configuration; - namespace MediaBrowser.Common.Configuration { /// @@ -17,8 +15,7 @@ namespace MediaBrowser.Common.Configuration /// Gets the path to the web UI resources folder. /// /// - /// This value is not relevant if the server is configured to not host any static web content. Additionally, - /// the value for takes precedence over this one. + /// This value is not relevant if the server is configured to not host any static web content. /// string WebPath { get; } diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index 5867cd4a0c..67f7e8f14a 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -9,6 +9,16 @@ namespace MediaBrowser.Common.Json /// public static class JsonDefaults { + /// + /// Pascal case json profile media type. + /// + public const string PascalCaseMediaType = "application/json; profile=\"PascalCase\""; + + /// + /// Camel case json profile media type. + /// + public const string CamelCaseMediaType = "application/json; profile=\"CamelCase\""; + /// /// Gets the default options. /// diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 6e258371cd..70dcc2397c 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -18,9 +18,9 @@ + + - - diff --git a/MediaBrowser.Common/Net/CacheMode.cs b/MediaBrowser.Common/Net/CacheMode.cs deleted file mode 100644 index 78fa3bf9bb..0000000000 --- a/MediaBrowser.Common/Net/CacheMode.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1602 - -namespace MediaBrowser.Common.Net -{ - public enum CacheMode - { - None = 0, - Unconditional = 1 - } -} diff --git a/MediaBrowser.Common/Net/CompressionMethods.cs b/MediaBrowser.Common/Net/CompressionMethods.cs deleted file mode 100644 index 39b72609fe..0000000000 --- a/MediaBrowser.Common/Net/CompressionMethods.cs +++ /dev/null @@ -1,15 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1602 - -using System; - -namespace MediaBrowser.Common.Net -{ - [Flags] - public enum CompressionMethods - { - None = 0b00000001, - Deflate = 0b00000010, - Gzip = 0b00000100 - } -} diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs deleted file mode 100644 index 347fc98332..0000000000 --- a/MediaBrowser.Common/Net/HttpRequestOptions.cs +++ /dev/null @@ -1,105 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Threading; -using Microsoft.Net.Http.Headers; - -namespace MediaBrowser.Common.Net -{ - /// - /// Class HttpRequestOptions. - /// - public class HttpRequestOptions - { - /// - /// Initializes a new instance of the class. - /// - public HttpRequestOptions() - { - RequestHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - - CacheMode = CacheMode.None; - DecompressionMethod = CompressionMethods.Deflate; - } - - /// - /// Gets or sets the URL. - /// - /// The URL. - public string Url { get; set; } - - public CompressionMethods DecompressionMethod { get; set; } - - /// - /// Gets or sets the accept header. - /// - /// The accept header. - public string AcceptHeader - { - get => GetHeaderValue(HeaderNames.Accept); - set => RequestHeaders[HeaderNames.Accept] = value; - } - - /// - /// Gets or sets the cancellation token. - /// - /// The cancellation token. - public CancellationToken CancellationToken { get; set; } - - /// - /// Gets or sets the user agent. - /// - /// The user agent. - public string UserAgent - { - get => GetHeaderValue(HeaderNames.UserAgent); - set => RequestHeaders[HeaderNames.UserAgent] = value; - } - - /// - /// Gets or sets the referrer. - /// - /// The referrer. - public string Referer - { - get => GetHeaderValue(HeaderNames.Referer); - set => RequestHeaders[HeaderNames.Referer] = value; - } - - /// - /// Gets or sets the host. - /// - /// The host. - public string Host - { - get => GetHeaderValue(HeaderNames.Host); - set => RequestHeaders[HeaderNames.Host] = value; - } - - public Dictionary RequestHeaders { get; private set; } - - public string RequestContentType { get; set; } - - public string RequestContent { get; set; } - - public bool BufferContent { get; set; } - - public bool LogErrorResponseBody { get; set; } - - public bool EnableKeepAlive { get; set; } - - public CacheMode CacheMode { get; set; } - - public TimeSpan CacheLength { get; set; } - - public bool EnableDefaultUserAgent { get; set; } - - private string GetHeaderValue(string name) - { - RequestHeaders.TryGetValue(name, out var value); - - return value; - } - } -} diff --git a/MediaBrowser.Common/Net/HttpResponseInfo.cs b/MediaBrowser.Common/Net/HttpResponseInfo.cs deleted file mode 100644 index d4fee6c78e..0000000000 --- a/MediaBrowser.Common/Net/HttpResponseInfo.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Http.Headers; - -namespace MediaBrowser.Common.Net -{ - /// - /// Class HttpResponseInfo. - /// - public sealed class HttpResponseInfo : IDisposable - { -#pragma warning disable CS1591 - public HttpResponseInfo() - { - } - - public HttpResponseInfo(HttpResponseHeaders headers, HttpContentHeaders contentHeader) - { - Headers = headers; - ContentHeaders = contentHeader; - } - -#pragma warning restore CS1591 - - /// - /// Gets or sets the type of the content. - /// - /// The type of the content. - public string ContentType { get; set; } - - /// - /// Gets or sets the response URL. - /// - /// The response URL. - public string ResponseUrl { get; set; } - - /// - /// Gets or sets the content. - /// - /// The content. - public Stream Content { get; set; } - - /// - /// Gets or sets the status code. - /// - /// The status code. - public HttpStatusCode StatusCode { get; set; } - - /// - /// Gets or sets the temp file path. - /// - /// The temp file path. - public string TempFilePath { get; set; } - - /// - /// Gets or sets the length of the content. - /// - /// The length of the content. - public long? ContentLength { get; set; } - - /// - /// Gets or sets the headers. - /// - /// The headers. - public HttpResponseHeaders Headers { get; set; } - - /// - /// Gets or sets the content headers. - /// - /// The content headers. - public HttpContentHeaders ContentHeaders { get; set; } - - /// - public void Dispose() - { - // backwards compatibility - } - } -} diff --git a/MediaBrowser.Common/Net/IHttpClient.cs b/MediaBrowser.Common/Net/IHttpClient.cs deleted file mode 100644 index 534e22eddf..0000000000 --- a/MediaBrowser.Common/Net/IHttpClient.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; - -namespace MediaBrowser.Common.Net -{ - /// - /// Interface IHttpClient. - /// - public interface IHttpClient - { - /// - /// Gets the response. - /// - /// The options. - /// Task{HttpResponseInfo}. - Task GetResponse(HttpRequestOptions options); - - /// - /// Gets the specified options. - /// - /// The options. - /// Task{Stream}. - Task Get(HttpRequestOptions options); - - /// - /// Warning: Deprecated function, - /// use 'Task{HttpResponseInfo} SendAsync(HttpRequestOptions options, HttpMethod httpMethod);' instead - /// Sends the asynchronous. - /// - /// The options. - /// The HTTP method. - /// Task{HttpResponseInfo}. - [Obsolete("Use 'Task{HttpResponseInfo} SendAsync(HttpRequestOptions options, HttpMethod httpMethod);' instead")] - Task SendAsync(HttpRequestOptions options, string httpMethod); - - /// - /// Sends the asynchronous. - /// - /// The options. - /// The HTTP method. - /// Task{HttpResponseInfo}. - Task SendAsync(HttpRequestOptions options, HttpMethod httpMethod); - - /// - /// Posts the specified options. - /// - /// The options. - /// Task{HttpResponseInfo}. - Task Post(HttpRequestOptions options); - } -} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index a5c22e50f4..68126bd8a8 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -60,8 +60,6 @@ namespace MediaBrowser.Controller.Entities protected BaseItem() { - ThemeSongIds = Array.Empty(); - ThemeVideoIds = Array.Empty(); Tags = Array.Empty(); Genres = Array.Empty(); Studios = Array.Empty(); @@ -100,12 +98,52 @@ namespace MediaBrowser.Controller.Entities }; [JsonIgnore] - public Guid[] ThemeSongIds { get; set; } + public Guid[] ThemeSongIds + { + get + { + if (_themeSongIds == null) + { + _themeSongIds = GetExtras() + .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeSong) + .Select(song => song.Id) + .ToArray(); + } + + return _themeSongIds; + } + + private set + { + _themeSongIds = value; + } + } + [JsonIgnore] - public Guid[] ThemeVideoIds { get; set; } + public Guid[] ThemeVideoIds + { + get + { + if (_themeVideoIds == null) + { + _themeVideoIds = GetExtras() + .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeVideo) + .Select(song => song.Id) + .ToArray(); + } + + return _themeVideoIds; + } + + private set + { + _themeVideoIds = value; + } + } [JsonIgnore] public string PreferredMetadataCountryCode { get; set; } + [JsonIgnore] public string PreferredMetadataLanguage { get; set; } @@ -635,6 +673,9 @@ namespace MediaBrowser.Controller.Entities } private string _sortName; + private Guid[] _themeSongIds; + private Guid[] _themeVideoIds; + /// /// Gets the name of the sort. /// @@ -1582,7 +1623,8 @@ namespace MediaBrowser.Controller.Entities await Task.WhenAll(tasks).ConfigureAwait(false); - item.ThemeVideoIds = newThemeVideoIds; + // They are expected to be sorted by SortName + item.ThemeVideoIds = newThemeVideos.OrderBy(i => i.SortName).Select(i => i.Id).ToArray(); return themeVideosChanged; } @@ -1619,7 +1661,8 @@ namespace MediaBrowser.Controller.Entities await Task.WhenAll(tasks).ConfigureAwait(false); - item.ThemeSongIds = newThemeSongIds; + // They are expected to be sorted by SortName + item.ThemeSongIds = newThemeSongs.OrderBy(i => i.SortName).Select(i => i.Id).ToArray(); return themeSongsChanged; } @@ -2910,12 +2953,12 @@ namespace MediaBrowser.Controller.Entities public IEnumerable GetThemeSongs() { - return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeSong)).OrderBy(i => i.SortName); + return ThemeSongIds.Select(LibraryManager.GetItemById); } public IEnumerable GetThemeVideos() { - return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeVideo)).OrderBy(i => i.SortName); + return ThemeVideoIds.Select(LibraryManager.GetItemById); } /// diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs index 856b91b5d0..b35f830960 100644 --- a/MediaBrowser.Controller/IDisplayPreferencesManager.cs +++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs @@ -35,15 +35,8 @@ namespace MediaBrowser.Controller IList ListItemDisplayPreferences(Guid userId, string client); /// - /// Saves changes to the provided display preferences. + /// Saves changes made to the database. /// - /// The display preferences to save. - void SaveChanges(DisplayPreferences preferences); - - /// - /// Saves changes to the provided item display preferences. - /// - /// The item display preferences to save. - void SaveChanges(ItemDisplayPreferences preferences); + void SaveChanges(); } } diff --git a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs index d7afd21184..44bd38b54f 100644 --- a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs @@ -62,6 +62,7 @@ namespace MediaBrowser.Controller.LiveTv /// /// null if [has image] contains no value, true if [has image]; otherwise, false. public bool? HasImage { get; set; } + /// /// Gets or sets a value indicating whether this instance is favorite. /// diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 3674181b10..9854ec520f 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -14,9 +14,9 @@ + + - - diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 550916f823..2c30ca4588 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -456,6 +456,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; + var isNvencHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1; var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); @@ -515,6 +516,24 @@ namespace MediaBrowser.Controller.MediaEncoding } } + if (state.IsVideoRequest + && string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + { + var isColorDepth10 = IsColorDepth10(state); + + if (isNvencHevcDecoder && isColorDepth10 + && _mediaEncoder.SupportsHwaccel("opencl") + && encodingOptions.EnableTonemapping + && !string.IsNullOrEmpty(state.VideoStream.VideoRange) + && state.VideoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) + { + arg.Append("-init_hw_device opencl=ocl:") + .Append(encodingOptions.OpenclDevice) + .Append(' ') + .Append("-filter_hw_device ocl "); + } + } + if (state.IsVideoRequest && string.Equals(encodingOptions.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) { @@ -1003,11 +1022,33 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) && !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) { param = "-pix_fmt yuv420p " + param; } + if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) + { + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty; + var videoStream = state.VideoStream; + var isColorDepth10 = IsColorDepth10(state); + + if (videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1 + && isColorDepth10 + && _mediaEncoder.SupportsHwaccel("opencl") + && encodingOptions.EnableTonemapping + && !string.IsNullOrEmpty(videoStream.VideoRange) + && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) + { + param = "-pix_fmt nv12 " + param; + } + else + { + param = "-pix_fmt yuv420p " + param; + } + } + if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) { param = "-pix_fmt nv21 " + param; @@ -1611,64 +1652,45 @@ namespace MediaBrowser.Controller.MediaEncoding var outputSizeParam = ReadOnlySpan.Empty; var request = state.BaseRequest; - // Add resolution params, if specified - if (request.Width.HasValue - || request.Height.HasValue - || request.MaxHeight.HasValue - || request.MaxWidth.HasValue) + outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"'); + + // All possible beginning of video filters + // Don't break the order + string[] beginOfOutputSizeParam = new[] { - outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"'); + // for tonemap_opencl + "hwupload,tonemap_opencl", // hwupload=extra_hw_frames=64,vpp_qsv (for overlay_qsv on linux) - var index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase); + "hwupload=extra_hw_frames", + + // vpp_qsv + "vpp", + + // hwdownload,format=p010le (hardware decode + software encode for vaapi) + "hwdownload", + + // format=nv12|vaapi,hwupload,scale_vaapi + "format", + + // bwdif,scale=expr + "bwdif", + + // yadif,scale=expr + "yadif", + + // scale=expr + "scale" + }; + + var index = -1; + foreach (var param in beginOfOutputSizeParam) + { + index = outputSizeParam.IndexOf(param, StringComparison.OrdinalIgnoreCase); if (index != -1) { outputSizeParam = outputSizeParam.Slice(index); - } - else - { - // vpp_qsv - index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - outputSizeParam = outputSizeParam.Slice(index); - } - else - { - // hwdownload,format=p010le (hardware decode + software encode for vaapi) - index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - outputSizeParam = outputSizeParam.Slice(index); - } - else - { - // format=nv12|vaapi,hwupload,scale_vaapi - index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - outputSizeParam = outputSizeParam.Slice(index); - } - else - { - // yadif,scale=expr - index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - outputSizeParam = outputSizeParam.Slice(index); - } - else - { - // scale=expr - index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - outputSizeParam = outputSizeParam.Slice(index); - } - } - } - } - } + break; } } @@ -1747,9 +1769,9 @@ namespace MediaBrowser.Controller.MediaEncoding */ if (isLinux) { - retStr = !outputSizeParam.IsEmpty ? - " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\"" : - " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\""; + retStr = !outputSizeParam.IsEmpty + ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\"" + : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\""; } } @@ -2084,12 +2106,61 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1; var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1; + var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1; var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1; var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + var isColorDepth10 = IsColorDepth10(state); var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + // If double rate deinterlacing is enabled and the input framerate is 30fps or below, otherwise the output framerate will be too high for many devices + var doubleRateDeinterlace = options.DeinterlaceDoubleRate && (videoStream?.RealFrameRate ?? 60) <= 30; + + // Currently only with the use of NVENC decoder can we get a decent performance. + // Currently only the HEVC/H265 format is supported. + // NVIDIA Pascal and Turing or higher are recommended. + if (isNvdecHevcDecoder && isColorDepth10 + && _mediaEncoder.SupportsHwaccel("opencl") + && options.EnableTonemapping + && !string.IsNullOrEmpty(videoStream.VideoRange) + && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) + { + var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}"; + + if (options.TonemappingParam != 0) + { + parameters += ":param={4}"; + } + + if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase)) + { + parameters += ":range={5}"; + } + + // Upload the HDR10 or HLG data to the OpenCL device, + // use tonemap_opencl filter for tone mapping, + // and then download the SDR data to memory. + filters.Add("hwupload"); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + parameters, + options.TonemappingAlgorithm, + options.TonemappingDesat, + options.TonemappingThreshold, + options.TonemappingPeak, + options.TonemappingParam, + options.TonemappingRange)); + filters.Add("hwdownload"); + + if (hasGraphicalSubs || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true) + || string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) + { + filters.Add("format=nv12"); + } + } + // When the input may or may not be hardware VAAPI decodable if (isVaapiH264Encoder) { @@ -2107,7 +2178,6 @@ namespace MediaBrowser.Controller.MediaEncoding else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder) { var codec = videoStream.Codec.ToLowerInvariant(); - var isColorDepth10 = IsColorDepth10(state); // Assert 10-bit hardware VAAPI decodable if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) @@ -2136,35 +2206,38 @@ namespace MediaBrowser.Controller.MediaEncoding { if (isVaapiH264Encoder) { - filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_vaapi")); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "deinterlace_vaapi=rate={0}", + doubleRateDeinterlace ? "field" : "frame")); } } // Add software deinterlace filter before scaling filter - if (state.DeInterlace("h264", true) - || state.DeInterlace("avc", true) - || state.DeInterlace("h265", true) - || state.DeInterlace("hevc", true)) + if ((state.DeInterlace("h264", true) + || state.DeInterlace("avc", true) + || state.DeInterlace("h265", true) + || state.DeInterlace("hevc", true)) + && !isVaapiH264Encoder + && !isQsvH264Encoder + && !isNvdecH264Decoder) { - string deintParam; - var inputFramerate = videoStream?.RealFrameRate; - - // If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle - if (string.Equals(options.DeinterlaceMethod, "yadif_bob", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30) + if (string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase)) { - deintParam = "yadif=1:-1:0"; + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "bwdif={0}:-1:0", + doubleRateDeinterlace ? "1" : "0")); } else { - deintParam = "yadif=0:-1:0"; - } - - if (!string.IsNullOrEmpty(deintParam)) - { - if (!isVaapiH264Encoder && !isQsvH264Encoder && !isNvdecH264Decoder) - { - filters.Add(deintParam); - } + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "yadif={0}:-1:0", + doubleRateDeinterlace ? "1" : "0")); } } @@ -2397,6 +2470,11 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.DeInterlace("h264", true)) { inputModifier += " -deint 1"; + + if (!encodingOptions.DeinterlaceDoubleRate || (videoStream?.RealFrameRate ?? 60) > 30) + { + inputModifier += " -drop_second_field 1"; + } } } } diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 814edd7323..6ead93e09b 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -24,6 +24,7 @@ + diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs index b7b23deff5..8a7c032c54 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs @@ -282,6 +282,20 @@ namespace MediaBrowser.MediaEncoding.Probing [JsonPropertyName("disposition")] public IReadOnlyDictionary Disposition { get; set; } + /// + /// Gets or sets the color range. + /// + /// The color range. + [JsonPropertyName("color_range")] + public string ColorRange { get; set; } + + /// + /// Gets or sets the color space. + /// + /// The color space. + [JsonPropertyName("color_space")] + public string ColorSpace { get; set; } + /// /// Gets or sets the color transfer. /// diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 40a3b43e1c..22537a4d95 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -714,6 +714,16 @@ namespace MediaBrowser.MediaEncoding.Probing stream.RefFrames = streamInfo.Refs; } + if (!string.IsNullOrEmpty(streamInfo.ColorRange)) + { + stream.ColorRange = streamInfo.ColorRange; + } + + if (!string.IsNullOrEmpty(streamInfo.ColorSpace)) + { + stream.ColorSpace = streamInfo.ColorSpace; + } + if (!string.IsNullOrEmpty(streamInfo.ColorTransfer)) { stream.ColorTransfer = streamInfo.ColorTransfer; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 6ac5ac2ff1..0a9958b9eb 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -31,7 +32,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; private readonly IMediaEncoder _mediaEncoder; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IMediaSourceManager _mediaSourceManager; /// @@ -46,7 +47,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IMediaSourceManager mediaSourceManager) { _libraryManager = libraryManager; @@ -54,7 +55,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles _appPaths = appPaths; _fileSystem = fileSystem; _mediaEncoder = mediaEncoder; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _mediaSourceManager = mediaSourceManager; } @@ -750,24 +751,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - private Task GetStream(string path, MediaProtocol protocol, CancellationToken cancellationToken) + private async Task GetStream(string path, MediaProtocol protocol, CancellationToken cancellationToken) { switch (protocol) { case MediaProtocol.Http: - var opts = new HttpRequestOptions() - { - Url = path, - CancellationToken = cancellationToken, - - // Needed for seeking - BufferContent = true - }; - - return _httpClient.Get(opts); + { + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(path, cancellationToken) + .ConfigureAwait(false); + return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + } case MediaProtocol.File: - return Task.FromResult(File.OpenRead(path)); + return File.OpenRead(path); default: throw new ArgumentOutOfRangeException(nameof(protocol)); } diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 9a30f7e9f0..2cd637c5b0 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -11,6 +11,8 @@ namespace MediaBrowser.Model.Configuration public double DownMixAudioBoost { get; set; } + public int MaxMuxingQueueSize { get; set; } + public bool EnableThrottling { get; set; } public int ThrottleDelaySeconds { get; set; } @@ -29,12 +31,30 @@ namespace MediaBrowser.Model.Configuration public string VaapiDevice { get; set; } + public string OpenclDevice { get; set; } + + public bool EnableTonemapping { get; set; } + + public string TonemappingAlgorithm { get; set; } + + public string TonemappingRange { get; set; } + + public double TonemappingDesat { get; set; } + + public double TonemappingThreshold { get; set; } + + public double TonemappingPeak { get; set; } + + public double TonemappingParam { get; set; } + public int H264Crf { get; set; } public int H265Crf { get; set; } public string EncoderPreset { get; set; } + public bool DeinterlaceDoubleRate { get; set; } + public string DeinterlaceMethod { get; set; } public bool EnableDecodingColorDepth10Hevc { get; set; } @@ -50,13 +70,26 @@ namespace MediaBrowser.Model.Configuration public EncodingOptions() { DownMixAudioBoost = 2; + MaxMuxingQueueSize = 2048; EnableThrottling = false; ThrottleDelaySeconds = 180; EncodingThreadCount = -1; - // This is a DRM device that is almost guaranteed to be there on every intel platform, plus it's the default one in ffmpeg if you don't specify anything + // This is a DRM device that is almost guaranteed to be there on every intel platform, + // plus it's the default one in ffmpeg if you don't specify anything VaapiDevice = "/dev/dri/renderD128"; + // This is the OpenCL device that is used for tonemapping. + // The left side of the dot is the platform number, and the right side is the device number on the platform. + OpenclDevice = "0.0"; + EnableTonemapping = false; + TonemappingAlgorithm = "reinhard"; + TonemappingRange = "auto"; + TonemappingDesat = 0; + TonemappingThreshold = 0.8; + TonemappingPeak = 0; + TonemappingParam = 0; H264Crf = 23; H265Crf = 28; + DeinterlaceDoubleRate = false; DeinterlaceMethod = "yadif"; EnableDecodingColorDepth10Hevc = true; EnableDecodingColorDepth10Vp9 = true; diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index c4fb12e762..68dc1cc83d 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -166,12 +166,6 @@ namespace MediaBrowser.Model.Configuration /// true if [enable dashboard response caching]; otherwise, false. public bool EnableDashboardResponseCaching { get; set; } - /// - /// Gets or sets a custom path to serve the dashboard from. - /// - /// The dashboard source path, or null if the default path should be used. - public string DashboardSourcePath { get; set; } - /// /// Gets or sets the image saving convention. /// diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index f9ec0d2388..2d37618c27 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -35,6 +35,18 @@ namespace MediaBrowser.Model.Entities /// The language. public string Language { get; set; } + /// + /// Gets or sets the color range. + /// + /// The color range. + public string ColorRange { get; set; } + + /// + /// Gets or sets the color space. + /// + /// The color space. + public string ColorSpace { get; set; } + /// /// Gets or sets the color transfer. /// @@ -47,12 +59,6 @@ namespace MediaBrowser.Model.Entities /// The color primaries. public string ColorPrimaries { get; set; } - /// - /// Gets or sets the color space. - /// - /// The color space. - public string ColorSpace { get; set; } - /// /// Gets or sets the comment. /// diff --git a/MediaBrowser.Model/IO/FileSystemMetadata.cs b/MediaBrowser.Model/IO/FileSystemMetadata.cs index b23119d08f..118c78e801 100644 --- a/MediaBrowser.Model/IO/FileSystemMetadata.cs +++ b/MediaBrowser.Model/IO/FileSystemMetadata.cs @@ -56,7 +56,7 @@ namespace MediaBrowser.Model.IO public DateTime CreationTimeUtc { get; set; } /// - /// Gets a value indicating whether this instance is directory. + /// Gets or sets a value indicating whether this instance is directory. /// /// true if this instance is directory; otherwise, false. public bool IsDirectory { get; set; } diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index bba69d4b46..dc65497876 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -201,9 +201,9 @@ namespace MediaBrowser.Model.IO IEnumerable GetFileSystemEntryPaths(string path, bool recursive = false); void SetHidden(string path, bool isHidden); - void SetReadOnly(string path, bool readOnly); + void SetAttributes(string path, bool isHidden, bool readOnly); + List GetDrives(); - void SetExecutable(string path); } } diff --git a/MediaBrowser.Model/IO/IShortcutHandler.cs b/MediaBrowser.Model/IO/IShortcutHandler.cs index 5c663aa0d1..14d5c4b62f 100644 --- a/MediaBrowser.Model/IO/IShortcutHandler.cs +++ b/MediaBrowser.Model/IO/IShortcutHandler.cs @@ -22,7 +22,6 @@ namespace MediaBrowser.Model.IO /// /// The shortcut path. /// The target path. - /// System.String. void Create(string shortcutPath, string targetPath); } } diff --git a/MediaBrowser.Model/IO/IStreamHelper.cs b/MediaBrowser.Model/IO/IStreamHelper.cs index af5ba5b17f..0e09db16e8 100644 --- a/MediaBrowser.Model/IO/IStreamHelper.cs +++ b/MediaBrowser.Model/IO/IStreamHelper.cs @@ -13,8 +13,6 @@ namespace MediaBrowser.Model.IO Task CopyToAsync(Stream source, Stream destination, int bufferSize, int emptyReadLimit, CancellationToken cancellationToken); - Task CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken); - Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken); Task CopyUntilCancelled(Stream source, Stream target, int bufferSize, CancellationToken cancellationToken); diff --git a/MediaBrowser.Model/IO/IZipClient.cs b/MediaBrowser.Model/IO/IZipClient.cs index 2daa54f227..fca52ebae6 100644 --- a/MediaBrowser.Model/IO/IZipClient.cs +++ b/MediaBrowser.Model/IO/IZipClient.cs @@ -26,6 +26,7 @@ namespace MediaBrowser.Model.IO void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles); void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles); + void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName); /// diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 4ae38ade9c..c0a75009ae 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -34,7 +34,7 @@ - + diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs index 771ca84f7c..afe7351d32 100644 --- a/MediaBrowser.Model/Net/MimeTypes.cs +++ b/MediaBrowser.Model/Net/MimeTypes.cs @@ -125,7 +125,7 @@ namespace MediaBrowser.Model.Net { ".wmv", "video/x-ms-wmv" }, // Type audio - { ".aac", "audio/mp4" }, + { ".aac", "audio/aac" }, { ".ac3", "audio/ac3" }, { ".ape", "audio/x-ape" }, { ".dsf", "audio/dsf" }, diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 155e8cb8ab..171b824ca6 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -156,7 +156,7 @@ namespace MediaBrowser.Providers.Manager /// public async Task SaveImage(BaseItem item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken) { - var httpClient = _httpClientFactory.CreateClient(); + var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); var contentType = response.Content.Headers.ContentType.MediaType; diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 85966b3bf5..39f93c479b 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -16,9 +16,9 @@ - - - + + + diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs index c9dac9ecd8..e3a1decb88 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -96,7 +97,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// public Task GetImageResponse(string url, CancellationToken cancellationToken) { - var httpClient = _httpClientFactory.CreateClient(); + var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); return httpClient.GetAsync(url, cancellationToken); } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs index 321144edf9..e6d89e6880 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; @@ -173,7 +174,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb Directory.CreateDirectory(Path.GetDirectoryName(path)); - using var response = await _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs index 6512668681..54851c4d07 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -137,7 +138,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public Task GetImageResponse(string url, CancellationToken cancellationToken) { - var httpClient = _httpClientFactory.CreateClient(); + var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); return httpClient.GetAsync(url, cancellationToken); } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs index 708426500b..670c0cd05a 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; @@ -154,7 +155,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); - using var response = await _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); Directory.CreateDirectory(Path.GetDirectoryName(path)); diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs index 7f10e6922b..8414c93281 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs @@ -14,6 +14,7 @@ using System.Threading; using System.Threading.Tasks; using System.Xml; using MediaBrowser.Common; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -765,7 +766,7 @@ namespace MediaBrowser.Providers.Music _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds); _stopWatchMusicBrainz.Restart(); - response = await _httpClientFactory.CreateClient().SendAsync(options).ConfigureAwait(false); + response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options).ConfigureAwait(false); // We retry a finite number of times, and only whilst MB is indicating 503 (throttling) } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs index c18725e0aa..8b8fea09e8 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -82,7 +83,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } public string Name => "The Open Movie Database"; diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs index 102ad82e17..d53eba7e94 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -9,6 +9,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -129,7 +130,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var url = OmdbProvider.GetOmdbUrl(urlQuery); - using var response = await OmdbProvider.GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false); + using var response = await OmdbProvider.GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var resultList = new List(); @@ -274,7 +275,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } class SearchResult diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index c45149c3a7..32dab60a6b 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -10,6 +10,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; @@ -296,7 +297,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb "i={0}&plot=short&tomatoes=true&r=json", imdbParam)); - using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false); + using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var rootObject = await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false); Directory.CreateDirectory(Path.GetDirectoryName(path)); @@ -334,7 +335,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb imdbParam, seasonId)); - using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false); + using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var rootObject = await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false); Directory.CreateDirectory(Path.GetDirectoryName(path)); diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs index 4e7c0e5a60..de2f6875f8 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs @@ -6,6 +6,7 @@ using System.Net.Http; using System.Globalization; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; @@ -116,7 +117,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs index 90436c7c99..c088d8cec1 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; @@ -153,6 +154,13 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb item.IndexNumber = Convert.ToInt32(episode.DvdEpisodeNumber ?? episode.AiredEpisodeNumber); item.ParentIndexNumber = episode.DvdSeason ?? episode.AiredSeason; } + else if (string.Equals(id.SeriesDisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase)) + { + if (episode.AbsoluteNumber.GetValueOrDefault() != 0) + { + item.IndexNumber = episode.AbsoluteNumber; + } + } else if (episode.AiredEpisodeNumber.HasValue) { item.IndexNumber = episode.AiredEpisodeNumber; @@ -244,7 +252,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } public int Order => 0; diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs index 388a4e3e72..dc3c60dee0 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -106,7 +107,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb /// public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs index ff8c3455fa..49576d488d 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; @@ -148,7 +149,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs index d287828eb5..d96840e51c 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -146,7 +147,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs index c6dd8a5f3d..ca9b1d738f 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs @@ -8,6 +8,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -410,7 +411,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs index f434440283..f6592afe46 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Providers; @@ -155,7 +156,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs index 4da2c042f6..e627550f13 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs @@ -10,6 +10,7 @@ using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; @@ -271,7 +272,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs index 60d7a599ea..a975fb8f6a 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Providers; @@ -204,7 +205,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index d8918bb6b9..f8bc19395f 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -383,7 +384,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies internal Task GetMovieDbResponse(HttpRequestMessage message) { message.Headers.UserAgent.ParseAdd(_appHost.ApplicationUserAgent); - return _httpClientFactory.CreateClient().SendAsync(message); + return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(message); } /// @@ -392,7 +393,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies /// public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs index f31a7faea2..f2d2c8120e 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; @@ -31,9 +32,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People _httpClientFactory = httpClientFactory; } + public static string ProviderName => TmdbUtils.ProviderName; + + /// public string Name => ProviderName; - public static string ProviderName => TmdbUtils.ProviderName; + /// + public int Order => 0; public bool Supports(BaseItem item) { @@ -125,11 +130,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return profile.Iso_639_1?.ToString(); } - public int Order => 0; - public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index e9fb5c7034..6bf04b81ae 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; @@ -32,7 +33,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People { const string DataFileName = "info.json"; - internal static TmdbPersonProvider Current { get; private set; } + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; @@ -55,6 +56,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People Current = this; } + internal static TmdbPersonProvider Current { get; private set; } + public string Name => TmdbUtils.ProviderName; public async Task> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken) @@ -95,7 +98,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return new List(); } - var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(searchInfo.Name), TmdbUtils.ApiKey); + var url = string.Format( + CultureInfo.InvariantCulture, + TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}", + WebUtility.UrlEncode(searchInfo.Name), + TmdbUtils.ApiKey); using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); foreach (var header in TmdbUtils.AcceptHeaders) @@ -200,8 +207,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return result; } - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - /// /// Gets the TMDB id. /// @@ -226,7 +231,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return; } - var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", TmdbUtils.ApiKey, id); + var url = string.Format( + CultureInfo.InvariantCulture, + TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", + TmdbUtils.ApiKey, + id); using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); foreach (var header in TmdbUtils.AcceptHeaders) @@ -259,7 +268,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs index 5705885b46..30b7674e33 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; @@ -138,7 +139,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV protected Task GetResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs index 787514d05d..e7e2fd05b8 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; @@ -38,7 +39,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs index e59504cc6d..73ed13267d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs @@ -9,6 +9,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -124,7 +125,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } private async Task GetSeasonInfo(string seriesTmdbId, int season, string preferredMetadataLanguage, diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs index f11eeb15b3..125560175e 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; @@ -182,7 +183,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 0eded32330..aaba6ffc06 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -10,6 +10,7 @@ using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -551,7 +552,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs index 10374bde9e..25296387ba 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Providers; @@ -36,7 +37,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs index 321153c6be..76dc7df7f7 100644 --- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; @@ -122,7 +123,7 @@ namespace MediaBrowser.Providers.Studios public Task GetImageResponse(string url, CancellationToken cancellationToken) { - var httpClient = _httpClientFactory.CreateClient(); + var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); return httpClient.GetAsync(url, cancellationToken); } @@ -140,7 +141,7 @@ namespace MediaBrowser.Providers.Studios if (!fileInfo.Exists || (DateTime.UtcNow - fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays > 1) { - var httpClient = _httpClientFactory.CreateClient(); + var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); Directory.CreateDirectory(Path.GetDirectoryName(file)); await using var response = await httpClient.GetStreamAsync(url).ConfigureAwait(false); diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 75587da1f8..fd45dca2af 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -66,8 +66,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api.Tests", "tests\MediaBrowser.Api.Tests\MediaBrowser.Api.Tests.csproj", "{7C93C84F-105C-48E5-A878-406FA0A5B296}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -178,10 +176,6 @@ Global {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.Build.0 = Debug|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.ActiveCfg = Release|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.Build.0 = Release|Any CPU - {7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7C93C84F-105C-48E5-A878-406FA0A5B296}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7C93C84F-105C-48E5-A878-406FA0A5B296}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/debian/rules b/debian/rules index 2a5d41a696..96541f41b8 100755 --- a/debian/rules +++ b/debian/rules @@ -40,7 +40,7 @@ override_dh_clistrip: override_dh_auto_build: dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \ - "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server + "-p:DebugSymbols=false;DebugType=none" Jellyfin.Server override_dh_auto_clean: dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64 index f9c6a16748..1ac5f76d6c 100644 --- a/deployment/Dockerfile.debian.amd64 +++ b/deployment/Dockerfile.debian.amd64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64 index 5c08444df0..68381e7bfd 100644 --- a/deployment/Dockerfile.debian.arm64 +++ b/deployment/Dockerfile.debian.arm64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf index b54fc3f916..ce1b100c1c 100644 --- a/deployment/Dockerfile.debian.armhf +++ b/deployment/Dockerfile.debian.armhf @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.docker.amd64 b/deployment/Dockerfile.docker.amd64 index 204ded3a49..e04442606e 100644 --- a/deployment/Dockerfile.docker.amd64 +++ b/deployment/Dockerfile.docker.amd64 @@ -12,4 +12,4 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 # because of changes in docker and systemd we need to not build in parallel at the moment # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting -RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" +RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none" diff --git a/deployment/Dockerfile.docker.arm64 b/deployment/Dockerfile.docker.arm64 index eedbaac333..a7ac40492a 100644 --- a/deployment/Dockerfile.docker.arm64 +++ b/deployment/Dockerfile.docker.arm64 @@ -12,4 +12,4 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 # because of changes in docker and systemd we need to not build in parallel at the moment # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting -RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" +RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none" diff --git a/deployment/Dockerfile.docker.armhf b/deployment/Dockerfile.docker.armhf index 2a500246b0..b5a42f55ff 100644 --- a/deployment/Dockerfile.docker.armhf +++ b/deployment/Dockerfile.docker.armhf @@ -12,4 +12,4 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 # because of changes in docker and systemd we need to not build in parallel at the moment # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting -RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" +RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none" diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64 index 3a2f67615c..b4a3c1b76d 100644 --- a/deployment/Dockerfile.linux.amd64 +++ b/deployment/Dockerfile.linux.amd64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.macos b/deployment/Dockerfile.macos index d1c0784ff8..7912e018e5 100644 --- a/deployment/Dockerfile.macos +++ b/deployment/Dockerfile.macos @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable index 7270188fd1..949f1ef8f8 100644 --- a/deployment/Dockerfile.portable +++ b/deployment/Dockerfile.portable @@ -15,7 +15,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index b7d3b4bde1..9518d84936 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index dc90f9fbdf..0174f2f2a9 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index db98610c9d..0e02240c8f 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.windows.amd64 b/deployment/Dockerfile.windows.amd64 index 95fd10cf40..d1f2f9e48b 100644 --- a/deployment/Dockerfile.windows.amd64 +++ b/deployment/Dockerfile.windows.amd64 @@ -15,7 +15,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/build.linux.amd64 b/deployment/build.linux.amd64 index a7fb0544ad..a1e7f661a5 100755 --- a/deployment/build.linux.amd64 +++ b/deployment/build.linux.amd64 @@ -16,7 +16,7 @@ else fi # Build archives -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true" tar -czf jellyfin-server_${version}_linux-amd64.tar.gz -C dist jellyfin-server_${version} rm -rf dist/jellyfin-server_${version} diff --git a/deployment/build.macos b/deployment/build.macos index d808141acc..6255c80cb2 100755 --- a/deployment/build.macos +++ b/deployment/build.macos @@ -16,7 +16,7 @@ else fi # Build archives -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true" tar -czf jellyfin-server_${version}_macos-amd64.tar.gz -C dist jellyfin-server_${version} rm -rf dist/jellyfin-server_${version} diff --git a/deployment/build.portable b/deployment/build.portable index 24a8cbf32e..ea40ade5d9 100755 --- a/deployment/build.portable +++ b/deployment/build.portable @@ -16,7 +16,7 @@ else fi # Build archives -dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" +dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true" tar -czf jellyfin-server_${version}_portable.tar.gz -C dist jellyfin-server_${version} rm -rf dist/jellyfin-server_${version} diff --git a/deployment/build.windows.amd64 b/deployment/build.windows.amd64 index bd5dc64381..afe4905769 100755 --- a/deployment/build.windows.amd64 +++ b/deployment/build.windows.amd64 @@ -23,7 +23,7 @@ fi output_dir="dist/jellyfin-server_${version}" # Build binary -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output ${output_dir}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output ${output_dir}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true" # Prepare addins addin_build_dir="$( mktemp -d )" diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index a1e0b5f143..37b573e505 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -54,7 +54,7 @@ The Jellyfin media server backend. export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \ - "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server + "-p:DebugSymbols=false;DebugType=none" Jellyfin.Server %{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/jellyfin/LICENSE %{__install} -D -m 0644 %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf %{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json diff --git a/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs b/tests/Jellyfin.Api.Tests/BrandingServiceTests.cs similarity index 97% rename from tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs rename to tests/Jellyfin.Api.Tests/BrandingServiceTests.cs index 5d7f7765cd..6fc287420b 100644 --- a/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs +++ b/tests/Jellyfin.Api.Tests/BrandingServiceTests.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using MediaBrowser.Model.Branding; using Xunit; -namespace MediaBrowser.Api.Tests +namespace Jellyfin.Api.Tests { public sealed class BrandingServiceTests : IClassFixture { diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index f77eba376b..bcba3a2032 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -14,12 +14,13 @@ - - - - + + + + + - + diff --git a/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs similarity index 99% rename from tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs rename to tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs index 2029f88e92..77f1640fa3 100644 --- a/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs @@ -15,7 +15,7 @@ using Microsoft.Extensions.Logging; using Serilog; using Serilog.Extensions.Logging; -namespace MediaBrowser.Api.Tests +namespace Jellyfin.Api.Tests { /// /// Factory for bootstrapping the Jellyfin application in memory for functional end to end tests. diff --git a/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs b/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs new file mode 100644 index 0000000000..3a85b55141 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs @@ -0,0 +1,42 @@ +using System.IO; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; +using MediaBrowser.Model.Branding; +using Xunit; +using Xunit.Abstractions; + +namespace Jellyfin.Api.Tests +{ + public sealed class OpenApiSpecTests : IClassFixture + { + private readonly JellyfinApplicationFactory _factory; + private readonly ITestOutputHelper _outputHelper; + + public OpenApiSpecTests(JellyfinApplicationFactory factory, ITestOutputHelper outputHelper) + { + _factory = factory; + _outputHelper = outputHelper; + } + + [Fact] + public async Task GetSpec_ReturnsCorrectResponse() + { + // Arrange + var client = _factory.CreateClient(); + + // Act + var response = await client.GetAsync("/api-docs/openapi.json"); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString()); + + // Write out for publishing + var responseBody = await response.Content.ReadAsStringAsync(); + string outputPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "openapi.json")); + _outputHelper.WriteLine("Writing OpenAPI Spec JSON to '{0}'.", outputPath); + File.WriteAllText(outputPath, responseBody); + } + } +} diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index 7464740445..e3f87d29b7 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -13,9 +13,9 @@ - + - + diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index 1559f70ab3..5de02a29ba 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -13,9 +13,9 @@ - + - + diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index e1a0895476..3ac60819b4 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -19,9 +19,9 @@ - + - + diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index 0e9e915632..37d0a9929a 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -13,9 +13,9 @@ - + - + diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs index 917d8fb3a9..ed971eed7b 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs @@ -47,6 +47,10 @@ namespace Jellyfin.Naming.Tests.Video // FIXME: [InlineData("Robin Hood [Multi-Subs] [2018].mkv", "Robin Hood", 2018)] [InlineData(@"3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again [InlineData("3 days to kill (2005).mkv", "3 days to kill", 2005)] + [InlineData("My Movie 2013.12.09", "My Movie 2013.12.09", null)] + [InlineData("My Movie 2013-12-09", "My Movie 2013-12-09", null)] + [InlineData("My Movie 20131209", "My Movie 20131209", null)] + [InlineData("My Movie 2013-12-09 2013", "My Movie 2013-12-09", 2013)] public void CleanDateTimeTest(string input, string expectedName, int? expectedYear) { input = Path.GetFileName(input); diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 03187f4b9c..d1679c2796 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -15,10 +15,11 @@ - + + - + diff --git a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj deleted file mode 100644 index a4ef10648b..0000000000 --- a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - netcoreapp3.1 - false - true - enable - - - - - - - - - - - - - - - - - - - - - - - - ../jellyfin-tests.ruleset - - - diff --git a/tests/jellyfin-tests.ruleset b/tests/jellyfin-tests.ruleset index 5a113e955e..e2abaf5bb8 100644 --- a/tests/jellyfin-tests.ruleset +++ b/tests/jellyfin-tests.ruleset @@ -1,5 +1,5 @@ - + @@ -17,6 +17,6 @@ - + diff --git a/windows/build-jellyfin.ps1 b/windows/build-jellyfin.ps1 index b76a8e0bbe..b65e619ee1 100644 --- a/windows/build-jellyfin.ps1 +++ b/windows/build-jellyfin.ps1 @@ -40,7 +40,7 @@ function Build-JellyFin { Write-Verbose "windowsversion-Architecture: $windowsversion-$Architecture" Write-Verbose "InstallLocation: $ResolvedInstallLocation" Write-Verbose "DotNetVerbosity: $DotNetVerbosity" - dotnet publish --self-contained -c $BuildType --output $ResolvedInstallLocation -v $DotNetVerbosity -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:DebugType=none --runtime `"$windowsversion-$Architecture`" Jellyfin.Server + dotnet publish --self-contained -c $BuildType --output $ResolvedInstallLocation -v $DotNetVerbosity -p:GenerateDocumentationFile=true -p:DebugSymbols=false -p:DebugType=none --runtime `"$windowsversion-$Architecture`" Jellyfin.Server } function Install-FFMPEG {