diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml index 4b82eedb4..547a514f8 100644 --- a/.ci/azure-pipelines-abi.yml +++ b/.ci/azure-pipelines-abi.yml @@ -7,7 +7,7 @@ parameters: default: "ubuntu-latest" - name: DotNetSdkVersion type: string - default: 7.0.x + default: 8.0.x jobs: - job: CompatibilityCheck diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml index 020d7fff4..0702aeb6b 100644 --- a/.ci/azure-pipelines-main.yml +++ b/.ci/azure-pipelines-main.yml @@ -1,7 +1,7 @@ parameters: LinuxImage: 'ubuntu-latest' RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj' - DotNetSdkVersion: 7.0.x + DotNetSdkVersion: 8.0.x jobs: - job: Build diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index c91a084e5..39f98e063 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -208,10 +208,10 @@ jobs: steps: - task: UseDotNet@2 - displayName: 'Use .NET 7.0 sdk' + displayName: 'Use .NET 8.0 sdk' inputs: packageType: 'sdk' - version: '7.0.x' + version: '8.0.x' - task: DotNetCoreCLI@2 displayName: 'Build Stable Nuget packages' diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml index 81362aab2..3549c691c 100644 --- a/.ci/azure-pipelines-test.yml +++ b/.ci/azure-pipelines-test.yml @@ -10,7 +10,7 @@ parameters: default: "tests/**/*Tests.csproj" - name: DotNetSdkVersion type: string - default: 7.0.x + default: 8.0.x jobs: - job: Test @@ -94,5 +94,5 @@ jobs: displayName: 'Publish OpenAPI Artifact' condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) inputs: - targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net7.0/openapi.json" + targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json" artifactName: 'OpenAPI Spec' diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index dbe78984a..c03564f97 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,10 +3,10 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "7.0.13", + "version": "8.0.0", "commands": [ "dotnet-ef" ] } } -} \ No newline at end of file +} diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index f43d743f0..2a60d1805 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -24,14 +24,14 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: - dotnet-version: '7.0.x' + dotnet-version: '8.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/init@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/autobuild@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/analyze@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8 diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 8c463a8fc..62445a1ca 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -21,7 +21,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: - dotnet-version: '7.0.x' + dotnet-version: '8.0.x' - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json @@ -30,7 +30,7 @@ jobs: name: openapi-head retention-days: 14 if-no-files-found: error - path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net7.0/openapi.json + path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json openapi-base: name: OpenAPI - BASE @@ -55,7 +55,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: - dotnet-version: '7.0.x' + dotnet-version: '8.0.x' - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json @@ -64,7 +64,7 @@ jobs: name: openapi-base retention-days: 14 if-no-files-found: error - path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net7.0/openapi.json + path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json openapi-diff: permissions: diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 36686e64b..3188828e1 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -9,7 +9,7 @@ on: pull_request: env: - SDK_VERSION: "7.0.x" + SDK_VERSION: "8.0.x" jobs: run-tests: @@ -34,7 +34,7 @@ jobs: --verbosity minimal - name: Merge code coverage results - uses: danielpalme/ReportGenerator-GitHub-Action@873ee34c88a6234bdab7fd264d3666fd1ab417f7 # 5 + uses: danielpalme/ReportGenerator-GitHub-Action@4d510cbed8a05af5aefea46c7fd6e05b95844c89 # 5 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" diff --git a/.vscode/launch.json b/.vscode/launch.json index 55e6508a9..be55764fd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net7.0/jellyfin.dll", + "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll", "args": [], "cwd": "${workspaceFolder}/Jellyfin.Server", "console": "internalConsole", @@ -22,7 +22,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net7.0/jellyfin.dll", + "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll", "args": ["--nowebclient"], "cwd": "${workspaceFolder}/Jellyfin.Server", "console": "internalConsole", diff --git a/Directory.Packages.props b/Directory.Packages.props index 9109a5a18..b0765c0de 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,32 +23,31 @@ - + - + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - @@ -58,9 +57,9 @@ - + - + @@ -77,9 +76,9 @@ - - - + + + @@ -88,4 +87,4 @@ - \ No newline at end of file + diff --git a/Dockerfile b/Dockerfile index 9be319311..d3f10cd12 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ ##################################### # Requires binfm_misc registration # https://github.com/multiarch/qemu-user-static#binfmt_misc-register -ARG DOTNET_VERSION=7.0 +ARG DOTNET_VERSION=8.0 FROM node:20-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master diff --git a/Dockerfile.arm b/Dockerfile.arm index e8ec6398e..db1acc838 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -2,7 +2,7 @@ ##################################### # Requires binfm_misc registration # https://github.com/multiarch/qemu-user-static#binfmt_misc-register -ARG DOTNET_VERSION=7.0 +ARG DOTNET_VERSION=8.0 FROM node:20-alpine as web-builder diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 83137ee89..3eb5f45fc 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -2,7 +2,7 @@ ##################################### # Requires binfm_misc registration # https://github.com/multiarch/qemu-user-static#binfmt_misc-register -ARG DOTNET_VERSION=7.0 +ARG DOTNET_VERSION=8.0 FROM node:20-alpine as web-builder diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index efbef0564..7336482e5 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -17,7 +17,7 @@ - net7.0 + net8.0 false true diff --git a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs index 87ec14d95..82c80070a 100644 --- a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs +++ b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using System.Net.Http; using System.Text; using Emby.Dlna.ConnectionManager; using Emby.Dlna.ContentDirectory; +using Emby.Dlna.Main; using Emby.Dlna.MediaReceiverRegistrar; using Emby.Dlna.Ssdp; using MediaBrowser.Common.Net; @@ -65,5 +66,7 @@ public static class DlnaServiceCollectionExtensions { IsShared = true }); + + services.AddHostedService(); } } diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs deleted file mode 100644 index aa7012487..000000000 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ /dev/null @@ -1,363 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Net.Sockets; -using System.Threading.Tasks; -using Emby.Dlna.PlayTo; -using Emby.Dlna.Ssdp; -using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Extensions; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Globalization; -using Microsoft.Extensions.Logging; -using Rssdp; -using Rssdp.Infrastructure; - -namespace Emby.Dlna.Main -{ - public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup - { - private readonly IServerConfigurationManager _config; - private readonly ILogger _logger; - private readonly IServerApplicationHost _appHost; - private readonly ISessionManager _sessionManager; - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILibraryManager _libraryManager; - private readonly IUserManager _userManager; - private readonly IDlnaManager _dlnaManager; - private readonly IImageProcessor _imageProcessor; - private readonly IUserDataManager _userDataManager; - private readonly ILocalizationManager _localization; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly IDeviceDiscovery _deviceDiscovery; - private readonly ISsdpCommunicationsServer _communicationsServer; - private readonly INetworkManager _networkManager; - private readonly object _syncLock = new(); - private readonly bool _disabled; - - private PlayToManager _manager; - private SsdpDevicePublisher _publisher; - - private bool _disposed; - - public DlnaEntryPoint( - IServerConfigurationManager config, - ILoggerFactory loggerFactory, - IServerApplicationHost appHost, - ISessionManager sessionManager, - IHttpClientFactory httpClientFactory, - ILibraryManager libraryManager, - IUserManager userManager, - IDlnaManager dlnaManager, - IImageProcessor imageProcessor, - IUserDataManager userDataManager, - ILocalizationManager localizationManager, - IMediaSourceManager mediaSourceManager, - IDeviceDiscovery deviceDiscovery, - IMediaEncoder mediaEncoder, - ISsdpCommunicationsServer communicationsServer, - INetworkManager networkManager) - { - _config = config; - _appHost = appHost; - _sessionManager = sessionManager; - _httpClientFactory = httpClientFactory; - _libraryManager = libraryManager; - _userManager = userManager; - _dlnaManager = dlnaManager; - _imageProcessor = imageProcessor; - _userDataManager = userDataManager; - _localization = localizationManager; - _mediaSourceManager = mediaSourceManager; - _deviceDiscovery = deviceDiscovery; - _mediaEncoder = mediaEncoder; - _communicationsServer = communicationsServer; - _networkManager = networkManager; - _logger = loggerFactory.CreateLogger(); - - var netConfig = config.GetConfiguration(NetworkConfigurationStore.StoreKey); - _disabled = appHost.ListenWithHttps && netConfig.RequireHttps; - - if (_disabled && _config.GetDlnaConfiguration().EnableServer) - { - _logger.LogError("The DLNA specification does not support HTTPS."); - } - } - - public async Task RunAsync() - { - await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); - - if (_disabled) - { - // No use starting as dlna won't work, as we're running purely on HTTPS. - return; - } - - ReloadComponents(); - - _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; - } - - private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) - { - if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) - { - ReloadComponents(); - } - } - - private void ReloadComponents() - { - var options = _config.GetDlnaConfiguration(); - StartDeviceDiscovery(); - - if (options.EnableServer) - { - StartDevicePublisher(options); - } - else - { - DisposeDevicePublisher(); - } - - if (options.EnablePlayTo) - { - StartPlayToManager(); - } - else - { - DisposePlayToManager(); - } - } - - private void StartDeviceDiscovery() - { - try - { - ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error starting device discovery"); - } - } - - public void StartDevicePublisher(Configuration.DlnaOptions options) - { - if (_publisher is not null) - { - return; - } - - try - { - _publisher = new SsdpDevicePublisher( - _communicationsServer, - Environment.OSVersion.Platform.ToString(), - // Can not use VersionString here since that includes OS and version - Environment.OSVersion.Version.ToString(), - _config.GetDlnaConfiguration().SendOnlyMatchedHost) - { - LogFunction = (msg) => _logger.LogDebug("{Msg}", msg), - SupportPnpRootDevice = false - }; - - RegisterServerEndpoints(); - - if (options.BlastAliveMessages) - { - _publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error registering endpoint"); - } - } - - private void RegisterServerEndpoints() - { - var udn = CreateUuid(_appHost.SystemId); - var descriptorUri = "/dlna/" + udn + "/description.xml"; - - // Only get bind addresses in LAN - // IPv6 is currently unsupported - var validInterfaces = _networkManager.GetInternalBindAddresses() - .Where(x => x.Address is not null) - .Where(x => x.AddressFamily != AddressFamily.InterNetworkV6) - .ToList(); - - if (validInterfaces.Count == 0) - { - // No interfaces returned, fall back to loopback - validInterfaces = _networkManager.GetLoopbacks().ToList(); - } - - foreach (var intf in validInterfaces) - { - var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; - - _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address); - - var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri); - - var device = new SsdpRootDevice - { - CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. - Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document. - Address = intf.Address, - PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.Prefix), - FriendlyName = "Jellyfin", - Manufacturer = "Jellyfin", - ModelName = "Jellyfin Server", - Uuid = udn - // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. - }; - - SetProperties(device, fullService); - _publisher.AddDevice(device); - - var embeddedDevices = new[] - { - "urn:schemas-upnp-org:service:ContentDirectory:1", - "urn:schemas-upnp-org:service:ConnectionManager:1", - // "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" - }; - - foreach (var subDevice in embeddedDevices) - { - var embeddedDevice = new SsdpEmbeddedDevice - { - FriendlyName = device.FriendlyName, - Manufacturer = device.Manufacturer, - ModelName = device.ModelName, - Uuid = udn - // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. - }; - - SetProperties(embeddedDevice, subDevice); - device.AddDevice(embeddedDevice); - } - } - } - - private static string CreateUuid(string text) - { - if (!Guid.TryParse(text, out var guid)) - { - guid = text.GetMD5(); - } - - return guid.ToString("D", CultureInfo.InvariantCulture); - } - - private static void SetProperties(SsdpDevice device, string fullDeviceType) - { - var serviceParts = fullDeviceType - .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase) - .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase) - .Split(':'); - - device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-'); - device.DeviceClass = serviceParts[1]; - device.DeviceType = serviceParts[2]; - } - - private void StartPlayToManager() - { - lock (_syncLock) - { - if (_manager is not null) - { - return; - } - - try - { - _manager = new PlayToManager( - _logger, - _sessionManager, - _libraryManager, - _userManager, - _dlnaManager, - _appHost, - _imageProcessor, - _deviceDiscovery, - _httpClientFactory, - _userDataManager, - _localization, - _mediaSourceManager, - _mediaEncoder); - - _manager.Start(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error starting PlayTo manager"); - } - } - } - - private void DisposePlayToManager() - { - lock (_syncLock) - { - if (_manager is not null) - { - try - { - _logger.LogInformation("Disposing PlayToManager"); - _manager.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error disposing PlayTo manager"); - } - - _manager = null; - } - } - } - - public void DisposeDevicePublisher() - { - if (_publisher is not null) - { - _logger.LogInformation("Disposing SsdpDevicePublisher"); - _publisher.Dispose(); - _publisher = null; - } - } - - /// - public void Dispose() - { - if (_disposed) - { - return; - } - - DisposeDevicePublisher(); - DisposePlayToManager(); - _disposed = true; - } - } -} diff --git a/Emby.Dlna/Main/DlnaHost.cs b/Emby.Dlna/Main/DlnaHost.cs new file mode 100644 index 000000000..58db7c26f --- /dev/null +++ b/Emby.Dlna/Main/DlnaHost.cs @@ -0,0 +1,387 @@ +#pragma warning disable CA1031 // Do not catch general exception types. + +using System; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Emby.Dlna.PlayTo; +using Emby.Dlna.Ssdp; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Globalization; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Rssdp; +using Rssdp.Infrastructure; + +namespace Emby.Dlna.Main; + +/// +/// A that manages a DLNA server. +/// +public sealed class DlnaHost : IHostedService, IDisposable +{ + private readonly ILogger _logger; + private readonly IServerConfigurationManager _config; + private readonly IServerApplicationHost _appHost; + private readonly ISessionManager _sessionManager; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDlnaManager _dlnaManager; + private readonly IImageProcessor _imageProcessor; + private readonly IUserDataManager _userDataManager; + private readonly ILocalizationManager _localization; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IDeviceDiscovery _deviceDiscovery; + private readonly ISsdpCommunicationsServer _communicationsServer; + private readonly INetworkManager _networkManager; + private readonly object _syncLock = new(); + + private SsdpDevicePublisher? _publisher; + private PlayToManager? _manager; + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + public DlnaHost( + IServerConfigurationManager config, + ILoggerFactory loggerFactory, + IServerApplicationHost appHost, + ISessionManager sessionManager, + IHttpClientFactory httpClientFactory, + ILibraryManager libraryManager, + IUserManager userManager, + IDlnaManager dlnaManager, + IImageProcessor imageProcessor, + IUserDataManager userDataManager, + ILocalizationManager localizationManager, + IMediaSourceManager mediaSourceManager, + IDeviceDiscovery deviceDiscovery, + IMediaEncoder mediaEncoder, + ISsdpCommunicationsServer communicationsServer, + INetworkManager networkManager) + { + _config = config; + _appHost = appHost; + _sessionManager = sessionManager; + _httpClientFactory = httpClientFactory; + _libraryManager = libraryManager; + _userManager = userManager; + _dlnaManager = dlnaManager; + _imageProcessor = imageProcessor; + _userDataManager = userDataManager; + _localization = localizationManager; + _mediaSourceManager = mediaSourceManager; + _deviceDiscovery = deviceDiscovery; + _mediaEncoder = mediaEncoder; + _communicationsServer = communicationsServer; + _networkManager = networkManager; + _logger = loggerFactory.CreateLogger(); + } + + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + var netConfig = _config.GetConfiguration(NetworkConfigurationStore.StoreKey); + if (_appHost.ListenWithHttps && netConfig.RequireHttps) + { + if (_config.GetDlnaConfiguration().EnableServer) + { + _logger.LogError("The DLNA specification does not support HTTPS."); + } + + // No use starting as dlna won't work, as we're running purely on HTTPS. + return; + } + + await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); + ReloadComponents(); + + _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; + } + + /// + public Task StopAsync(CancellationToken cancellationToken) + { + Stop(); + + return Task.CompletedTask; + } + + /// + public void Dispose() + { + if (!_disposed) + { + Stop(); + _disposed = true; + } + } + + private void OnNamedConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs e) + { + if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) + { + ReloadComponents(); + } + } + + private void ReloadComponents() + { + var options = _config.GetDlnaConfiguration(); + StartDeviceDiscovery(); + + if (options.EnableServer) + { + StartDevicePublisher(options); + } + else + { + DisposeDevicePublisher(); + } + + if (options.EnablePlayTo) + { + StartPlayToManager(); + } + else + { + DisposePlayToManager(); + } + } + + private static string CreateUuid(string text) + { + if (!Guid.TryParse(text, out var guid)) + { + guid = text.GetMD5(); + } + + return guid.ToString("D", CultureInfo.InvariantCulture); + } + + private static void SetProperties(SsdpDevice device, string fullDeviceType) + { + var serviceParts = fullDeviceType + .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase) + .Split(':'); + + device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-'); + device.DeviceClass = serviceParts[1]; + device.DeviceType = serviceParts[2]; + } + + private void StartDeviceDiscovery() + { + try + { + ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error starting device discovery"); + } + } + + private void StartDevicePublisher(Configuration.DlnaOptions options) + { + if (_publisher is not null) + { + return; + } + + try + { + _publisher = new SsdpDevicePublisher( + _communicationsServer, + Environment.OSVersion.Platform.ToString(), + // Can not use VersionString here since that includes OS and version + Environment.OSVersion.Version.ToString(), + _config.GetDlnaConfiguration().SendOnlyMatchedHost) + { + LogFunction = msg => _logger.LogDebug("{Msg}", msg), + SupportPnpRootDevice = false + }; + + RegisterServerEndpoints(); + + if (options.BlastAliveMessages) + { + _publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error registering endpoint"); + } + } + + private void RegisterServerEndpoints() + { + var udn = CreateUuid(_appHost.SystemId); + var descriptorUri = "/dlna/" + udn + "/description.xml"; + + // Only get bind addresses in LAN + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses() + .Where(x => x.AddressFamily != AddressFamily.InterNetworkV6) + .ToList(); + + if (validInterfaces.Count == 0) + { + // No interfaces returned, fall back to loopback + validInterfaces = _networkManager.GetLoopbacks().ToList(); + } + + foreach (var intf in validInterfaces) + { + var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; + + _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address); + + var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri); + + var device = new SsdpRootDevice + { + CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. + Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document. + Address = intf.Address, + PrefixLength = NetworkUtils.MaskToCidr(intf.Subnet.Prefix), + FriendlyName = "Jellyfin", + Manufacturer = "Jellyfin", + ModelName = "Jellyfin Server", + Uuid = udn + // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. + }; + + SetProperties(device, fullService); + _publisher!.AddDevice(device); + + var embeddedDevices = new[] + { + "urn:schemas-upnp-org:service:ContentDirectory:1", + "urn:schemas-upnp-org:service:ConnectionManager:1", + // "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" + }; + + foreach (var subDevice in embeddedDevices) + { + var embeddedDevice = new SsdpEmbeddedDevice + { + FriendlyName = device.FriendlyName, + Manufacturer = device.Manufacturer, + ModelName = device.ModelName, + Uuid = udn + // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. + }; + + SetProperties(embeddedDevice, subDevice); + device.AddDevice(embeddedDevice); + } + } + } + + private void StartPlayToManager() + { + lock (_syncLock) + { + if (_manager is not null) + { + return; + } + + try + { + _manager = new PlayToManager( + _logger, + _sessionManager, + _libraryManager, + _userManager, + _dlnaManager, + _appHost, + _imageProcessor, + _deviceDiscovery, + _httpClientFactory, + _userDataManager, + _localization, + _mediaSourceManager, + _mediaEncoder); + + _manager.Start(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error starting PlayTo manager"); + } + } + } + + private void DisposePlayToManager() + { + lock (_syncLock) + { + if (_manager is not null) + { + try + { + _logger.LogInformation("Disposing PlayToManager"); + _manager.Dispose(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error disposing PlayTo manager"); + } + + _manager = null; + } + } + } + + private void DisposeDevicePublisher() + { + if (_publisher is not null) + { + _logger.LogInformation("Disposing SsdpDevicePublisher"); + _publisher.Dispose(); + _publisher = null; + } + } + + private void Stop() + { + DisposeDevicePublisher(); + DisposePlayToManager(); + } +} diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index bc7548189..97015efd0 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -6,7 +6,7 @@ - net7.0 + net8.0 false true true @@ -41,10 +41,6 @@ GPL-3.0-only - - - - diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index 5a04bbe49..55dbe393c 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -19,7 +19,7 @@ - net7.0 + net8.0 false true diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index c9bf7f085..4540ab205 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -41,7 +41,6 @@ using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; using Jellyfin.Drawing; using Jellyfin.MediaEncoding.Hls.Playlist; -using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; using Jellyfin.Server.Implementations; using MediaBrowser.Common; @@ -100,6 +99,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; +using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager; namespace Emby.Server.Implementations @@ -310,7 +310,9 @@ namespace Emby.Server.Implementations { _creatingInstances.Add(type); Logger.LogDebug("Creating instance of {Type}", type); - return ActivatorUtilities.CreateInstance(ServiceProvider, type); + return ServiceProvider is null + ? Activator.CreateInstance(type) + : ActivatorUtilities.CreateInstance(ServiceProvider, type); } catch (Exception ex) { @@ -866,7 +868,7 @@ namespace Emby.Server.Implementations yield return typeof(MediaBrowser.MediaEncoding.Encoder.MediaEncoder).Assembly; // Dlna - yield return typeof(DlnaEntryPoint).Assembly; + yield return typeof(DlnaHost).Assembly; // Local metadata yield return typeof(BoxSetXmlSaver).Assembly; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index b48e389ac..905f36e43 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -40,7 +40,7 @@ - net7.0 + net8.0 false true diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index d6da597b8..c4cd935c3 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -9,7 +9,7 @@ using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Plugins; diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index be36bbd2c..a83d7a410 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -1,7 +1,3 @@ -#nullable disable - -#pragma warning disable CS1591 - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -23,476 +19,382 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.EntryPoints +namespace Emby.Server.Implementations.EntryPoints; + +/// +/// A that notifies users when libraries are updated. +/// +public sealed class LibraryChangedNotifier : IServerEntryPoint { - public class LibraryChangedNotifier : IServerEntryPoint + private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _configurationManager; + private readonly IProviderManager _providerManager; + private readonly ISessionManager _sessionManager; + private readonly IUserManager _userManager; + private readonly ILogger _logger; + + private readonly object _libraryChangedSyncLock = new(); + private readonly List _foldersAddedTo = new(); + private readonly List _foldersRemovedFrom = new(); + private readonly List _itemsAdded = new(); + private readonly List _itemsRemoved = new(); + private readonly List _itemsUpdated = new(); + private readonly ConcurrentDictionary _lastProgressMessageTimes = new(); + + private Timer? _libraryUpdateTimer; + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + public LibraryChangedNotifier( + ILibraryManager libraryManager, + IServerConfigurationManager configurationManager, + ISessionManager sessionManager, + IUserManager userManager, + ILogger logger, + IProviderManager providerManager) { - private readonly ILibraryManager _libraryManager; - private readonly IServerConfigurationManager _configurationManager; - private readonly IProviderManager _providerManager; - private readonly ISessionManager _sessionManager; - private readonly IUserManager _userManager; - private readonly ILogger _logger; + _libraryManager = libraryManager; + _configurationManager = configurationManager; + _sessionManager = sessionManager; + _userManager = userManager; + _logger = logger; + _providerManager = providerManager; + } - /// - /// The library changed sync lock. - /// - private readonly object _libraryChangedSyncLock = new object(); + /// + public Task RunAsync() + { + _libraryManager.ItemAdded += OnLibraryItemAdded; + _libraryManager.ItemUpdated += OnLibraryItemUpdated; + _libraryManager.ItemRemoved += OnLibraryItemRemoved; - private readonly List _foldersAddedTo = new List(); - private readonly List _foldersRemovedFrom = new List(); - private readonly List _itemsAdded = new List(); - private readonly List _itemsRemoved = new List(); - private readonly List _itemsUpdated = new List(); - private readonly ConcurrentDictionary _lastProgressMessageTimes = new ConcurrentDictionary(); + _providerManager.RefreshCompleted += OnProviderRefreshCompleted; + _providerManager.RefreshStarted += OnProviderRefreshStarted; + _providerManager.RefreshProgress += OnProviderRefreshProgress; - public LibraryChangedNotifier( - ILibraryManager libraryManager, - IServerConfigurationManager configurationManager, - ISessionManager sessionManager, - IUserManager userManager, - ILogger logger, - IProviderManager providerManager) + return Task.CompletedTask; + } + + private void OnProviderRefreshProgress(object? sender, GenericEventArgs> e) + { + var item = e.Argument.Item1; + + if (!EnableRefreshMessage(item)) { - _libraryManager = libraryManager; - _configurationManager = configurationManager; - _sessionManager = sessionManager; - _userManager = userManager; - _logger = logger; - _providerManager = providerManager; + return; } - /// - /// Gets or sets the library update timer. - /// - /// The library update timer. - private Timer LibraryUpdateTimer { get; set; } + var progress = e.Argument.Item2; - public Task RunAsync() + if (_lastProgressMessageTimes.TryGetValue(item.Id, out var lastMessageSendTime)) { - _libraryManager.ItemAdded += OnLibraryItemAdded; - _libraryManager.ItemUpdated += OnLibraryItemUpdated; - _libraryManager.ItemRemoved += OnLibraryItemRemoved; - - _providerManager.RefreshCompleted += OnProviderRefreshCompleted; - _providerManager.RefreshStarted += OnProviderRefreshStarted; - _providerManager.RefreshProgress += OnProviderRefreshProgress; - - return Task.CompletedTask; - } - - private void OnProviderRefreshProgress(object sender, GenericEventArgs> e) - { - var item = e.Argument.Item1; - - if (!EnableRefreshMessage(item)) + if (progress > 0 && progress < 100 && (DateTime.UtcNow - lastMessageSendTime).TotalMilliseconds < 1000) { return; } + } - var progress = e.Argument.Item2; + _lastProgressMessageTimes.AddOrUpdate(item.Id, _ => DateTime.UtcNow, (_, _) => DateTime.UtcNow); - if (_lastProgressMessageTimes.TryGetValue(item.Id, out var lastMessageSendTime)) + var dict = new Dictionary(); + dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture); + dict["Progress"] = progress.ToString(CultureInfo.InvariantCulture); + + try + { + _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None); + } + catch + { + } + + var collectionFolders = _libraryManager.GetCollectionFolders(item); + + foreach (var collectionFolder in collectionFolders) + { + var collectionFolderDict = new Dictionary { - if (progress > 0 && progress < 100 && (DateTime.UtcNow - lastMessageSendTime).TotalMilliseconds < 1000) - { - return; - } - } - - _lastProgressMessageTimes.AddOrUpdate(item.Id, _ => DateTime.UtcNow, (_, _) => DateTime.UtcNow); - - var dict = new Dictionary(); - dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture); - dict["Progress"] = progress.ToString(CultureInfo.InvariantCulture); + ["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture), + ["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture) + }; try { - _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None); + _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None); } catch { } + } + } - var collectionFolders = _libraryManager.GetCollectionFolders(item); + private void OnProviderRefreshStarted(object? sender, GenericEventArgs e) + { + OnProviderRefreshProgress(sender, new GenericEventArgs>(new Tuple(e.Argument, 0))); + } - foreach (var collectionFolder in collectionFolders) - { - var collectionFolderDict = new Dictionary - { - ["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture), - ["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture) - }; + private void OnProviderRefreshCompleted(object? sender, GenericEventArgs e) + { + OnProviderRefreshProgress(sender, new GenericEventArgs>(new Tuple(e.Argument, 100))); - try - { - _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None); - } - catch - { - } - } + _lastProgressMessageTimes.TryRemove(e.Argument.Id, out _); + } + + private static bool EnableRefreshMessage(BaseItem item) + => item is Folder { IsRoot: false, IsTopParent: true } + and not (AggregateFolder or UserRootFolder or UserView or Channel); + + private void OnLibraryItemAdded(object? sender, ItemChangeEventArgs e) + => OnLibraryChange(e.Item, e.Parent, _itemsAdded, _foldersAddedTo); + + private void OnLibraryItemUpdated(object? sender, ItemChangeEventArgs e) + => OnLibraryChange(e.Item, e.Parent, _itemsUpdated, null); + + private void OnLibraryItemRemoved(object? sender, ItemChangeEventArgs e) + => OnLibraryChange(e.Item, e.Parent, _itemsRemoved, _foldersRemovedFrom); + + private void OnLibraryChange(BaseItem item, BaseItem parent, List itemsList, List? foldersList) + { + if (!FilterItem(item)) + { + return; } - private void OnProviderRefreshStarted(object sender, GenericEventArgs e) + lock (_libraryChangedSyncLock) { - OnProviderRefreshProgress(sender, new GenericEventArgs>(new Tuple(e.Argument, 0))); + var updateDuration = TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration); + + if (_libraryUpdateTimer is null) + { + _libraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, updateDuration, Timeout.InfiniteTimeSpan); + } + else + { + _libraryUpdateTimer.Change(updateDuration, Timeout.InfiniteTimeSpan); + } + + if (foldersList is not null && parent is Folder folder) + { + foldersList.Add(folder); + } + + itemsList.Add(item); + } + } + + private async void LibraryUpdateTimerCallback(object? state) + { + List foldersAddedTo; + List foldersRemovedFrom; + List itemsUpdated; + List itemsAdded; + List itemsRemoved; + lock (_libraryChangedSyncLock) + { + // Remove dupes in case some were saved multiple times + foldersAddedTo = _foldersAddedTo + .DistinctBy(x => x.Id) + .ToList(); + + foldersRemovedFrom = _foldersRemovedFrom + .DistinctBy(x => x.Id) + .ToList(); + + itemsUpdated = _itemsUpdated + .Where(i => !_itemsAdded.Contains(i)) + .DistinctBy(x => x.Id) + .ToList(); + + itemsAdded = _itemsAdded.ToList(); + itemsRemoved = _itemsRemoved.ToList(); + + if (_libraryUpdateTimer is not null) + { + _libraryUpdateTimer.Dispose(); + _libraryUpdateTimer = null; + } + + _itemsAdded.Clear(); + _itemsRemoved.Clear(); + _itemsUpdated.Clear(); + _foldersAddedTo.Clear(); + _foldersRemovedFrom.Clear(); } - private void OnProviderRefreshCompleted(object sender, GenericEventArgs e) + await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false); + } + + private async Task SendChangeNotifications( + List itemsAdded, + List itemsUpdated, + List itemsRemoved, + List foldersAddedTo, + List foldersRemovedFrom, + CancellationToken cancellationToken) + { + var userIds = _sessionManager.Sessions + .Select(i => i.UserId) + .Where(i => !i.Equals(default)) + .Distinct() + .ToArray(); + + foreach (var userId in userIds) { - OnProviderRefreshProgress(sender, new GenericEventArgs>(new Tuple(e.Argument, 100))); + LibraryUpdateInfo info; - _lastProgressMessageTimes.TryRemove(e.Argument.Id, out _); - } - - private static bool EnableRefreshMessage(BaseItem item) - { - if (item is not Folder folder) + try { - return false; + info = GetLibraryUpdateInfo(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, userId); } - - if (folder.IsRoot) - { - return false; - } - - if (folder is AggregateFolder || folder is UserRootFolder) - { - return false; - } - - if (folder is UserView || folder is Channel) - { - return false; - } - - if (!folder.IsTopParent) - { - return false; - } - - return true; - } - - /// - /// Handles the ItemAdded event of the libraryManager control. - /// - /// The source of the event. - /// The instance containing the event data. - private void OnLibraryItemAdded(object sender, ItemChangeEventArgs e) - { - if (!FilterItem(e.Item)) + catch (Exception ex) { + _logger.LogError(ex, "Error in GetLibraryUpdateInfo"); return; } - lock (_libraryChangedSyncLock) + if (info.IsEmpty) { - if (LibraryUpdateTimer is null) - { - LibraryUpdateTimer = new Timer( - LibraryUpdateTimerCallback, - null, - TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), - Timeout.InfiniteTimeSpan); - } - else - { - LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); - } + continue; + } - if (e.Item.GetParent() is Folder parent) - { - _foldersAddedTo.Add(parent); - } - - _itemsAdded.Add(e.Item); + try + { + await _sessionManager.SendMessageToUserSessions( + new List { userId }, + SessionMessageType.LibraryChanged, + info, + cancellationToken) + .ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending LibraryChanged message"); } } + } - /// - /// Handles the ItemUpdated event of the libraryManager control. - /// - /// The source of the event. - /// The instance containing the event data. - private void OnLibraryItemUpdated(object sender, ItemChangeEventArgs e) + private LibraryUpdateInfo GetLibraryUpdateInfo( + List itemsAdded, + List itemsUpdated, + List itemsRemoved, + List foldersAddedTo, + List foldersRemovedFrom, + Guid userId) + { + var user = _userManager.GetUserById(userId); + ArgumentNullException.ThrowIfNull(user); + + var newAndRemoved = new List(); + newAndRemoved.AddRange(foldersAddedTo); + newAndRemoved.AddRange(foldersRemovedFrom); + + var allUserRootChildren = _libraryManager.GetUserRootFolder() + .GetChildren(user, true) + .OfType() + .ToList(); + + return new LibraryUpdateInfo { - if (!FilterItem(e.Item)) - { - return; - } - - lock (_libraryChangedSyncLock) - { - if (LibraryUpdateTimer is null) - { - LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); - } - else - { - LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); - } - - _itemsUpdated.Add(e.Item); - } - } - - /// - /// Handles the ItemRemoved event of the libraryManager control. - /// - /// The source of the event. - /// The instance containing the event data. - private void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e) - { - if (!FilterItem(e.Item)) - { - return; - } - - lock (_libraryChangedSyncLock) - { - if (LibraryUpdateTimer is null) - { - LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); - } - else - { - LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); - } - - if (e.Parent is Folder parent) - { - _foldersRemovedFrom.Add(parent); - } - - _itemsRemoved.Add(e.Item); - } - } - - /// - /// Libraries the update timer callback. - /// - /// The state. - private async void LibraryUpdateTimerCallback(object state) - { - List foldersAddedTo; - List foldersRemovedFrom; - List itemsUpdated; - List itemsAdded; - List itemsRemoved; - lock (_libraryChangedSyncLock) - { - // Remove dupes in case some were saved multiple times - foldersAddedTo = _foldersAddedTo - .DistinctBy(x => x.Id) - .ToList(); - - foldersRemovedFrom = _foldersRemovedFrom - .DistinctBy(x => x.Id) - .ToList(); - - itemsUpdated = _itemsUpdated - .Where(i => !_itemsAdded.Contains(i)) - .DistinctBy(x => x.Id) - .ToList(); - - itemsAdded = _itemsAdded.ToList(); - itemsRemoved = _itemsRemoved.ToList(); - - if (LibraryUpdateTimer is not null) - { - LibraryUpdateTimer.Dispose(); - LibraryUpdateTimer = null; - } - - _itemsAdded.Clear(); - _itemsRemoved.Clear(); - _itemsUpdated.Clear(); - _foldersAddedTo.Clear(); - _foldersRemovedFrom.Clear(); - } - - await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false); - } - - /// - /// Sends the change notifications. - /// - /// The items added. - /// The items updated. - /// The items removed. - /// The folders added to. - /// The folders removed from. - /// The cancellation token. - private async Task SendChangeNotifications(List itemsAdded, List itemsUpdated, List itemsRemoved, List foldersAddedTo, List foldersRemovedFrom, CancellationToken cancellationToken) - { - var userIds = _sessionManager.Sessions - .Select(i => i.UserId) - .Where(i => !i.Equals(default)) + ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) .Distinct() - .ToArray(); + .ToArray(), + ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) + .Distinct() + .ToArray(), + ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, true)) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) + .Distinct() + .ToArray(), + FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) + .Distinct() + .ToArray(), + FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) + .Distinct() + .ToArray(), + CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray() + }; + } - foreach (var userId in userIds) - { - LibraryUpdateInfo info; - - try - { - info = GetLibraryUpdateInfo(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, userId); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in GetLibraryUpdateInfo"); - return; - } - - if (info.IsEmpty) - { - continue; - } - - try - { - await _sessionManager.SendMessageToUserSessions(new List { userId }, SessionMessageType.LibraryChanged, info, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error sending LibraryChanged message"); - } - } - } - - /// - /// Gets the library update info. - /// - /// The items added. - /// The items updated. - /// The items removed. - /// The folders added to. - /// The folders removed from. - /// The user id. - /// LibraryUpdateInfo. - private LibraryUpdateInfo GetLibraryUpdateInfo(List itemsAdded, List itemsUpdated, List itemsRemoved, List foldersAddedTo, List foldersRemovedFrom, Guid userId) + private static bool FilterItem(BaseItem item) + { + if (!item.IsFolder && !item.HasPathProtocol) { - var user = _userManager.GetUserById(userId); - - var newAndRemoved = new List(); - newAndRemoved.AddRange(foldersAddedTo); - newAndRemoved.AddRange(foldersRemovedFrom); - - var allUserRootChildren = _libraryManager.GetUserRootFolder().GetChildren(user, true).OfType().ToList(); - - return new LibraryUpdateInfo - { - ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(), - - ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(), - - ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, true)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(), - - FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(), - - FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(), - - CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray() - }; + return false; } - private static bool FilterItem(BaseItem item) + if (item is IItemByName && item is not MusicArtist) { - if (!item.IsFolder && !item.HasPathProtocol) - { - return false; - } - - if (item is IItemByName && item is not MusicArtist) - { - return false; - } - - return item.SourceType == SourceType.Library; + return false; } - private IEnumerable GetTopParentIds(List items, List allUserRootChildren) - { - var list = new List(); + return item.SourceType == SourceType.Library; + } - foreach (var item in items) - { - // If the physical root changed, return the user root - if (item is AggregateFolder) - { - continue; - } + private IEnumerable GetTopParentIds(List items, List allUserRootChildren) + { + var list = new List(); - foreach (var folder in allUserRootChildren) - { - list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture)); - } - } - - return list.Distinct(StringComparer.Ordinal); - } - - /// - /// Translates the physical item to user library. - /// - /// The type of item. - /// The item. - /// The user. - /// if set to true [include if not found]. - /// IEnumerable{``0}. - private IEnumerable TranslatePhysicalItemToUserLibrary(T item, User user, bool includeIfNotFound = false) - where T : BaseItem + foreach (var item in items) { // If the physical root changed, return the user root if (item is AggregateFolder) { - return new[] { _libraryManager.GetUserRootFolder() as T }; + continue; } - // Return it only if it's in the user's library - if (includeIfNotFound || item.IsVisibleStandalone(user)) + foreach (var folder in allUserRootChildren) { - return new[] { item }; + list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture)); } - - return Array.Empty(); } - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() + return list.Distinct(StringComparer.Ordinal); + } + + private IEnumerable TranslatePhysicalItemToUserLibrary(T item, User user, bool includeIfNotFound = false) + where T : BaseItem + { + // If the physical root changed, return the user root + if (item is AggregateFolder) { - Dispose(true); - GC.SuppressFinalize(this); + return _libraryManager.GetUserRootFolder() is T t ? new[] { t } : Array.Empty(); } - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool dispose) + // Return it only if it's in the user's library + if (includeIfNotFound || item.IsVisibleStandalone(user)) { - if (dispose) - { - if (LibraryUpdateTimer is not null) - { - LibraryUpdateTimer.Dispose(); - LibraryUpdateTimer = null; - } + return new[] { item }; + } - _libraryManager.ItemAdded -= OnLibraryItemAdded; - _libraryManager.ItemUpdated -= OnLibraryItemUpdated; - _libraryManager.ItemRemoved -= OnLibraryItemRemoved; + return Array.Empty(); + } - _providerManager.RefreshCompleted -= OnProviderRefreshCompleted; - _providerManager.RefreshStarted -= OnProviderRefreshStarted; - _providerManager.RefreshProgress -= OnProviderRefreshProgress; - } + /// + public void Dispose() + { + _libraryManager.ItemAdded -= OnLibraryItemAdded; + _libraryManager.ItemUpdated -= OnLibraryItemUpdated; + _libraryManager.ItemRemoved -= OnLibraryItemRemoved; + + _providerManager.RefreshCompleted -= OnProviderRefreshCompleted; + _providerManager.RefreshStarted -= OnProviderRefreshStarted; + _providerManager.RefreshProgress -= OnProviderRefreshProgress; + + if (_libraryUpdateTimer is not null) + { + _libraryUpdateTimer.Dispose(); + _libraryUpdateTimer = null; } } } diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 7e4994f1a..56ccb21ee 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -6,14 +6,13 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Udp; -using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Plugins; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; namespace Emby.Server.Implementations.EntryPoints { @@ -92,7 +91,7 @@ namespace Emby.Server.Implementations.EntryPoints var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); foreach (var intf in validInterfaces) { - var broadcastAddress = NetworkExtensions.GetBroadcastAddress(intf.Subnet); + var broadcastAddress = NetworkUtils.GetBroadcastAddress(intf.Subnet); _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress, PortNumber); server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index 3ae9e256b..767b94136 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -84,15 +84,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return Task.CompletedTask; } - public Task Close() + public async Task Close() { EnableStreamSharing = false; Logger.LogInformation("Closing {Type}", GetType().Name); - LiveStreamCancellationTokenSource.Cancel(); - - return Task.CompletedTask; + await LiveStreamCancellationTokenSource.CancelAsync().ConfigureAwait(false); } public Stream GetStream() diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json index 9fbf364ef..ecea8df6a 100644 --- a/Emby.Server.Implementations/Localization/Core/af.json +++ b/Emby.Server.Implementations/Localization/Core/af.json @@ -123,5 +123,7 @@ "TaskKeyframeExtractorDescription": "Haal keyframes vanuit video lêers om meer presiese HLS afspeellyste te maak. Dit kan lank duur.", "TaskKeyframeExtractor": "Keyframe Ekstraktor", "External": "Ekstern", - "HearingImpaired": "gehoorgestremd" + "HearingImpaired": "gehoorgestremd", + "TaskRefreshTrickplayImages": "Genereer Fopspeel Beelde", + "TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling." } diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 93d50e6e3..ecdc01a3d 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -124,5 +124,6 @@ "TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.", "TaskKeyframeExtractor": "مستخرج الإطار الرئيسي", "External": "خارجي", - "HearingImpaired": "ضعاف السمع" + "HearingImpaired": "ضعاف السمع", + "TaskRefreshTrickplayImages": "توليد صور Trickplay" } diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index ce8d8fc32..e7279994b 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractor": "Pagrindinių kadrų ištraukėjas", "TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazė, gali pagerinti greitaveiką.", "External": "Išorinis", - "HearingImpaired": "Su klausos sutrikimais" + "HearingImpaired": "Su klausos sutrikimais", + "TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus", + "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose." } diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json index a8fb26b91..13c58e0ab 100644 --- a/Emby.Server.Implementations/Localization/Core/mr.json +++ b/Emby.Server.Implementations/Localization/Core/mr.json @@ -123,5 +123,7 @@ "DeviceOnlineWithName": "{0} कनेक्ट झाले", "DeviceOfflineWithName": "{0} डिस्कनेक्ट झाला आहे", "AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत", - "HearingImpaired": "कर्णबधीर" + "HearingImpaired": "कर्णबधीर", + "TaskRefreshTrickplayImages": "ट्रिकप्ले प्रतिमा तयार करा", + "TaskRefreshTrickplayImagesDescription": "सक्षम लायब्ररीमधील व्हिडिओंसाठी ट्रिकप्ले पूर्वावलोकन तयार करते." } diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index b9b93b7b6..2c8c46050 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractor": "Extrator de quadro-chave", "TaskKeyframeExtractorDescription": "Extrai quadros-chave de arquivos de vídeo para criar listas de reprodução HLS mais precisas. Esta tarefa pode ser executada por um longo tempo.", "External": "Externo", - "HearingImpaired": "Deficiência Auditiva" + "HearingImpaired": "Deficiência Auditiva", + "TaskRefreshTrickplayImages": "Gerar imagens Trickplay", + "TaskRefreshTrickplayImagesDescription": "Cria prévias Trickplay para vídeos em bibliotecas em que o recurso está habilitado." } diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index 2c10bb477..537a6d3f2 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -123,5 +123,7 @@ "TaskKeyframeExtractorDescription": "Extrage cadrele cheie din fișierele video pentru a crea liste de redare HLS mai precise. Această sarcină poate rula o perioadă lungă de timp.", "External": "Extern", "TaskKeyframeExtractor": "Extractor de cadre cheie", - "HearingImpaired": "Ascultare Impară" + "HearingImpaired": "Ascultare Impară", + "TaskRefreshTrickplayImages": "Generează imagini Trickplay", + "TaskRefreshTrickplayImagesDescription": "Generează previzualizările trickplay pentru videourile din librăriile selectate." } diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 20793ee39..db82a2900 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -12,10 +12,11 @@ using System.Threading.Tasks; using Emby.Server.Implementations.Library; using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json.Converters; -using MediaBrowser.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Plugins; @@ -37,7 +38,7 @@ namespace Emby.Server.Implementations.Plugins private readonly List _assemblyLoadContexts; private readonly JsonSerializerOptions _jsonOptions; private readonly ILogger _logger; - private readonly IApplicationHost _appHost; + private readonly IServerApplicationHost _appHost; private readonly ServerConfiguration _config; private readonly List _plugins; private readonly Version _minimumVersion; @@ -48,13 +49,13 @@ namespace Emby.Server.Implementations.Plugins /// Initializes a new instance of the class. /// /// The . - /// The . + /// The . /// The . /// The plugin path. /// The application version. public PluginManager( ILogger logger, - IApplicationHost appHost, + IServerApplicationHost appHost, ServerConfiguration config, string pluginsPath, Version appVersion) @@ -222,7 +223,7 @@ namespace Emby.Server.Implementations.Plugins try { var instance = (IPluginServiceRegistrator?)Activator.CreateInstance(pluginServiceRegistrator); - instance?.RegisterServices(serviceCollection); + instance?.RegisterServices(serviceCollection, _appHost); } #pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index c717744b1..b31b4116d 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -551,8 +551,7 @@ namespace Emby.Server.Implementations.Updates } stream.Position = 0; - using var reader = new ZipArchive(stream); - reader.ExtractToDirectory(targetDir, true); + ZipFile.ExtractToDirectory(stream, targetDir, true); // Ensure we create one or populate existing ones with missing data. await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false); diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index bd3e7d9e3..2853e69b0 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -27,13 +27,12 @@ namespace Jellyfin.Api.Auth /// Options monitor. /// The logger. /// The url encoder. - /// The system clock. public CustomAuthenticationHandler( IAuthService authService, IOptionsMonitor options, ILoggerFactory logger, - UrlEncoder encoder, - ISystemClock clock) : base(options, logger, encoder, clock) + UrlEncoder encoder) + : base(options, logger, encoder) { _authService = authService; _logger = logger.CreateLogger(); diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index c3d02976e..a19a203b5 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -2,6 +2,7 @@ using System; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Data.Queries; +using MediaBrowser.Common.Api; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs index 991f8cbf2..3363d7bad 100644 --- a/Jellyfin.Api/Controllers/ApiKeyController.cs +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Security; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index 2db04afb8..2d9f1ed69 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Dto; using MediaBrowser.Model.Collections; diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 9007dfc41..8db22f7eb 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.ConfigurationDtos; using Jellyfin.Extensions.Json; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index aa0dff212..aa200a722 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Queries; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Devices; diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs index 415385463..79a41ce3b 100644 --- a/Jellyfin.Api/Controllers/DlnaController.cs +++ b/Jellyfin.Api/Controllers/DlnaController.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Dlna; using MediaBrowser.Model.Dlna; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index 42576934b..ce8d910ff 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Emby.Dlna; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Dlna; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs index 8c9ee1a19..284a97621 100644 --- a/Jellyfin.Api/Controllers/EnvironmentController.cs +++ b/Jellyfin.Api/Controllers/EnvironmentController.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.EnvironmentDtos; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Extensions; using MediaBrowser.Model.IO; using Microsoft.AspNetCore.Authorization; @@ -168,7 +169,7 @@ public class EnvironmentController : BaseJellyfinApiController // Check if unc share var index = path.LastIndexOf(UncSeparator); - if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0) + if (index != -1 && path[0] == UncSeparator) { parent = path.Substring(0, index); diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 6eedfd8c7..392d9955f 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -160,7 +160,7 @@ public class HlsSegmentController : BaseJellyfinApiController var pathExtension = Path.GetExtension(path); if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase) || string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase)) - && path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) + && path.Contains(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase)) { playlistPath = path; break; diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 7b10ea170..c031ce338 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; @@ -79,7 +80,7 @@ public class ImageController : BaseJellyfinApiController _appPaths = appPaths; } - private static Stream GetFromBase64Stream(Stream inputStream) + private static CryptoStream GetFromBase64Stream(Stream inputStream) => new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read); /// @@ -2079,30 +2080,30 @@ public class ImageController : BaseJellyfinApiController foreach (var (key, value) in headers) { - Response.Headers.Add(key, value); + Response.Headers.Append(key, value); } Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain; - Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture)); - Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept); + Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture)); + Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept); if (disableCaching) { - Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate"); - Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate"); + Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate"); + Response.Headers.Append(HeaderNames.Pragma, "no-cache, no-store, must-revalidate"); } else { if (cacheDuration.HasValue) { - Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds); + Response.Headers.Append(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds); } else { - Response.Headers.Add(HeaderNames.CacheControl, "public"); + Response.Headers.Append(HeaderNames.CacheControl, "public"); } - Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture)); + Response.Headers.Append(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture)); // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue) diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index b030e74dd..e3aee1bf7 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs index b8f6e91ad..0a8522e1c 100644 --- a/Jellyfin.Api/Controllers/ItemRefreshController.cs +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 3be891b93..4e5ed60d5 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 3cd78b086..af9a93719 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -15,6 +15,7 @@ using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index b012ff42e..d483ca4d2 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryStructureDto; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 58159406a..425086895 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -16,6 +16,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs index b9772a069..f65d95c41 100644 --- a/Jellyfin.Api/Controllers/LocalizationController.cs +++ b/Jellyfin.Api/Controllers/LocalizationController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 94c899357..69b904264 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -150,7 +150,7 @@ public class MusicGenresController : BaseJellyfinApiController MusicGenre? item; - if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1) + if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase)) { item = GetItemFromSlugName(_libraryManager, genreName, dtoOptions, BaseItemKind.MusicGenre); } diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 0ba5e995f..c5e940108 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Updates; diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 72ad14a28..f63e63927 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Extensions.Json; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Model.Net; diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 5c77db240..595cab2df 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index c8fa11ac6..065466cbc 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Model.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index e20cf034d..f0e578e7a 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -10,6 +10,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.SessionDtos; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 1098733b2..41b0858d1 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -3,7 +3,8 @@ using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.StartupDtos; -using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Api; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index c9e256af3..49ca058bd 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -14,6 +14,7 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.SubtitleDtos; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 23abba7dc..383978197 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.SyncPlayDtos; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.SyncPlay; diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 11095a97f..3d4df0386 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net.Mime; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 1be40111d..f9f27f148 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index c0ec646ed..7aa5d01e2 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -12,6 +12,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs index 2d7a56d91..7d9823c25 100644 --- a/Jellyfin.Api/Extensions/DtoExtensions.cs +++ b/Jellyfin.Api/Extensions/DtoExtensions.cs @@ -38,10 +38,10 @@ public static class DtoExtensions if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount)) { - if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1) + if (client.Contains("kodi", StringComparison.OrdinalIgnoreCase) || + client.Contains("wmc", StringComparison.OrdinalIgnoreCase) || + client.Contains("media center", StringComparison.OrdinalIgnoreCase) || + client.Contains("classic", StringComparison.OrdinalIgnoreCase)) { int oldLen = dtoOptions.Fields.Count; var arr = new ItemFields[oldLen + 1]; @@ -53,13 +53,13 @@ public static class DtoExtensions if (!dtoOptions.ContainsField(ItemFields.ChildCount)) { - if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1) + if (client.Contains("kodi", StringComparison.OrdinalIgnoreCase) || + client.Contains("wmc", StringComparison.OrdinalIgnoreCase) || + client.Contains("media center", StringComparison.OrdinalIgnoreCase) || + client.Contains("classic", StringComparison.OrdinalIgnoreCase) || + client.Contains("roku", StringComparison.OrdinalIgnoreCase) || + client.Contains("samsung", StringComparison.OrdinalIgnoreCase) || + client.Contains("androidtv", StringComparison.OrdinalIgnoreCase)) { int oldLen = dtoOptions.Fields.Count; var arr = new ItemFields[oldLen + 1]; diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 24082fcff..a8df628f0 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -147,7 +147,7 @@ public class DynamicHlsHelper cancellationTokenSource.Token) .ConfigureAwait(false); - _httpContextAccessor.HttpContext.Response.Headers.Add(HeaderNames.Expires, "0"); + _httpContextAccessor.HttpContext.Response.Headers.Append(HeaderNames.Expires, "0"); if (isHeadRequest) { return new FileContentResult(Array.Empty(), MimeTypes.GetMimeType("playlist.m3u8")); @@ -568,7 +568,7 @@ public class DynamicHlsHelper && state.VideoStream is not null && state.VideoStream.Level.HasValue) { - levelString = state.VideoStream.Level.ToString() ?? string.Empty; + levelString = state.VideoStream.Level.Value.ToString(CultureInfo.InvariantCulture) ?? string.Empty; } else { diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs index 2155e305d..e2d3bfb19 100644 --- a/Jellyfin.Api/Helpers/HlsHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsHelpers.cs @@ -53,7 +53,7 @@ public static class HlsHelpers break; } - if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1) + if (line.Contains("#EXTINF:", StringComparison.OrdinalIgnoreCase)) { count++; if (count >= segmentCount) diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 6fbbceeab..7d9a38931 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -279,15 +279,15 @@ public static class StreamingHelpers var profile = state.DeviceProfile; StringValues transferMode = request.Headers["transferMode.dlna.org"]; - responseHeaders.Add("transferMode.dlna.org", string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode.ToString()); - responseHeaders.Add("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"); + responseHeaders.Append("transferMode.dlna.org", string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode.ToString()); + responseHeaders.Append("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"); if (state.RunTimeTicks.HasValue) { if (string.Equals(request.Headers["getMediaInfo.sec"], "1", StringComparison.OrdinalIgnoreCase)) { var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds; - responseHeaders.Add("MediaInfo.sec", string.Format( + responseHeaders.Append("MediaInfo.sec", string.Format( CultureInfo.InvariantCulture, "SEC_Duration={0};", Convert.ToInt32(ms))); @@ -305,7 +305,7 @@ public static class StreamingHelpers if (!state.IsVideoRequest) { - responseHeaders.Add("contentFeatures.dlna.org", ContentFeatureBuilder.BuildAudioHeader( + responseHeaders.Append("contentFeatures.dlna.org", ContentFeatureBuilder.BuildAudioHeader( profile, state.OutputContainer, audioCodec, @@ -321,7 +321,7 @@ public static class StreamingHelpers { var videoCodec = state.ActualOutputVideoCodec; - responseHeaders.Add( + responseHeaders.Append( "contentFeatures.dlna.org", ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoRangeType, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty); } @@ -404,12 +404,12 @@ public static class StreamingHelpers var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture); var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture); - responseHeaders.Add("TimeSeekRange.dlna.org", string.Format( + responseHeaders.Append("TimeSeekRange.dlna.org", string.Format( CultureInfo.InvariantCulture, "npt={0}-{1}/{1}", startSeconds, runtimeSeconds)); - responseHeaders.Add("X-AvailableSeekRange", string.Format( + responseHeaders.Append("X-AvailableSeekRange", string.Format( CultureInfo.InvariantCulture, "1 npt={0}-{1}", startSeconds, diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index c16a586d6..77d3edbd6 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -280,6 +280,7 @@ public class TranscodingJobHelper : IDisposable if (job.CancellationTokenSource?.IsCancellationRequested == false) { +#pragma warning disable CA1849 // Can't await in lock block job.CancellationTokenSource.Cancel(); } } @@ -291,7 +292,6 @@ public class TranscodingJobHelper : IDisposable lock (job.ProcessLock!) { -#pragma warning disable CA1849 // Can't await in lock block job.TranscodingThrottler?.Stop().GetAwaiter().GetResult(); var process = job.Process; @@ -405,7 +405,7 @@ public class TranscodingJobHelper : IDisposable var name = Path.GetFileNameWithoutExtension(outputFilePath); var filesToDelete = _fileSystem.GetFilePaths(directory) - .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1); + .Where(f => f.Contains(name, StringComparison.OrdinalIgnoreCase)); List? exs = null; foreach (var file in filesToDelete) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 03dd97367..2473fb288 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -6,7 +6,7 @@ - net7.0 + net8.0 true diff --git a/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs index 2241c68e7..cbd948db0 100644 --- a/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs +++ b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; diff --git a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs index 94de30d1b..d8c95ddff 100644 --- a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs index b75272d3f..f249f3bc6 100644 --- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs +++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs @@ -86,11 +86,11 @@ public class StreamState : EncodingJobInfo, IDisposable { var userAgent = UserAgent ?? string.Empty; - if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1 - || userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1 - || userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 - || userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 - || userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1) + if (userAgent.Contains("AppleTV", StringComparison.OrdinalIgnoreCase) + || userAgent.Contains("cfnetwork", StringComparison.OrdinalIgnoreCase) + || userAgent.Contains("ipad", StringComparison.OrdinalIgnoreCase) + || userAgent.Contains("iphone", StringComparison.OrdinalIgnoreCase) + || userAgent.Contains("ipod", StringComparison.OrdinalIgnoreCase)) { return 6; } diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 847853ca0..75912abf0 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 false true true @@ -23,10 +23,6 @@ GPL-3.0-only - - - - diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs deleted file mode 100644 index 90ebcd390..000000000 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ /dev/null @@ -1,176 +0,0 @@ -#pragma warning disable CA1819 // Properties should not return arrays - -using System; - -namespace Jellyfin.Networking.Configuration -{ - /// - /// Defines the . - /// - public class NetworkConfiguration - { - /// - /// The default value for . - /// - public const int DefaultHttpPort = 8096; - - /// - /// The default value for and . - /// - public const int DefaultHttpsPort = 8920; - - private string _baseUrl = string.Empty; - - /// - /// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at. - /// - public string BaseUrl - { - get => _baseUrl; - set - { - // Normalize the start of the string - if (string.IsNullOrWhiteSpace(value)) - { - // If baseUrl is empty, set an empty prefix string - _baseUrl = string.Empty; - return; - } - - if (value[0] != '/') - { - // If baseUrl was not configured with a leading slash, append one for consistency - value = "/" + value; - } - - // Normalize the end of the string - if (value[^1] == '/') - { - // If baseUrl was configured with a trailing slash, remove it for consistency - value = value.Remove(value.Length - 1); - } - - _baseUrl = value; - } - } - - /// - /// Gets or sets a value indicating whether to use HTTPS. - /// - /// - /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be - /// provided for and . - /// - public bool EnableHttps { get; set; } - - /// - /// Gets or sets a value indicating whether the server should force connections over HTTPS. - /// - public bool RequireHttps { get; set; } - - /// - /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. - /// - public string CertificatePath { get; set; } = string.Empty; - - /// - /// Gets or sets the password required to access the X.509 certificate data in the file specified by . - /// - public string CertificatePassword { get; set; } = string.Empty; - - /// - /// Gets or sets the internal HTTP server port. - /// - /// The HTTP server port. - public int InternalHttpPort { get; set; } = DefaultHttpPort; - - /// - /// Gets or sets the internal HTTPS server port. - /// - /// The HTTPS server port. - public int InternalHttpsPort { get; set; } = DefaultHttpsPort; - - /// - /// Gets or sets the public HTTP port. - /// - /// The public HTTP port. - public int PublicHttpPort { get; set; } = DefaultHttpPort; - - /// - /// Gets or sets the public HTTPS port. - /// - /// The public HTTPS port. - public int PublicHttpsPort { get; set; } = DefaultHttpsPort; - - /// - /// Gets or sets a value indicating whether Autodiscovery is enabled. - /// - public bool AutoDiscovery { get; set; } = true; - - /// - /// Gets or sets a value indicating whether to enable automatic port forwarding. - /// - public bool EnableUPnP { get; set; } - - /// - /// Gets or sets a value indicating whether IPv6 is enabled. - /// - public bool EnableIPv4 { get; set; } = true; - - /// - /// Gets or sets a value indicating whether IPv6 is enabled. - /// - public bool EnableIPv6 { get; set; } - - /// - /// Gets or sets a value indicating whether access from outside of the LAN is permitted. - /// - public bool EnableRemoteAccess { get; set; } = true; - - /// - /// Gets or sets the subnets that are deemed to make up the LAN. - /// - public string[] LocalNetworkSubnets { get; set; } = Array.Empty(); - - /// - /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used. - /// - public string[] LocalNetworkAddresses { get; set; } = Array.Empty(); - - /// - /// Gets or sets the known proxies. - /// - public string[] KnownProxies { get; set; } = Array.Empty(); - - /// - /// Gets or sets a value indicating whether address names that match should be ignored for the purposes of binding. - /// - public bool IgnoreVirtualInterfaces { get; set; } = true; - - /// - /// Gets or sets a value indicating the interface name prefixes that should be ignored. The list can be comma separated and values are case-insensitive. . - /// - public string[] VirtualInterfaceNames { get; set; } = new string[] { "veth" }; - - /// - /// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests. - /// - public bool EnablePublishedServerUriByRequest { get; set; } = false; - - /// - /// Gets or sets the PublishedServerUriBySubnet - /// Gets or sets PublishedServerUri to advertise for specific subnets. - /// - public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); - - /// - /// Gets or sets the filter for remote IP connectivity. Used in conjunction with . - /// - public string[] RemoteIPFilter { get; set; } = Array.Empty(); - - /// - /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist. - /// - public bool IsRemoteIPFilterBlacklist { get; set; } - } -} diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs deleted file mode 100644 index 3ba6bb8fc..000000000 --- a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediaBrowser.Common.Configuration; - -namespace Jellyfin.Networking.Configuration -{ - /// - /// Defines the . - /// - public static class NetworkConfigurationExtensions - { - /// - /// Retrieves the network configuration. - /// - /// The . - /// The . - public static NetworkConfiguration GetNetworkConfiguration(this IConfigurationManager config) - { - return config.GetConfiguration(NetworkConfigurationStore.StoreKey); - } - } -} diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs deleted file mode 100644 index 14726565a..000000000 --- a/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Common.Configuration; - -namespace Jellyfin.Networking.Configuration -{ - /// - /// Defines the . - /// - public class NetworkConfigurationFactory : IConfigurationFactory - { - /// - /// The GetConfigurations. - /// - /// The . - public IEnumerable GetConfigurations() - { - return new[] - { - new NetworkConfigurationStore() - }; - } - } -} diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs deleted file mode 100644 index a268ebb68..000000000 --- a/Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs +++ /dev/null @@ -1,24 +0,0 @@ -using MediaBrowser.Common.Configuration; - -namespace Jellyfin.Networking.Configuration -{ - /// - /// A configuration that stores network related settings. - /// - public class NetworkConfigurationStore : ConfigurationStore - { - /// - /// The name of the configuration in the storage. - /// - public const string StoreKey = "network"; - - /// - /// Initializes a new instance of the class. - /// - public NetworkConfigurationStore() - { - ConfigurationType = typeof(NetworkConfiguration); - Key = StoreKey; - } - } -} diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index 59e6956c7..d59e4e5e3 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -65,7 +65,7 @@ namespace Jellyfin.Networking.HappyEyeballs // See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details. if (await Task.WhenAny(tryConnectAsyncIPv6, Task.Delay(200, cancelIPv6.Token)).ConfigureAwait(false) == tryConnectAsyncIPv6 && tryConnectAsyncIPv6.IsCompletedSuccessfully) { - cancelIPv6.Cancel(); + await cancelIPv6.CancelAsync().ConfigureAwait(false); return tryConnectAsyncIPv6.GetAwaiter().GetResult(); } @@ -76,7 +76,7 @@ namespace Jellyfin.Networking.HappyEyeballs { if (tryConnectAsyncIPv6.IsCompletedSuccessfully) { - cancelIPv4.Cancel(); + await cancelIPv4.CancelAsync().ConfigureAwait(false); return tryConnectAsyncIPv6.GetAwaiter().GetResult(); } @@ -86,7 +86,7 @@ namespace Jellyfin.Networking.HappyEyeballs { if (tryConnectAsyncIPv4.IsCompletedSuccessfully) { - cancelIPv6.Cancel(); + await cancelIPv6.CancelAsync().ConfigureAwait(false); return tryConnectAsyncIPv4.GetAwaiter().GetResult(); } diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index 43d08c37a..30f41aeb2 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -1,6 +1,6 @@ - net7.0 + net8.0 false true diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 9c59500d7..749e0abbb 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -7,9 +7,6 @@ using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; -using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Constants; -using Jellyfin.Networking.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; @@ -18,6 +15,8 @@ using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; +using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace Jellyfin.Networking.Manager { @@ -289,12 +288,12 @@ namespace Jellyfin.Networking.Manager if (IsIPv4Enabled) { - interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); + interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); } if (IsIPv6Enabled) { - interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); + interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); } } @@ -319,24 +318,24 @@ namespace Jellyfin.Networking.Manager var subnets = config.LocalNetworkSubnets; // If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN - if (!NetworkExtensions.TryParseToSubnets(subnets, out var lanSubnets, false) || lanSubnets.Count == 0) + if (!NetworkUtils.TryParseToSubnets(subnets, out var lanSubnets, false) || lanSubnets.Count == 0) { _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); var fallbackLanSubnets = new List(); if (IsIPv6Enabled) { - fallbackLanSubnets.Add(Network.IPv6RFC4291Loopback); // RFC 4291 (Loopback) - fallbackLanSubnets.Add(Network.IPv6RFC4291SiteLocal); // RFC 4291 (Site local) - fallbackLanSubnets.Add(Network.IPv6RFC4193UniqueLocal); // RFC 4193 (Unique local) + fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4291Loopback); // RFC 4291 (Loopback) + fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4291SiteLocal); // RFC 4291 (Site local) + fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4193UniqueLocal); // RFC 4193 (Unique local) } if (IsIPv4Enabled) { - fallbackLanSubnets.Add(Network.IPv4RFC5735Loopback); // RFC 5735 (Loopback) - fallbackLanSubnets.Add(Network.IPv4RFC1918PrivateClassA); // RFC 1918 (private Class A) - fallbackLanSubnets.Add(Network.IPv4RFC1918PrivateClassB); // RFC 1918 (private Class B) - fallbackLanSubnets.Add(Network.IPv4RFC1918PrivateClassC); // RFC 1918 (private Class C) + fallbackLanSubnets.Add(NetworkConstants.IPv4RFC5735Loopback); // RFC 5735 (Loopback) + fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassA); // RFC 1918 (private Class A) + fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassB); // RFC 1918 (private Class B) + fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassC); // RFC 1918 (private Class C) } _lanSubnets = fallbackLanSubnets; @@ -346,7 +345,7 @@ namespace Jellyfin.Networking.Manager _lanSubnets = lanSubnets; } - _excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true) + _excludedSubnets = NetworkUtils.TryParseToSubnets(subnets, out var excludedSubnets, true) ? excludedSubnets : new List(); } @@ -364,7 +363,7 @@ namespace Jellyfin.Networking.Manager var localNetworkAddresses = config.LocalNetworkAddresses; if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0])) { - var bindAddresses = localNetworkAddresses.Select(p => NetworkExtensions.TryParseToSubnet(p, out var network) + var bindAddresses = localNetworkAddresses.Select(p => NetworkUtils.TryParseToSubnet(p, out var network) ? network.Prefix : (interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)) .Select(x => x.Address) @@ -375,12 +374,12 @@ namespace Jellyfin.Networking.Manager if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback))) { - interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); + interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); } if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback))) { - interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); + interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); } } @@ -426,12 +425,12 @@ namespace Jellyfin.Networking.Manager { // Parse config values into filter collection var remoteIPFilter = config.RemoteIPFilter; - if (remoteIPFilter.Any() && !string.IsNullOrWhiteSpace(remoteIPFilter.First())) + if (remoteIPFilter.Length != 0 && !string.IsNullOrWhiteSpace(remoteIPFilter[0])) { // Parse all IPs with netmask to a subnet var remoteAddressFilter = new List(); var remoteFilteredSubnets = remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray(); - if (NetworkExtensions.TryParseToSubnets(remoteFilteredSubnets, out var remoteAddressFilterResult, false)) + if (NetworkUtils.TryParseToSubnets(remoteFilteredSubnets, out var remoteAddressFilterResult, false)) { remoteAddressFilter = remoteAddressFilterResult.ToList(); } @@ -442,7 +441,7 @@ namespace Jellyfin.Networking.Manager { if (IPAddress.TryParse(ip, out var ipp)) { - remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize)); + remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize)); } } @@ -470,13 +469,13 @@ namespace Jellyfin.Networking.Manager { publishedServerUrls.Add( new PublishedServerUriOverride( - new IPData(IPAddress.Any, Network.IPv4Any), + new IPData(IPAddress.Any, NetworkConstants.IPv4Any), startupOverrideKey, true, true)); publishedServerUrls.Add( new PublishedServerUriOverride( - new IPData(IPAddress.IPv6Any, Network.IPv6Any), + new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any), startupOverrideKey, true, true)); @@ -502,13 +501,13 @@ namespace Jellyfin.Networking.Manager publishedServerUrls.Clear(); publishedServerUrls.Add( new PublishedServerUriOverride( - new IPData(IPAddress.Any, Network.IPv4Any), + new IPData(IPAddress.Any, NetworkConstants.IPv4Any), replacement, true, true)); publishedServerUrls.Add( new PublishedServerUriOverride( - new IPData(IPAddress.IPv6Any, Network.IPv6Any), + new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any), replacement, true, true)); @@ -518,13 +517,13 @@ namespace Jellyfin.Networking.Manager { publishedServerUrls.Add( new PublishedServerUriOverride( - new IPData(IPAddress.Any, Network.IPv4Any), + new IPData(IPAddress.Any, NetworkConstants.IPv4Any), replacement, false, true)); publishedServerUrls.Add( new PublishedServerUriOverride( - new IPData(IPAddress.IPv6Any, Network.IPv6Any), + new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any), replacement, false, true)); @@ -542,7 +541,7 @@ namespace Jellyfin.Networking.Manager false)); } } - else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null) + else if (NetworkUtils.TryParseToSubnet(identifier, out var result) && result is not null) { var data = new IPData(result.Prefix, result); publishedServerUrls.Add( @@ -608,7 +607,7 @@ namespace Jellyfin.Networking.Manager foreach (var details in interfaceList) { var parts = details.Split(','); - if (NetworkExtensions.TryParseToSubnet(parts[0], out var subnet)) + if (NetworkUtils.TryParseToSubnet(parts[0], out var subnet)) { var address = subnet.Prefix; var index = int.Parse(parts[1], CultureInfo.InvariantCulture); @@ -724,12 +723,12 @@ namespace Jellyfin.Networking.Manager var loopbackNetworks = new List(); if (IsIPv4Enabled) { - loopbackNetworks.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); + loopbackNetworks.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); } if (IsIPv6Enabled) { - loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); + loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); } return loopbackNetworks; @@ -748,11 +747,11 @@ namespace Jellyfin.Networking.Manager if (IsIPv4Enabled && IsIPv6Enabled) { // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default - result.Add(new IPData(IPAddress.IPv6Any, Network.IPv6Any)); + result.Add(new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any)); } else if (IsIPv4Enabled) { - result.Add(new IPData(IPAddress.Any, Network.IPv4Any)); + result.Add(new IPData(IPAddress.Any, NetworkConstants.IPv4Any)); } else if (IsIPv6Enabled) { @@ -772,7 +771,7 @@ namespace Jellyfin.Networking.Manager /// public string GetBindAddress(string source, out int? port) { - if (!NetworkExtensions.TryParseHost(source, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) + if (!NetworkUtils.TryParseHost(source, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) { addresses = Array.Empty(); } @@ -847,7 +846,7 @@ namespace Jellyfin.Networking.Manager // If no source address is given, use the preferred (first) interface if (source is null) { - result = NetworkExtensions.FormatIPString(availableInterfaces.First().Address); + result = NetworkUtils.FormatIPString(availableInterfaces.First().Address); _logger.LogDebug("{Source}: Using first internal interface as bind address: {Result}", source, result); return result; } @@ -858,14 +857,14 @@ namespace Jellyfin.Networking.Manager { if (intf.Subnet.Contains(source)) { - result = NetworkExtensions.FormatIPString(intf.Address); + result = NetworkUtils.FormatIPString(intf.Address); _logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result); return result; } } // Fallback to first available interface - result = NetworkExtensions.FormatIPString(availableInterfaces[0].Address); + result = NetworkUtils.FormatIPString(availableInterfaces[0].Address); _logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result); return result; } @@ -882,12 +881,12 @@ namespace Jellyfin.Networking.Manager /// public bool IsInLocalNetwork(string address) { - if (NetworkExtensions.TryParseToSubnet(address, out var subnet)) + if (NetworkUtils.TryParseToSubnet(address, out var subnet)) { return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix))); } - if (NetworkExtensions.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) + if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) { foreach (var ept in addresses) { @@ -1045,7 +1044,7 @@ namespace Jellyfin.Networking.Manager .Select(x => x.Address) .First(); - result = NetworkExtensions.FormatIPString(bindAddress); + result = NetworkUtils.FormatIPString(bindAddress); _logger.LogDebug("{Source}: External request received, matching external bind address found: {Result}", source, result); return true; } @@ -1064,7 +1063,7 @@ namespace Jellyfin.Networking.Manager if (bindAddress is not null) { - result = NetworkExtensions.FormatIPString(bindAddress); + result = NetworkUtils.FormatIPString(bindAddress); _logger.LogDebug("{Source}: Internal request received, matching internal bind address found: {Result}", source, result); return true; } @@ -1098,14 +1097,14 @@ namespace Jellyfin.Networking.Manager { if (intf.Subnet.Contains(source)) { - result = NetworkExtensions.FormatIPString(intf.Address); + result = NetworkUtils.FormatIPString(intf.Address); _logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result); return true; } } // Fallback to first external interface. - result = NetworkExtensions.FormatIPString(extResult[0].Address); + result = NetworkUtils.FormatIPString(extResult[0].Address); _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result); return true; } diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index df1d5a3e1..0ed1578c7 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 false true diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index 77f8f7071..6bda12c5b 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -60,7 +60,7 @@ namespace Jellyfin.Server.Implementations.Security } private async Task GetAuthorizationInfoFromDictionary( - IReadOnlyDictionary? auth, + Dictionary? auth, IHeaderDictionary headers, IQueryCollection queryString) { diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index edae4cfc5..990b9a5bd 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -748,7 +748,7 @@ namespace Jellyfin.Server.Implementations.Users return GetPasswordResetProviders(user)[0]; } - private IList GetAuthenticationProviders(User? user) + private List GetAuthenticationProviders(User? user) { var authenticationProviderId = user?.AuthenticationProviderId; @@ -775,7 +775,7 @@ namespace Jellyfin.Server.Implementations.Users return providers; } - private IList GetPasswordResetProviders(User user) + private IPasswordResetProvider[] GetPasswordResetProviders(User user) { var passwordResetProviderId = user.PasswordResetProviderId; var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray(); diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index b6af9baec..6066893de 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using Jellyfin.Api.Middleware; -using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.OpenApi.Models; diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 16b58808f..46df173bf 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -20,11 +20,10 @@ using Jellyfin.Api.Formatters; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; -using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Constants; -using Jellyfin.Networking.Extensions; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; +using MediaBrowser.Common.Api; +using MediaBrowser.Common.Net; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authentication; @@ -38,6 +37,7 @@ using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes; +using IPNetwork = System.Net.IPNetwork; namespace Jellyfin.Server.Extensions { @@ -275,20 +275,20 @@ namespace Jellyfin.Server.Extensions { if (IPAddress.TryParse(allowedProxies[i], out var addr)) { - AddIPAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize); + AddIPAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize); } - else if (NetworkExtensions.TryParseToSubnet(allowedProxies[i], out var subnet)) + else if (NetworkUtils.TryParseToSubnet(allowedProxies[i], out var subnet)) { if (subnet is not null) { AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); } } - else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6)) + else if (NetworkUtils.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6)) { foreach (var address in addresses) { - AddIPAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize); + AddIPAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize); } } } @@ -306,13 +306,13 @@ namespace Jellyfin.Server.Extensions return; } - if (prefixLength == Network.MinimumIPv4PrefixSize) + if (prefixLength == NetworkConstants.MinimumIPv4PrefixSize) { options.KnownProxies.Add(addr); } else { - options.KnownNetworks.Add(new IPNetwork(addr, prefixLength)); + options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(addr, prefixLength)); } } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 5479d2296..1d4d97551 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -8,7 +8,7 @@ jellyfin Exe - net7.0 + net8.0 false false true diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs index c6d86b8cd..d92c00991 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs @@ -3,7 +3,7 @@ using System.IO; using System.Xml; using System.Xml.Serialization; using Emby.Server.Implementations; -using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Net; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.PreStartupRoutines; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index ac5047401..247e1d845 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -78,11 +78,7 @@ namespace Jellyfin.Server.Migrations.Routines } else { - var ratingValue = _localizationManager.GetRatingLevel(ratingString).ToString(); - if (string.IsNullOrEmpty(ratingValue)) - { - ratingValue = "NULL"; - } + var ratingValue = _localizationManager.GetRatingLevel(ratingString)?.ToString(CultureInfo.InvariantCulture) ?? "NULL"; using var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;"); statement.TryBind("@Value", ratingValue); diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index f9259d0d9..c70ef1719 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -40,7 +40,7 @@ namespace Jellyfin.Server /// public const string LoggingConfigFileSystem = "logging.json"; - private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); + private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 2acddb243..49f5bf232 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -7,7 +7,6 @@ using System.Text; using Emby.Dlna.Extensions; using Jellyfin.Api.Middleware; using Jellyfin.MediaEncoding.Hls.Extensions; -using Jellyfin.Networking.Configuration; using Jellyfin.Networking.HappyEyeballs; using Jellyfin.Server.Extensions; using Jellyfin.Server.HealthChecks; @@ -36,7 +35,7 @@ namespace Jellyfin.Server /// public class Startup { - private readonly IServerApplicationHost _serverApplicationHost; + private readonly CoreAppHost _serverApplicationHost; private readonly IServerConfigurationManager _serverConfigurationManager; /// diff --git a/Jellyfin.Api/Constants/Policies.cs b/MediaBrowser.Common/Api/Policies.cs similarity index 98% rename from Jellyfin.Api/Constants/Policies.cs rename to MediaBrowser.Common/Api/Policies.cs index 02fdef150..e5427b8ef 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/MediaBrowser.Common/Api/Policies.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Api.Constants; +namespace MediaBrowser.Common.Api; /// /// Policies for the API authorization. diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 7d0d7a173..8ad626b41 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -21,7 +21,6 @@ - @@ -29,7 +28,7 @@ - net7.0 + net8.0 false true true diff --git a/MediaBrowser.Common/Net/NetworkConfiguration.cs b/MediaBrowser.Common/Net/NetworkConfiguration.cs new file mode 100644 index 000000000..61a51c99e --- /dev/null +++ b/MediaBrowser.Common/Net/NetworkConfiguration.cs @@ -0,0 +1,175 @@ +#pragma warning disable CA1819 // Properties should not return arrays + +using System; + +namespace MediaBrowser.Common.Net; + +/// +/// Defines the . +/// +public class NetworkConfiguration +{ + /// + /// The default value for . + /// + public const int DefaultHttpPort = 8096; + + /// + /// The default value for and . + /// + public const int DefaultHttpsPort = 8920; + + private string _baseUrl = string.Empty; + + /// + /// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at. + /// + public string BaseUrl + { + get => _baseUrl; + set + { + // Normalize the start of the string + if (string.IsNullOrWhiteSpace(value)) + { + // If baseUrl is empty, set an empty prefix string + _baseUrl = string.Empty; + return; + } + + if (value[0] != '/') + { + // If baseUrl was not configured with a leading slash, append one for consistency + value = "/" + value; + } + + // Normalize the end of the string + if (value[^1] == '/') + { + // If baseUrl was configured with a trailing slash, remove it for consistency + value = value.Remove(value.Length - 1); + } + + _baseUrl = value; + } + } + + /// + /// Gets or sets a value indicating whether to use HTTPS. + /// + /// + /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be + /// provided for and . + /// + public bool EnableHttps { get; set; } + + /// + /// Gets or sets a value indicating whether the server should force connections over HTTPS. + /// + public bool RequireHttps { get; set; } + + /// + /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. + /// + public string CertificatePath { get; set; } = string.Empty; + + /// + /// Gets or sets the password required to access the X.509 certificate data in the file specified by . + /// + public string CertificatePassword { get; set; } = string.Empty; + + /// + /// Gets or sets the internal HTTP server port. + /// + /// The HTTP server port. + public int InternalHttpPort { get; set; } = DefaultHttpPort; + + /// + /// Gets or sets the internal HTTPS server port. + /// + /// The HTTPS server port. + public int InternalHttpsPort { get; set; } = DefaultHttpsPort; + + /// + /// Gets or sets the public HTTP port. + /// + /// The public HTTP port. + public int PublicHttpPort { get; set; } = DefaultHttpPort; + + /// + /// Gets or sets the public HTTPS port. + /// + /// The public HTTPS port. + public int PublicHttpsPort { get; set; } = DefaultHttpsPort; + + /// + /// Gets or sets a value indicating whether Autodiscovery is enabled. + /// + public bool AutoDiscovery { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to enable automatic port forwarding. + /// + public bool EnableUPnP { get; set; } + + /// + /// Gets or sets a value indicating whether IPv6 is enabled. + /// + public bool EnableIPv4 { get; set; } = true; + + /// + /// Gets or sets a value indicating whether IPv6 is enabled. + /// + public bool EnableIPv6 { get; set; } + + /// + /// Gets or sets a value indicating whether access from outside of the LAN is permitted. + /// + public bool EnableRemoteAccess { get; set; } = true; + + /// + /// Gets or sets the subnets that are deemed to make up the LAN. + /// + public string[] LocalNetworkSubnets { get; set; } = Array.Empty(); + + /// + /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used. + /// + public string[] LocalNetworkAddresses { get; set; } = Array.Empty(); + + /// + /// Gets or sets the known proxies. + /// + public string[] KnownProxies { get; set; } = Array.Empty(); + + /// + /// Gets or sets a value indicating whether address names that match should be ignored for the purposes of binding. + /// + public bool IgnoreVirtualInterfaces { get; set; } = true; + + /// + /// Gets or sets a value indicating the interface name prefixes that should be ignored. The list can be comma separated and values are case-insensitive. . + /// + public string[] VirtualInterfaceNames { get; set; } = new string[] { "veth" }; + + /// + /// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests. + /// + public bool EnablePublishedServerUriByRequest { get; set; } = false; + + /// + /// Gets or sets the PublishedServerUriBySubnet + /// Gets or sets PublishedServerUri to advertise for specific subnets. + /// + public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); + + /// + /// Gets or sets the filter for remote IP connectivity. Used in conjunction with . + /// + public string[] RemoteIPFilter { get; set; } = Array.Empty(); + + /// + /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist. + /// + public bool IsRemoteIPFilterBlacklist { get; set; } +} diff --git a/MediaBrowser.Common/Net/NetworkConfigurationExtensions.cs b/MediaBrowser.Common/Net/NetworkConfigurationExtensions.cs new file mode 100644 index 000000000..9288964d2 --- /dev/null +++ b/MediaBrowser.Common/Net/NetworkConfigurationExtensions.cs @@ -0,0 +1,19 @@ +using MediaBrowser.Common.Configuration; + +namespace MediaBrowser.Common.Net; + +/// +/// Defines the . +/// +public static class NetworkConfigurationExtensions +{ + /// + /// Retrieves the network configuration. + /// + /// The . + /// The . + public static NetworkConfiguration GetNetworkConfiguration(this IConfigurationManager config) + { + return config.GetConfiguration(NetworkConfigurationStore.StoreKey); + } +} diff --git a/MediaBrowser.Common/Net/NetworkConfigurationFactory.cs b/MediaBrowser.Common/Net/NetworkConfigurationFactory.cs new file mode 100644 index 000000000..9309834f4 --- /dev/null +++ b/MediaBrowser.Common/Net/NetworkConfigurationFactory.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; + +namespace MediaBrowser.Common.Net; + +/// +/// Defines the . +/// +public class NetworkConfigurationFactory : IConfigurationFactory +{ + /// + /// The GetConfigurations. + /// + /// The . + public IEnumerable GetConfigurations() + { + return new[] + { + new NetworkConfigurationStore() + }; + } +} diff --git a/MediaBrowser.Common/Net/NetworkConfigurationStore.cs b/MediaBrowser.Common/Net/NetworkConfigurationStore.cs new file mode 100644 index 000000000..d2f518707 --- /dev/null +++ b/MediaBrowser.Common/Net/NetworkConfigurationStore.cs @@ -0,0 +1,23 @@ +using MediaBrowser.Common.Configuration; + +namespace MediaBrowser.Common.Net; + +/// +/// A configuration that stores network related settings. +/// +public class NetworkConfigurationStore : ConfigurationStore +{ + /// + /// The name of the configuration in the storage. + /// + public const string StoreKey = "network"; + + /// + /// Initializes a new instance of the class. + /// + public NetworkConfigurationStore() + { + ConfigurationType = typeof(NetworkConfiguration); + Key = StoreKey; + } +} diff --git a/Jellyfin.Networking/Constants/Network.cs b/MediaBrowser.Common/Net/NetworkConstants.cs similarity index 94% rename from Jellyfin.Networking/Constants/Network.cs rename to MediaBrowser.Common/Net/NetworkConstants.cs index 7fadc74bb..b18058fa9 100644 --- a/Jellyfin.Networking/Constants/Network.cs +++ b/MediaBrowser.Common/Net/NetworkConstants.cs @@ -1,12 +1,12 @@ using System.Net; -using Microsoft.AspNetCore.HttpOverrides; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; -namespace Jellyfin.Networking.Constants; +namespace MediaBrowser.Common.Net; /// /// Networking constants. /// -public static class Network +public static class NetworkConstants { /// /// IPv4 mask bytes. diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkUtils.cs similarity index 83% rename from Jellyfin.Networking/Extensions/NetworkExtensions.cs rename to MediaBrowser.Common/Net/NetworkUtils.cs index a1e1140f1..e482089f0 100644 --- a/Jellyfin.Networking/Extensions/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -5,15 +5,14 @@ using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; using Jellyfin.Extensions; -using Jellyfin.Networking.Constants; -using Microsoft.AspNetCore.HttpOverrides; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; -namespace Jellyfin.Networking.Extensions; +namespace MediaBrowser.Common.Net; /// -/// Defines the . +/// Defines the . /// -public static partial class NetworkExtensions +public static partial class NetworkUtils { // Use regular expression as CheckHostName isn't RFC5892 compliant. // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation @@ -59,7 +58,7 @@ public static partial class NetworkExtensions /// String value of the subnet mask in dotted decimal notation. public static IPAddress CidrToMask(byte cidr, AddressFamily family) { - uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize) - cidr); + uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize) - cidr); addr = ((addr & 0xff000000) >> 24) | ((addr & 0x00ff0000) >> 8) | ((addr & 0x0000ff00) << 8) @@ -75,7 +74,7 @@ public static partial class NetworkExtensions /// String value of the subnet mask in dotted decimal notation. public static IPAddress CidrToMask(int cidr, AddressFamily family) { - uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize) - cidr); + uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize) - cidr); addr = ((addr & 0xff000000) >> 24) | ((addr & 0x00ff0000) >> 8) | ((addr & 0x0000ff00) << 8) @@ -100,7 +99,7 @@ public static partial class NetworkExtensions } // GetAddressBytes - Span bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? Network.IPv4MaskBytes : Network.IPv6MaskBytes]; + Span bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.IPv4MaskBytes : NetworkConstants.IPv6MaskBytes]; if (!mask.TryWriteBytes(bytes, out var bytesWritten)) { Console.WriteLine("Unable to write address bytes, only ${bytesWritten} bytes written."); @@ -198,46 +197,29 @@ public static partial class NetworkExtensions /// True if parsing was successful. public static bool TryParseToSubnet(ReadOnlySpan value, [NotNullWhen(true)] out IPNetwork? result, bool negated = false) { - var splitString = value.Trim().Split('/'); - if (splitString.MoveNext()) + value = value.Trim(); + if (value.Contains('/')) { - var ipBlock = splitString.Current; - var address = IPAddress.None; - if (negated && ipBlock.StartsWith("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress)) + if (negated && value.StartsWith("!") && IPNetwork.TryParse(value[1..], out result)) { - address = tmpAddress; + return true; } - else if (!negated && IPAddress.TryParse(ipBlock, out tmpAddress)) + else if (!negated && IPNetwork.TryParse(value, out result)) { - address = tmpAddress; + return true; } - - if (address != IPAddress.None) + } + else if (IPAddress.TryParse(value, out var address)) + { + if (address.AddressFamily == AddressFamily.InterNetwork) { - if (splitString.MoveNext()) - { - var subnetBlock = splitString.Current; - if (int.TryParse(subnetBlock, out var netmask)) - { - result = new IPNetwork(address, netmask); - return true; - } - else if (IPAddress.TryParse(subnetBlock, out var netmaskAddress)) - { - result = new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress)); - return true; - } - } - else if (address.AddressFamily == AddressFamily.InterNetwork) - { - result = address.Equals(IPAddress.Any) ? Network.IPv4Any : new IPNetwork(address, Network.MinimumIPv4PrefixSize); - return true; - } - else if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - result = address.Equals(IPAddress.IPv6Any) ? Network.IPv6Any : new IPNetwork(address, Network.MinimumIPv6PrefixSize); - return true; - } + result = address.Equals(IPAddress.Any) ? NetworkConstants.IPv4Any : new IPNetwork(address, NetworkConstants.MinimumIPv4PrefixSize); + return true; + } + else if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + result = address.Equals(IPAddress.IPv6Any) ? NetworkConstants.IPv6Any : new IPNetwork(address, NetworkConstants.MinimumIPv6PrefixSize); + return true; } } diff --git a/MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs b/MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs deleted file mode 100644 index 3afe874c5..000000000 --- a/MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace MediaBrowser.Common.Plugins -{ - using Microsoft.Extensions.DependencyInjection; - - /// - /// Defines the . - /// - public interface IPluginServiceRegistrator - { - /// - /// Registers the plugin's services with the service collection. - /// - /// - /// This interface is only used for service registration and requires a parameterless constructor. - /// - /// The service collection. - void RegisterServices(IServiceCollection serviceCollection); - } -} diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index f9468f6cd..f237993fd 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -20,7 +20,6 @@ - @@ -35,7 +34,7 @@ - net7.0 + net8.0 false true true diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 6621ae284..46fd1ae47 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -20,6 +20,7 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Configuration; +using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; namespace MediaBrowser.Controller.MediaEncoding { diff --git a/MediaBrowser.Controller/Plugins/IPluginServiceRegistrator.cs b/MediaBrowser.Controller/Plugins/IPluginServiceRegistrator.cs new file mode 100644 index 000000000..8b62f3808 --- /dev/null +++ b/MediaBrowser.Controller/Plugins/IPluginServiceRegistrator.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace MediaBrowser.Controller.Plugins; + +/// +/// Defines the . +/// +/// +/// This interface is only used for service registration and requires a parameterless constructor. +/// +public interface IPluginServiceRegistrator +{ + /// + /// Registers the plugin's services with the service collection. + /// + /// The service collection. + /// The server application host. + void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost); +} diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index a39bc238a..05177ac39 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -11,7 +11,7 @@ - net7.0 + net8.0 false true diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index f12ef7e63..0d1d27ae8 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -121,7 +121,7 @@ namespace MediaBrowser.MediaEncoding.Encoder "yadif_videotoolbox" }; - private static readonly IReadOnlyDictionary _filterOptionsDict = new Dictionary + private static readonly Dictionary _filterOptionsDict = new Dictionary { { 0, new string[] { "scale_cuda", "Output format (default \"same\")" } }, { 1, new string[] { "tonemap_cuda", "GPU accelerated HDR to SDR tonemapping" } }, @@ -132,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Encoder }; // These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below - private static readonly IReadOnlyDictionary _ffmpegMinimumLibraryVersions = new Dictionary + private static readonly Dictionary _ffmpegMinimumLibraryVersions = new Dictionary { { "libavutil", new Version(56, 14) }, { "libavcodec", new Version(58, 18) }, @@ -197,7 +197,7 @@ namespace MediaBrowser.MediaEncoding.Encoder internal bool ValidateVersionInternal(string versionOutput) { - if (versionOutput.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1) + if (versionOutput.Contains("Libav developers", StringComparison.OrdinalIgnoreCase)) { _logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported"); return false; @@ -333,7 +333,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// /// The 'ffmpeg -version' output. /// The library names and major.minor version numbers. - private static IReadOnlyDictionary GetFFmpegLibraryVersions(string output) + private static Dictionary GetFFmpegLibraryVersions(string output) { var map = new Dictionary(); @@ -537,9 +537,9 @@ namespace MediaBrowser.MediaEncoding.Encoder return found; } - private IDictionary GetFFmpegFiltersWithOption() + private Dictionary GetFFmpegFiltersWithOption() { - IDictionary dict = new Dictionary(); + Dictionary dict = new Dictionary(); for (int i = 0; i < _filterOptionsDict.Count; i++) { if (_filterOptionsDict.TryGetValue(i, out var val) && val.Length == 2) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs index 04128c911..c5f500e76 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs @@ -59,7 +59,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// System.String. private static string GetFileInputArgument(string path, string inputPrefix) { - if (path.IndexOf("://", StringComparison.Ordinal) != -1) + if (path.Contains("://", StringComparison.Ordinal)) { return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", path); } diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 1f39e88cd..a4e8194c1 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -6,7 +6,7 @@ - net7.0 + net8.0 false true diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index be1e8a172..629c30060 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -516,7 +516,7 @@ namespace MediaBrowser.MediaEncoding.Probing private void ProcessPairs(string key, List pairs, MediaInfo info) { - IList peoples = new List(); + List peoples = new List(); if (string.Equals(key, "studio", StringComparison.OrdinalIgnoreCase)) { info.Studios = pairs.Select(p => p.Value) @@ -612,11 +612,11 @@ namespace MediaBrowser.MediaEncoding.Probing { codec = "dvbsub"; } - else if ((codec ?? string.Empty).IndexOf("PGS", StringComparison.OrdinalIgnoreCase) != -1) + else if ((codec ?? string.Empty).Contains("PGS", StringComparison.OrdinalIgnoreCase)) { codec = "PGSSUB"; } - else if ((codec ?? string.Empty).IndexOf("DVD", StringComparison.OrdinalIgnoreCase) != -1) + else if ((codec ?? string.Empty).Contains("DVD", StringComparison.OrdinalIgnoreCase)) { codec = "DVDSUB"; } @@ -1182,7 +1182,7 @@ namespace MediaBrowser.MediaEncoding.Probing info.Size = string.IsNullOrEmpty(data.Format.Size) ? null : long.Parse(data.Format.Size, CultureInfo.InvariantCulture); } - private void SetAudioInfoFromTags(MediaInfo audio, IReadOnlyDictionary tags) + private void SetAudioInfoFromTags(MediaInfo audio, Dictionary tags) { var people = new List(); if (tags.TryGetValue("composer", out var composer) && !string.IsNullOrWhiteSpace(composer)) @@ -1339,7 +1339,7 @@ namespace MediaBrowser.MediaEncoding.Probing { // Only use the comma as a delimiter if there are no slashes or pipes. // We want to be careful not to split names that have commas in them - var delimiter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i, StringComparison.Ordinal) != -1) ? + var delimiter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.Contains(i, StringComparison.Ordinal)) ? _nameDelimiters : new[] { ',' }; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs index 0d4489517..fd55db4ba 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs @@ -88,7 +88,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles public bool SupportsFileExtension(string fileExtension) => _subtitleFormats.ContainsKey(fileExtension); - private IEnumerable GetSubtitleFormats() + private List GetSubtitleFormats() { var subtitleFormats = new List(); var assembly = typeof(SubtitleFormat).Assembly; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 8eea773d8..459d854bf 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -63,7 +63,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); - private Stream ConvertSubtitles( + private MemoryStream ConvertSubtitles( Stream stream, string inputFormat, string outputFormat, diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 666e78795..bf18d46dc 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -835,11 +835,6 @@ namespace MediaBrowser.Model.Dlna playlistItem.SetOption(qualifier, "profile", videoStream.Profile.ToLowerInvariant()); } - if (videoStream is not null && videoStream.Level != 0) - { - playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString() ?? string.Empty); - } - // Prefer matching audio codecs, could do better here var audioCodecs = ContainerProfile.SplitValue(audioCodec); @@ -866,16 +861,16 @@ namespace MediaBrowser.Model.Dlna // Copy matching audio codec options playlistItem.AudioSampleRate = audioStream.SampleRate; - playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString() ?? string.Empty); + playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels?.ToString(CultureInfo.InvariantCulture) ?? string.Empty); if (!string.IsNullOrEmpty(audioStream.Profile)) { playlistItem.SetOption(audioStream.Codec, "profile", audioStream.Profile.ToLowerInvariant()); } - if (audioStream.Level != 0) + if (audioStream.Level.HasValue && audioStream.Level.Value != 0) { - playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.ToString() ?? string.Empty); + playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.Value.ToString(CultureInfo.InvariantCulture)); } } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 75c5bc6f0..7af46f8a0 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -14,7 +14,7 @@ - net7.0 + net8.0 false true true @@ -34,7 +34,6 @@ - all diff --git a/MediaBrowser.Model/Net/IPData.cs b/MediaBrowser.Model/Net/IPData.cs index e9fcd6797..c116d883e 100644 --- a/MediaBrowser.Model/Net/IPData.cs +++ b/MediaBrowser.Model/Net/IPData.cs @@ -1,6 +1,6 @@ using System.Net; using System.Net.Sockets; -using Microsoft.AspNetCore.HttpOverrides; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace MediaBrowser.Model.Net; diff --git a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs index 7f1ecd743..a10ff198b 100644 --- a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs +++ b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs @@ -125,7 +125,7 @@ public class LrcLyricParser : ILyricParser /// /// The metadata from the LRC file. /// A lyricMetadata object with mapped property data. - private static LyricMetadata MapMetadataValues(IDictionary metaData) + private static LyricMetadata MapMetadataValues(Dictionary metaData) { LyricMetadata lyricMetadata = new(); diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index dab36625e..1a5dbd7a5 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -175,7 +175,7 @@ namespace MediaBrowser.Providers.Manager IDynamicImageProvider provider, ImageRefreshOptions refreshOptions, TypeOptions savedOptions, - ICollection downloadedImages, + List downloadedImages, RefreshResult result, CancellationToken cancellationToken) { @@ -263,7 +263,7 @@ namespace MediaBrowser.Providers.Manager ImageRefreshOptions refreshOptions, TypeOptions savedOptions, int backdropLimit, - ICollection downloadedImages, + List downloadedImages, RefreshResult result, CancellationToken cancellationToken) { diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 8471f6fa1..7a50c6cf4 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -27,7 +27,7 @@ - net7.0 + net8.0 false true ../jellyfin.ruleset diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs index b4b1895f5..d1c0ddb37 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs @@ -82,8 +82,8 @@ namespace MediaBrowser.Providers.MediaInfo { Directory.CreateDirectory(Path.GetDirectoryName(path)); - var imageStream = imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).IndexOf("front", StringComparison.OrdinalIgnoreCase) != -1) ?? - imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).IndexOf("cover", StringComparison.OrdinalIgnoreCase) != -1) ?? + var imageStream = imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).Contains("front", StringComparison.OrdinalIgnoreCase)) ?? + imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).Contains("cover", StringComparison.OrdinalIgnoreCase)) ?? imageStreams.FirstOrDefault(); var imageStreamIndex = imageStream?.Index; diff --git a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs index 909cbb9b9..f846aa5de 100644 --- a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs @@ -183,7 +183,7 @@ namespace MediaBrowser.Providers.MediaInfo files.AddRange(directoryService.GetFilePaths(internalMetadataPath, clearCache, true)); } - if (!files.Any()) + if (files.Count == 0) { return Array.Empty(); } diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index 0ddb2ad67..e4f34776b 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -148,7 +148,7 @@ namespace MediaBrowser.Providers.Music .ToArray(); var id = item.GetProviderId(provider); - if (ids.Any()) + if (ids.Length != 0) { var firstId = ids[0]; if (!string.IsNullOrEmpty(firstId) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs index 7f73afc53..8a516e1ce 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs @@ -73,7 +73,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb return Enumerable.Empty(); } - private IEnumerable GetImages(AudioDbAlbumProvider.Album item) + private List GetImages(AudioDbAlbumProvider.Album item) { var list = new List(); diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs index 2232dfa0d..4e7757cd2 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs @@ -78,7 +78,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb return Enumerable.Empty(); } - private IEnumerable GetImages(AudioDbArtistProvider.Artist item) + private List GetImages(AudioDbArtistProvider.Artist item) { var list = new List(); diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index e01c0f483..01c07d633 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -121,7 +121,7 @@ namespace MediaBrowser.Providers.TV var seasonNumber = virtualSeason.IndexNumber; // If there's a physical season with the same number or no episodes in the season, delete it if ((seasonNumber.HasValue && physicalSeasonNumbers.Contains(seasonNumber.Value)) - || !virtualSeason.GetEpisodes().Any()) + || virtualSeason.GetEpisodes().Count == 0) { Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason.IndexNumber, series.Name); diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index d7e34fd22..c20073eea 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -15,7 +15,7 @@ - net7.0 + net8.0 false true diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index bf66a3145..1399ac307 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -314,11 +314,11 @@ namespace MediaBrowser.XbmcMetadata.Savers { var codec = stream.Codec; - if ((stream.CodecTag ?? string.Empty).IndexOf("xvid", StringComparison.OrdinalIgnoreCase) != -1) + if ((stream.CodecTag ?? string.Empty).Contains("xvid", StringComparison.OrdinalIgnoreCase)) { codec = "xvid"; } - else if ((stream.CodecTag ?? string.Empty).IndexOf("divx", StringComparison.OrdinalIgnoreCase) != -1) + else if ((stream.CodecTag ?? string.Empty).Contains("divx", StringComparison.OrdinalIgnoreCase)) { codec = "divx"; } diff --git a/README.md b/README.md index 2362741b4..15dd0ae67 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ These instructions will help you get set up with a local development environment ### Prerequisites -Before the project can be built, you must first install the [.NET 7.0 SDK](https://dotnet.microsoft.com/download/dotnet) on your system. +Before the project can be built, you must first install the [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet) on your system. Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET 6 development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2022) and [Visual Studio Code](https://code.visualstudio.com/Download). @@ -137,7 +137,7 @@ A second option is to build the project and then run the resulting executable fi ```bash dotnet build # Build the project -cd Jellyfin.Server/bin/Debug/net7.0 # Change into the build output directory +cd Jellyfin.Server/bin/Debug/net8.0 # Change into the build output directory ``` 2. Execute the build output. On Linux, Mac, etc. use `./jellyfin` and on Windows use `jellyfin.exe`. diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj index df5d982f6..3f24de4e6 100644 --- a/RSSDP/RSSDP.csproj +++ b/RSSDP/RSSDP.csproj @@ -11,7 +11,7 @@ - net7.0 + net8.0 false AllDisabledByDefault disable diff --git a/debian/control b/debian/control index 0b9dd570e..5e0460de9 100644 --- a/debian/control +++ b/debian/control @@ -3,7 +3,7 @@ Section: misc Priority: optional Maintainer: Jellyfin Team Build-Depends: debhelper (>= 9), - dotnet-sdk-7.0, + dotnet-sdk-8.0, libc6-dev, libcurl4-openssl-dev, libfontconfig1-dev, diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index e5cf638c1..7c9bbf39e 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-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.amd64 b/deployment/Dockerfile.debian.amd64 index 1e1f6e54e..d344c5964 100644 --- a/deployment/Dockerfile.debian.amd64 +++ b/deployment/Dockerfile.debian.amd64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64 index bbed2c534..8a5411f05 100644 --- a/deployment/Dockerfile.debian.arm64 +++ b/deployment/Dockerfile.debian.arm64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf index 79373519c..e95ba1696 100644 --- a/deployment/Dockerfile.debian.armhf +++ b/deployment/Dockerfile.debian.armhf @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.docker.amd64 b/deployment/Dockerfile.docker.amd64 index 3a6ad95e8..1749ca563 100644 --- a/deployment/Dockerfile.docker.amd64 +++ b/deployment/Dockerfile.docker.amd64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim ARG SOURCE_DIR=/src ARG ARTIFACT_DIR=/jellyfin diff --git a/deployment/Dockerfile.docker.arm64 b/deployment/Dockerfile.docker.arm64 index ca7239304..bbddb61e4 100644 --- a/deployment/Dockerfile.docker.arm64 +++ b/deployment/Dockerfile.docker.arm64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim ARG SOURCE_DIR=/src ARG ARTIFACT_DIR=/jellyfin diff --git a/deployment/Dockerfile.docker.armhf b/deployment/Dockerfile.docker.armhf index 26cce1958..3de1d6887 100644 --- a/deployment/Dockerfile.docker.armhf +++ b/deployment/Dockerfile.docker.armhf @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim ARG SOURCE_DIR=/src ARG ARTIFACT_DIR=/jellyfin diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 777d92c11..66ead37d7 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-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.linux.amd64 b/deployment/Dockerfile.linux.amd64 index 39169bd2a..386f7cefe 100644 --- a/deployment/Dockerfile.linux.amd64 +++ b/deployment/Dockerfile.linux.amd64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl index 636a34544..56c877333 100644 --- a/deployment/Dockerfile.linux.amd64-musl +++ b/deployment/Dockerfile.linux.amd64-musl @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.linux.arm64 b/deployment/Dockerfile.linux.arm64 index ba8ce82f0..c9692c440 100644 --- a/deployment/Dockerfile.linux.arm64 +++ b/deployment/Dockerfile.linux.arm64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.linux.armhf b/deployment/Dockerfile.linux.armhf index d771e9991..230461556 100644 --- a/deployment/Dockerfile.linux.armhf +++ b/deployment/Dockerfile.linux.armhf @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.linux.musl-linux-arm64 b/deployment/Dockerfile.linux.musl-linux-arm64 index 846561181..240d09186 100644 --- a/deployment/Dockerfile.linux.musl-linux-arm64 +++ b/deployment/Dockerfile.linux.musl-linux-arm64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.macos.amd64 b/deployment/Dockerfile.macos.amd64 index 7ebf35442..1b054dfc4 100644 --- a/deployment/Dockerfile.macos.amd64 +++ b/deployment/Dockerfile.macos.amd64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.macos.arm64 b/deployment/Dockerfile.macos.arm64 index 5041ff967..07e18da55 100644 --- a/deployment/Dockerfile.macos.arm64 +++ b/deployment/Dockerfile.macos.arm64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable index 822b66ee6..36135f7a6 100644 --- a/deployment/Dockerfile.portable +++ b/deployment/Dockerfile.portable @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index f34c0358d..84fa2028e 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-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 87466d20e..ca3aa3508 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-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 261deb3e4..e52b7fba3 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-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 805c63f8c..08587aa7e 100644 --- a/deployment/Dockerfile.windows.amd64 +++ b/deployment/Dockerfile.windows.amd64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/build.debian.amd64 b/deployment/build.debian.amd64 index d92953ad1..7e968192b 100755 --- a/deployment/build.debian.amd64 +++ b/deployment/build.debian.amd64 @@ -9,9 +9,9 @@ set -o xtrace pushd ${SOURCE_DIR} if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-7.0, since it's installed manually + # Remove build-dep for dotnet-sdk-8.0, since it's installed manually cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-7.0,/d' debian/control + sed -i '/dotnet-sdk-8.0,/d' debian/control fi # Modify changelog to unstable configuration if IS_UNSTABLE diff --git a/deployment/build.debian.arm64 b/deployment/build.debian.arm64 index 618a121b6..7b7b603d6 100755 --- a/deployment/build.debian.arm64 +++ b/deployment/build.debian.arm64 @@ -9,9 +9,9 @@ set -o xtrace pushd ${SOURCE_DIR} if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-7.0, since it's installed manually + # Remove build-dep for dotnet-sdk-8.0, since it's installed manually cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-7.0,/d' debian/control + sed -i '/dotnet-sdk-8.0,/d' debian/control fi # Modify changelog to unstable configuration if IS_UNSTABLE diff --git a/deployment/build.debian.armhf b/deployment/build.debian.armhf index d1631d022..3d894ba20 100755 --- a/deployment/build.debian.armhf +++ b/deployment/build.debian.armhf @@ -9,9 +9,9 @@ set -o xtrace pushd ${SOURCE_DIR} if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-7.0, since it's installed manually + # Remove build-dep for dotnet-sdk-8.0, since it's installed manually cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-7.0,/d' debian/control + sed -i '/dotnet-sdk-8.0,/d' debian/control fi # Modify changelog to unstable configuration if IS_UNSTABLE diff --git a/deployment/build.ubuntu.amd64 b/deployment/build.ubuntu.amd64 index 4254103fa..5f25cb610 100755 --- a/deployment/build.ubuntu.amd64 +++ b/deployment/build.ubuntu.amd64 @@ -9,9 +9,9 @@ set -o xtrace pushd ${SOURCE_DIR} if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-7.0, since it's installed manually + # Remove build-dep for dotnet-sdk-8.0, since it's installed manually cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-7.0,/d' debian/control + sed -i '/dotnet-sdk-8.0,/d' debian/control fi # Modify changelog to unstable configuration if IS_UNSTABLE diff --git a/deployment/build.ubuntu.arm64 b/deployment/build.ubuntu.arm64 index 42f111a01..334ced997 100755 --- a/deployment/build.ubuntu.arm64 +++ b/deployment/build.ubuntu.arm64 @@ -9,9 +9,9 @@ set -o xtrace pushd ${SOURCE_DIR} if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-7.0, since it's installed manually + # Remove build-dep for dotnet-sdk-8.0, since it's installed manually cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-7.0,/d' debian/control + sed -i '/dotnet-sdk-8.0,/d' debian/control fi # Modify changelog to unstable configuration if IS_UNSTABLE diff --git a/deployment/build.ubuntu.armhf b/deployment/build.ubuntu.armhf index 357d63626..77e33c307 100755 --- a/deployment/build.ubuntu.armhf +++ b/deployment/build.ubuntu.armhf @@ -9,9 +9,9 @@ set -o xtrace pushd ${SOURCE_DIR} if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-7.0, since it's installed manually + # Remove build-dep for dotnet-sdk-8.0, since it's installed manually cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-7.0,/d' debian/control + sed -i '/dotnet-sdk-8.0,/d' debian/control fi # Modify changelog to unstable configuration if IS_UNSTABLE diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index e78368906..fb9fb2f7d 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -26,7 +26,7 @@ BuildRequires: systemd BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel # Requirements not packaged in RHEL 7 main repos, added via Makefile # https://packages.microsoft.com/rhel/7/prod/ -BuildRequires: dotnet-runtime-7.0, dotnet-sdk-7.0 +BuildRequires: dotnet-runtime-8.0, dotnet-sdk-8.0 Requires: %{name}-server = %{version}-%{release}, %{name}-web = %{version}-%{release} # Temporary (hopefully?) fix for https://github.com/jellyfin/jellyfin/issues/7471 @@ -73,7 +73,7 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti %install # Jellyfin files %{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir} -%{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin +%{__cp} -r Jellyfin.Server/bin/Release/net8.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin %{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin sed -i -e 's|/usr/lib64|%{_libdir}|g' %{buildroot}%{_bindir}/jellyfin diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj index 1e3f8a048..73aae3f3d 100644 --- a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj +++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 diff --git a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh index aa2a34cdd..80a5cd7c1 100755 --- a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh +++ b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh @@ -8,4 +8,4 @@ cp bin/Emby.Server.Implementations.dll . dotnet build mkdir -p Findings -AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net7.0/Emby.Server.Implementations.Fuzz "$1" +AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net8.0/Emby.Server.Implementations.Fuzz "$1" diff --git a/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj index da46e63a5..faac7d976 100644 --- a/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj +++ b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 diff --git a/fuzz/Jellyfin.Api.Fuzz/fuzz.sh b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh index edf965562..96b0192cf 100755 --- a/fuzz/Jellyfin.Api.Fuzz/fuzz.sh +++ b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh @@ -8,4 +8,4 @@ cp bin/Jellyfin.Api.dll . dotnet build mkdir -p Findings -AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net7.0/Jellyfin.Api.Fuzz "$1" +AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net8.0/Jellyfin.Api.Fuzz "$1" diff --git a/global.json b/global.json index 24335d7a0..9db4b532c 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.0", + "version": "8.0.0", "rollForward": "latestMinor" } } diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 870cf253f..10225e3af 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -140,6 +140,9 @@ + + + diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 3c417f8ff..0590ded32 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -6,9 +6,11 @@ - net7.0 + net8.0 false true + + NU1903 diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj index d7ef6f8e7..23c4c0a9a 100644 --- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj +++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj @@ -6,7 +6,7 @@ - net7.0 + net8.0 false true diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 997df6dbe..c91f5d008 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 false true true diff --git a/src/Jellyfin.Extensions/Json/JsonDefaults.cs b/src/Jellyfin.Extensions/Json/JsonDefaults.cs index 4d56ca615..9e6d4c3f8 100644 --- a/src/Jellyfin.Extensions/Json/JsonDefaults.cs +++ b/src/Jellyfin.Extensions/Json/JsonDefaults.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Jellyfin.Extensions.Json.Converters; namespace Jellyfin.Extensions.Json @@ -41,7 +42,8 @@ namespace Jellyfin.Extensions.Json new JsonNullableStructConverterFactory(), new JsonDateTimeConverter(), new JsonStringConverter() - } + }, + TypeInfoResolver = new DefaultJsonTypeInfoResolver() }; private static readonly JsonSerializerOptions _pascalCaseJsonSerializerOptions = new(_jsonSerializerOptions) diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj index 76dde1cf6..ee79802a1 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj +++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true diff --git a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs index 479e6ffdc..720d987f1 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs +++ b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs @@ -11,8 +11,6 @@ namespace Jellyfin.MediaEncoding.Keyframes.FfProbe; /// public static class FfProbeKeyframeExtractor { - private const string DefaultArguments = "-fflags +genpts -v error -skip_frame nokey -show_entries format=duration -show_entries stream=duration -show_entries packet=pts_time,flags -select_streams v -of csv \"{0}\""; - /// /// Extracts the keyframes using the ffprobe executable at the specified path. /// @@ -26,7 +24,10 @@ public static class FfProbeKeyframeExtractor StartInfo = new ProcessStartInfo { FileName = ffProbePath, - Arguments = string.Format(CultureInfo.InvariantCulture, DefaultArguments, filePath), + Arguments = string.Format( + CultureInfo.InvariantCulture, + "-fflags +genpts -v error -skip_frame nokey -show_entries format=duration -show_entries stream=duration -show_entries packet=pts_time,flags -select_streams v -of csv \"{0}\"", + filePath), CreateNoWindow = true, UseShellExecute = false, diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj index 0d91a447b..c79dcee3c 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index de8fc1bb8..bec3481cb 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -4,7 +4,7 @@ - net7.0 + net8.0 false $(MSBuildThisFileDirectory)/jellyfin-tests.ruleset diff --git a/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs index 3f965d0ff..c7331c718 100644 --- a/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs @@ -109,7 +109,7 @@ public class UserControllerTests v.ErrorMessage.Contains("required", StringComparison.CurrentCultureIgnoreCase)); } - private IList Validate(object model) + private List Validate(object model) { var result = new List(); var context = new ValidationContext(model, null, null); diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs index f2ca2ff08..61105b42b 100644 --- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs @@ -7,135 +7,128 @@ using Xunit; namespace Jellyfin.Extensions.Tests.Json.Converters { - public static class JsonCommaDelimitedArrayTests + public class JsonCommaDelimitedArrayTests { - [Fact] - public static void Deserialize_String_Null_Success() + private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() { - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": null }", options); + Converters = + { + new JsonStringEnumConverter() + } + }; + + [Fact] + public void Deserialize_String_Null_Success() + { + var value = JsonSerializer.Deserialize>(@"{ ""Value"": null }", _jsonOptions); Assert.Null(value?.Value); } [Fact] - public static void Deserialize_Empty_Success() + public void Deserialize_Empty_Success() { var desiredValue = new GenericBodyArrayModel { Value = Array.Empty() }; - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": """" }", options); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": """" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_String_Valid_Success() + public void Deserialize_String_Valid_Success() { var desiredValue = new GenericBodyArrayModel { Value = new[] { "a", "b", "c" } }; - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a,b,c"" }", options); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_String_Space_Valid_Success() + public void Deserialize_String_Space_Valid_Success() { var desiredValue = new GenericBodyArrayModel { Value = new[] { "a", "b", "c" } }; - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a, b, c"" }", options); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a, b, c"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_Valid_Success() + public void Deserialize_GenericCommandType_Valid_Success() { var desiredValue = new GenericBodyArrayModel { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,MoveDown"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_EmptyEntry_Success() + public void Deserialize_GenericCommandType_EmptyEntry_Success() { var desiredValue = new GenericBodyArrayModel { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", options); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_Invalid_Success() + public void Deserialize_GenericCommandType_Invalid_Success() { var desiredValue = new GenericBodyArrayModel { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", options); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_Space_Valid_Success() + public void Deserialize_GenericCommandType_Space_Valid_Success() { var desiredValue = new GenericBodyArrayModel { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp, MoveDown"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_String_Array_Valid_Success() + public void Deserialize_String_Array_Valid_Success() { var desiredValue = new GenericBodyArrayModel { Value = new[] { "a", "b", "c" } }; - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""a"",""b"",""c""] }", options); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""a"",""b"",""c""] }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_Array_Valid_Success() + public void Deserialize_GenericCommandType_Array_Valid_Success() { var desiredValue = new GenericBodyArrayModel { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } } diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs index 92886dcd2..9b977b9a5 100644 --- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs @@ -6,86 +6,85 @@ using Xunit; namespace Jellyfin.Extensions.Tests.Json.Converters { - public static class JsonCommaDelimitedIReadOnlyListTests + public class JsonCommaDelimitedIReadOnlyListTests { + private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() + { + Converters = + { + new JsonStringEnumConverter() + } + }; + [Fact] - public static void Deserialize_String_Valid_Success() + public void Deserialize_String_Valid_Success() { var desiredValue = new GenericBodyIReadOnlyListModel { Value = new[] { "a", "b", "c" } }; - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a,b,c"" }", options); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_String_Space_Valid_Success() + public void Deserialize_String_Space_Valid_Success() { var desiredValue = new GenericBodyIReadOnlyListModel { Value = new[] { "a", "b", "c" } }; - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a, b, c"" }", options); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a, b, c"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_Valid_Success() + public void Deserialize_GenericCommandType_Valid_Success() { var desiredValue = new GenericBodyIReadOnlyListModel { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,MoveDown"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_Space_Valid_Success() + public void Deserialize_GenericCommandType_Space_Valid_Success() { var desiredValue = new GenericBodyIReadOnlyListModel { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp, MoveDown"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_String_Array_Valid_Success() + public void Deserialize_String_Array_Valid_Success() { var desiredValue = new GenericBodyIReadOnlyListModel { Value = new[] { "a", "b", "c" } }; - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""a"",""b"",""c""] }", options); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""a"",""b"",""c""] }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_Array_Valid_Success() + public void Deserialize_GenericCommandType_Array_Valid_Success() { var desiredValue = new GenericBodyIReadOnlyListModel { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } } diff --git a/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs b/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs index a78b872df..30726f1d3 100644 --- a/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs +++ b/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs @@ -1,4 +1,4 @@ -using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Net; using Xunit; namespace Jellyfin.Networking.Tests.Configuration; diff --git a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs index 072e0a8c5..01546aa2b 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs @@ -1,6 +1,6 @@ using FsCheck; using FsCheck.Xunit; -using Jellyfin.Networking.Extensions; +using MediaBrowser.Common.Net; using Xunit; namespace Jellyfin.Networking.Tests @@ -26,15 +26,15 @@ namespace Jellyfin.Networking.Tests [InlineData("192.168.1.2/255.255.255.0")] [InlineData("192.168.1.2/24")] public static void TryParse_ValidHostStrings_True(string address) - => Assert.True(NetworkExtensions.TryParseHost(address, out _, true, true)); + => Assert.True(NetworkUtils.TryParseHost(address, out _, true, true)); [Property] public static Property TryParse_IPv4Address_True(IPv4Address address) - => NetworkExtensions.TryParseHost(address.Item.ToString(), out _, true, true).ToProperty(); + => NetworkUtils.TryParseHost(address.Item.ToString(), out _, true, true).ToProperty(); [Property] public static Property TryParse_IPv6Address_True(IPv6Address address) - => NetworkExtensions.TryParseHost(address.Item.ToString(), out _, true, true).ToProperty(); + => NetworkUtils.TryParseHost(address.Item.ToString(), out _, true, true).ToProperty(); /// /// All should be invalid address strings. @@ -47,6 +47,6 @@ namespace Jellyfin.Networking.Tests [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")] public static void TryParse_InvalidAddressString_False(string address) - => Assert.False(NetworkExtensions.TryParseHost(address, out _, true, true)); + => Assert.False(NetworkUtils.TryParseHost(address, out _, true, true)); } } diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs index 2302f90b8..0333d98e6 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs @@ -1,6 +1,6 @@ using System.Net; -using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; +using MediaBrowser.Common.Net; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 022b8a3d0..3b7c43100 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -2,15 +2,15 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; -using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Extensions; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; +using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; namespace Jellyfin.Networking.Tests { @@ -71,7 +71,6 @@ namespace Jellyfin.Networking.Tests [InlineData("127.0.0.1/8")] [InlineData("192.168.1.2")] [InlineData("192.168.1.2/24")] - [InlineData("192.168.1.2/255.255.255.0")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]")] [InlineData("fe80::7add:12ff:febb:c67b%16")] @@ -80,7 +79,7 @@ namespace Jellyfin.Networking.Tests [InlineData("[fe80::7add:12ff:febb:c67b%16]")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] public static void TryParseValidIPStringsTrue(string address) - => Assert.True(NetworkExtensions.TryParseToSubnet(address, out _)); + => Assert.True(NetworkUtils.TryParseToSubnet(address, out _)); /// /// Checks invalid IP address formats. @@ -93,7 +92,7 @@ namespace Jellyfin.Networking.Tests [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")] public static void TryParseInvalidIPStringsFalse(string address) - => Assert.False(NetworkExtensions.TryParseToSubnet(address, out _)); + => Assert.False(NetworkUtils.TryParseToSubnet(address, out _)); /// /// Checks if IPv4 address is within a defined subnet. @@ -103,17 +102,15 @@ namespace Jellyfin.Networking.Tests [Theory] [InlineData("192.168.5.85/24", "192.168.5.1")] [InlineData("192.168.5.85/24", "192.168.5.254")] - [InlineData("192.168.5.85/255.255.255.0", "192.168.5.254")] [InlineData("10.128.240.50/30", "10.128.240.48")] [InlineData("10.128.240.50/30", "10.128.240.49")] [InlineData("10.128.240.50/30", "10.128.240.50")] [InlineData("10.128.240.50/30", "10.128.240.51")] - [InlineData("10.128.240.50/255.255.255.252", "10.128.240.51")] [InlineData("127.0.0.1/8", "127.0.0.1")] public void IPv4SubnetMaskMatchesValidIPAddress(string netMask, string ipAddress) { var ipa = IPAddress.Parse(ipAddress); - Assert.True(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); + Assert.True(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); } /// @@ -124,16 +121,14 @@ namespace Jellyfin.Networking.Tests [Theory] [InlineData("192.168.5.85/24", "192.168.4.254")] [InlineData("192.168.5.85/24", "191.168.5.254")] - [InlineData("192.168.5.85/255.255.255.252", "192.168.4.254")] [InlineData("10.128.240.50/30", "10.128.240.47")] [InlineData("10.128.240.50/30", "10.128.240.52")] [InlineData("10.128.240.50/30", "10.128.239.50")] [InlineData("10.128.240.50/30", "10.127.240.51")] - [InlineData("10.128.240.50/255.255.255.252", "10.127.240.51")] public void IPv4SubnetMaskDoesNotMatchInvalidIPAddress(string netMask, string ipAddress) { var ipa = IPAddress.Parse(ipAddress); - Assert.False(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); + Assert.False(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); } /// @@ -149,7 +144,7 @@ namespace Jellyfin.Networking.Tests [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")] public void IPv6SubnetMaskMatchesValidIPAddress(string netMask, string ipAddress) { - Assert.True(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); + Assert.True(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); } [Theory] @@ -160,7 +155,7 @@ namespace Jellyfin.Networking.Tests [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0001")] public void IPv6SubnetMaskDoesNotMatchInvalidIPAddress(string netMask, string ipAddress) { - Assert.False(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); + Assert.False(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); } [Theory] @@ -207,7 +202,7 @@ namespace Jellyfin.Networking.Tests NetworkManager.MockNetworkSettings = string.Empty; // Check to see if DNS resolution is working. If not, skip test. - if (!NetworkExtensions.TryParseHost(source, out var host)) + if (!NetworkUtils.TryParseHost(source, out var host)) { return; } diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs index d5ab6ab4b..be5a401b1 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs @@ -27,7 +27,7 @@ namespace Jellyfin.Providers.Tests.Manager { public partial class ItemImageProviderTests { - private const string TestDataImagePath = "Test Data/Images/blank{0}.jpg"; + private static readonly CompositeFormat _testDataImagePath = CompositeFormat.Parse("Test Data/Images/blank{0}.jpg"); [GeneratedRegex("[0-9]+")] private static partial Regex NumbersRegex(); @@ -275,7 +275,7 @@ namespace Jellyfin.Providers.Tests.Manager { HasImage = true, Format = ImageFormat.Jpg, - Path = responseHasPath ? string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 0) : null, + Path = responseHasPath ? string.Format(CultureInfo.InvariantCulture, _testDataImagePath, 0) : null, Protocol = protocol }; @@ -563,21 +563,21 @@ namespace Jellyfin.Providers.Tests.Manager mockFileSystem.Setup(fs => fs.GetFilePaths(It.IsAny(), It.IsAny())) .Returns(new[] { - string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 0), - string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 1) + string.Format(CultureInfo.InvariantCulture, _testDataImagePath, 0), + string.Format(CultureInfo.InvariantCulture, _testDataImagePath, 1) }); return new ItemImageProvider(new NullLogger(), providerManager, mockFileSystem.Object); } - private static BaseItem GetItemWithImages(ImageType type, int count, bool validPaths) + private static Video GetItemWithImages(ImageType type, int count, bool validPaths) { // Has to exist for querying DateModified time on file, results stored but not checked so not populating BaseItem.FileSystem ??= Mock.Of(); var item = new Video(); - var path = validPaths ? TestDataImagePath : "invalid path {0}"; + var path = validPaths ? _testDataImagePath.Format : "invalid path {0}"; for (int i = 0; i < count; i++) { item.SetImagePath(type, i, new FileSystemMetadata @@ -604,7 +604,7 @@ namespace Jellyfin.Providers.Tests.Manager /// private static LocalImageInfo[] GetImages(ImageType type, int count, bool validPaths) { - var path = validPaths ? TestDataImagePath : "invalid path {0}"; + var path = validPaths ? _testDataImagePath.Format : "invalid path {0}"; var images = new LocalImageInfo[count]; for (int i = 0; i < count; i++) { diff --git a/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs index 1dd49b2cf..ad85bdb6e 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs @@ -9,22 +9,21 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting { public class AiredEpisodeOrderComparerTests { + private readonly AiredEpisodeOrderComparer _cmp = new AiredEpisodeOrderComparer(); + [Theory] [ClassData(typeof(EpisodeBadData))] public void Compare_GivenNull_ThrowsArgumentNullException(BaseItem? x, BaseItem? y) { - var cmp = new AiredEpisodeOrderComparer(); - Assert.Throws(() => cmp.Compare(x, y)); + Assert.Throws(() => _cmp.Compare(x, y)); } [Theory] [ClassData(typeof(EpisodeTestData))] public void AiredEpisodeOrderCompareTest(BaseItem x, BaseItem y, int expected) { - var cmp = new AiredEpisodeOrderComparer(); - - Assert.Equal(expected, cmp.Compare(x, y)); - Assert.Equal(-expected, cmp.Compare(y, x)); + Assert.Equal(expected, _cmp.Compare(x, y)); + Assert.Equal(-expected, _cmp.Compare(y, x)); } private sealed class EpisodeBadData : TheoryData diff --git a/tests/Jellyfin.Server.Implementations.Tests/Sorting/IndexNumberComparerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Sorting/IndexNumberComparerTests.cs index 18588bd67..52f71ef4a 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Sorting/IndexNumberComparerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Sorting/IndexNumberComparerTests.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting; public class IndexNumberComparerTests { - private readonly IBaseItemComparer _cmp = new IndexNumberComparer(); + private readonly IndexNumberComparer _cmp = new IndexNumberComparer(); public static TheoryData Compare_GivenNull_ThrowsArgumentNullException_TestData() => new() diff --git a/tests/Jellyfin.Server.Implementations.Tests/Sorting/ParentIndexNumberComparerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Sorting/ParentIndexNumberComparerTests.cs index 261092e01..bedd187eb 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Sorting/ParentIndexNumberComparerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Sorting/ParentIndexNumberComparerTests.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting; public class ParentIndexNumberComparerTests { - private readonly IBaseItemComparer _cmp = new ParentIndexNumberComparer(); + private readonly ParentIndexNumberComparer _cmp = new ParentIndexNumberComparer(); public static TheoryData Compare_GivenNull_ThrowsArgumentNullException_TestData() => new() diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index 1c87d11f1..a078eff77 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -8,9 +8,9 @@ using Jellyfin.Server.Helpers; using MediaBrowser.Common; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Serilog; @@ -39,9 +39,9 @@ namespace Jellyfin.Server.Integration.Tests } /// - protected override IWebHostBuilder CreateWebHostBuilder() + protected override IHostBuilder CreateHostBuilder() { - return new WebHostBuilder(); + return new HostBuilder(); } /// @@ -95,18 +95,17 @@ namespace Jellyfin.Server.Integration.Tests } /// - protected override TestServer CreateServer(IWebHostBuilder builder) + protected override IHost CreateHost(IHostBuilder builder) { - // Create the test server using the base implementation - var testServer = base.CreateServer(builder); - - // Finish initializing the app host - var appHost = (TestAppHost)testServer.Services.GetRequiredService(); - appHost.ServiceProvider = testServer.Services; + var host = builder.Build(); + var appHost = (TestAppHost)host.Services.GetRequiredService(); + appHost.ServiceProvider = host.Services; appHost.InitializeServices().GetAwaiter().GetResult(); + host.Start(); + appHost.RunStartupTasksAsync().GetAwaiter().GetResult(); - return testServer; + return host; } /// diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index 288102037..123266d29 100644 --- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -1,16 +1,17 @@ using System; using System.Linq; using System.Net; -using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; using Jellyfin.Server.Extensions; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; +using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace Jellyfin.Server.Tests {