From 82ff3fa75d7da1d160ee5b774bd2238b5727d5ea Mon Sep 17 00:00:00 2001 From: Erik Rigtorp Date: Sun, 3 May 2020 20:44:18 -0700 Subject: Add additional resolver tests --- tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs | 1 + tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs | 10 ++++++++++ 2 files changed, 11 insertions(+) (limited to 'tests') diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs index 917d8fb3a..49820df62 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs @@ -47,6 +47,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME: [InlineData("Robin Hood [Multi-Subs] [2018].mkv", "Robin Hood", 2018)] [InlineData(@"3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again [InlineData("3 days to kill (2005).mkv", "3 days to kill", 2005)] + [InlineData(@"Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4", "Rain Man", 1988)] public void CleanDateTimeTest(string input, string expectedName, int? expectedYear) { input = Path.GetFileName(input); diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs index 99828b2eb..096ebb836 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs @@ -174,6 +174,16 @@ namespace Jellyfin.Naming.Tests.Video Year = 2006, } }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4", + Container = "mp4", + Name = "Rain Man", + Year = 1988, + } + }; } [Theory] -- cgit v1.2.3 From 9ef79d190b2490a03c566bfaaf963fbba7d124a9 Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Sat, 12 Sep 2020 16:41:37 +0100 Subject: Large number of files --- Emby.Dlna/Main/DlnaEntryPoint.cs | 42 +- Emby.Dlna/PlayTo/PlayToManager.cs | 10 +- Emby.Server.Implementations/ApplicationHost.cs | 218 +--- .../Emby.Server.Implementations.csproj | 2 +- .../LiveTv/LiveTvMediaSourceProvider.cs | 2 +- .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 1 + .../TunerHosts/HdHomerun/HdHomerunUdpStream.cs | 3 +- .../LiveTv/TunerHosts/M3UTunerHost.cs | 1 + .../Networking/NetworkManager.cs | 556 --------- Emby.Server.Implementations/Udp/UdpServer.cs | 2 +- Jellyfin.Api/Auth/BaseAuthorizationHandler.cs | 3 +- .../DefaultAuthorizationHandler.cs | 3 +- .../Auth/DownloadPolicy/DownloadHandler.cs | 3 +- ...FirstTimeOrIgnoreParentalControlSetupHandler.cs | 3 +- .../FirstTimeSetupOrDefaultHandler.cs | 1 + .../FirstTimeSetupOrElevatedHandler.cs | 1 + .../IgnoreParentalControlHandler.cs | 3 +- .../LocalAccessOrRequiresElevationHandler.cs | 3 +- .../Auth/LocalAccessPolicy/LocalAccessHandler.cs | 3 +- .../RequiresElevationHandler.cs | 1 + Jellyfin.Api/Controllers/SystemController.cs | 11 +- Jellyfin.Api/Controllers/UserController.cs | 3 +- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 3 +- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 3 +- Jellyfin.Networking/Jellyfin.Networking.csproj | 39 + Jellyfin.Networking/Manager/INetworkManager.cs | 189 +++ Jellyfin.Networking/Manager/NetworkManager.cs | 1203 ++++++++++++++++++++ .../Jellyfin.Server.Implementations.csproj | 1 + .../Users/UserManager.cs | 4 +- Jellyfin.Server/CoreAppHost.cs | 4 +- .../IpBasedAccessValidationMiddleware.cs | 37 +- .../Middleware/LanFilteringMiddleware.cs | 24 +- Jellyfin.Server/Program.cs | 67 +- MediaBrowser.Common/Net/INetworkManager.cs | 97 -- MediaBrowser.Controller/IServerApplicationHost.cs | 30 +- .../Configuration/PathSubstitution.cs | 19 + .../Configuration/ServerConfiguration.cs | 388 ++++--- MediaBrowser.sln | 34 +- RSSDP/RSSDP.csproj | 1 + RSSDP/SsdpCommunicationsServer.cs | 5 +- RSSDP/SsdpDevicePublisher.cs | 11 +- .../LocalAccessPolicy/LocalAccessHandlerTests.cs | 3 +- .../JellyfinApplicationFactory.cs | 3 - 43 files changed, 1836 insertions(+), 1204 deletions(-) delete mode 100644 Emby.Server.Implementations/Networking/NetworkManager.cs create mode 100644 Jellyfin.Networking/Jellyfin.Networking.csproj create mode 100644 Jellyfin.Networking/Manager/INetworkManager.cs create mode 100644 Jellyfin.Networking/Manager/NetworkManager.cs delete mode 100644 MediaBrowser.Common/Net/INetworkManager.cs create mode 100644 MediaBrowser.Model/Configuration/PathSubstitution.cs (limited to 'tests') diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 40c2cc0e0..98f50c09a 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -2,12 +2,14 @@ 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 Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -25,6 +27,7 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Net; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; +using NetworkCollection; using Rssdp; using Rssdp.Infrastructure; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; @@ -134,20 +137,20 @@ namespace Emby.Dlna.Main { await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); - await ReloadComponents().ConfigureAwait(false); + ReloadComponents(); _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; } - private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) + private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) { if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) { - await ReloadComponents().ConfigureAwait(false); + ReloadComponents(); } } - private async Task ReloadComponents() + private void ReloadComponents() { var options = _config.GetDlnaConfiguration(); @@ -155,7 +158,7 @@ namespace Emby.Dlna.Main if (options.EnableServer) { - await StartDevicePublisher(options).ConfigureAwait(false); + StartDevicePublisher(options); } else { @@ -225,7 +228,7 @@ namespace Emby.Dlna.Main } } - public async Task StartDevicePublisher(Configuration.DlnaOptions options) + public void StartDevicePublisher(Configuration.DlnaOptions options) { if (!options.BlastAliveMessages) { @@ -245,7 +248,7 @@ namespace Emby.Dlna.Main SupportPnpRootDevice = false }; - await RegisterServerEndpoints().ConfigureAwait(false); + RegisterServerEndpoints(); _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); } @@ -255,39 +258,46 @@ namespace Emby.Dlna.Main } } - private async Task RegisterServerEndpoints() + private void RegisterServerEndpoints() { - var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false); + var bindAddresses = _networkManager.GetInternalBindAddresses() + .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)); var udn = CreateUuid(_appHost.SystemId); - foreach (var address in addresses) + if (!bindAddresses.Any()) { - if (address.AddressFamily == AddressFamily.InterNetworkV6) + // No interfaces returned, so use loopback. + bindAddresses = _networkManager.GetLoopbacks(); + } + + foreach (var addr in bindAddresses) + { + if (addr.AddressFamily == AddressFamily.InterNetworkV6) { // Not supporting IPv6 right now continue; } // Limit to LAN addresses only - if (!_networkManager.IsAddressInSubnets(address, true, true)) + if (!_networkManager.IsInLocalNetwork(addr)) { continue; } var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; - _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); + _logger.LogInformation("Registering publisher for {0} on {1}", fullService, addr); var descriptorUri = "/dlna/" + udn + "/description.xml"; - var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri); + var uri = new Uri(_appHost.GetSmartApiUrl(addr.Address) + descriptorUri); var device = new SsdpRootDevice { CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. Location = uri, // Must point to the URL that serves your devices UPnP description document. - Address = address, - SubnetMask = _networkManager.GetLocalIpSubnetMask(address), + Address = addr.Address, + SubnetMask = ((IPNetAddress)addr).Mask, // MIGRATION: This fields is going. FriendlyName = "Jellyfin", Manufacturer = "Jellyfin", ModelName = "Jellyfin Server", diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index 21877f121..10887bf60 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -177,15 +177,7 @@ namespace Emby.Dlna.PlayTo _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); - string serverAddress; - if (info.LocalIpAddress == null || info.LocalIpAddress.Equals(IPAddress.Any) || info.LocalIpAddress.Equals(IPAddress.IPv6Any)) - { - serverAddress = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - } - else - { - serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress); - } + string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress); controller = new PlayToController( sessionInfo, diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 642e2fdbe..cc04cb03f 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -46,6 +46,7 @@ using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; +using Jellyfin.Networking.Manager; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; @@ -97,6 +98,7 @@ using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -117,7 +119,6 @@ namespace Emby.Server.Implementations private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" }; private readonly IFileSystem _fileSystemManager; - private readonly INetworkManager _networkManager; private readonly IXmlSerializer _xmlSerializer; private readonly IStartupOptions _startupOptions; @@ -188,6 +189,11 @@ namespace Emby.Server.Implementations /// The plugins. public IReadOnlyList Plugins => _plugins; + /// + /// Gets the NetworkManager object. + /// + private readonly INetworkManager _networkManager; + /// /// Gets the logger factory. /// @@ -211,7 +217,7 @@ namespace Emby.Server.Implementations private readonly List _disposableParts = new List(); /// - /// Gets the configuration manager. + /// Gets or sets the configuration manager. /// /// The configuration manager. protected IConfigurationManager ConfigurationManager { get; set; } @@ -244,28 +250,25 @@ namespace Emby.Server.Implementations /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. /// Instance of the interface. public ApplicationHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, - INetworkManager networkManager, IServiceCollection serviceCollection) { _xmlSerializer = new MyXmlSerializer(); ServiceCollection = serviceCollection; - _networkManager = networkManager; - networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets; - ApplicationPaths = applicationPaths; LoggerFactory = loggerFactory; _fileSystemManager = fileSystem; ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); + _networkManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger()); + Logger = LoggerFactory.CreateLogger(); _startupOptions = options; @@ -278,8 +281,6 @@ namespace Emby.Server.Implementations fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); - _networkManager.NetworkChanged += OnNetworkChanged; - CertificateInfo = new CertificateInfo { Path = ServerConfigurationManager.Configuration.CertificatePath, @@ -308,16 +309,6 @@ namespace Emby.Server.Implementations .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase); } - private string[] GetConfiguredLocalSubnets() - { - return ServerConfigurationManager.Configuration.LocalNetworkSubnets; - } - - private void OnNetworkChanged(object sender, EventArgs e) - { - _validAddressResults.Clear(); - } - /// public Version ApplicationVersion { get; } @@ -398,7 +389,7 @@ namespace Emby.Server.Implementations /// /// Resolves this instance. /// - /// The type + /// The type. /// ``0. public T Resolve() => ServiceProvider.GetService(); @@ -1091,13 +1082,10 @@ namespace Emby.Server.Implementations /// /// Gets the system status. /// - /// The cancellation token. + /// Where this request originated. /// SystemInfo. - public async Task GetSystemInfo(CancellationToken cancellationToken) + public SystemInfo GetSystemInfo(IPAddress source) { - var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - var transcodingTempPath = ConfigurationManager.GetTranscodePath(); - return new SystemInfo { HasPendingRestart = HasPendingRestart, @@ -1117,9 +1105,9 @@ namespace Emby.Server.Implementations CanSelfRestart = CanSelfRestart, CanLaunchWebBrowser = CanLaunchWebBrowser, HasUpdateAvailable = HasUpdateAvailable, - TranscodingTempPath = transcodingTempPath, + TranscodingTempPath = ConfigurationManager.GetTranscodePath(), ServerName = FriendlyName, - LocalAddress = localAddress, + LocalAddress = GetSmartApiUrl(source), SupportsLibraryMonitor = true, EncoderLocation = _mediaEncoder.EncoderLocation, SystemArchitecture = RuntimeInformation.OSArchitecture, @@ -1132,10 +1120,8 @@ namespace Emby.Server.Implementations .Select(i => new WakeOnLanInfo(i)) .ToList(); - public async Task GetPublicSystemInfo(CancellationToken cancellationToken) + public PublicSystemInfo GetPublicSystemInfo(IPAddress source) { - var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - return new PublicSystemInfo { Version = ApplicationVersionString, @@ -1143,8 +1129,8 @@ namespace Emby.Server.Implementations Id = SystemId, OperatingSystem = OperatingSystem.Id.ToString(), ServerName = FriendlyName, - LocalAddress = localAddress, - StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted + LocalAddress = GetSmartApiUrl(source), + StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted }; } @@ -1152,186 +1138,51 @@ namespace Emby.Server.Implementations public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps; /// - public async Task GetLocalApiUrl(CancellationToken cancellationToken) + public string GetSmartApiUrl(object source) { - try - { - // Return the first matched address, if found, or the first known local address - var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false); - if (addresses.Count == 0) - { - return null; - } - - return GetLocalApiUrl(addresses[0]); - } - catch (Exception ex) + // Published server ends with a / + if (_startupOptions.PublishedServerUrl != null) { - Logger.LogError(ex, "Error getting local Ip address information"); + // Published server ends with a '/', so we need to remove it. + return _startupOptions.PublishedServerUrl.ToString().Trim('/'); } - return null; - } + string smart = _networkManager.GetBindInterface(source, out int? port); - /// - /// Removes the scope id from IPv6 addresses. - /// - /// The IPv6 address. - /// The IPv6 address without the scope id. - private ReadOnlySpan RemoveScopeId(ReadOnlySpan address) - { - var index = address.IndexOf('%'); - if (index == -1) + // If the smartAPI doesn't start with http then treat it as a host or ip. + if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - return address; + return smart.Trim('/'); } - return address.Slice(0, index); + return GetLocalApiUrl(smart.Trim('/'), source is HttpRequest request ? request.Scheme : null, port); } - /// - public string GetLocalApiUrl(IPAddress ipAddress) + /// + public string GetLoopbackHttpApiUrl() { - if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) + if (NetworkManager.IsIP6Enabled) { - var str = RemoveScopeId(ipAddress.ToString()); - Span span = new char[str.Length + 2]; - span[0] = '['; - str.CopyTo(span.Slice(1)); - span[^1] = ']'; - - return GetLocalApiUrl(span); + return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort); } - return GetLocalApiUrl(ipAddress.ToString()); - } - - /// - public string GetLoopbackHttpApiUrl() - { return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort); } /// - public string GetLocalApiUrl(ReadOnlySpan host, string scheme = null, int? port = null) + public string GetLocalApiUrl(string host, string scheme = null, int? port = null) { // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does // not. For consistency, always trim the trailing slash. return new UriBuilder { Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), - Host = host.ToString(), + Host = host, Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), Path = ServerConfigurationManager.Configuration.BaseUrl }.ToString().TrimEnd('/'); } - public Task> GetLocalIpAddresses(CancellationToken cancellationToken) - { - return GetLocalIpAddressesInternal(true, 0, cancellationToken); - } - - private async Task> GetLocalIpAddressesInternal(bool allowLoopback, int limit, CancellationToken cancellationToken) - { - var addresses = ServerConfigurationManager - .Configuration - .LocalNetworkAddresses - .Select(x => NormalizeConfiguredLocalAddress(x)) - .Where(i => i != null) - .ToList(); - - if (addresses.Count == 0) - { - addresses.AddRange(_networkManager.GetLocalIpAddresses()); - } - - var resultList = new List(); - - foreach (var address in addresses) - { - if (!allowLoopback) - { - if (address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback)) - { - continue; - } - } - - if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false)) - { - resultList.Add(address); - - if (limit > 0 && resultList.Count >= limit) - { - return resultList; - } - } - } - - return resultList; - } - - public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan address) - { - var index = address.Trim('/').IndexOf('/'); - if (index != -1) - { - address = address.Slice(index + 1); - } - - if (IPAddress.TryParse(address.Trim('/'), out IPAddress result)) - { - return result; - } - - return null; - } - - private readonly ConcurrentDictionary _validAddressResults = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - - private async Task IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken) - { - if (address.Equals(IPAddress.Loopback) - || address.Equals(IPAddress.IPv6Loopback)) - { - return true; - } - - var apiUrl = GetLocalApiUrl(address) + "/system/ping"; - - if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult)) - { - return cachedResult; - } - - try - { - using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - var result = await System.Text.Json.JsonSerializer.DeserializeAsync(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false); - var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase); - - _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); - Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid); - return valid; - } - catch (OperationCanceledException) - { - Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, "Cancelled"); - throw; - } - catch (Exception ex) - { - Logger.LogDebug(ex, "Ping test result to {0}. Success: {1}", apiUrl, false); - - _validAddressResults.AddOrUpdate(apiUrl, false, (k, v) => false); - return false; - } - } - public string FriendlyName => string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName) ? Environment.MachineName @@ -1486,6 +1337,7 @@ namespace Emby.Server.Implementations _disposed = true; } + } internal class CertificateInfo diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 0a348f0d0..db8116743 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -36,7 +36,7 @@ - + diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index 8a0c0043a..598cf0af7 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -76,7 +76,7 @@ namespace Emby.Server.Implementations.LiveTv } var list = sources.ToList(); - var serverUrl = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); + var serverUrl = _appHost.GetSmartApiUrl(string.Empty); foreach (var source in list) { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 28e30fac8..1cf129ad2 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -10,6 +10,7 @@ using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 6730751d5..02ee302d0 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -7,6 +7,7 @@ using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -57,7 +58,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var mediaSource = OriginalMediaSource; var uri = new Uri(mediaSource.Path); - var localPort = _networkManager.GetRandomUnusedUdpPort(); + var localPort = 50000; // Will return to random after next PR. Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath)); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 8107bc427..f297ecd5d 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs deleted file mode 100644 index 089ec30e6..000000000 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ /dev/null @@ -1,556 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.NetworkInformation; -using System.Net.Sockets; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Networking -{ - /// - /// Class to take care of network interface management. - /// - public class NetworkManager : INetworkManager - { - private readonly ILogger _logger; - - private IPAddress[] _localIpAddresses; - private readonly object _localIpAddressSyncLock = new object(); - - private readonly object _subnetLookupLock = new object(); - private readonly Dictionary> _subnetLookup = new Dictionary>(StringComparer.Ordinal); - - private List _macAddresses; - - /// - /// Initializes a new instance of the class. - /// - /// Logger to use for messages. - public NetworkManager(ILogger logger) - { - _logger = logger; - - NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; - NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; - } - - /// - public event EventHandler NetworkChanged; - - /// - public Func LocalSubnetsFn { get; set; } - - private void OnNetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e) - { - _logger.LogDebug("NetworkAvailabilityChanged"); - OnNetworkChanged(); - } - - private void OnNetworkAddressChanged(object sender, EventArgs e) - { - _logger.LogDebug("NetworkAddressChanged"); - OnNetworkChanged(); - } - - private void OnNetworkChanged() - { - lock (_localIpAddressSyncLock) - { - _localIpAddresses = null; - _macAddresses = null; - } - - NetworkChanged?.Invoke(this, EventArgs.Empty); - } - - /// - public IPAddress[] GetLocalIpAddresses() - { - lock (_localIpAddressSyncLock) - { - if (_localIpAddresses == null) - { - var addresses = GetLocalIpAddressesInternal().ToArray(); - - _localIpAddresses = addresses; - } - - return _localIpAddresses; - } - } - - private List GetLocalIpAddressesInternal() - { - var list = GetIPsDefault().ToList(); - - if (list.Count == 0) - { - list = GetLocalIpAddressesFallback().GetAwaiter().GetResult().ToList(); - } - - var listClone = new List(); - - var subnets = LocalSubnetsFn(); - - foreach (var i in list) - { - if (i.IsIPv6LinkLocal || i.ToString().StartsWith("169.254.", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (Array.IndexOf(subnets, $"[{i}]") == -1) - { - listClone.Add(i); - } - } - - return listClone - .OrderBy(i => i.AddressFamily == AddressFamily.InterNetwork ? 0 : 1) - // .ThenBy(i => listClone.IndexOf(i)) - .GroupBy(i => i.ToString()) - .Select(x => x.First()) - .ToList(); - } - - /// - public bool IsInPrivateAddressSpace(string endpoint) - { - return IsInPrivateAddressSpace(endpoint, true); - } - - // Checks if the address in endpoint is an RFC1918, RFC1122, or RFC3927 address - private bool IsInPrivateAddressSpace(string endpoint, bool checkSubnets) - { - if (string.Equals(endpoint, "::1", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - // IPV6 - if (endpoint.Split('.').Length > 4) - { - // Handle ipv4 mapped to ipv6 - var originalEndpoint = endpoint; - endpoint = endpoint.Replace("::ffff:", string.Empty, StringComparison.OrdinalIgnoreCase); - - if (string.Equals(endpoint, originalEndpoint, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - - // Private address space: - - if (string.Equals(endpoint, "localhost", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - if (!IPAddress.TryParse(endpoint, out var ipAddress)) - { - return false; - } - - byte[] octet = ipAddress.GetAddressBytes(); - - if ((octet[0] == 10) || - (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918 - (octet[0] == 192 && octet[1] == 168) || // RFC1918 - (octet[0] == 127) || // RFC1122 - (octet[0] == 169 && octet[1] == 254)) // RFC3927 - { - return true; - } - - if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint)) - { - return true; - } - - return false; - } - - /// - public bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint) - { - if (endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase)) - { - var endpointFirstPart = endpoint.Split('.')[0]; - - var subnets = GetSubnets(endpointFirstPart); - - foreach (var subnet_Match in subnets) - { - // logger.LogDebug("subnet_Match:" + subnet_Match); - - if (endpoint.StartsWith(subnet_Match + ".", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - } - - return false; - } - - // Gives a list of possible subnets from the system whose interface ip starts with endpointFirstPart - private List GetSubnets(string endpointFirstPart) - { - lock (_subnetLookupLock) - { - if (_subnetLookup.TryGetValue(endpointFirstPart, out var subnets)) - { - return subnets; - } - - subnets = new List(); - - foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces()) - { - foreach (var unicastIPAddressInformation in adapter.GetIPProperties().UnicastAddresses) - { - if (unicastIPAddressInformation.Address.AddressFamily == AddressFamily.InterNetwork && endpointFirstPart == unicastIPAddressInformation.Address.ToString().Split('.')[0]) - { - int subnet_Test = 0; - foreach (string part in unicastIPAddressInformation.IPv4Mask.ToString().Split('.')) - { - if (part.Equals("0", StringComparison.Ordinal)) - { - break; - } - - subnet_Test++; - } - - var subnet_Match = string.Join(".", unicastIPAddressInformation.Address.ToString().Split('.').Take(subnet_Test).ToArray()); - - // TODO: Is this check necessary? - if (adapter.OperationalStatus == OperationalStatus.Up) - { - subnets.Add(subnet_Match); - } - } - } - } - - _subnetLookup[endpointFirstPart] = subnets; - - return subnets; - } - } - - /// - public bool IsInLocalNetwork(string endpoint) - { - return IsInLocalNetworkInternal(endpoint, true); - } - - /// - public bool IsAddressInSubnets(string addressString, string[] subnets) - { - return IsAddressInSubnets(IPAddress.Parse(addressString), addressString, subnets); - } - - /// - public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC) - { - byte[] octet = address.GetAddressBytes(); - - if ((octet[0] == 127) || // RFC1122 - (octet[0] == 169 && octet[1] == 254)) // RFC3927 - { - // don't use on loopback or 169 interfaces - return false; - } - - string addressString = address.ToString(); - string excludeAddress = "[" + addressString + "]"; - var subnets = LocalSubnetsFn(); - - // Include any address if LAN subnets aren't specified - if (subnets.Length == 0) - { - return true; - } - - // Exclude any addresses if they appear in the LAN list in [ ] - if (Array.IndexOf(subnets, excludeAddress) != -1) - { - return false; - } - - return IsAddressInSubnets(address, addressString, subnets); - } - - /// - /// Checks if the give address falls within the ranges given in [subnets]. The addresses in subnets can be hosts or subnets in the CIDR format. - /// - /// IPAddress version of the address. - /// The address to check. - /// If true, check against addresses in the LAN settings which have [] arroud and return true if it matches the address give in address. - /// falseif the address isn't in the subnets, true otherwise. - private static bool IsAddressInSubnets(IPAddress address, string addressString, string[] subnets) - { - foreach (var subnet in subnets) - { - var normalizedSubnet = subnet.Trim(); - // Is the subnet a host address and does it match the address being passes? - if (string.Equals(normalizedSubnet, addressString, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - // Parse CIDR subnets and see if address falls within it. - if (normalizedSubnet.Contains('/', StringComparison.Ordinal)) - { - try - { - var ipNetwork = IPNetwork.Parse(normalizedSubnet); - if (ipNetwork.Contains(address)) - { - return true; - } - } - catch - { - // Ignoring - invalid subnet passed encountered. - } - } - } - - return false; - } - - private bool IsInLocalNetworkInternal(string endpoint, bool resolveHost) - { - if (string.IsNullOrEmpty(endpoint)) - { - throw new ArgumentNullException(nameof(endpoint)); - } - - if (IPAddress.TryParse(endpoint, out var address)) - { - var addressString = address.ToString(); - - var localSubnetsFn = LocalSubnetsFn; - if (localSubnetsFn != null) - { - var localSubnets = localSubnetsFn(); - foreach (var subnet in localSubnets) - { - // Only validate if there's at least one valid entry. - if (!string.IsNullOrWhiteSpace(subnet)) - { - return IsAddressInSubnets(address, addressString, localSubnets) || IsInPrivateAddressSpace(addressString, false); - } - } - } - - int lengthMatch = 100; - if (address.AddressFamily == AddressFamily.InterNetwork) - { - lengthMatch = 4; - if (IsInPrivateAddressSpace(addressString, true)) - { - return true; - } - } - else if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - lengthMatch = 9; - if (IsInPrivateAddressSpace(endpoint, true)) - { - return true; - } - } - - // Should be even be doing this with ipv6? - if (addressString.Length >= lengthMatch) - { - var prefix = addressString.Substring(0, lengthMatch); - - if (GetLocalIpAddresses().Any(i => i.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase))) - { - return true; - } - } - } - else if (resolveHost) - { - if (Uri.TryCreate(endpoint, UriKind.RelativeOrAbsolute, out var uri)) - { - try - { - var host = uri.DnsSafeHost; - _logger.LogDebug("Resolving host {0}", host); - - address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault(); - - if (address != null) - { - _logger.LogDebug("{0} resolved to {1}", host, address); - - return IsInLocalNetworkInternal(address.ToString(), false); - } - } - catch (InvalidOperationException) - { - // Can happen with reverse proxy or IIS url rewriting? - } - catch (Exception ex) - { - _logger.LogError(ex, "Error resolving hostname"); - } - } - } - - return false; - } - - private static Task GetIpAddresses(string hostName) - { - return Dns.GetHostAddressesAsync(hostName); - } - - private IEnumerable GetIPsDefault() - { - IEnumerable interfaces; - - try - { - interfaces = NetworkInterface.GetAllNetworkInterfaces() - .Where(x => x.OperationalStatus == OperationalStatus.Up - || x.OperationalStatus == OperationalStatus.Unknown); - } - catch (NetworkInformationException ex) - { - _logger.LogError(ex, "Error in GetAllNetworkInterfaces"); - return Enumerable.Empty(); - } - - return interfaces.SelectMany(network => - { - var ipProperties = network.GetIPProperties(); - - // Exclude any addresses if they appear in the LAN list in [ ] - - return ipProperties.UnicastAddresses - .Select(i => i.Address) - .Where(i => i.AddressFamily == AddressFamily.InterNetwork || i.AddressFamily == AddressFamily.InterNetworkV6); - }).GroupBy(i => i.ToString()) - .Select(x => x.First()); - } - - private static async Task> GetLocalIpAddressesFallback() - { - var host = await Dns.GetHostEntryAsync(Dns.GetHostName()).ConfigureAwait(false); - - // Reverse them because the last one is usually the correct one - // It's not fool-proof so ultimately the consumer will have to examine them and decide - return host.AddressList - .Where(i => i.AddressFamily == AddressFamily.InterNetwork || i.AddressFamily == AddressFamily.InterNetworkV6) - .Reverse(); - } - - /// - /// Gets a random port number that is currently available. - /// - /// System.Int32. - public int GetRandomUnusedTcpPort() - { - var listener = new TcpListener(IPAddress.Any, 0); - listener.Start(); - var port = ((IPEndPoint)listener.LocalEndpoint).Port; - listener.Stop(); - return port; - } - - /// - public int GetRandomUnusedUdpPort() - { - var localEndPoint = new IPEndPoint(IPAddress.Any, 0); - using (var udpClient = new UdpClient(localEndPoint)) - { - return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; - } - } - - /// - public List GetMacAddresses() - { - return _macAddresses ??= GetMacAddressesInternal().ToList(); - } - - private static IEnumerable GetMacAddressesInternal() - => NetworkInterface.GetAllNetworkInterfaces() - .Where(i => i.NetworkInterfaceType != NetworkInterfaceType.Loopback) - .Select(x => x.GetPhysicalAddress()) - .Where(x => !x.Equals(PhysicalAddress.None)); - - /// - public bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask) - { - IPAddress network1 = GetNetworkAddress(address1, subnetMask); - IPAddress network2 = GetNetworkAddress(address2, subnetMask); - return network1.Equals(network2); - } - - private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask) - { - byte[] ipAdressBytes = address.GetAddressBytes(); - byte[] subnetMaskBytes = subnetMask.GetAddressBytes(); - - if (ipAdressBytes.Length != subnetMaskBytes.Length) - { - throw new ArgumentException("Lengths of IP address and subnet mask do not match."); - } - - byte[] broadcastAddress = new byte[ipAdressBytes.Length]; - for (int i = 0; i < broadcastAddress.Length; i++) - { - broadcastAddress[i] = (byte)(ipAdressBytes[i] & subnetMaskBytes[i]); - } - - return new IPAddress(broadcastAddress); - } - - /// - public IPAddress GetLocalIpSubnetMask(IPAddress address) - { - NetworkInterface[] interfaces; - - try - { - var validStatuses = new[] { OperationalStatus.Up, OperationalStatus.Unknown }; - - interfaces = NetworkInterface.GetAllNetworkInterfaces() - .Where(i => validStatuses.Contains(i.OperationalStatus)) - .ToArray(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in GetAllNetworkInterfaces"); - return null; - } - - foreach (NetworkInterface ni in interfaces) - { - foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses) - { - if (ip.Address.Equals(address) && ip.IPv4Mask != null) - { - return ip.IPv4Mask; - } - } - } - - return null; - } - } -} diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index b7a59cee2..3dc34da5c 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Udp { string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey]) ? _config[AddressOverrideConfigKey] - : await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); + : _appHost.GetSmartApiUrl(string.Empty); // MIGRATION: Temp value. if (!string.IsNullOrEmpty(localUrl)) { diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs index d732b6bc6..08746b346 100644 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -1,6 +1,7 @@ -using System.Security.Claims; +using System.Security.Claims; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index b5913daab..69e6a8fb2 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs index b61680ab1..d1297119c 100644 --- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs +++ b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs index 31482a930..53b5d4778 100644 --- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs index 9815e252e..abdf2858d 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs index decbe0c03..ada8a0d4e 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs index 5213bc4cb..475e3cdac 100644 --- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs +++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs index 14722aa57..d022c9067 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -1,5 +1,6 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs index af73352bc..418d63de6 100644 --- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs index b235c4b63..a1cddbca3 100644 --- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 4cb1984a2..6876b47b4 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -64,9 +65,9 @@ namespace Jellyfin.Api.Controllers [HttpGet("Info")] [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetSystemInfo() + public ActionResult GetSystemInfo() { - return await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false); + return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); } /// @@ -76,9 +77,9 @@ namespace Jellyfin.Api.Controllers /// A with public info about the system. [HttpGet("Info/Public")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetPublicSystemInfo() + public ActionResult GetPublicSystemInfo() { - return await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false); + return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); } /// diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 630e9df6a..152e650bc 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -7,6 +7,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index af0519ffa..3be8734b9 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -8,6 +8,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 1207fb513..d63e3ab11 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.Linq; using System.Text.Json; @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj new file mode 100644 index 000000000..b6834cb0e --- /dev/null +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -0,0 +1,39 @@ + + + Jellyfin.Networking + Library + netstandard2.1 + false + true + true + enable + + + + + + + + + + + + + + + ../jellyfin.ruleset + + + + + + + + + + + + + + + diff --git a/Jellyfin.Networking/Manager/INetworkManager.cs b/Jellyfin.Networking/Manager/INetworkManager.cs new file mode 100644 index 000000000..ba571750b --- /dev/null +++ b/Jellyfin.Networking/Manager/INetworkManager.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; +using MediaBrowser.Model.Configuration; +using NetworkCollection; + +namespace Jellyfin.Networking.Manager +{ + /// + /// Interface for the NetworkManager class. + /// + public interface INetworkManager + { + /// + /// Event triggered on network changes. + /// + event EventHandler NetworkChanged; + + /// + /// Gets the Published server override list. + /// + Dictionary PublishedServerOverrides { get; } + + /// + /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. + /// + public bool TrustAllIP6Interfaces { get; } + + /// + /// Gets returns the remote address filter. + /// + NetCollection RemoteAddressFilter { get; } + + /// + /// Calculates the list of interfaces to use for Kestrel. + /// + /// A NetCollection object containing all the interfaces to bind. + /// If all the interfaces are specified, and none are excluded, it returns zero items + /// to represent any address. + NetCollection GetAllBindInterfaces(); + + /// + /// Returns a collection containing the loopback interfaces. + /// + /// Netcollection. + public NetCollection GetLoopbacks(); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// The priority of selection is as follows:- + /// + /// The value contained in the startup parameter --published-server-url. + /// + /// If the user specified custom subnet overrides, the correct subnet for the source address. + /// + /// If the user specified bind interfaces to use:- + /// The bind interface that contains the source subnet. + /// The first bind interface specified that suits best first the source's endpoint. eg. external or internal. + /// + /// If the source is from a public subnet address range and the user hasn't specified any bind addresses:- + /// The first public interface that isn't a loopback and contains the source subnet. + /// The first public interface that isn't a loopback. Priority is given to interfaces with gateways. + /// An internal interface if there are no public ip addresses. + /// + /// If the source is from a private subnet address range and the user hasn't specified any bind addresses:- + /// The first private interface that contains the source subnet. + /// The first private interface that isn't a loopback. Priority is given to interfaces with gateways. + /// + /// If no interfaces meet any of these criteria, then a loopback address is returned. + /// + /// Interface that have been specifically excluded from binding are not used in any of the calculations. + /// + /// Source of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(object? source, out int? port); + + /// + /// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses. + /// + /// IP address to check. + /// True if it is. + bool IsExcludedInterface(IPAddress address); + + /// + /// Get a list of all the MAC addresses associated with active interfaces. + /// + /// List of MAC addresses. + List GetMacAddresses(); + + /// + /// Checks to see if the IP Address provided matches an interface that has a gateway. + /// + /// IP to check. Can be an IPAddress or an IPObject. + /// Result of the check. + public bool IsGatewayInterface(object? addressObj); + + /// + /// Returns true if the address is a private address. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// Address to check. + /// True or False. + bool IsPrivateAddressRange(IPObject address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(string address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(IPObject address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(IPAddress address); + + /// + /// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes. + /// eg. "eth1", or "TP-LINK Wireless USB Adapter". + /// + /// Token to parse. + /// Resultant object if successful. + /// Success of the operation. + bool TryParseInterface(string token, out IPNetAddress result); + + /// + /// Parses an array of strings into a NetCollection. + /// + /// Values to parse. + /// When true, only include values in []. When false, ignore bracketed values. + /// IPCollection object containing the value strings. + NetCollection CreateIPCollection(string[] values, bool bracketed = false); + + /// + /// Returns all the internal Bind interface addresses. + /// + /// An internal list of interfaces addresses. + NetCollection GetInternalBindAddresses(); + + /// + /// Checks to see if an IP address is still a valid interface address. + /// + /// IP address to check. + /// True if it is. + bool IsValidInterfaceAddress(IPAddress address); + + /// + /// Returns true if the IP address is in the excluded list. + /// + /// IP to check. + /// True if excluded. + bool IsExcluded(IPAddress ip); + + /// + /// Returns true if the IP address is in the excluded list. + /// + /// IP to check. + /// True if excluded. + bool IsExcluded(EndPoint ip); + + /// + /// Gets the filtered LAN ip addresses. + /// + /// Optional filter for the list. + /// Returns a filtered list of LAN addresses. + NetCollection GetFilteredLANSubnets(NetCollection? filter = null); + + /// + /// Reloads all settings and re-initialises the instance. + /// + /// to use. + public void UpdateSettings(ServerConfiguration config); + } +} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs new file mode 100644 index 000000000..36a0a94a0 --- /dev/null +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -0,0 +1,1203 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using NetworkCollection; + +namespace Jellyfin.Networking.Manager +{ + /// + /// Class to take care of network interface management. + /// + public class NetworkManager : INetworkManager, IDisposable + { + private static NetworkManager? _instance; + + /// + /// Contains the description of the interface along with its index. + /// + private readonly SortedList _interfaceNames; + + /// + /// Threading lock for network interfaces. + /// + private readonly object _intLock = new object(); + + /// + /// List of all interface addresses and masks. + /// + private readonly NetCollection _interfaceAddresses; + + /// + /// List of all interface MAC addresses. + /// + private readonly List _macAddresses; + + private readonly ILogger _logger; + + private readonly IConfigurationManager _configurationManager; + + /// + /// Holds the bind address overrides. + /// + private readonly Dictionary _overrideUrls; + + /// + /// Used to stop "event-racing conditions". + /// + private bool _eventfire; + + /// + /// Unfiltered user defined LAN subnets. (Configuration.LocalNetworkSubnets). + /// or internal interface network subnets if undefined by user. + /// + private NetCollection _lanSubnets; + + /// + /// User defined list of subnets to excluded from the LAN. + /// + private NetCollection _excludedSubnets; + + /// + /// List of interface addresses to bind the WS. + /// + private NetCollection _bindAddresses; + + /// + /// List of interface addresses to exclude from bind. + /// + private NetCollection _bindExclusions; + + /// + /// Caches list of all internal filtered interface addresses and masks. + /// + private NetCollection _internalInterfaces; + + /// + /// Flag set when no custom LAN has been defined in the config. + /// + private bool _usingPrivateAddresses; + + /// + /// True if this object is disposed. + /// + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// IServerConfigurationManager instance. + /// Logger to use for messages. +#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in InitialiseLAN function. Compiler doesn't yet recognise this. + public NetworkManager(IConfigurationManager configurationManager, ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _configurationManager = configurationManager ?? throw new ArgumentNullException(nameof(configurationManager)); + + _interfaceAddresses = new NetCollection(unique: false); + _macAddresses = new List(); + _interfaceNames = new SortedList(); + _overrideUrls = new Dictionary(); + + UpdateSettings((ServerConfiguration)_configurationManager.CommonConfiguration); + if (!IsIP6Enabled && !IsIP4Enabled) + { + throw new ApplicationException("IPv4 and IPv6 cannot both be disabled."); + } + + NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; + NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; + + _configurationManager.ConfigurationUpdated += ConfigurationUpdated; + + Instance = this; + } +#pragma warning restore CS8618 // Non-nullable field is uninitialized. + + /// + /// Event triggered on network changes. + /// + public event EventHandler? NetworkChanged; + + /// + /// Gets the singleton of this object. + /// + public static NetworkManager Instance + { + get => GetInstance(); + + internal set + { + _instance = value; + } + } + + /// + /// Gets the unique network location signature, which is updated on every network change. + /// + public static string NetworkLocationSignature { get; internal set; } = Guid.NewGuid().ToString(); + + /// + /// Gets a value indicating whether IP6 is enabled. + /// + public static bool IsIP6Enabled { get; internal set; } + + /// + /// Gets a value indicating whether IP4 is enabled. + /// + public static bool IsIP4Enabled { get; internal set; } = true; + + /// + /// Gets a value indicating whether is multi-socket binding available. + /// + public static bool EnableMultiSocketBinding { get; internal set; } = true; + + /// + /// Gets the number of times the network address has changed. + /// + public static int NetworkChangeCount { get; internal set; } = 1; + + /// + public NetCollection RemoteAddressFilter { get; private set; } + + /// + /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. + /// + public bool TrustAllIP6Interfaces { get; internal set; } + + /// + /// Gets the Published server override list. + /// + public Dictionary PublishedServerOverrides => _overrideUrls; + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public List GetMacAddresses() + { + // Populated in construction - so always has values. + lock (_intLock) + { + return _macAddresses.ToList(); + } + } + + /// + public bool IsGatewayInterface(object? addressObj) + { + var address = (addressObj is IPAddress addressIP) ? + addressIP : (addressObj is IPObject addressIPObj) ? + addressIPObj.Address : IPAddress.None; + + lock (_intLock) + { + return _internalInterfaces.Where(i => i.Address.Equals(address) && (i.Tag < 0)).Any(); + } + } + + /// + public NetCollection GetLoopbacks() + { + NetCollection nc = new NetCollection(); + if (IsIP4Enabled) + { + nc.Add(IPAddress.Loopback); + } + + if (IsIP6Enabled) + { + nc.Add(IPAddress.IPv6Loopback); + } + + return nc; + } + + /// + public bool IsExcluded(IPAddress ip) + { + return _excludedSubnets.Contains(ip); + } + + /// + public bool IsExcluded(EndPoint ip) + { + if (ip != null) + { + return _excludedSubnets.Contains(((IPEndPoint)ip).Address); + } + + return false; + } + + /// + public NetCollection CreateIPCollection(string[] values, bool bracketed = false) + { + NetCollection col = new NetCollection(); + if (values != null) + { + for (int a = 0; a < values.Length; a++) + { + string v = values[a].Trim(); + + try + { + if (v.StartsWith("[", StringComparison.OrdinalIgnoreCase) && v.EndsWith("]", StringComparison.OrdinalIgnoreCase)) + { + if (bracketed) + { + AddToCollection(col, v.Remove(v.Length - 1).Substring(1)); + } + } + else if (v.StartsWith("!", StringComparison.OrdinalIgnoreCase)) + { + if (bracketed) + { + AddToCollection(col, v.Substring(1)); + } + } + else if (!bracketed) + { + AddToCollection(col, v); + } + } + catch (ArgumentException e) + { + _logger.LogInformation("Ignoring LAN value {value}. Reason : {reason}", v, e.Message); + } + } + } + + return col; + } + + /// + public NetCollection GetAllBindInterfaces() + { + lock (_intLock) + { + int count = _bindAddresses.Count; + + if (count == 0) + { + if (_bindExclusions.Count > 0) + { + // Return all the interfaces except the ones specifically excluded. + return _interfaceAddresses.Exclude(_bindExclusions); + } + + // No bind address and no exclusions, so listen on all interfaces. + NetCollection result = new NetCollection(); + + if (IsIP4Enabled) + { + result.Add(IPAddress.Any); + } + + if (IsIP6Enabled) + { + result.Add(IPAddress.IPv6Any); + } + + return result; + } + + // Remove any excluded bind interfaces. + return _bindAddresses.Exclude(_bindExclusions); + } + } + + /// + public string GetBindInterface(object? source, out int? port) + { + bool chromeCast = false; + port = null; + // Parse the source object in an attempt to discover where the request originated. + IPObject sourceAddr; + if (source is HttpRequest sourceReq) + { + port = sourceReq.Host.Port; + if (IPHost.TryParse(sourceReq.Host.Host, out IPHost host)) + { + sourceAddr = host; + } + else + { + // Assume it's external, as we cannot resolve the host. + sourceAddr = IPHost.None; + } + } + else if (source is string sourceStr && !string.IsNullOrEmpty(sourceStr)) + { + if (string.Equals(sourceStr, "chromecast", StringComparison.OrdinalIgnoreCase)) + { + chromeCast = true; + // Just assign a variable so has source = true; + sourceAddr = IPNetAddress.IP4Loopback; + } + + if (IPHost.TryParse(sourceStr, out IPHost host)) + { + sourceAddr = host; + } + else + { + // Assume it's external, as we cannot resolve the host. + sourceAddr = IPHost.None; + } + } + else if (source is IPAddress sourceIP) + { + sourceAddr = new IPNetAddress(sourceIP); + } + else + { + // If we have no idea, then assume it came from an external address. + sourceAddr = IPHost.None; + } + + // Do we have a source? + bool haveSource = !sourceAddr.Address.Equals(IPAddress.None); + + if (haveSource) + { + if (!IsIP6Enabled && sourceAddr.AddressFamily == AddressFamily.InterNetworkV6) + { + _logger.LogWarning("IPv6 is disabled in JellyFin, but enabled in the OS. This may affect how the interface is selected."); + } + + if (!IsIP4Enabled && sourceAddr.AddressFamily == AddressFamily.InterNetwork) + { + _logger.LogWarning("IPv4 is disabled in JellyFin, but enabled in the OS. This may affect how the interface is selected."); + } + } + + bool isExternal = haveSource && !IsInLocalNetwork(sourceAddr); + + string bindPreference = string.Empty; + if (haveSource) + { + // Check for user override. + foreach (var addr in _overrideUrls) + { + // Remaining. Match anything. + if (addr.Key.Equals(IPAddress.Broadcast)) + { + bindPreference = addr.Value; + break; + } + else if ((addr.Key.Equals(IPAddress.Any) || addr.Key.Equals(IPAddress.IPv6Any)) && (isExternal || chromeCast)) + { + // External. + bindPreference = addr.Value; + break; + } + else if (addr.Key.Contains(sourceAddr)) + { + // Match ip address. + bindPreference = addr.Value; + break; + } + } + } + + _logger.LogDebug("GetBindInterface: Souce: {0}, External: {1}:", haveSource, isExternal); + + if (!string.IsNullOrEmpty(bindPreference)) + { + // Has it got a port defined? + var parts = bindPreference.Split(':'); + if (parts.Length > 1) + { + if (int.TryParse(parts[1], out int p)) + { + bindPreference = parts[0]; + port = p; + } + } + + _logger.LogInformation("{0}: Using BindAddress {1}:{2}", sourceAddr, bindPreference, port); + return bindPreference; + } + + string ipresult; + + // No preference given, so move on to bind addresses. + lock (_intLock) + { + var nc = _bindAddresses.Exclude(_bindExclusions).Where(p => !p.IsLoopback()); + + int count = nc.Count(); + if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses.Equals(IPAddress.IPv6Any))) + { + // Ignore IPAny addresses. + count = 0; + } + + if (count != 0) + { + // Check to see if any of the bind interfaces are in the same subnet. + + IEnumerable bindResult; + IPAddress? defaultGateway = null; + + if (isExternal) + { + // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first. + bindResult = nc.Where(p => !IsInLocalNetwork(p)).OrderBy(p => p.Tag); + defaultGateway = bindResult.FirstOrDefault()?.Address; + bindResult = bindResult.Where(p => p.Contains(sourceAddr)).OrderBy(p => p.Tag); + } + else + { + // Look for the best internal address. + bindResult = nc.Where(p => IsInLocalNetwork(p) && p.Contains(sourceAddr)).OrderBy(p => p.Tag); + } + + if (bindResult.Any()) + { + ipresult = FormatIP6String(bindResult.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Has source, found a match bind interface subnets. {1}", sourceAddr, ipresult); + return ipresult; + } + + if (isExternal && defaultGateway != null) + { + ipresult = FormatIP6String(defaultGateway); + _logger.LogDebug("{0}: GetBindInterface: Using first user defined external interface. {1}", sourceAddr, ipresult); + return ipresult; + } + + ipresult = FormatIP6String(nc.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Selected first user defined interface. {1}", sourceAddr, ipresult); + + if (isExternal) + { + // TODO: remove this after testing. + _logger.LogWarning("{0}: External request received, however, only an internal interface bind found.", sourceAddr); + } + + return ipresult; + } + + if (isExternal) + { + // Get the first WAN interface address that isn't a loopback. + var extResult = _interfaceAddresses + .Exclude(_bindExclusions) + .Where(p => !IsInLocalNetwork(p)) + .OrderBy(p => p.Tag); + + if (extResult.Any()) + { + // Does the request originate in one of the interface subnets? + // (For systems with multiple internal network cards, and multiple subnets) + foreach (var intf in extResult) + { + if (!IsInLocalNetwork(intf) && intf.Contains(sourceAddr)) + { + ipresult = FormatIP6String(intf.Address); + _logger.LogDebug("{0}: GetBindInterface: Selected best external on interface on range. {1}", sourceAddr, ipresult); + return ipresult; + } + } + + ipresult = FormatIP6String(extResult.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Selected first external interface. {0}", sourceAddr, ipresult); + return ipresult; + } + + // Have to return something, so return an internal address + + // TODO: remove this after testing. + _logger.LogWarning("{0}: External request received, however, no WAN interface found.", sourceAddr); + } + + // Get the first LAN interface address that isn't a loopback. + var result = _interfaceAddresses + .Exclude(_bindExclusions) + .Where(p => IsInLocalNetwork(p)) + .OrderBy(p => p.Tag); + + if (result.Any()) + { + if (haveSource) + { + // Does the request originate in one of the interface subnets? + // (For systems with multiple internal network cards, and multiple subnets) + foreach (var intf in result) + { + if (intf.Contains(sourceAddr)) + { + ipresult = FormatIP6String(intf.Address); + _logger.LogDebug("{0}: GetBindInterface: Has source, matched best internal interface on range. {1}", sourceAddr, ipresult); + return ipresult; + } + } + } + + ipresult = FormatIP6String(result.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Matched first internal interface. {1}", sourceAddr, ipresult); + return ipresult; + } + + // There isn't any others, so we'll use the loopback. + ipresult = IsIP6Enabled ? "::" : "127.0.0.1"; + _logger.LogWarning("{0}: GetBindInterface: Loopback return.", sourceAddr, ipresult); + return ipresult; + } + } + + /// + public NetCollection GetInternalBindAddresses() + { + lock (_intLock) + { + int count = _bindAddresses.Count; + + if (count == 0) + { + if (_bindExclusions.Count > 0) + { + // Return all the internal interfaces except the ones excluded. + return new NetCollection(_internalInterfaces.Where(p => !_bindExclusions.Contains(p))); + } + + // No bind address, so return all internal interfaces. + return new NetCollection(_internalInterfaces.Where(p => !p.IsLoopback())); + } + + return new NetCollection(_bindAddresses.Where(p => !p.IsLoopback())); + } + } + + /// + public bool IsInLocalNetwork(IPObject address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.Equals(IPAddress.None)) + { + return false; + } + + // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. + if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + { + return true; + } + + lock (_intLock) + { + // As private addresses can be redefined by Configuration.LocalNetworkAddresses + return _lanSubnets.Contains(address) && !_excludedSubnets.Contains(address); + } + } + + /// + public bool IsInLocalNetwork(string address) + { + if (IPHost.TryParse(address, out IPHost ep)) + { + lock (_intLock) + { + return _lanSubnets.Contains(ep) && !_excludedSubnets.Contains(ep); + } + } + + return false; + } + + /// + public bool IsInLocalNetwork(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. + if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + { + return true; + } + + lock (_intLock) + { + // As private addresses can be redefined by Configuration.LocalNetworkAddresses + return _lanSubnets.Contains(address) && !_excludedSubnets.Contains(address); + } + } + + /// + public bool IsPrivateAddressRange(IPObject address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. + if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + { + return true; + } + else + { + return address.IsPrivateAddressRange(); + } + } + + /// + public bool IsExcludedInterface(IPAddress address) + { + lock (_intLock) + { + if (_bindExclusions.Count > 0) + { + return _bindExclusions.Contains(address); + } + + return false; + } + } + + /// + public NetCollection GetFilteredLANSubnets(NetCollection? filter = null) + { + lock (_intLock) + { + if (filter == null) + { + return NetCollection.AsNetworks(_lanSubnets.Exclude(_excludedSubnets)); + } + + return _lanSubnets.Exclude(filter); + } + } + + /// + public bool IsValidInterfaceAddress(IPAddress address) + { + lock (_intLock) + { + return _interfaceAddresses.Contains(address); + } + } + + /// + public bool TryParseInterface(string token, out IPNetAddress result) + { + if (string.IsNullOrEmpty(token)) + { + result = IPNetAddress.None; + return false; + } + + if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index)) + { + _logger.LogInformation("Interface {0} used in settings. Using its interface addresses.", token); + + // Replace interface tags with the interface IP's. + foreach (IPNetAddress iface in _interfaceAddresses) + { + if (Math.Abs(iface.Tag) == index && + ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || + (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) + { + result = iface; + return true; + } + } + } + + return IPNetAddress.TryParse(token, out result); + } + + /// + /// Reloads all settings and re-initialises the instance. + /// + /// to use. + public void UpdateSettings(ServerConfiguration config) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + IsIP4Enabled = Socket.OSSupportsIPv6 && config.EnableIPV4; + IsIP6Enabled = Socket.OSSupportsIPv6 && config.EnableIPV6; + TrustAllIP6Interfaces = config.TrustAllIP6Interfaces; + EnableMultiSocketBinding = config.EnableMultiSocketBinding; + + InitialiseInterfaces(); + InitialiseLAN(config); + InitialiseBind(config); + InitialiseRemote(config); + InitialiseOverrides(config); + } + + /// + /// Protected implementation of Dispose pattern. + /// + /// True to dispose the managed state. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _configurationManager.ConfigurationUpdated -= ConfigurationUpdated; + NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged; + NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged; + } + + _disposed = true; + } + } + + private static NetworkManager GetInstance() + { + if (_instance == null) + { + throw new ApplicationException("NetworkManager is not initialised."); + } + + return _instance; + } + + private void ConfigurationUpdated(object? sender, EventArgs args) + { + UpdateSettings((ServerConfiguration)_configurationManager.CommonConfiguration); + } + + /// + /// Converts an IPAddress into a string. + /// Ipv6 addresses are returned in [ ], with their scope removed. + /// + /// Address to convert. + /// URI save conversion of the address. + private string FormatIP6String(IPAddress address) + { + var str = address.ToString(); + if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + int i = str.IndexOf("%", StringComparison.OrdinalIgnoreCase); + + if (i != -1) + { + str = str.Substring(0, i); + } + + return $"[{str}]"; + } + + return str; + } + + /// + /// Parses strings into the collection, replacing any interface references. + /// + /// Collection. + /// String to parse. + private void AddToCollection(NetCollection col, string token) + { + // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1. + // Null check required here for automated testing. + if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index)) + { + _logger.LogInformation("Interface {0} used in settings. Using its interface addresses.", token); + + // Replace interface tags with the interface IP's. + foreach (IPNetAddress iface in _interfaceAddresses) + { + if (Math.Abs(iface.Tag) == index && + ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || + (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) + { + col.Add(iface); + } + } + } + else if (NetCollection.TryParse(token, out IPObject obj)) + { + if (!IsIP6Enabled) + { + // Remove IP6 addresses from multi-homed IPHosts. + obj.Remove(AddressFamily.InterNetworkV6); + if (!obj.IsIP6()) + { + col.Add(obj); + } + } + else if (!IsIP4Enabled) + { + // Remove IP4 addresses from multi-homed IPHosts. + obj.Remove(AddressFamily.InterNetwork); + if (obj.IsIP6()) + { + col.Add(obj); + } + } + else + { + col.Add(obj); + } + } + else + { + _logger.LogDebug("Invalid or unknown network {0}.", token); + } + } + + /// + /// Handler for network change events. + /// + /// Sender. + /// Network availablity information. + private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) + { + _logger.LogDebug("Network availability changed."); + OnNetworkChanged(); + } + + /// + /// Handler for network change events. + /// + /// Sender. + /// Event arguments. + private void OnNetworkAddressChanged(object? sender, EventArgs e) + { + _logger.LogDebug("Network address change detected."); + OnNetworkChanged(); + } + + /// + /// Async task that waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession. + /// + /// The network change async. + private async Task OnNetworkChangeAsync() + { + try + { + await Task.Delay(2000).ConfigureAwait(false); + InitialiseInterfaces(); + // Recalculate LAN caches. + InitialiseLAN((ServerConfiguration)_configurationManager.CommonConfiguration); + + NetworkChanged?.Invoke(this, EventArgs.Empty); + } + finally + { + _eventfire = false; + } + } + + /// + /// Triggers our event, and re-loads interface information. + /// + private void OnNetworkChanged() + { + // As per UPnP Device Architecture v1.0 Annex A - IPv6 Support. + NetworkLocationSignature = Guid.NewGuid().ToString(); + NetworkChangeCount++; + if (NetworkChangeCount > 99) + { + NetworkChangeCount = 1; + } + + if (!_eventfire) + { + _logger.LogDebug("Network Address Change Event."); + // As network events tend to fire one after the other only fire once every second. + _eventfire = true; + _ = OnNetworkChangeAsync(); + } + } + + /// + /// Parses the user defined overrides into the dictionary object. + /// Overrides are the equivalent of localised publishedServerUrl, enabling + /// different addresses to be advertised over different subnets. + /// format is subnet=ipaddress|host|uri + /// when subnet = 0.0.0.0, any external address matches. + /// + private void InitialiseOverrides(ServerConfiguration config) + { + string[] overrides = config.PublishedServerUriBySubnet; + if (overrides == null) + { + lock (_intLock) + { + _overrideUrls.Clear(); + } + + return; + } + + lock (_intLock) + { + _overrideUrls.Clear(); + + foreach (var entry in overrides) + { + var parts = entry.Split('='); + if (parts.Length != 2) + { + _logger.LogError("Unable to parse bind override. {0}", entry); + } + else + { + var replacement = parts[1].Trim(); + if (string.Equals(parts[0], "remaining", StringComparison.OrdinalIgnoreCase)) + { + _overrideUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement; + } + else if (string.Equals(parts[0], "external", StringComparison.OrdinalIgnoreCase)) + { + _overrideUrls[new IPNetAddress(IPAddress.Any)] = replacement; + } + else if (TryParseInterface(parts[0], out IPNetAddress address)) + { + _overrideUrls[address] = replacement; + } + else + { + _logger.LogError("Unable to parse bind ip address. {0}", parts[1]); + } + } + } + } + } + + private void InitialiseBind(ServerConfiguration config) + { + string[] ba = config.LocalNetworkAddresses; + + // TODO: remove when bug fixed: https://github.com/jellyfin/jellyfin-web/issues/1334 + + if (ba.Length == 1 && ba[0].IndexOf(',', StringComparison.OrdinalIgnoreCase) != -1) + { + ba = ba[0].Split(','); + } + + // TODO: end fix. + + // Read and parse bind addresses and exclusions, removing ones that don't exist. + _bindAddresses = CreateIPCollection(ba).Union(_interfaceAddresses); + _bindExclusions = CreateIPCollection(ba, true).Union(_interfaceAddresses); + _logger.LogInformation("Using bind addresses: {0}", _bindAddresses); + _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions); + } + + private void InitialiseRemote(ServerConfiguration config) + { + RemoteAddressFilter = CreateIPCollection(config.RemoteIPFilter); + } + + /// + /// Initialises internal LAN cache settings. + /// + private void InitialiseLAN(ServerConfiguration config) + { + lock (_intLock) + { + _logger.LogDebug("Refreshing LAN information."); + + // Get config options. + string[] subnets = config.LocalNetworkSubnets; + + // Create lists from user settings. + + _lanSubnets = CreateIPCollection(subnets); + _excludedSubnets = NetCollection.AsNetworks(CreateIPCollection(subnets, true)); + + // If no LAN addresses are specified - all private subnets are deemed to be the LAN + _usingPrivateAddresses = _lanSubnets.Count == 0; + + // NOTE: The order of the commands in this statement matters. + if (_usingPrivateAddresses) + { + _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); + // Internal interfaces must be private and not excluded. + _internalInterfaces = new NetCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.Contains(i))); + + // Subnets are the same as the calculated internal interface. + _lanSubnets = new NetCollection(); + + // We must listen on loopback for LiveTV to function regardless of the settings. + if (IsIP6Enabled) + { + _lanSubnets.Add(IPNetAddress.IP6Loopback); + _lanSubnets.Add(IPNetAddress.Parse("fc00::/7")); // ULA + _lanSubnets.Add(IPNetAddress.Parse("fe80::/10")); // Site local + } + + if (IsIP4Enabled) + { + _lanSubnets.Add(IPNetAddress.IP4Loopback); + _lanSubnets.Add(IPNetAddress.Parse("10.0.0.0/8")); + _lanSubnets.Add(IPNetAddress.Parse("172.16.0.0/12")); + _lanSubnets.Add(IPNetAddress.Parse("192.168.0.0/16")); + } + } + else + { + // We must listen on loopback for LiveTV to function regardless of the settings. + if (IsIP6Enabled) + { + _lanSubnets.Add(IPNetAddress.IP6Loopback); + } + + if (IsIP4Enabled) + { + _lanSubnets.Add(IPNetAddress.IP4Loopback); + } + + // Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet. + _internalInterfaces = new NetCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i) && !_excludedSubnets.Contains(i) && _lanSubnets.Contains(i))); + } + + _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets); + _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets); + _logger.LogInformation("Using LAN addresses: {0}", NetCollection.AsNetworks(_lanSubnets.Exclude(_excludedSubnets))); + } + } + + /// + /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state. + /// Generate a list of all active mac addresses that aren't loopback addreses. + /// + private void InitialiseInterfaces() + { + lock (_intLock) + { + _logger.LogDebug("Refreshing interfaces."); + + _interfaceNames.Clear(); + _interfaceAddresses.Clear(); + + try + { + IEnumerable nics = NetworkInterface.GetAllNetworkInterfaces() + .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up); + + foreach (NetworkInterface adapter in nics) + { + try + { + IPInterfaceProperties ipProperties = adapter.GetIPProperties(); + PhysicalAddress mac = adapter.GetPhysicalAddress(); + + // populate mac list + if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && mac != null && mac != PhysicalAddress.None) + { + _macAddresses.Add(mac); + } + + // populate interface address list + foreach (UnicastIPAddressInformation info in ipProperties.UnicastAddresses) + { + if (IsIP4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) + { + IPNetAddress nw = new IPNetAddress(info.Address, info.IPv4Mask) + { + // Keep the number of gateways on this interface, along with its index. + Tag = ipProperties.GetIPv4Properties().Index + }; + + int tag = nw.Tag; + /* Mono on OSX doesn't give any gateway addresses, so check DNS entries */ + if ((ipProperties.GatewayAddresses.Count > 0 || ipProperties.DnsAddresses.Count > 0) && !nw.IsLoopback()) + { + // -ve Tags signify the interface has a gateway. + nw.Tag *= -1; + } + + _interfaceAddresses.Add(nw); + + // Store interface name so we can use the name in Collections. + _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; + _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; + } + else if (IsIP6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) + { + IPNetAddress nw = new IPNetAddress(info.Address, (byte)info.PrefixLength) + { + // Keep the number of gateways on this interface, along with its index. + Tag = ipProperties.GetIPv6Properties().Index + }; + + int tag = nw.Tag; + /* Mono on OSX doesn't give any gateway addresses, so check DNS entries */ + if ((ipProperties.GatewayAddresses.Count > 0 || ipProperties.DnsAddresses.Count > 0) && !nw.IsLoopback()) + { + // -ve Tags signify the interface has a gateway. + nw.Tag *= -1; + } + + _interfaceAddresses.Add(nw); + + // Store interface name so we can use the name in Collections. + _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; + _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; + } + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch + { + // Ignore error, and attempt to continue. + } +#pragma warning restore CA1031 // Do not catch general exception types + } + + _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count); + _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses); + + // If for some reason we don't have an interface info, resolve our DNS name. + if (_interfaceAddresses.Count == 0) + { + _logger.LogWarning("No interfaces information available. Using loopback."); + + IPHost host = new IPHost(Dns.GetHostName()); + foreach (var a in host.GetAddresses()) + { + _interfaceAddresses.Add(a); + } + + if (_interfaceAddresses.Count == 0) + { + _logger.LogError("No interfaces information available. Resolving DNS name."); + // Last ditch attempt - use loopback address. + _interfaceAddresses.Add(IPNetAddress.IP4Loopback); + if (IsIP6Enabled) + { + _interfaceAddresses.Add(IPNetAddress.IP6Loopback); + } + } + } + } + catch (NetworkInformationException ex) + { + _logger.LogError(ex, "Error in InitialiseInterfaces."); + } + } + } + } +} diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 30ed3e6af..335a1a915 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -36,6 +36,7 @@ + diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 8f04baa08..8c19665cc 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable #pragma warning disable CA1307 using System; @@ -12,10 +12,10 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Events.Users; +using Jellyfin.Networking.Manager; using MediaBrowser.Common; using MediaBrowser.Common.Cryptography; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Events; diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 8d569a779..566ba0ad8 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -5,6 +5,7 @@ using System.Reflection; using Emby.Drawing; using Emby.Server.Implementations; using Jellyfin.Drawing.Skia; +using Jellyfin.Networking.Manager; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Events; @@ -34,21 +35,18 @@ namespace Jellyfin.Server /// The to be used by the . /// The to be used by the . /// The to be used by the . - /// The to be used by the . /// The to be used by the . public CoreAppHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, - INetworkManager networkManager, IServiceCollection collection) : base( applicationPaths, loggerFactory, options, fileSystem, - networkManager, collection) { } diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs index 4bda8f273..110290027 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -1,9 +1,11 @@ using System.Linq; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; +using NetworkCollection; namespace Jellyfin.Server.Middleware { @@ -38,36 +40,35 @@ namespace Jellyfin.Server.Middleware return; } - var remoteIp = httpContext.GetNormalizedRemoteIp(); + var remoteIp = httpContext.Connection.RemoteIpAddress; if (serverConfigurationManager.Configuration.EnableRemoteAccess) { - var addressFilter = serverConfigurationManager.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely. + // If left blank, all remote addresses will be allowed. + NetCollection remoteAddressFilter = networkManager.RemoteAddressFilter; - if (addressFilter.Length > 0 && !networkManager.IsInLocalNetwork(remoteIp)) + if (remoteAddressFilter.Count > 0 && !networkManager.IsInLocalNetwork(remoteIp)) { - if (serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist) + // remoteAddressFilter is a whitelist or blacklist. + bool isListed = remoteAddressFilter.Contains(remoteIp); + if (!serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist) { - if (networkManager.IsAddressInSubnets(remoteIp, addressFilter)) - { - return; - } + // Black list, so flip over. + isListed = !isListed; } - else + + if (!isListed) { - if (!networkManager.IsAddressInSubnets(remoteIp, addressFilter)) - { - return; - } + // If your name isn't on the list, you arn't coming in. + return; } } } - else + else if (!networkManager.IsInLocalNetwork(remoteIp)) { - if (!networkManager.IsInLocalNetwork(remoteIp)) - { - return; - } + // Remote not enabled. So everyone should be LAN. + return; } await _next(httpContext).ConfigureAwait(false); diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs index 9d795145a..2ff6f8a76 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; @@ -32,32 +33,13 @@ namespace Jellyfin.Server.Middleware /// The async task. public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) { - var currentHost = httpContext.Request.Host.ToString(); - var hosts = serverConfigurationManager - .Configuration - .LocalNetworkAddresses - .Select(NormalizeConfiguredLocalAddress) - .ToList(); + var host = httpContext.Connection.RemoteIpAddress; - if (hosts.Count == 0) + if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.Configuration.EnableRemoteAccess) { - await _next(httpContext).ConfigureAwait(false); return; } - currentHost ??= string.Empty; - - if (networkManager.IsInPrivateAddressSpace(currentHost)) - { - hosts.Add("localhost"); - hosts.Add("127.0.0.1"); - - if (hosts.All(i => currentHost.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1)) - { - return; - } - } - await _next(httpContext).ConfigureAwait(false); } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index c933d679f..8549e39db 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -12,8 +12,8 @@ using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; using Emby.Server.Implementations.IO; -using Emby.Server.Implementations.Networking; using Jellyfin.Api.Controllers; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; @@ -24,6 +24,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using NetworkCollection; using Serilog; using Serilog.Extensions.Logging; using SQLitePCL; @@ -161,7 +162,6 @@ namespace Jellyfin.Server _loggerFactory, options, new ManagedFileSystem(_loggerFactory.CreateLogger(), appPaths), - new NetworkManager(_loggerFactory.CreateLogger()), serviceCollection); try @@ -272,57 +272,16 @@ namespace Jellyfin.Server return builder .UseKestrel((builderContext, options) => { - var addresses = appHost.ServerConfigurationManager - .Configuration - .LocalNetworkAddresses - .Select(x => appHost.NormalizeConfiguredLocalAddress(x)) - .Where(i => i != null) - .ToHashSet(); - if (addresses.Count > 0 && !addresses.Contains(IPAddress.Any)) - { - if (!addresses.Contains(IPAddress.Loopback)) - { - // we must listen on loopback for LiveTV to function regardless of the settings - addresses.Add(IPAddress.Loopback); - } + NetCollection addresses = NetworkManager.Instance.GetAllBindInterfaces(); - foreach (var address in addresses) - { - _logger.LogInformation("Kestrel listening on {IpAddress}", address); - options.Listen(address, appHost.HttpPort); - if (appHost.ListenWithHttps) - { - options.Listen(address, appHost.HttpsPort, listenOptions => - { - listenOptions.UseHttps(appHost.Certificate); - listenOptions.Protocols = HttpProtocols.Http1AndHttp2; - }); - } - else if (builderContext.HostingEnvironment.IsDevelopment()) - { - try - { - options.Listen(address, appHost.HttpsPort, listenOptions => - { - listenOptions.UseHttps(); - listenOptions.Protocols = HttpProtocols.Http1AndHttp2; - }); - } - catch (InvalidOperationException ex) - { - _logger.LogError(ex, "Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted."); - } - } - } - } - else + bool flagged = false; + foreach (IPObject netAdd in addresses) { - _logger.LogInformation("Kestrel listening on all interfaces"); - options.ListenAnyIP(appHost.HttpPort); - + _logger.LogInformation("Kestrel listening on {0}", netAdd); + options.Listen(netAdd.Address, appHost.HttpPort); if (appHost.ListenWithHttps) { - options.ListenAnyIP(appHost.HttpsPort, listenOptions => + options.Listen(netAdd.Address, appHost.HttpsPort, listenOptions => { listenOptions.UseHttps(appHost.Certificate); listenOptions.Protocols = HttpProtocols.Http1AndHttp2; @@ -332,15 +291,19 @@ namespace Jellyfin.Server { try { - options.ListenAnyIP(appHost.HttpsPort, listenOptions => + options.Listen(netAdd.Address, appHost.HttpsPort, listenOptions => { listenOptions.UseHttps(); listenOptions.Protocols = HttpProtocols.Http1AndHttp2; }); } - catch (InvalidOperationException ex) + catch (InvalidOperationException) { - _logger.LogError(ex, "Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted."); + if (!flagged) + { + _logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted."); + flagged = true; + } } } } diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs deleted file mode 100644 index a0330afef..000000000 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ /dev/null @@ -1,97 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.NetworkInformation; - -namespace MediaBrowser.Common.Net -{ - public interface INetworkManager - { - event EventHandler NetworkChanged; - - /// - /// Gets or sets a function to return the list of user defined LAN addresses. - /// - Func LocalSubnetsFn { get; set; } - - /// - /// Gets a random port TCP number that is currently available. - /// - /// System.Int32. - int GetRandomUnusedTcpPort(); - - /// - /// Gets a random port UDP number that is currently available. - /// - /// System.Int32. - int GetRandomUnusedUdpPort(); - - /// - /// Returns the MAC Address from first Network Card in Computer. - /// - /// The MAC Address. - List GetMacAddresses(); - - /// - /// Determines whether [is in private address space] [the specified endpoint]. - /// - /// The endpoint. - /// true if [is in private address space] [the specified endpoint]; otherwise, false. - bool IsInPrivateAddressSpace(string endpoint); - - /// - /// Determines whether [is in private address space 10.x.x.x] [the specified endpoint] and exists in the subnets returned by GetSubnets(). - /// - /// The endpoint. - /// true if [is in private address space 10.x.x.x] [the specified endpoint]; otherwise, false. - bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint); - - /// - /// Determines whether [is in local network] [the specified endpoint]. - /// - /// The endpoint. - /// true if [is in local network] [the specified endpoint]; otherwise, false. - bool IsInLocalNetwork(string endpoint); - - /// - /// Investigates an caches a list of interface addresses, excluding local link and LAN excluded addresses. - /// - /// The list of ipaddresses. - IPAddress[] GetLocalIpAddresses(); - - /// - /// Checks if the given address falls within the ranges given in [subnets]. The addresses in subnets can be hosts or subnets in the CIDR format. - /// - /// The address to check. - /// If true, check against addresses in the LAN settings surrounded by brackets ([]). - /// trueif the address is in at least one of the given subnets, false otherwise. - bool IsAddressInSubnets(string addressString, string[] subnets); - - /// - /// Returns true if address is in the LAN list in the config file. - /// - /// The address to check. - /// If true, check against addresses in the LAN settings which have [] arroud and return true if it matches the address give in address. - /// If true, returns false if address is in the 127.x.x.x or 169.128.x.x range. - /// falseif the address isn't in the LAN list, true if the address has been defined as a LAN address. - bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC); - - /// - /// Checks if address is in the LAN list in the config file. - /// - /// Source address to check. - /// Destination address to check against. - /// Destination subnet to check against. - /// true/falsedepending on whether address1 is in the same subnet as IPAddress2 with subnetMask. - bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask); - - /// - /// Returns the subnet mask of an interface with the given address. - /// - /// The address to check. - /// Returns the subnet mask of an interface with the given address, or null if an interface match cannot be found. - IPAddress GetLocalIpSubnetMask(IPAddress address); - } -} diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index cfad17fb7..f147e6a86 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -56,41 +56,27 @@ namespace MediaBrowser.Controller /// /// Gets the system info. /// + /// The originator of the request. /// SystemInfo. - Task GetSystemInfo(CancellationToken cancellationToken); + SystemInfo GetSystemInfo(IPAddress source); - Task GetPublicSystemInfo(CancellationToken cancellationToken); - - /// - /// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request - /// to the API that should exist at the address. - /// - /// A cancellation token that can be used to cancel the task. - /// A list containing all the local IP addresses of the server. - Task> GetLocalIpAddresses(CancellationToken cancellationToken); + PublicSystemInfo GetPublicSystemInfo(IPAddress address); /// /// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured - /// IP address that can be found via . HTTPS will be preferred when available. + /// HTTPS will be preferred when available. /// - /// A cancellation token that can be used to cancel the task. + /// The source of the request. /// The server URL. - Task GetLocalApiUrl(CancellationToken cancellationToken); + string GetSmartApiUrl(object source); /// - /// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1) + /// Gets a localhost URL that can be used to access the API using the loop-back IP address. /// over HTTP (not HTTPS). /// /// The API URL. string GetLoopbackHttpApiUrl(); - /// - /// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available. - /// - /// The IP address to use as the hostname in the URL. - /// The API URL. - string GetLocalApiUrl(IPAddress address); - /// /// Gets a local (LAN) URL that can be used to access the API. /// Note: if passing non-null scheme or port it is up to the caller to ensure they form the correct pair. @@ -105,7 +91,7 @@ namespace MediaBrowser.Controller /// preferring the HTTPS port, if available. /// /// The API URL. - string GetLocalApiUrl(ReadOnlySpan hostname, string scheme = null, int? port = null); + string GetLocalApiUrl(string hostname, string scheme = null, int? port = null); /// /// Open a URL in an external browser window. diff --git a/MediaBrowser.Model/Configuration/PathSubstitution.cs b/MediaBrowser.Model/Configuration/PathSubstitution.cs new file mode 100644 index 000000000..40eb36c2e --- /dev/null +++ b/MediaBrowser.Model/Configuration/PathSubstitution.cs @@ -0,0 +1,19 @@ +#nullable enable +#pragma warning disable CS1591 +#pragma warning disable CA1819 + +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Model.Configuration +{ + + public class PathSubstitution + { + public string From { get; set; } = string.Empty; + + public string To { get; set; } = string.Empty; + } +} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 48d1a7346..073a62982 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -1,5 +1,6 @@ #nullable disable #pragma warning disable CS1591 +#pragma warning disable CA1819 using System; using System.Collections.Generic; @@ -15,41 +16,174 @@ namespace MediaBrowser.Model.Configuration { public const int DefaultHttpPort = 8096; public const int DefaultHttpsPort = 8920; - private string _baseUrl; + private string _baseUrl = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + public ServerConfiguration() + { + MetadataOptions = new[] + { + new MetadataOptions() + { + ItemType = "Book" + }, + new MetadataOptions() + { + ItemType = "Movie" + }, + new MetadataOptions + { + ItemType = "MusicVideo", + DisabledMetadataFetchers = new[] { "The Open Movie Database" }, + DisabledImageFetchers = new[] { "The Open Movie Database" } + }, + new MetadataOptions + { + ItemType = "Series", + DisabledMetadataFetchers = new[] { "TheMovieDb" }, + DisabledImageFetchers = new[] { "TheMovieDb" } + }, + new MetadataOptions + { + ItemType = "MusicAlbum", + DisabledMetadataFetchers = new[] { "TheAudioDB" } + }, + new MetadataOptions + { + ItemType = "MusicArtist", + DisabledMetadataFetchers = new[] { "TheAudioDB" } + }, + new MetadataOptions + { + ItemType = "BoxSet" + }, + new MetadataOptions + { + ItemType = "Season", + DisabledMetadataFetchers = new[] { "TheMovieDb" }, + }, + new MetadataOptions + { + ItemType = "Episode", + DisabledMetadataFetchers = new[] { "The Open Movie Database", "TheMovieDb" }, + DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" } + } + }; + } /// /// Gets or sets a value indicating whether to enable automatic port forwarding. /// - public bool EnableUPnP { get; set; } + public bool EnableUPnP { get; set; } = false; /// /// Gets or sets a value indicating whether to enable prometheus metrics exporting. /// - public bool EnableMetrics { get; set; } + public bool EnableMetrics { get; set; } = false; /// /// Gets or sets the public mapped port. /// /// The public mapped port. - public int PublicPort { get; set; } + public int PublicPort { get; set; } = DefaultHttpPort; + + /// + /// Gets or sets a value indicating whether the http port should be mapped as part of UPnP automatic port forwarding. + /// + public bool UPnPCreateHttpPortMap { get; set; } = false; + + /// + /// Gets or sets client udp port range. + /// + public string UDPPortRange { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether gets or sets IPV6 capability. + /// + public bool EnableIPV6 { get; set; } = false; + + /// + /// Gets or sets a value indicating whether gets or sets IPV4 capability. + /// + public bool EnableIPV4 { get; set; } = true; + + /// + /// Gets or sets a value indicating whether detailed ssdp logs are sent to the console/log. + /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work. + /// + public bool EnableSSDPTracing { get; set; } = false; + + /// + /// Gets or sets a value indicating whether an IP address is to be used to filter the detailed ssdp logs that are being sent to the console/log. + /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work. + /// + public string SSDPTracingFilter { get; set; } = string.Empty; + + /// + /// Gets or sets the number of times SSDP UDP messages are sent. + /// + public int UDPSendCount { get; set; } = 2; + + /// + /// Gets or sets the delay between each groups of SSDP messages (in ms). + /// + public int UDPSendDelay { get; set; } = 100; + + /// + /// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor. + /// + public int GatewayMonitorPeriod { get; set; } = 60; + + /// + /// Gets a value indicating whether is multi-socket binding available. + /// + public bool EnableMultiSocketBinding { get; } = true; + + /// + /// Gets or sets a value indicating whether all IPv6 interfaces should be treated as on the internal network. + /// Depending on the address range implemented ULA ranges might not be used. + /// + public bool TrustAllIP6Interfaces { get; set; } = false; + + /// + /// Gets or sets the ports that HDHomerun uses. + /// + public string HDHomerunPortRange { get; set; } = string.Empty; + + /// + /// Gets or sets PublishedServerUri to advertise for specific subnets. + /// + public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); + + /// + /// Gets or sets a value indicating whether gets or sets Autodiscovery tracing. + /// + public bool AutoDiscoveryTracing { get; set; } = false; + + /// + /// Gets or sets a value indicating whether Autodiscovery is enabled. + /// + public bool AutoDiscovery { get; set; } = true; /// /// Gets or sets the public HTTPS port. /// /// The public HTTPS port. - public int PublicHttpsPort { get; set; } + public int PublicHttpsPort { get; set; } = DefaultHttpsPort; /// /// Gets or sets the HTTP server port number. /// /// The HTTP server port number. - public int HttpServerPortNumber { get; set; } + public int HttpServerPortNumber { get; set; } = DefaultHttpPort; /// /// Gets or sets the HTTPS server port number. /// /// The HTTPS server port number. - public int HttpsPortNumber { get; set; } + public int HttpsPortNumber { get; set; } = DefaultHttpsPort; /// /// Gets or sets a value indicating whether to use HTTPS. @@ -58,19 +192,19 @@ namespace MediaBrowser.Model.Configuration /// 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; } + public bool EnableHttps { get; set; } = false; - public bool EnableNormalizedItemByNameIds { get; set; } + public bool EnableNormalizedItemByNameIds { get; set; } = true; /// /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. /// - public string CertificatePath { get; set; } + 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; } + public string CertificatePassword { get; set; } = string.Empty; /// /// Gets or sets a value indicating whether this instance is port authorized. @@ -79,92 +213,95 @@ namespace MediaBrowser.Model.Configuration public bool IsPortAuthorized { get; set; } /// - /// Gets or sets if quick connect is available for use on this server. + /// Gets or sets a value indicating whether quick connect is available for use on this server. /// - public bool QuickConnectAvailable { get; set; } + public bool QuickConnectAvailable { get; set; } = false; - public bool AutoRunWebApp { get; set; } + public bool AutoRunWebApp { get; set; } = true; - public bool EnableRemoteAccess { get; set; } + /// + /// Gets or sets a value indicating whether access outside of the LAN is permitted. + /// + public bool EnableRemoteAccess { get; set; } = true; /// /// Gets or sets a value indicating whether [enable case sensitive item ids]. /// /// true if [enable case sensitive item ids]; otherwise, false. - public bool EnableCaseSensitiveItemIds { get; set; } + public bool EnableCaseSensitiveItemIds { get; set; } = true; - public bool DisableLiveTvChannelUserDataName { get; set; } + public bool DisableLiveTvChannelUserDataName { get; set; } = true; /// /// Gets or sets the metadata path. /// /// The metadata path. - public string MetadataPath { get; set; } + public string MetadataPath { get; set; } = string.Empty; - public string MetadataNetworkPath { get; set; } + public string MetadataNetworkPath { get; set; } = string.Empty; /// /// Gets or sets the preferred metadata language. /// /// The preferred metadata language. - public string PreferredMetadataLanguage { get; set; } + public string PreferredMetadataLanguage { get; set; } = string.Empty; /// /// Gets or sets the metadata country code. /// /// The metadata country code. - public string MetadataCountryCode { get; set; } + public string MetadataCountryCode { get; set; } = "US"; /// - /// Characters to be replaced with a ' ' in strings to create a sort name. + /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name. /// /// The sort replace characters. - public string[] SortReplaceCharacters { get; set; } + public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" }; /// - /// Characters to be removed from strings to create a sort name. + /// Gets or sets characters to be removed from strings to create a sort name. /// /// The sort remove characters. - public string[] SortRemoveCharacters { get; set; } + public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" }; /// - /// Words to be removed from strings to create a sort name. + /// Gets or sets words to be removed from strings to create a sort name. /// /// The sort remove words. - public string[] SortRemoveWords { get; set; } + public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" }; /// /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated. /// /// The min resume PCT. - public int MinResumePct { get; set; } + public int MinResumePct { get; set; } = 5; /// /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched. /// /// The max resume PCT. - public int MaxResumePct { get; set; } + public int MaxResumePct { get; set; } = 90; /// /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates.. /// /// The min resume duration seconds. - public int MinResumeDurationSeconds { get; set; } + public int MinResumeDurationSeconds { get; set; } = 300; /// - /// The delay in seconds that we will wait after a file system change to try and discover what has been added/removed + /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several /// different directories and files. /// /// The file watcher delay. - public int LibraryMonitorDelay { get; set; } + public int LibraryMonitorDelay { get; set; } = 60; /// /// Gets or sets a value indicating whether [enable dashboard response caching]. /// Allows potential contributors without visual studio to modify production dashboard code and test changes. /// /// true if [enable dashboard response caching]; otherwise, false. - public bool EnableDashboardResponseCaching { get; set; } + public bool EnableDashboardResponseCaching { get; set; } = true; /// /// Gets or sets the image saving convention. @@ -174,9 +311,9 @@ namespace MediaBrowser.Model.Configuration public MetadataOptions[] MetadataOptions { get; set; } - public bool SkipDeserializationForBasicTypes { get; set; } + public bool SkipDeserializationForBasicTypes { get; set; } = true; - public string ServerName { get; set; } + public string ServerName { get; set; } = string.Empty; public string BaseUrl { @@ -208,189 +345,80 @@ namespace MediaBrowser.Model.Configuration } } - public string UICulture { get; set; } - - public bool SaveMetadataHidden { get; set; } + public string UICulture { get; set; } = "en-US"; - public NameValuePair[] ContentTypes { get; set; } + public bool SaveMetadataHidden { get; set; } = false; - public int RemoteClientBitrateLimit { get; set; } + public NameValuePair[] ContentTypes { get; set; } = Array.Empty(); - public bool EnableFolderView { get; set; } + public int RemoteClientBitrateLimit { get; set; } = 0; - public bool EnableGroupingIntoCollections { get; set; } + public bool EnableFolderView { get; set; } = false; - public bool DisplaySpecialsWithinSeasons { get; set; } + public bool EnableGroupingIntoCollections { get; set; } = false; - public string[] LocalNetworkSubnets { get; set; } + public bool DisplaySpecialsWithinSeasons { get; set; } = true; - public string[] LocalNetworkAddresses { get; set; } + /// + /// Gets or sets the subnets that are deemed to make up the LAN. + /// + public string[] LocalNetworkSubnets { get; set; } = Array.Empty(); - public string[] CodecsUsed { get; set; } + /// + /// 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(); - public List PluginRepositories { get; set; } + public string[] CodecsUsed { get; set; } = Array.Empty(); - public bool IgnoreVirtualInterfaces { get; set; } + public List PluginRepositories { get; set; } = new List(); - public bool EnableExternalContentInSuggestions { get; set; } + public bool EnableExternalContentInSuggestions { get; set; } = true; /// /// Gets or sets a value indicating whether the server should force connections over HTTPS. /// - public bool RequireHttps { get; set; } + public bool RequireHttps { get; set; } = false; - public bool EnableNewOmdbSupport { get; set; } + public bool EnableNewOmdbSupport { get; set; } = true; - public string[] RemoteIPFilter { get; set; } + /// + /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with . + /// + public string[] RemoteIPFilter { get; set; } = Array.Empty(); - public bool IsRemoteIPFilterBlacklist { get; set; } + /// + /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist. + /// + public bool IsRemoteIPFilterBlacklist { get; set; } = false; - public int ImageExtractionTimeoutMs { get; set; } + public int ImageExtractionTimeoutMs { get; set; } = 0; - public PathSubstitution[] PathSubstitutions { get; set; } + public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty(); - public bool EnableSimpleArtistDetection { get; set; } + public bool EnableSimpleArtistDetection { get; set; } = false; - public string[] UninstalledPlugins { get; set; } + public string[] UninstalledPlugins { get; set; } = Array.Empty(); /// /// Gets or sets a value indicating whether slow server responses should be logged as a warning. /// - public bool EnableSlowResponseWarning { get; set; } + public bool EnableSlowResponseWarning { get; set; } = true; /// /// Gets or sets the threshold for the slow response time warning in ms. /// - public long SlowResponseThresholdMs { get; set; } + public long SlowResponseThresholdMs { get; set; } = 500; /// /// Gets or sets the cors hosts. /// - public string[] CorsHosts { get; set; } + public string[] CorsHosts { get; set; } = new[] { "*" }; /// /// Gets or sets the known proxies. /// - public string[] KnownProxies { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public ServerConfiguration() - { - UninstalledPlugins = Array.Empty(); - RemoteIPFilter = Array.Empty(); - LocalNetworkSubnets = Array.Empty(); - LocalNetworkAddresses = Array.Empty(); - CodecsUsed = Array.Empty(); - PathSubstitutions = Array.Empty(); - IgnoreVirtualInterfaces = false; - EnableSimpleArtistDetection = false; - SkipDeserializationForBasicTypes = true; - - PluginRepositories = new List(); - - DisplaySpecialsWithinSeasons = true; - EnableExternalContentInSuggestions = true; - - ImageSavingConvention = ImageSavingConvention.Compatible; - PublicPort = DefaultHttpPort; - PublicHttpsPort = DefaultHttpsPort; - HttpServerPortNumber = DefaultHttpPort; - HttpsPortNumber = DefaultHttpsPort; - EnableMetrics = false; - EnableHttps = false; - EnableDashboardResponseCaching = true; - EnableCaseSensitiveItemIds = true; - EnableNormalizedItemByNameIds = true; - DisableLiveTvChannelUserDataName = true; - EnableNewOmdbSupport = true; - - AutoRunWebApp = true; - EnableRemoteAccess = true; - QuickConnectAvailable = false; - - EnableUPnP = false; - MinResumePct = 5; - MaxResumePct = 90; - - // 5 minutes - MinResumeDurationSeconds = 300; - - LibraryMonitorDelay = 60; - - ContentTypes = Array.Empty(); - - PreferredMetadataLanguage = "en"; - MetadataCountryCode = "US"; - - SortReplaceCharacters = new[] { ".", "+", "%" }; - SortRemoveCharacters = new[] { ",", "&", "-", "{", "}", "'" }; - SortRemoveWords = new[] { "the", "a", "an" }; - - BaseUrl = string.Empty; - UICulture = "en-US"; - - MetadataOptions = new[] - { - new MetadataOptions() - { - ItemType = "Book" - }, - new MetadataOptions() - { - ItemType = "Movie" - }, - new MetadataOptions - { - ItemType = "MusicVideo", - DisabledMetadataFetchers = new[] { "The Open Movie Database" }, - DisabledImageFetchers = new[] { "The Open Movie Database" } - }, - new MetadataOptions - { - ItemType = "Series", - DisabledMetadataFetchers = new[] { "TheMovieDb" }, - DisabledImageFetchers = new[] { "TheMovieDb" } - }, - new MetadataOptions - { - ItemType = "MusicAlbum", - DisabledMetadataFetchers = new[] { "TheAudioDB" } - }, - new MetadataOptions - { - ItemType = "MusicArtist", - DisabledMetadataFetchers = new[] { "TheAudioDB" } - }, - new MetadataOptions - { - ItemType = "BoxSet" - }, - new MetadataOptions - { - ItemType = "Season", - DisabledMetadataFetchers = new[] { "TheMovieDb" }, - }, - new MetadataOptions - { - ItemType = "Episode", - DisabledMetadataFetchers = new[] { "The Open Movie Database", "TheMovieDb" }, - DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" } - } - }; - - EnableSlowResponseWarning = true; - SlowResponseThresholdMs = 500; - CorsHosts = new[] { "*" }; - KnownProxies = Array.Empty(); - } - } - - public class PathSubstitution - { - public string From { get; set; } + public string[] KnownProxies { get; set; } = Array.Empty(); - public string To { get; set; } } } diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 25402aee1..d460c0ab0 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.3 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30503.244 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}" EndProject @@ -66,12 +66,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.Build.0 = Release|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -132,10 +138,6 @@ Global {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.Build.0 = Debug|Any CPU {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.ActiveCfg = Release|Any CPU {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.Build.0 = Release|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.Build.0 = Release|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.Build.0 = Debug|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -176,10 +178,22 @@ Global {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.Build.0 = Debug|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.ActiveCfg = Release|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.Build.0 = Release|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} EndGlobalSection @@ -201,12 +215,4 @@ Global $0.DotNetNamingPolicy = $2 $2.DirectoryNamespaceAssociation = PrefixedHierarchical EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - EndGlobalSection EndGlobal diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj index 664663bd7..3266a05cb 100644 --- a/RSSDP/RSSDP.csproj +++ b/RSSDP/RSSDP.csproj @@ -6,6 +6,7 @@ + diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index a4be32e7d..e28a2f48d 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -7,6 +7,7 @@ using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; @@ -352,7 +353,7 @@ namespace Rssdp.Infrastructure if (_enableMultiSocketBinding) { - foreach (var address in _networkManager.GetLocalIpAddresses()) + foreach (var address in _networkManager.GetAllBindInterfaces()) { if (address.AddressFamily == AddressFamily.InterNetworkV6) { @@ -362,7 +363,7 @@ namespace Rssdp.Infrastructure try { - sockets.Add(_SocketFactory.CreateSsdpUdpSocket(address, _LocalPort)); + sockets.Add(_SocketFactory.CreateSsdpUdpSocket(address.Address, _LocalPort)); } catch (Exception ex) { diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 1a8577d8d..43fccdad4 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -5,7 +5,9 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; +using NetworkCollection; namespace Rssdp.Infrastructure { @@ -300,17 +302,14 @@ namespace Rssdp.Infrastructure foreach (var device in deviceList) { - if (!_sendOnlyMatchedHost || - _networkManager.IsInSameSubnet(device.ToRootDevice().Address, remoteEndPoint.Address, device.ToRootDevice().SubnetMask)) + var ip1 = new IPNetAddress(device.ToRootDevice().Address, device.ToRootDevice().SubnetMask); + var ip2 = new IPNetAddress(remoteEndPoint.Address, device.ToRootDevice().SubnetMask); + if (!_sendOnlyMatchedHost || ip1.NetworkAddress.Equals(ip2.NetworkAddress)) { SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); } } } - else - { - // WriteTrace(String.Format("Sending 0 search responses.")); - } }); } diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs index 09ffa8468..2c7f0c4f9 100644 --- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.LocalAccessPolicy; using Jellyfin.Api.Constants; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs index 77f1640fa..939023a95 100644 --- a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs @@ -3,8 +3,6 @@ using System.Collections.Concurrent; using System.IO; using Emby.Server.Implementations; using Emby.Server.Implementations.IO; -using Emby.Server.Implementations.Networking; -using Jellyfin.Drawing.Skia; using Jellyfin.Server; using MediaBrowser.Common; using Microsoft.AspNetCore.Hosting; @@ -81,7 +79,6 @@ namespace Jellyfin.Api.Tests loggerFactory, commandLineOpts, new ManagedFileSystem(loggerFactory.CreateLogger(), appPaths), - new NetworkManager(loggerFactory.CreateLogger()), serviceCollection); _disposableComponents.Add(appHost); appHost.Init(); -- cgit v1.2.3 From b44455ad0d6e78b5baed535c06a7f7d49116d1ee Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Mon, 14 Sep 2020 15:46:38 +0100 Subject: Update based on PR1 changes. --- Emby.Dlna/Main/DlnaEntryPoint.cs | 2 +- Emby.Server.Implementations/ApplicationHost.cs | 62 ++- .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 2 +- .../TunerHosts/HdHomerun/HdHomerunUdpStream.cs | 2 +- .../LiveTv/TunerHosts/M3UTunerHost.cs | 2 +- Jellyfin.Api/Auth/BaseAuthorizationHandler.cs | 2 +- .../DefaultAuthorizationHandler.cs | 2 +- .../Auth/DownloadPolicy/DownloadHandler.cs | 2 +- ...FirstTimeOrIgnoreParentalControlSetupHandler.cs | 2 +- .../FirstTimeSetupOrDefaultHandler.cs | 2 +- .../FirstTimeSetupOrElevatedHandler.cs | 2 +- .../IgnoreParentalControlHandler.cs | 2 +- .../LocalAccessOrRequiresElevationHandler.cs | 2 +- .../Auth/LocalAccessPolicy/LocalAccessHandler.cs | 2 +- .../RequiresElevationHandler.cs | 2 +- Jellyfin.Api/Controllers/SystemController.cs | 2 +- Jellyfin.Api/Controllers/UserController.cs | 2 +- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 2 +- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 2 +- Jellyfin.Networking/Manager/INetworkManager.cs | 189 --------- Jellyfin.Networking/Manager/NetworkManager.cs | 463 +++++++++++---------- .../Users/UserManager.cs | 3 +- Jellyfin.Server/CoreAppHost.cs | 2 +- .../IpBasedAccessValidationMiddleware.cs | 2 +- .../Middleware/LanFilteringMiddleware.cs | 2 +- Jellyfin.Server/Program.cs | 4 +- MediaBrowser.Common/MediaBrowser.Common.csproj | 3 +- MediaBrowser.Common/Net/INetworkManager.cs | 221 ++++++++++ MediaBrowser.Controller/IServerApplicationHost.cs | 26 +- RSSDP/SsdpCommunicationsServer.cs | 2 +- RSSDP/SsdpDevicePublisher.cs | 2 +- .../LocalAccessPolicy/LocalAccessHandlerTests.cs | 2 +- 32 files changed, 572 insertions(+), 447 deletions(-) delete mode 100644 Jellyfin.Networking/Manager/INetworkManager.cs create mode 100644 MediaBrowser.Common/Net/INetworkManager.cs (limited to 'tests') diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 98f50c09a..a5da2fc5c 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -9,7 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Emby.Dlna.PlayTo; using Emby.Dlna.Ssdp; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index cc04cb03f..67a352fc9 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -160,6 +160,11 @@ namespace Emby.Server.Implementations } } + /// + /// Gets the singleton instance. + /// + public INetworkManager NetManager { get; internal set; } + /// /// Occurs when [has pending restart changed]. /// @@ -189,11 +194,6 @@ namespace Emby.Server.Implementations /// The plugins. public IReadOnlyList Plugins => _plugins; - /// - /// Gets the NetworkManager object. - /// - private readonly INetworkManager _networkManager; - /// /// Gets the logger factory. /// @@ -267,7 +267,7 @@ namespace Emby.Server.Implementations ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); - _networkManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger()); + NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger()); Logger = LoggerFactory.CreateLogger(); @@ -524,7 +524,7 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton(_fileSystemManager); ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(_networkManager); + ServiceCollection.AddSingleton(NetManager); ServiceCollection.AddSingleton(); @@ -1116,7 +1116,7 @@ namespace Emby.Server.Implementations } public IEnumerable GetWakeOnLanInfo() - => _networkManager.GetMacAddresses() + => NetManager.GetMacAddresses() .Select(i => new WakeOnLanInfo(i)) .ToList(); @@ -1138,7 +1138,47 @@ namespace Emby.Server.Implementations public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps; /// - public string GetSmartApiUrl(object source) + public string GetSmartApiUrl(IPAddress ipAddress, int? port = null) + { + // Published server ends with a / + if (_startupOptions.PublishedServerUrl != null) + { + // Published server ends with a '/', so we need to remove it. + return _startupOptions.PublishedServerUrl.ToString().Trim('/'); + } + + string smart = NetManager.GetBindInterface(ipAddress, out port); + // If the smartAPI doesn't start with http then treat it as a host or ip. + if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return smart.Trim('/'); + } + + return GetLocalApiUrl(smart.Trim('/'), null, port); + } + + /// + public string GetSmartApiUrl(HttpRequest request, int? port = null) + { + // Published server ends with a / + if (_startupOptions.PublishedServerUrl != null) + { + // Published server ends with a '/', so we need to remove it. + return _startupOptions.PublishedServerUrl.ToString().Trim('/'); + } + + string smart = NetManager.GetBindInterface(request, out port); + // If the smartAPI doesn't start with http then treat it as a host or ip. + if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return smart.Trim('/'); + } + + return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port); + } + + /// + public string GetSmartApiUrl(string hostname, int? port = null) { // Published server ends with a / if (_startupOptions.PublishedServerUrl != null) @@ -1147,7 +1187,7 @@ namespace Emby.Server.Implementations return _startupOptions.PublishedServerUrl.ToString().Trim('/'); } - string smart = _networkManager.GetBindInterface(source, out int? port); + string smart = NetManager.GetBindInterface(hostname, out port); // If the smartAPI doesn't start with http then treat it as a host or ip. if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase)) @@ -1155,7 +1195,7 @@ namespace Emby.Server.Implementations return smart.Trim('/'); } - return GetLocalApiUrl(smart.Trim('/'), source is HttpRequest request ? request.Scheme : null, port); + return GetLocalApiUrl(smart.Trim('/'), null, port); } /// diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 1cf129ad2..27937d2f8 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -10,7 +10,7 @@ using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 02ee302d0..efb6d162a 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -7,7 +7,7 @@ using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index f297ecd5d..531a785a0 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -8,7 +8,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs index 08746b346..bd76b93bf 100644 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -1,7 +1,7 @@ using System.Security.Claims; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index 69e6a8fb2..dfa366796 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs index d1297119c..3183d1318 100644 --- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs +++ b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs index 53b5d4778..1cee962e9 100644 --- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs index abdf2858d..214198e00 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs index ada8a0d4e..9867ea4ca 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs index 475e3cdac..affd95551 100644 --- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs +++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs index d022c9067..fab464b50 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs index 418d63de6..801ee2f4c 100644 --- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs index a1cddbca3..7adf72c3d 100644 --- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 6876b47b4..b87e275d0 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -8,7 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; -using Jellyfin.Networking.Manager; + 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 152e650bc..c60497e30 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -7,7 +7,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 3be8734b9..f81fb88fb 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -8,7 +8,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Models.StreamingDtos; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index d63e3ab11..9e4def774 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Networking/Manager/INetworkManager.cs b/Jellyfin.Networking/Manager/INetworkManager.cs deleted file mode 100644 index ba571750b..000000000 --- a/Jellyfin.Networking/Manager/INetworkManager.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.NetworkInformation; -using MediaBrowser.Model.Configuration; -using NetworkCollection; - -namespace Jellyfin.Networking.Manager -{ - /// - /// Interface for the NetworkManager class. - /// - public interface INetworkManager - { - /// - /// Event triggered on network changes. - /// - event EventHandler NetworkChanged; - - /// - /// Gets the Published server override list. - /// - Dictionary PublishedServerOverrides { get; } - - /// - /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. - /// - public bool TrustAllIP6Interfaces { get; } - - /// - /// Gets returns the remote address filter. - /// - NetCollection RemoteAddressFilter { get; } - - /// - /// Calculates the list of interfaces to use for Kestrel. - /// - /// A NetCollection object containing all the interfaces to bind. - /// If all the interfaces are specified, and none are excluded, it returns zero items - /// to represent any address. - NetCollection GetAllBindInterfaces(); - - /// - /// Returns a collection containing the loopback interfaces. - /// - /// Netcollection. - public NetCollection GetLoopbacks(); - - /// - /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) - /// If no bind addresses are specified, an internal interface address is selected. - /// The priority of selection is as follows:- - /// - /// The value contained in the startup parameter --published-server-url. - /// - /// If the user specified custom subnet overrides, the correct subnet for the source address. - /// - /// If the user specified bind interfaces to use:- - /// The bind interface that contains the source subnet. - /// The first bind interface specified that suits best first the source's endpoint. eg. external or internal. - /// - /// If the source is from a public subnet address range and the user hasn't specified any bind addresses:- - /// The first public interface that isn't a loopback and contains the source subnet. - /// The first public interface that isn't a loopback. Priority is given to interfaces with gateways. - /// An internal interface if there are no public ip addresses. - /// - /// If the source is from a private subnet address range and the user hasn't specified any bind addresses:- - /// The first private interface that contains the source subnet. - /// The first private interface that isn't a loopback. Priority is given to interfaces with gateways. - /// - /// If no interfaces meet any of these criteria, then a loopback address is returned. - /// - /// Interface that have been specifically excluded from binding are not used in any of the calculations. - /// - /// Source of the request. - /// Optional port returned, if it's part of an override. - /// IP Address to use, or loopback address if all else fails. - string GetBindInterface(object? source, out int? port); - - /// - /// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses. - /// - /// IP address to check. - /// True if it is. - bool IsExcludedInterface(IPAddress address); - - /// - /// Get a list of all the MAC addresses associated with active interfaces. - /// - /// List of MAC addresses. - List GetMacAddresses(); - - /// - /// Checks to see if the IP Address provided matches an interface that has a gateway. - /// - /// IP to check. Can be an IPAddress or an IPObject. - /// Result of the check. - public bool IsGatewayInterface(object? addressObj); - - /// - /// Returns true if the address is a private address. - /// The config option TrustIP6Interfaces overrides this functions behaviour. - /// - /// Address to check. - /// True or False. - bool IsPrivateAddressRange(IPObject address); - - /// - /// Returns true if the address is part of the user defined LAN. - /// The config option TrustIP6Interfaces overrides this functions behaviour. - /// - /// IP to check. - /// True if endpoint is within the LAN range. - bool IsInLocalNetwork(string address); - - /// - /// Returns true if the address is part of the user defined LAN. - /// The config option TrustIP6Interfaces overrides this functions behaviour. - /// - /// IP to check. - /// True if endpoint is within the LAN range. - bool IsInLocalNetwork(IPObject address); - - /// - /// Returns true if the address is part of the user defined LAN. - /// The config option TrustIP6Interfaces overrides this functions behaviour. - /// - /// IP to check. - /// True if endpoint is within the LAN range. - bool IsInLocalNetwork(IPAddress address); - - /// - /// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes. - /// eg. "eth1", or "TP-LINK Wireless USB Adapter". - /// - /// Token to parse. - /// Resultant object if successful. - /// Success of the operation. - bool TryParseInterface(string token, out IPNetAddress result); - - /// - /// Parses an array of strings into a NetCollection. - /// - /// Values to parse. - /// When true, only include values in []. When false, ignore bracketed values. - /// IPCollection object containing the value strings. - NetCollection CreateIPCollection(string[] values, bool bracketed = false); - - /// - /// Returns all the internal Bind interface addresses. - /// - /// An internal list of interfaces addresses. - NetCollection GetInternalBindAddresses(); - - /// - /// Checks to see if an IP address is still a valid interface address. - /// - /// IP address to check. - /// True if it is. - bool IsValidInterfaceAddress(IPAddress address); - - /// - /// Returns true if the IP address is in the excluded list. - /// - /// IP to check. - /// True if excluded. - bool IsExcluded(IPAddress ip); - - /// - /// Returns true if the IP address is in the excluded list. - /// - /// IP to check. - /// True if excluded. - bool IsExcluded(EndPoint ip); - - /// - /// Gets the filtered LAN ip addresses. - /// - /// Optional filter for the list. - /// Returns a filtered list of LAN addresses. - NetCollection GetFilteredLANSubnets(NetCollection? filter = null); - - /// - /// Reloads all settings and re-initialises the instance. - /// - /// to use. - public void UpdateSettings(ServerConfiguration config); - } -} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 36a0a94a0..760938c40 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -7,6 +7,7 @@ using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -19,8 +20,6 @@ namespace Jellyfin.Networking.Manager /// public class NetworkManager : INetworkManager, IDisposable { - private static NetworkManager? _instance; - /// /// Contains the description of the interface along with its index. /// @@ -48,7 +47,7 @@ namespace Jellyfin.Networking.Manager /// /// Holds the bind address overrides. /// - private readonly Dictionary _overrideUrls; + private readonly Dictionary _publishedServerUrls; /// /// Used to stop "event-racing conditions". @@ -105,7 +104,7 @@ namespace Jellyfin.Networking.Manager _interfaceAddresses = new NetCollection(unique: false); _macAddresses = new List(); _interfaceNames = new SortedList(); - _overrideUrls = new Dictionary(); + _publishedServerUrls = new Dictionary(); UpdateSettings((ServerConfiguration)_configurationManager.CommonConfiguration); if (!IsIP6Enabled && !IsIP4Enabled) @@ -117,8 +116,6 @@ namespace Jellyfin.Networking.Manager NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; _configurationManager.ConfigurationUpdated += ConfigurationUpdated; - - Instance = this; } #pragma warning restore CS8618 // Non-nullable field is uninitialized. @@ -127,19 +124,6 @@ namespace Jellyfin.Networking.Manager /// public event EventHandler? NetworkChanged; - /// - /// Gets the singleton of this object. - /// - public static NetworkManager Instance - { - get => GetInstance(); - - internal set - { - _instance = value; - } - } - /// /// Gets the unique network location signature, which is updated on every network change. /// @@ -176,7 +160,7 @@ namespace Jellyfin.Networking.Manager /// /// Gets the Published server override list. /// - public Dictionary PublishedServerOverrides => _overrideUrls; + public Dictionary PublishedServerUrls => _publishedServerUrls; /// public void Dispose() @@ -198,9 +182,12 @@ namespace Jellyfin.Networking.Manager /// public bool IsGatewayInterface(object? addressObj) { - var address = (addressObj is IPAddress addressIP) ? - addressIP : (addressObj is IPObject addressIPObj) ? - addressIPObj.Address : IPAddress.None; + var address = addressObj switch + { + IPAddress addressIp => addressIp, + IPObject addressIpObj => addressIpObj.Address, + _ => IPAddress.None + }; lock (_intLock) { @@ -320,243 +307,127 @@ namespace Jellyfin.Networking.Manager } /// - public string GetBindInterface(object? source, out int? port) + public string GetBindInterface(string source, out int? port) { - bool chromeCast = false; - port = null; - // Parse the source object in an attempt to discover where the request originated. - IPObject sourceAddr; - if (source is HttpRequest sourceReq) + if (!string.IsNullOrEmpty(source)) { - port = sourceReq.Host.Port; - if (IPHost.TryParse(sourceReq.Host.Host, out IPHost host)) + if (string.Equals(source, "chromecast", StringComparison.OrdinalIgnoreCase)) { - sourceAddr = host; - } - else - { - // Assume it's external, as we cannot resolve the host. - sourceAddr = IPHost.None; - } - } - else if (source is string sourceStr && !string.IsNullOrEmpty(sourceStr)) - { - if (string.Equals(sourceStr, "chromecast", StringComparison.OrdinalIgnoreCase)) - { - chromeCast = true; // Just assign a variable so has source = true; - sourceAddr = IPNetAddress.IP4Loopback; + return GetBindInterface(IPNetAddress.IP4Loopback, out port); } - if (IPHost.TryParse(sourceStr, out IPHost host)) + if (IPHost.TryParse(source, out IPHost host)) { - sourceAddr = host; - } - else - { - // Assume it's external, as we cannot resolve the host. - sourceAddr = IPHost.None; + return GetBindInterface(host, out port); } } - else if (source is IPAddress sourceIP) + + return GetBindInterface(IPHost.None, out port); + } + + /// + public string GetBindInterface(IPAddress source, out int? port) + { + return GetBindInterface(new IPNetAddress(source), out port); + } + + /// + public string GetBindInterface(HttpRequest source, out int? port) + { + string result; + + if (source != null && IPHost.TryParse(source.Host.Host, out IPHost host)) { - sourceAddr = new IPNetAddress(sourceIP); + result = GetBindInterface(host, out port); + port ??= source.Host.Port; } else { - // If we have no idea, then assume it came from an external address. - sourceAddr = IPHost.None; + result = GetBindInterface(IPNetAddress.None, out port); + port ??= source?.Host.Port; } + return result; + } + + /// + public string GetBindInterface(IPObject source, out int? port) + { + port = null; + bool isChromeCast = source == IPNetAddress.IP4Loopback; // Do we have a source? - bool haveSource = !sourceAddr.Address.Equals(IPAddress.None); + bool haveSource = !source.Address.Equals(IPAddress.None); + bool isExternal = false; if (haveSource) { - if (!IsIP6Enabled && sourceAddr.AddressFamily == AddressFamily.InterNetworkV6) + if (!IsIP6Enabled && source.AddressFamily == AddressFamily.InterNetworkV6) { _logger.LogWarning("IPv6 is disabled in JellyFin, but enabled in the OS. This may affect how the interface is selected."); } - if (!IsIP4Enabled && sourceAddr.AddressFamily == AddressFamily.InterNetwork) + if (!IsIP4Enabled && source.AddressFamily == AddressFamily.InterNetwork) { _logger.LogWarning("IPv4 is disabled in JellyFin, but enabled in the OS. This may affect how the interface is selected."); } - } - bool isExternal = haveSource && !IsInLocalNetwork(sourceAddr); + isExternal = !IsInLocalNetwork(source); - string bindPreference = string.Empty; - if (haveSource) - { - // Check for user override. - foreach (var addr in _overrideUrls) + if (MatchesPublishedServerUrl(source, isExternal, isChromeCast, out string result, out port)) { - // Remaining. Match anything. - if (addr.Key.Equals(IPAddress.Broadcast)) - { - bindPreference = addr.Value; - break; - } - else if ((addr.Key.Equals(IPAddress.Any) || addr.Key.Equals(IPAddress.IPv6Any)) && (isExternal || chromeCast)) - { - // External. - bindPreference = addr.Value; - break; - } - else if (addr.Key.Contains(sourceAddr)) - { - // Match ip address. - bindPreference = addr.Value; - break; - } + _logger.LogInformation("{0}: Using BindAddress {1}:{2}", source, result, port); + return result; } } _logger.LogDebug("GetBindInterface: Souce: {0}, External: {1}:", haveSource, isExternal); - if (!string.IsNullOrEmpty(bindPreference)) - { - // Has it got a port defined? - var parts = bindPreference.Split(':'); - if (parts.Length > 1) - { - if (int.TryParse(parts[1], out int p)) - { - bindPreference = parts[0]; - port = p; - } - } - - _logger.LogInformation("{0}: Using BindAddress {1}:{2}", sourceAddr, bindPreference, port); - return bindPreference; - } - - string ipresult; - // No preference given, so move on to bind addresses. lock (_intLock) { - var nc = _bindAddresses.Exclude(_bindExclusions).Where(p => !p.IsLoopback()); - - int count = nc.Count(); - if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses.Equals(IPAddress.IPv6Any))) - { - // Ignore IPAny addresses. - count = 0; - } - - if (count != 0) + if (MatchesBindInterface(source, isExternal, out string result)) { - // Check to see if any of the bind interfaces are in the same subnet. - - IEnumerable bindResult; - IPAddress? defaultGateway = null; - - if (isExternal) - { - // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first. - bindResult = nc.Where(p => !IsInLocalNetwork(p)).OrderBy(p => p.Tag); - defaultGateway = bindResult.FirstOrDefault()?.Address; - bindResult = bindResult.Where(p => p.Contains(sourceAddr)).OrderBy(p => p.Tag); - } - else - { - // Look for the best internal address. - bindResult = nc.Where(p => IsInLocalNetwork(p) && p.Contains(sourceAddr)).OrderBy(p => p.Tag); - } - - if (bindResult.Any()) - { - ipresult = FormatIP6String(bindResult.First().Address); - _logger.LogDebug("{0}: GetBindInterface: Has source, found a match bind interface subnets. {1}", sourceAddr, ipresult); - return ipresult; - } - - if (isExternal && defaultGateway != null) - { - ipresult = FormatIP6String(defaultGateway); - _logger.LogDebug("{0}: GetBindInterface: Using first user defined external interface. {1}", sourceAddr, ipresult); - return ipresult; - } - - ipresult = FormatIP6String(nc.First().Address); - _logger.LogDebug("{0}: GetBindInterface: Selected first user defined interface. {1}", sourceAddr, ipresult); - - if (isExternal) - { - // TODO: remove this after testing. - _logger.LogWarning("{0}: External request received, however, only an internal interface bind found.", sourceAddr); - } - - return ipresult; + return result; } - if (isExternal) + if (isExternal && MatchesExternalInterface(source, out result)) { - // Get the first WAN interface address that isn't a loopback. - var extResult = _interfaceAddresses - .Exclude(_bindExclusions) - .Where(p => !IsInLocalNetwork(p)) - .OrderBy(p => p.Tag); - - if (extResult.Any()) - { - // Does the request originate in one of the interface subnets? - // (For systems with multiple internal network cards, and multiple subnets) - foreach (var intf in extResult) - { - if (!IsInLocalNetwork(intf) && intf.Contains(sourceAddr)) - { - ipresult = FormatIP6String(intf.Address); - _logger.LogDebug("{0}: GetBindInterface: Selected best external on interface on range. {1}", sourceAddr, ipresult); - return ipresult; - } - } - - ipresult = FormatIP6String(extResult.First().Address); - _logger.LogDebug("{0}: GetBindInterface: Selected first external interface. {0}", sourceAddr, ipresult); - return ipresult; - } - - // Have to return something, so return an internal address - - // TODO: remove this after testing. - _logger.LogWarning("{0}: External request received, however, no WAN interface found.", sourceAddr); + return result; } // Get the first LAN interface address that isn't a loopback. - var result = _interfaceAddresses + var interfaces = new NetCollection(_interfaceAddresses .Exclude(_bindExclusions) .Where(p => IsInLocalNetwork(p)) - .OrderBy(p => p.Tag); + .OrderBy(p => p.Tag)); - if (result.Any()) + if (interfaces.Count > 0) { if (haveSource) { // Does the request originate in one of the interface subnets? // (For systems with multiple internal network cards, and multiple subnets) - foreach (var intf in result) + foreach (var intf in interfaces) { - if (intf.Contains(sourceAddr)) + if (intf.Contains(source)) { - ipresult = FormatIP6String(intf.Address); - _logger.LogDebug("{0}: GetBindInterface: Has source, matched best internal interface on range. {1}", sourceAddr, ipresult); - return ipresult; + result = FormatIP6String(intf.Address); + _logger.LogDebug("{0}: GetBindInterface: Has source, matched best internal interface on range. {1}", source, result); + return result; } } } - ipresult = FormatIP6String(result.First().Address); - _logger.LogDebug("{0}: GetBindInterface: Matched first internal interface. {1}", sourceAddr, ipresult); - return ipresult; + result = FormatIP6String(interfaces.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Matched first internal interface. {1}", source, result); + return result; } // There isn't any others, so we'll use the loopback. - ipresult = IsIP6Enabled ? "::" : "127.0.0.1"; - _logger.LogWarning("{0}: GetBindInterface: Loopback return.", sourceAddr, ipresult); - return ipresult; + result = IsIP6Enabled ? "::" : "127.0.0.1"; + _logger.LogWarning("{0}: GetBindInterface: Loopback return.", source, result); + return result; } } @@ -771,16 +642,6 @@ namespace Jellyfin.Networking.Manager } } - private static NetworkManager GetInstance() - { - if (_instance == null) - { - throw new ApplicationException("NetworkManager is not initialised."); - } - - return _instance; - } - private void ConfigurationUpdated(object? sender, EventArgs args) { UpdateSettings((ServerConfiguration)_configurationManager.CommonConfiguration); @@ -944,7 +805,7 @@ namespace Jellyfin.Networking.Manager { lock (_intLock) { - _overrideUrls.Clear(); + _publishedServerUrls.Clear(); } return; @@ -952,7 +813,7 @@ namespace Jellyfin.Networking.Manager lock (_intLock) { - _overrideUrls.Clear(); + _publishedServerUrls.Clear(); foreach (var entry in overrides) { @@ -966,15 +827,15 @@ namespace Jellyfin.Networking.Manager var replacement = parts[1].Trim(); if (string.Equals(parts[0], "remaining", StringComparison.OrdinalIgnoreCase)) { - _overrideUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement; + _publishedServerUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement; } else if (string.Equals(parts[0], "external", StringComparison.OrdinalIgnoreCase)) { - _overrideUrls[new IPNetAddress(IPAddress.Any)] = replacement; + _publishedServerUrls[new IPNetAddress(IPAddress.Any)] = replacement; } else if (TryParseInterface(parts[0], out IPNetAddress address)) { - _overrideUrls[address] = replacement; + _publishedServerUrls[address] = replacement; } else { @@ -1199,5 +1060,179 @@ namespace Jellyfin.Networking.Manager } } } + + /// + /// Attempts to match the source against a user defined bind interface. + /// + /// IP source address to use. + /// True if the source is in the external subnet. + /// True if the request is for a chromecast device. + /// The published server url that matches the source address. + /// The resultant port, if one exists. + /// True if a match is found. + private bool MatchesPublishedServerUrl(IPObject source, bool isExternal, bool isChromeCast, out string bindPreference, out int? port) + { + bindPreference = string.Empty; + port = null; + + // Check for user override. + foreach (var addr in _publishedServerUrls) + { + // Remaining. Match anything. + if (addr.Key.Equals(IPAddress.Broadcast)) + { + bindPreference = addr.Value; + break; + } + else if ((addr.Key.Equals(IPAddress.Any) || addr.Key.Equals(IPAddress.IPv6Any)) && (isExternal || isChromeCast)) + { + // External. + bindPreference = addr.Value; + break; + } + else if (addr.Key.Contains(source)) + { + // Match ip address. + bindPreference = addr.Value; + break; + } + } + + if (!string.IsNullOrEmpty(bindPreference)) + { + // Has it got a port defined? + var parts = bindPreference.Split(':'); + if (parts.Length > 1) + { + if (int.TryParse(parts[1], out int p)) + { + bindPreference = parts[0]; + port = p; + } + } + + return true; + } + + return false; + } + + /// + /// Attempts to match the source against a user defined bind interface. + /// + /// IP source address to use. + /// True if the source is in the external subnet. + /// The result, if a match is found. + /// True if a match is found. + private bool MatchesBindInterface(IPObject source, bool isExternal, out string result) + { + result = string.Empty; + var nc = new NetCollection(_bindAddresses.Exclude(_bindExclusions).Where(p => !p.IsLoopback())); + + int count = nc.Count; + if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses[0].Equals(IPAddress.IPv6Any))) + { + // Ignore IPAny addresses. + count = 0; + } + + if (count != 0) + { + // Check to see if any of the bind interfaces are in the same subnet. + + NetCollection bindResult; + IPAddress? defaultGateway = null; + IPAddress? bindAddress; + + if (isExternal) + { + // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first. + bindResult = new NetCollection(nc + .Where(p => !IsInLocalNetwork(p)) + .OrderBy(p => p.Tag)); + defaultGateway = bindResult.FirstOrDefault()?.Address; + bindAddress = bindResult + .Where(p => p.Contains(source)) + .OrderBy(p => p.Tag) + .FirstOrDefault()?.Address; + } + else + { + // Look for the best internal address. + bindAddress = nc + .Where(p => IsInLocalNetwork(p) && (p.Contains(source) || p.Equals(IPAddress.None))) + .OrderBy(p => p.Tag) + .FirstOrDefault()?.Address; + } + + if (bindAddress != null) + { + result = FormatIP6String(bindAddress); + _logger.LogDebug("{0}: GetBindInterface: Has source, found a match bind interface subnets. {1}", source, result); + return true; + } + + if (isExternal && defaultGateway != null) + { + result = FormatIP6String(defaultGateway); + _logger.LogDebug("{0}: GetBindInterface: Using first user defined external interface. {1}", source, result); + return true; + } + + result = FormatIP6String(nc.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Selected first user defined interface. {1}", source, result); + + if (isExternal) + { + // TODO: remove this after testing. + _logger.LogWarning("{0}: External request received, however, only an internal interface bind found.", source); + } + + return true; + } + + return false; + } + + /// + /// Attempts to match the source against am external interface. + /// + /// IP source address to use. + /// The result, if a match is found. + /// True if a match is found. + private bool MatchesExternalInterface(IPObject source, out string result) + { + result = string.Empty; + // Get the first WAN interface address that isn't a loopback. + var extResult = new NetCollection(_interfaceAddresses + .Exclude(_bindExclusions) + .Where(p => !IsInLocalNetwork(p)) + .OrderBy(p => p.Tag)); + + if (extResult.Count > 0) + { + // Does the request originate in one of the interface subnets? + // (For systems with multiple internal network cards, and multiple subnets) + foreach (var intf in extResult) + { + if (!IsInLocalNetwork(intf) && intf.Contains(source)) + { + result = FormatIP6String(intf.Address); + _logger.LogDebug("{0}: GetBindInterface: Selected best external on interface on range. {1}", source, result); + return true; + } + } + + result = FormatIP6String(extResult.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Selected first external interface. {0}", source, result); + return true; + } + + // Have to return something, so return an internal address + + // TODO: remove this after testing. + _logger.LogWarning("{0}: External request received, however, no WAN interface found.", source); + return false; + } } } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 8c19665cc..f73c917a0 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -12,10 +12,11 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Events.Users; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common; using MediaBrowser.Common.Cryptography; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Events; diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 566ba0ad8..e2d7305af 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -5,7 +5,7 @@ using System.Reflection; using Emby.Drawing; using Emby.Server.Implementations; using Jellyfin.Drawing.Skia; -using Jellyfin.Networking.Manager; + using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Events; diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs index ff82fe6cc..e927a147a 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -1,6 +1,6 @@ using System.Linq; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs index 87c82bf58..fa34a167b 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -2,7 +2,7 @@ using System; using System.Linq; using System.Net; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 8549e39db..939f61656 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -13,7 +13,7 @@ using CommandLine; using Emby.Server.Implementations; using Emby.Server.Implementations.IO; using Jellyfin.Api.Controllers; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; @@ -272,7 +272,7 @@ namespace Jellyfin.Server return builder .UseKestrel((builderContext, options) => { - NetCollection addresses = NetworkManager.Instance.GetAllBindInterfaces(); + NetCollection addresses = appHost.NetManager.GetAllBindInterfaces(); bool flagged = false; foreach (IPObject netAdd in addresses) diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 70dcc2397..0fda47788 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -20,8 +20,9 @@ - + + diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs new file mode 100644 index 000000000..32c017aee --- /dev/null +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -0,0 +1,221 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; +using MediaBrowser.Model.Configuration; +using Microsoft.AspNetCore.Http; +using NetworkCollection; + +namespace MediaBrowser.Common.Net +{ + /// + /// Interface for the NetworkManager class. + /// + public interface INetworkManager + { + /// + /// Event triggered on network changes. + /// + event EventHandler NetworkChanged; + + /// + /// Gets the published server urls list. + /// + Dictionary PublishedServerUrls { get; } + + /// + /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. + /// + public bool TrustAllIP6Interfaces { get; } + + /// + /// Gets the remote address filter. + /// + NetCollection RemoteAddressFilter { get; } + + /// + /// Calculates the list of interfaces to use for Kestrel. + /// + /// A NetCollection object containing all the interfaces to bind. + /// If all the interfaces are specified, and none are excluded, it returns zero items + /// to represent any address. + NetCollection GetAllBindInterfaces(); + + /// + /// Returns a collection containing the loopback interfaces. + /// + /// Netcollection. + public NetCollection GetLoopbacks(); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// The priority of selection is as follows:- + /// + /// The value contained in the startup parameter --published-server-url. + /// + /// If the user specified custom subnet overrides, the correct subnet for the source address. + /// + /// If the user specified bind interfaces to use:- + /// The bind interface that contains the source subnet. + /// The first bind interface specified that suits best first the source's endpoint. eg. external or internal. + /// + /// If the source is from a public subnet address range and the user hasn't specified any bind addresses:- + /// The first public interface that isn't a loopback and contains the source subnet. + /// The first public interface that isn't a loopback. Priority is given to interfaces with gateways. + /// An internal interface if there are no public ip addresses. + /// + /// If the source is from a private subnet address range and the user hasn't specified any bind addresses:- + /// The first private interface that contains the source subnet. + /// The first private interface that isn't a loopback. Priority is given to interfaces with gateways. + /// + /// If no interfaces meet any of these criteria, then a loopback address is returned. + /// + /// Interface that have been specifically excluded from binding are not used in any of the calculations. + /// + /// Source of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(IPObject source, out int? port); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// (See above). + /// + /// Source of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(HttpRequest source, out int? port); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// (See above). + /// + /// IP address of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(IPAddress source, out int? port); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// (See above). + /// + /// Source of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(string source, out int? port); + + /// + /// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses. + /// + /// IP address to check. + /// True if it is. + bool IsExcludedInterface(IPAddress address); + + /// + /// Get a list of all the MAC addresses associated with active interfaces. + /// + /// List of MAC addresses. + List GetMacAddresses(); + + /// + /// Checks to see if the IP Address provided matches an interface that has a gateway. + /// + /// IP to check. Can be an IPAddress or an IPObject. + /// Result of the check. + public bool IsGatewayInterface(object? addressObj); + + /// + /// Returns true if the address is a private address. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// Address to check. + /// True or False. + bool IsPrivateAddressRange(IPObject address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(string address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(IPObject address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(IPAddress address); + + /// + /// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes. + /// eg. "eth1", or "TP-LINK Wireless USB Adapter". + /// + /// Token to parse. + /// Resultant object if successful. + /// Success of the operation. + bool TryParseInterface(string token, out IPNetAddress result); + + /// + /// Parses an array of strings into a NetCollection. + /// + /// Values to parse. + /// When true, only include values in []. When false, ignore bracketed values. + /// IPCollection object containing the value strings. + NetCollection CreateIPCollection(string[] values, bool bracketed = false); + + /// + /// Returns all the internal Bind interface addresses. + /// + /// An internal list of interfaces addresses. + NetCollection GetInternalBindAddresses(); + + /// + /// Checks to see if an IP address is still a valid interface address. + /// + /// IP address to check. + /// True if it is. + bool IsValidInterfaceAddress(IPAddress address); + + /// + /// Returns true if the IP address is in the excluded list. + /// + /// IP to check. + /// True if excluded. + bool IsExcluded(IPAddress ip); + + /// + /// Returns true if the IP address is in the excluded list. + /// + /// IP to check. + /// True if excluded. + bool IsExcluded(EndPoint ip); + + /// + /// Gets the filtered LAN ip addresses. + /// + /// Optional filter for the list. + /// Returns a filtered list of LAN addresses. + NetCollection GetFilteredLANSubnets(NetCollection? filter = null); + + /// + /// Reloads all settings and re-initialises the instance. + /// + /// to use. + public void UpdateSettings(ServerConfiguration config); + } +} diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index f147e6a86..7bc1006fc 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -63,12 +63,28 @@ namespace MediaBrowser.Controller PublicSystemInfo GetPublicSystemInfo(IPAddress address); /// - /// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured - /// HTTPS will be preferred when available. + /// Gets a URL specific for the request. /// - /// The source of the request. - /// The server URL. - string GetSmartApiUrl(object source); + /// The instance. + /// Optional port number. + /// An accessible URL. + string GetSmartApiUrl(HttpRequest request, int? port = null); + + /// + /// Gets a URL specific for the request. + /// + /// The remote of the connection. + /// Optional port number. + /// An accessible URL. + string GetSmartApiUrl(IPAddress remoteAddr, int? port = null); + + /// + /// Gets a URL specific for the request. + /// + /// The hostname used in the connection. + /// Optional port number. + /// An accessible URL. + string GetSmartApiUrl(string hostname, int? port = null); /// /// Gets a localhost URL that can be used to access the API using the loop-back IP address. diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index e28a2f48d..932f6bbb4 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -7,7 +7,7 @@ using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 43fccdad4..84456f7dc 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using NetworkCollection; diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs index 2c7f0c4f9..05dd8f325 100644 --- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs @@ -4,7 +4,7 @@ using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.LocalAccessPolicy; using Jellyfin.Api.Constants; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; -- cgit v1.2.3 From 38b8110a3ec33267bb2b9e0b75ed0e2a704f41ca Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Mon, 14 Sep 2020 15:55:25 +0100 Subject: Removing blank lines. --- Emby.Dlna/Main/DlnaEntryPoint.cs | 1 - .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 1 - .../LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs | 1 - Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs | 1 - Jellyfin.Api/Auth/BaseAuthorizationHandler.cs | 1 - .../Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs | 1 - Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs | 1 - .../FirstTimeOrIgnoreParentalControlSetupHandler.cs | 1 - .../FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs | 1 - .../FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs | 1 - .../Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs | 1 - .../LocalAccessOrRequiresElevationHandler.cs | 1 - Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs | 1 - Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs | 1 - Jellyfin.Api/Controllers/UserController.cs | 1 - Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 1 - Jellyfin.Api/Helpers/MediaInfoHelper.cs | 1 - Jellyfin.Server.Implementations/Users/UserManager.cs | 2 -- Jellyfin.Server/CoreAppHost.cs | 1 - Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs | 1 - Jellyfin.Server/Middleware/LanFilteringMiddleware.cs | 1 - Jellyfin.Server/Program.cs | 1 - RSSDP/SsdpCommunicationsServer.cs | 1 - RSSDP/SsdpDevicePublisher.cs | 1 - .../Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs | 1 - 25 files changed, 26 deletions(-) (limited to 'tests') diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index a5da2fc5c..35ec15623 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -9,7 +9,6 @@ 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; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 27937d2f8..28e30fac8 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -10,7 +10,6 @@ using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; - using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index efb6d162a..a8d2a27f7 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -7,7 +7,6 @@ using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; - using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 531a785a0..8107bc427 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; - using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs index bd76b93bf..e245b5768 100644 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -1,7 +1,6 @@ using System.Security.Claims; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; - using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index dfa366796..e988ca6cd 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; - using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs index 3183d1318..cea5ac473 100644 --- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs +++ b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; - using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs index 1cee962e9..96cf4db4b 100644 --- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; - using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs index 214198e00..9815e252e 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; - using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs index 9867ea4ca..decbe0c03 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; - using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs index affd95551..3eb3ac6cd 100644 --- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs +++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; - using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs index fab464b50..7bede142e 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; - using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs index 801ee2f4c..2999c6c8a 100644 --- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; - using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs index 7adf72c3d..b235c4b63 100644 --- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; - using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index c60497e30..11cf41b83 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -7,7 +7,6 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; - using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index f81fb88fb..1d330a335 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -8,7 +8,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Models.StreamingDtos; - using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 9e4def774..b4b6e2d35 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; - using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index f73c917a0..ffd0066ee 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CA1307 using System; @@ -12,7 +11,6 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Events.Users; - using MediaBrowser.Common; using MediaBrowser.Common.Cryptography; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index e2d7305af..c2cb677eb 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -5,7 +5,6 @@ using System.Reflection; using Emby.Drawing; using Emby.Server.Implementations; using Jellyfin.Drawing.Skia; - using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Events; diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs index e927a147a..7f6b6bcce 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -1,6 +1,5 @@ using System.Linq; using System.Threading.Tasks; - using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs index fa34a167b..7963d0d8c 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using System.Net; using System.Threading.Tasks; - using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 939f61656..989cc439e 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -13,7 +13,6 @@ using CommandLine; using Emby.Server.Implementations; using Emby.Server.Implementations.IO; using Jellyfin.Api.Controllers; - using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 932f6bbb4..ea9e9a6fb 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -7,7 +7,6 @@ using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; - using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 84456f7dc..c70294b38 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; - using MediaBrowser.Common.Net; using NetworkCollection; diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs index 05dd8f325..553e6a9ca 100644 --- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs @@ -4,7 +4,6 @@ using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.LocalAccessPolicy; using Jellyfin.Api.Constants; - using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; -- cgit v1.2.3 From fae70bbbcfc75c06c6681bc34044944a26d58771 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sun, 4 Oct 2020 10:05:38 +0100 Subject: Update LocalAccessHandlerTests.cs --- .../Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs index 553e6a9ca..09ffa8468 100644 --- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; -- cgit v1.2.3 From 7565ae22cb91c472928167ac89c39f67324c72e1 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 11 Oct 2020 21:09:15 -0600 Subject: Add tests and switch to factory --- .../JsonCommaDelimitedArrayConverterFactory.cs | 28 ++++++++++ .../Json/JsonCommaDelimitedArrayTests.cs | 65 ++++++++++++++++++++++ .../Models/GenericBodyModel.cs | 20 +++++++ 3 files changed, 113 insertions(+) create mode 100644 MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs create mode 100644 tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs create mode 100644 tests/Jellyfin.Common.Tests/Models/GenericBodyModel.cs (limited to 'tests') diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs new file mode 100644 index 000000000..b7b1daf76 --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs @@ -0,0 +1,28 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// + /// Json comma delimited array converter factory. + /// + /// + /// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow. + /// + public class JsonCommaDelimitedArrayConverterFactory : JsonConverterFactory + { + /// + public override bool CanConvert(Type typeToConvert) + { + return true; + } + + /// + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var structType = typeToConvert.GetElementType(); + return (JsonConverter)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType)); + } + } +} \ No newline at end of file diff --git a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs new file mode 100644 index 000000000..c543cfee9 --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs @@ -0,0 +1,65 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Jellyfin.Common.Tests.Models; +using MediaBrowser.Model.Session; +using Xunit; + +namespace Jellyfin.Common.Tests.Json +{ + public static class JsonCommaDelimitedArrayTests + { + [Fact] + public static void Deserialize_String_Valid_Success() + { + var desiredValue = new GenericBodyModel + { + Value = new[] { "a", "b", "c" } + }; + + var options = new JsonSerializerOptions(); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a,b,c"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_GenericCommandType_Valid_Success() + { + var desiredValue = new GenericBodyModel + { + Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + }; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonStringEnumConverter()); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_String_Array_Valid_Success() + { + var desiredValue = new GenericBodyModel + { + Value = new[] { "a", "b", "c" } + }; + + var options = new JsonSerializerOptions(); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""a"",""b"",""c""] }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_GenericCommandType_Array_Valid_Success() + { + var desiredValue = new GenericBodyModel + { + Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + }; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonStringEnumConverter()); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + } +} \ No newline at end of file diff --git a/tests/Jellyfin.Common.Tests/Models/GenericBodyModel.cs b/tests/Jellyfin.Common.Tests/Models/GenericBodyModel.cs new file mode 100644 index 000000000..9a6c382fe --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Models/GenericBodyModel.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using MediaBrowser.Common.Json.Converters; + +namespace Jellyfin.Common.Tests.Models +{ + /// + /// The generic body model. + /// + /// The value type. + public class GenericBodyModel + { + /// + /// Gets or sets the value. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:Properties should not return arrays", MessageId = "Value", Justification = "Imported from ServiceStack")] + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + public T[] Value { get; set; } = default!; + } +} \ No newline at end of file -- cgit v1.2.3 From 74f4affcda2c0c8e6eebb96f8173533989675e66 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Mon, 12 Oct 2020 20:09:15 +0200 Subject: Fix AudioBookListResolver test coverage --- Emby.Naming/AudioBook/AudioBookResolver.cs | 17 ++++------------- .../AudioBook/AudioBookResolverTests.cs | 21 ++++++++++++++------- 2 files changed, 18 insertions(+), 20 deletions(-) (limited to 'tests') diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs index ed53bd04f..5807d4688 100644 --- a/Emby.Naming/AudioBook/AudioBookResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookResolver.cs @@ -1,3 +1,4 @@ +#nullable enable #pragma warning disable CS1591 using System; @@ -16,21 +17,11 @@ namespace Emby.Naming.AudioBook _options = options; } - public AudioBookFileInfo ParseFile(string path) + public AudioBookFileInfo? Resolve(string path, bool isDirectory = false) { - return Resolve(path, false); - } - - public AudioBookFileInfo ParseDirectory(string path) - { - return Resolve(path, true); - } - - public AudioBookFileInfo Resolve(string path, bool isDirectory = false) - { - if (string.IsNullOrEmpty(path)) + if (path.Length == 0) { - throw new ArgumentNullException(nameof(path)); + throw new ArgumentException("String can't be empty.", nameof(path)); } // TODO diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs index 83d44721c..673289436 100644 --- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Emby.Naming.AudioBook; using Emby.Naming.Common; using Xunit; @@ -42,16 +43,22 @@ namespace Jellyfin.Naming.Tests.AudioBook [Theory] [MemberData(nameof(GetResolveFileTestData))] - public void ResolveFile_ValidFileName_Success(AudioBookFileInfo expectedResult) + public void Resolve_ValidFileName_Success(AudioBookFileInfo expectedResult) { var result = new AudioBookResolver(_namingOptions).Resolve(expectedResult.Path); Assert.NotNull(result); - Assert.Equal(result.Path, expectedResult.Path); - Assert.Equal(result.Container, expectedResult.Container); - Assert.Equal(result.ChapterNumber, expectedResult.ChapterNumber); - Assert.Equal(result.PartNumber, expectedResult.PartNumber); - Assert.Equal(result.IsDirectory, expectedResult.IsDirectory); + Assert.Equal(result!.Path, expectedResult.Path); + Assert.Equal(result!.Container, expectedResult.Container); + Assert.Equal(result!.ChapterNumber, expectedResult.ChapterNumber); + Assert.Equal(result!.PartNumber, expectedResult.PartNumber); + Assert.Equal(result!.IsDirectory, expectedResult.IsDirectory); + } + + [Fact] + public void Resolve_EmptyFileName_ArgumentException() + { + Assert.Throws(() => new AudioBookResolver(_namingOptions).Resolve(string.Empty)); } } } -- cgit v1.2.3 From 4b6889615b877448b0a895b36c835e8d332584c6 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 14 Oct 2020 18:21:15 -0600 Subject: Fix tests --- tests/Jellyfin.Api.Tests/TestHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs index a4dd4e409..c4ce39885 100644 --- a/tests/Jellyfin.Api.Tests/TestHelpers.cs +++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs @@ -45,7 +45,7 @@ namespace Jellyfin.Api.Tests { new Claim(ClaimTypes.Role, role), new Claim(ClaimTypes.Name, "jellyfin"), - new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.UserId, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)), new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), new Claim(InternalClaimTypes.Device, "test"), new Claim(InternalClaimTypes.Client, "test"), -- cgit v1.2.3 From b2662894cf6e7e6b20d12ff22c79ee5159658a2a Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 16 Oct 2020 09:37:35 -0600 Subject: Add whitespace handling and tests --- .../Converters/JsonCommaDelimitedArrayConverter.cs | 2 +- .../Json/JsonCommaDelimitedArrayTests.cs | 27 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs index 4f6a68531..bf7048c37 100644 --- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.Common.Json.Converters var entries = new T[stringEntries.Length]; for (var i = 0; i < stringEntries.Length; i++) { - entries[i] = (T)_typeConverter.ConvertFrom(stringEntries[i]); + entries[i] = (T)_typeConverter.ConvertFrom(stringEntries[i].Trim()); } return entries; diff --git a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs index c543cfee9..16f8a690e 100644 --- a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs +++ b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs @@ -21,6 +21,19 @@ namespace Jellyfin.Common.Tests.Json Assert.Equal(desiredValue.Value, value?.Value); } + [Fact] + public static void Deserialize_String_Space_Valid_Success() + { + var desiredValue = new GenericBodyModel + { + Value = new[] { "a", "b", "c" } + }; + + var options = new JsonSerializerOptions(); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a, b, c"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + [Fact] public static void Deserialize_GenericCommandType_Valid_Success() { @@ -35,6 +48,20 @@ namespace Jellyfin.Common.Tests.Json Assert.Equal(desiredValue.Value, value?.Value); } + [Fact] + public static void Deserialize_GenericCommandType_Space_Valid_Success() + { + var desiredValue = new GenericBodyModel + { + Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + }; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonStringEnumConverter()); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + [Fact] public static void Deserialize_String_Array_Valid_Success() { -- cgit v1.2.3 From ed8899da4c2b3ea74ed78d9e6d89ca3514985b47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2020 12:00:52 +0000 Subject: Bump Moq from 4.14.6 to 4.14.7 Bumps [Moq](https://github.com/moq/moq4) from 4.14.6 to 4.14.7. - [Release notes](https://github.com/moq/moq4/releases) - [Changelog](https://github.com/moq/moq4/blob/master/CHANGELOG.md) - [Commits](https://github.com/moq/moq4/compare/v4.14.6...v4.14.7) Signed-off-by: dependabot[bot] --- tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj | 2 +- .../Jellyfin.Server.Implementations.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index aae436fb7..e7993d2e7 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -22,7 +22,7 @@ - + diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 75b67f460..6e5a90802 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -17,7 +17,7 @@ - + -- cgit v1.2.3 From 8b83e4e972243db618972e33705b959bf98ca9f4 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 28 Oct 2020 17:57:16 -0600 Subject: Fix tests --- tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs | 1 + 1 file changed, 1 insertion(+) (limited to 'tests') diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs index 4ea5094b6..33534abd2 100644 --- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -128,6 +128,7 @@ namespace Jellyfin.Api.Tests.Auth var authorizationInfo = _fixture.Create(); authorizationInfo.User = _fixture.Create(); authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin); + authorizationInfo.IsApiKey = false; _jellyfinAuthServiceMock.Setup( a => a.Authenticate( -- cgit v1.2.3 From 42c2ab97e7292cb8b61ec5dce2b2201450756d81 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 29 Oct 2020 11:32:02 -0600 Subject: Support IReadOnlyList for JsonCommaDelimitedArrayConverter --- .../JsonCommaDelimitedArrayConverterFactory.cs | 4 +- .../Json/JsonCommaDelimitedArrayTests.cs | 26 +++--- .../Json/JsonCommaDelimitedIReadOnlyListTests.cs | 92 ++++++++++++++++++++++ .../Models/GenericBodyArrayModel.cs | 20 +++++ .../Models/GenericBodyIReadOnlyListModel.cs | 19 +++++ .../Models/GenericBodyModel.cs | 20 ----- 6 files changed, 146 insertions(+), 35 deletions(-) create mode 100644 tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs create mode 100644 tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs create mode 100644 tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs delete mode 100644 tests/Jellyfin.Common.Tests/Models/GenericBodyModel.cs (limited to 'tests') diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs index b7b1daf76..24ed3ea19 100644 --- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs +++ b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs @@ -21,8 +21,8 @@ namespace MediaBrowser.Common.Json.Converters /// public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - var structType = typeToConvert.GetElementType(); + var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0]; return (JsonConverter)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType)); } } -} \ No newline at end of file +} diff --git a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs index 16f8a690e..0d2bdd1af 100644 --- a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs +++ b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs @@ -11,82 +11,82 @@ namespace Jellyfin.Common.Tests.Json [Fact] public static void Deserialize_String_Valid_Success() { - var desiredValue = new GenericBodyModel + 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"" }", options); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] public static void Deserialize_String_Space_Valid_Success() { - var desiredValue = new GenericBodyModel + 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"" }", options); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] public static void Deserialize_GenericCommandType_Valid_Success() { - var desiredValue = new GenericBodyModel + 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"" }", options); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] public static void Deserialize_GenericCommandType_Space_Valid_Success() { - var desiredValue = new GenericBodyModel + 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"" }", options); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] public static void Deserialize_String_Array_Valid_Success() { - var desiredValue = new GenericBodyModel + 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""] }", options); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] public static void Deserialize_GenericCommandType_Array_Valid_Success() { - var desiredValue = new GenericBodyModel + 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""] }", options); Assert.Equal(desiredValue.Value, value?.Value); } } -} \ No newline at end of file +} diff --git a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs new file mode 100644 index 000000000..34ad9bac7 --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs @@ -0,0 +1,92 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Jellyfin.Common.Tests.Models; +using MediaBrowser.Model.Session; +using Xunit; + +namespace Jellyfin.Common.Tests.Json +{ + public static class JsonCommaDelimitedIReadOnlyListTests + { + [Fact] + public static 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); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static 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); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static 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); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static 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); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static 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); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static 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); + Assert.Equal(desiredValue.Value, value?.Value); + } + } +} diff --git a/tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs b/tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs new file mode 100644 index 000000000..276e1bfbe --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using MediaBrowser.Common.Json.Converters; + +namespace Jellyfin.Common.Tests.Models +{ + /// + /// The generic body model. + /// + /// The value type. + public class GenericBodyArrayModel + { + /// + /// Gets or sets the value. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:Properties should not return arrays", MessageId = "Value", Justification = "Imported from ServiceStack")] + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + public T[] Value { get; set; } = default!; + } +} diff --git a/tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs b/tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs new file mode 100644 index 000000000..627454b25 --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using MediaBrowser.Common.Json.Converters; + +namespace Jellyfin.Common.Tests.Models +{ + /// + /// The generic body IReadOnlyList model. + /// + /// The value type. + public class GenericBodyIReadOnlyListModel + { + /// + /// Gets or sets the value. + /// + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + public IReadOnlyList Value { get; set; } = default!; + } +} diff --git a/tests/Jellyfin.Common.Tests/Models/GenericBodyModel.cs b/tests/Jellyfin.Common.Tests/Models/GenericBodyModel.cs deleted file mode 100644 index 9a6c382fe..000000000 --- a/tests/Jellyfin.Common.Tests/Models/GenericBodyModel.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; -using MediaBrowser.Common.Json.Converters; - -namespace Jellyfin.Common.Tests.Models -{ - /// - /// The generic body model. - /// - /// The value type. - public class GenericBodyModel - { - /// - /// Gets or sets the value. - /// - [SuppressMessage("Microsoft.Performance", "CA1819:Properties should not return arrays", MessageId = "Value", Justification = "Imported from ServiceStack")] - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public T[] Value { get; set; } = default!; - } -} \ No newline at end of file -- cgit v1.2.3 From 83af636c610744903b709117f7f2a7b7e34da1f0 Mon Sep 17 00:00:00 2001 From: Greenback Date: Sat, 31 Oct 2020 18:21:46 +0000 Subject: Updated with new NetManager --- Emby.Dlna/Main/DlnaEntryPoint.cs | 4 +- .../TunerHosts/HdHomerun/HdHomerunUdpStream.cs | 25 +- Jellyfin.Networking/Jellyfin.Networking.csproj | 4 - Jellyfin.Networking/Manager/NetworkManager.cs | 49 ++- .../IpBasedAccessValidationMiddleware.cs | 3 +- .../Middleware/LanFilteringMiddleware.cs | 1 - Jellyfin.Server/Program.cs | 4 +- MediaBrowser.Common/MediaBrowser.Common.csproj | 1 - MediaBrowser.Common/Net/INetworkManager.cs | 4 +- MediaBrowser.Common/Net/IPHost.cs | 447 +++++++++++++++++++++ MediaBrowser.Common/Net/IPNetAddress.cs | 277 +++++++++++++ MediaBrowser.Common/Net/IPObject.cs | 395 ++++++++++++++++++ MediaBrowser.Common/Net/NetworkExtensions.cs | 254 ++++++++++++ MediaBrowser.sln | 7 + RSSDP/SsdpDevicePublisher.cs | 1 - .../Jellyfin.Networking.Tests.csproj | 28 ++ .../NetworkTesting/UnitTesting.cs | 425 ++++++++++++++++++++ 17 files changed, 1895 insertions(+), 34 deletions(-) create mode 100644 MediaBrowser.Common/Net/IPHost.cs create mode 100644 MediaBrowser.Common/Net/IPNetAddress.cs create mode 100644 MediaBrowser.Common/Net/IPObject.cs create mode 100644 MediaBrowser.Common/Net/NetworkExtensions.cs create mode 100644 tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj create mode 100644 tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs (limited to 'tests') diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 504bde996..be618be2b 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Emby.Dlna.PlayTo; using Emby.Dlna.Ssdp; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -26,7 +27,6 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Net; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; -using NetworkCollection; using Rssdp; using Rssdp.Infrastructure; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; @@ -261,7 +261,7 @@ namespace Emby.Dlna.Main { var udn = CreateUuid(_appHost.SystemId); - var bindAddresses = new NetCollection( + var bindAddresses = NetworkManager.CreateCollection( _networkManager.GetInternalBindAddresses() .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0))); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index cfc5278ec..c4176eb7b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; +using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -16,7 +18,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; -using NetworkCollection.Udp; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { @@ -51,6 +52,25 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun EnableStreamSharing = true; } + /// + /// Returns an unused UDP port number in the range specified. + /// + /// Upper and Lower boundary of ports to select. + /// System.Int32. + private static int GetUdpPortFromRange((int Min, int Max) range) + { + var properties = IPGlobalProperties.GetIPGlobalProperties(); + + // Get active udp listeners. + var udpListenerPorts = properties.GetActiveUdpListeners() + .Where(n => n.Port >= range.Min && n.Port <= range.Max) + .Select(n => n.Port); + + return Enumerable.Range(range.Min, range.Max) + .Where(i => !udpListenerPorts.Contains(i)) + .FirstOrDefault(); + } + public override async Task Open(CancellationToken openCancellationToken) { LiveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested(); @@ -58,7 +78,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var mediaSource = OriginalMediaSource; var uri = new Uri(mediaSource.Path); - var localPort = UdpHelper.GetRandomUnusedUdpPort(); + // Temporary Code to reduce PR size. + var localPort = GetUdpPortFromRange((49152, 65535)); Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath)); diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index 330d36a80..1747a1dc7 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -23,10 +23,6 @@ ../jellyfin.ruleset - - - - diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 616774d04..76ac02d79 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -13,8 +13,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using NetworkCollection; -using NetworkCollection.Udp; +using NetCollection = System.Collections.ObjectModel.Collection; namespace Jellyfin.Networking.Manager { @@ -154,6 +153,22 @@ namespace Jellyfin.Networking.Manager /// public Dictionary PublishedServerUrls => _publishedServerUrls; + /// + /// Creates a new network collection. + /// + /// Items to assign the collection, or null. + /// The collection created. + public static NetCollection CreateCollection(IEnumerable? source) + { + var result = new NetCollection(); + if (source != null) + { + return result.AddRange(source); + } + + return result; + } + /// public void Dispose() { @@ -162,10 +177,10 @@ namespace Jellyfin.Networking.Manager } /// - public List GetMacAddresses() + public IReadOnlyCollection GetMacAddresses() { // Populated in construction - so always has values. - return _macAddresses.ToList(); + return _macAddresses.AsReadOnly(); } /// @@ -187,12 +202,12 @@ namespace Jellyfin.Networking.Manager NetCollection nc = new NetCollection(); if (IsIP4Enabled) { - nc.Add(IPAddress.Loopback); + nc.AddItem(IPAddress.Loopback); } if (IsIP6Enabled) { - nc.Add(IPAddress.IPv6Loopback); + nc.AddItem(IPAddress.IPv6Loopback); } return nc; @@ -276,12 +291,12 @@ namespace Jellyfin.Networking.Manager if (IsIP4Enabled) { - result.Add(IPAddress.Any); + result.AddItem(IPAddress.Any); } if (IsIP6Enabled) { - result.Add(IPAddress.IPv6Any); + result.AddItem(IPAddress.IPv6Any); } return result; @@ -375,7 +390,7 @@ namespace Jellyfin.Networking.Manager } // Get the first LAN interface address that isn't a loopback. - var interfaces = new NetCollection(_interfaceAddresses + var interfaces = CreateCollection(_interfaceAddresses .Exclude(_bindExclusions) .Where(p => IsInLocalNetwork(p)) .OrderBy(p => p.Tag)); @@ -418,11 +433,11 @@ namespace Jellyfin.Networking.Manager if (_bindExclusions.Count > 0) { // Return all the internal interfaces except the ones excluded. - return new NetCollection(_internalInterfaces.Where(p => !_bindExclusions.Contains(p))); + return CreateCollection(_internalInterfaces.Where(p => !_bindExclusions.Contains(p))); } // No bind address, so return all internal interfaces. - return new NetCollection(_internalInterfaces.Where(p => !p.IsLoopback())); + return CreateCollection(_internalInterfaces.Where(p => !p.IsLoopback())); } return new NetCollection(_bindAddresses); @@ -572,7 +587,7 @@ namespace Jellyfin.Networking.Manager } TrustAllIP6Interfaces = config.TrustAllIP6Interfaces; - UdpHelper.EnableMultiSocketBinding = config.EnableMultiSocketBinding; + // UdpHelper.EnableMultiSocketBinding = config.EnableMultiSocketBinding; if (string.IsNullOrEmpty(MockNetworkSettings)) { @@ -941,7 +956,7 @@ namespace Jellyfin.Networking.Manager { _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); // Internal interfaces must be private and not excluded. - _internalInterfaces = new NetCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.Contains(i))); + _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.Contains(i))); // Subnets are the same as the calculated internal interface. _lanSubnets = new NetCollection(); @@ -976,7 +991,7 @@ namespace Jellyfin.Networking.Manager } // Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet. - _internalInterfaces = new NetCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i) && !_excludedSubnets.Contains(i) && _lanSubnets.Contains(i))); + _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i) && !_excludedSubnets.Contains(i) && _lanSubnets.Contains(i))); } _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets); @@ -1082,7 +1097,7 @@ namespace Jellyfin.Networking.Manager IPHost host = new IPHost(Dns.GetHostName()); foreach (var a in host.GetAddresses()) { - _interfaceAddresses.Add(a); + _interfaceAddresses.AddItem(a); } if (_interfaceAddresses.Count == 0) @@ -1189,7 +1204,7 @@ namespace Jellyfin.Networking.Manager if (isExternal) { // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first. - bindResult = new NetCollection(nc + bindResult = CreateCollection(nc .Where(p => !IsInLocalNetwork(p)) .OrderBy(p => p.Tag)); defaultGateway = bindResult.FirstOrDefault()?.Address; @@ -1246,7 +1261,7 @@ namespace Jellyfin.Networking.Manager { result = string.Empty; // Get the first WAN interface address that isn't a loopback. - var extResult = new NetCollection(_interfaceAddresses + var extResult = CreateCollection(_interfaceAddresses .Exclude(_bindExclusions) .Where(p => !IsInLocalNetwork(p)) .OrderBy(p => p.Tag)); diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs index c3533d795..525cd9ffe 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -5,7 +5,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; -using NetworkCollection; namespace Jellyfin.Server.Middleware { @@ -47,7 +46,7 @@ namespace Jellyfin.Server.Middleware { // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely. // If left blank, all remote addresses will be allowed. - NetCollection remoteAddressFilter = networkManager.RemoteAddressFilter; + var remoteAddressFilter = networkManager.RemoteAddressFilter; if (remoteAddressFilter.Count > 0 && !networkManager.IsInLocalNetwork(remoteIp)) { diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs index 1f4e80053..8065054a1 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -7,7 +7,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; -using NetworkCollection; namespace Jellyfin.Server.Middleware { diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index fd300da7f..61f7da16a 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -14,6 +14,7 @@ using Emby.Server.Implementations; using Emby.Server.Implementations.IO; using Jellyfin.Api.Controllers; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -23,7 +24,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using NetworkCollection; using Serilog; using Serilog.Extensions.Logging; using SQLitePCL; @@ -271,7 +271,7 @@ namespace Jellyfin.Server return builder .UseKestrel((builderContext, options) => { - NetCollection addresses = appHost.NetManager.GetAllBindInterfaces(); + var addresses = appHost.NetManager.GetAllBindInterfaces(); bool flagged = false; foreach (IPObject netAdd in addresses) diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 0b8f431c0..777136f8b 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -22,7 +22,6 @@ - diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index f60f369d6..a7beabbdc 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Net; using System.Net.NetworkInformation; using Microsoft.AspNetCore.Http; -using NetworkCollection; +using NetCollection = System.Collections.ObjectModel.Collection; namespace MediaBrowser.Common.Net { @@ -130,7 +130,7 @@ namespace MediaBrowser.Common.Net /// Get a list of all the MAC addresses associated with active interfaces. /// /// List of MAC addresses. - List GetMacAddresses(); + IReadOnlyCollection GetMacAddresses(); /// /// Checks to see if the IP Address provided matches an interface that has a gateway. diff --git a/MediaBrowser.Common/Net/IPHost.cs b/MediaBrowser.Common/Net/IPHost.cs new file mode 100644 index 000000000..80052727a --- /dev/null +++ b/MediaBrowser.Common/Net/IPHost.cs @@ -0,0 +1,447 @@ +#nullable enable +using System; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Net +{ + /// + /// Object that holds a host name. + /// + public class IPHost : IPObject + { + /// + /// Represents an IPHost that has no value. + /// + public static readonly IPHost None = new IPHost(string.Empty, IPAddress.None); + + /// + /// Time when last resolved. + /// + private long _lastResolved; + + /// + /// Gets the IP Addresses, attempting to resolve the name, if there are none. + /// + private IPAddress[] _addresses; + + /// + /// Initializes a new instance of the class. + /// + /// Host name to assign. + public IPHost(string name) + { + HostName = name ?? throw new ArgumentNullException(nameof(name)); + _addresses = Array.Empty(); + Resolved = false; + } + + /// + /// Initializes a new instance of the class. + /// + /// Host name to assign. + /// Address to assign. + private IPHost(string name, IPAddress address) + { + HostName = name ?? throw new ArgumentNullException(nameof(name)); + _addresses = new IPAddress[] { address ?? throw new ArgumentNullException(nameof(address)) }; + Resolved = !address.Equals(IPAddress.None); + } + + /// + /// Gets or sets the object's first IP address. + /// + public override IPAddress Address + { + get + { + return ResolveHost() ? this[0] : IPAddress.None; + } + + set + { + // Not implemented. + } + } + + /// + /// Gets or sets the object's first IP's subnet prefix. + /// The setter does nothing, but shouldn't raise an exception. + /// + public override byte PrefixLength + { + get + { + return (byte)(ResolveHost() ? 128 : 0); + } + + set + { + // Not implemented. + } + } + + /// + /// Gets or sets timeout value before resolve required, in minutes. + /// + public byte Timeout { get; set; } = 30; + + /// + /// Gets a value indicating whether the address has a value. + /// + public bool HasAddress + { + get + { + return _addresses.Length > 0; + } + } + + /// + /// Gets the host name of this object. + /// + public string HostName { get; } + + /// + /// Gets a value indicating whether this host has attempted to be resolved. + /// + public bool Resolved { get; private set; } + + /// + /// Gets or sets the IP Addresses associated with this object. + /// + /// Index of address. + public IPAddress this[int index] + { + get + { + ResolveHost(); + return index >= 0 && index < _addresses.Length ? _addresses[index] : IPAddress.None; + } + } + + /// + /// Attempts to parse the host string. + /// + /// Host name to parse. + /// Object representing the string, if it has successfully been parsed. + /// Success result of the parsing. + public static bool TryParse(string host, out IPHost hostObj) + { + if (!string.IsNullOrEmpty(host)) + { + // See if it's an IPv6 with port address e.g. [::1]:120. + int i = host.IndexOf("]:", StringComparison.OrdinalIgnoreCase); + if (i != -1) + { + return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj); + } + else + { + // See if it's an IPv6 in [] with no port. + i = host.IndexOf("]", StringComparison.OrdinalIgnoreCase); + if (i != -1) + { + return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj); + } + + // Is it a host or IPv4 with port? + string[] hosts = host.Split(':'); + + if (hosts.Length > 2) + { + hostObj = new IPHost(string.Empty, IPAddress.None); + return false; + } + + // Remove port from IPv4 if it exists. + host = hosts[0]; + + if (string.Equals("localhost", host, StringComparison.OrdinalIgnoreCase)) + { + hostObj = new IPHost(host, new IPAddress(Ipv4Loopback)); + return true; + } + + if (IPNetAddress.TryParse(host, out IPNetAddress netIP)) + { + // Host name is an ip address, so fake resolve. + hostObj = new IPHost(host, netIP.Address); + return true; + } + } + + // Only thing left is to see if it's a host string. + if (!string.IsNullOrEmpty(host)) + { + // 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 + Regex re = new Regex(@"^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)$", RegexOptions.IgnoreCase | RegexOptions.Multiline); + if (re.Match(host).Success) + { + hostObj = new IPHost(host); + return true; + } + } + } + + hostObj = IPHost.None; + return false; + } + + /// + /// Attempts to parse the host string. + /// + /// Host name to parse. + /// Object representing the string, if it has successfully been parsed. + public static IPHost Parse(string host) + { + if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res)) + { + return res; + } + + throw new InvalidCastException("Host does not contain a valid value. {host}"); + } + + /// + /// Attempts to parse the host string, ensuring that it resolves only to a specific IP type. + /// + /// Host name to parse. + /// Addressfamily filter. + /// Object representing the string, if it has successfully been parsed. + public static IPHost Parse(string host, AddressFamily family) + { + if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res)) + { + if (family == AddressFamily.InterNetwork) + { + res.Remove(AddressFamily.InterNetworkV6); + } + else + { + res.Remove(AddressFamily.InterNetwork); + } + + return res; + } + + throw new InvalidCastException("Host does not contain a valid value. {host}"); + } + + /// + /// Returns the Addresses that this item resolved to. + /// + /// IPAddress Array. + public IPAddress[] GetAddresses() + { + ResolveHost(); + return _addresses; + } + + /// + public override bool Contains(IPAddress address) + { + if (address != null && !Address.Equals(IPAddress.None)) + { + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + foreach (var addr in GetAddresses()) + { + if (address.Equals(addr)) + { + return true; + } + } + } + + return false; + } + + /// + public override bool Equals(IPObject? other) + { + if (other is IPHost otherObj) + { + // Do we have the name Hostname? + if (string.Equals(otherObj.HostName, HostName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (!ResolveHost() || !otherObj.ResolveHost()) + { + return false; + } + + // Do any of our IP addresses match? + foreach (IPAddress addr in _addresses) + { + foreach (IPAddress otherAddress in otherObj._addresses) + { + if (addr.Equals(otherAddress)) + { + return true; + } + } + } + } + + return false; + } + + /// + public override bool IsIP6() + { + // Returns true if interfaces are only IP6. + if (ResolveHost()) + { + foreach (IPAddress i in _addresses) + { + if (i.AddressFamily != AddressFamily.InterNetworkV6) + { + return false; + } + } + + return true; + } + + return false; + } + + /// + public override string ToString() + { + // StringBuilder not optimum here. + string output = string.Empty; + if (_addresses.Length > 0) + { + bool moreThanOne = _addresses.Length > 1; + if (moreThanOne) + { + output = "["; + } + + foreach (var i in _addresses) + { + if (Address.Equals(IPAddress.None) && Address.AddressFamily == AddressFamily.Unspecified) + { + output += HostName + ","; + } + else if (i.Equals(IPAddress.Any)) + { + output += "Any IP4 Address,"; + } + else if (Address.Equals(IPAddress.IPv6Any)) + { + output += "Any IP6 Address,"; + } + else if (i.Equals(IPAddress.Broadcast)) + { + output += "Any Address,"; + } + else + { + output += $"{i}/32,"; + } + } + + output = output[0..^1]; + + if (moreThanOne) + { + output += "]"; + } + } + else + { + output = HostName; + } + + return output; + } + + /// + public override void Remove(AddressFamily family) + { + if (ResolveHost()) + { + _addresses = _addresses.Where(p => p.AddressFamily != family).ToArray(); + } + } + + /// + public override bool Contains(IPObject address) + { + // An IPHost cannot contain another IPObject, it can only be equal. + return Equals(address); + } + + /// + protected override IPObject CalculateNetworkAddress() + { + var netAddr = NetworkAddressOf(this[0], PrefixLength); + return new IPNetAddress(netAddr.Address, netAddr.PrefixLength); + } + + /// + /// Attempt to resolve the ip address of a host. + /// + /// The result of the comparison function. + private bool ResolveHost() + { + // When was the last time we resolved? + if (_lastResolved == 0) + { + _lastResolved = DateTime.Now.Ticks; + } + + // If we haven't resolved before, or out timer has run out... + if ((_addresses.Length == 0 && !Resolved) || (TimeSpan.FromTicks(DateTime.Now.Ticks - _lastResolved).TotalMinutes > Timeout)) + { + _lastResolved = DateTime.Now.Ticks; + ResolveHostInternal().GetAwaiter().GetResult(); + Resolved = true; + } + + return _addresses.Length > 0; + } + + /// + /// Task that looks up a Host name and returns its IP addresses. + /// + /// Array of IPAddress objects. + private async Task ResolveHostInternal() + { + if (!string.IsNullOrEmpty(HostName)) + { + // Resolves the host name - so save a DNS lookup. + if (string.Equals(HostName, "localhost", StringComparison.OrdinalIgnoreCase)) + { + _addresses = new IPAddress[] { new IPAddress(Ipv4Loopback), new IPAddress(Ipv6Loopback) }; + return; + } + + if (Uri.CheckHostName(HostName).Equals(UriHostNameType.Dns)) + { + try + { + IPHostEntry ip = await Dns.GetHostEntryAsync(HostName).ConfigureAwait(false); + _addresses = ip.AddressList; + } + catch (SocketException) + { + // Ignore socket errors, as the result value will just be an empty array. + } + } + } + } + } +} diff --git a/MediaBrowser.Common/Net/IPNetAddress.cs b/MediaBrowser.Common/Net/IPNetAddress.cs new file mode 100644 index 000000000..bcd049f3d --- /dev/null +++ b/MediaBrowser.Common/Net/IPNetAddress.cs @@ -0,0 +1,277 @@ +#nullable enable +using System; +using System.Net; +using System.Net.Sockets; + +namespace MediaBrowser.Common.Net +{ + /// + /// An object that holds and IP address and subnet mask. + /// + public class IPNetAddress : IPObject + { + /// + /// Represents an IPNetAddress that has no value. + /// + public static readonly IPNetAddress None = new IPNetAddress(IPAddress.None); + + /// + /// IPv4 multicast address. + /// + public static readonly IPAddress MulticastIPv4 = IPAddress.Parse("239.255.255.250"); + + /// + /// IPv6 local link multicast address. + /// + public static readonly IPAddress MulticastIPv6LinkLocal = IPAddress.Parse("ff02::C"); + + /// + /// IPv6 site local multicast address. + /// + public static readonly IPAddress MulticastIPv6SiteLocal = IPAddress.Parse("ff05::C"); + + /// + /// IP4Loopback address host. + /// + public static readonly IPNetAddress IP4Loopback = IPNetAddress.Parse("127.0.0.1/32"); + + /// + /// IP6Loopback address host. + /// + public static readonly IPNetAddress IP6Loopback = IPNetAddress.Parse("::1"); + + /// + /// Object's IP address. + /// + private IPAddress _address; + + /// + /// Initializes a new instance of the class. + /// + /// Address to assign. + public IPNetAddress(IPAddress address) + { + _address = address ?? throw new ArgumentNullException(nameof(address)); + PrefixLength = (byte)(address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); + } + + /// + /// Initializes a new instance of the class. + /// + /// IP Address. + /// Mask as a CIDR. + public IPNetAddress(IPAddress address, byte prefixLength) + { + if (address?.IsIPv4MappedToIPv6 ?? throw new ArgumentNullException(nameof(address))) + { + _address = address.MapToIPv4(); + } + else + { + _address = address; + } + + PrefixLength = prefixLength; + } + + /// + /// Gets or sets the object's IP address. + /// + public override IPAddress Address + { + get + { + return _address; + } + + set + { + _address = value ?? IPAddress.None; + } + } + + /// + public override byte PrefixLength { get; set; } + + /// + /// Try to parse the address and subnet strings into an IPNetAddress object. + /// + /// IP address to parse. Can be CIDR or X.X.X.X notation. + /// Resultant object. + /// True if the values parsed successfully. False if not, resulting in the IP being null. + public static bool TryParse(string addr, out IPNetAddress ip) + { + if (!string.IsNullOrEmpty(addr)) + { + addr = addr.Trim(); + + // Try to parse it as is. + if (IPAddress.TryParse(addr, out IPAddress res)) + { + ip = new IPNetAddress(res); + return true; + } + + // Is it a network? + string[] tokens = addr.Split("/"); + + if (tokens.Length == 2) + { + tokens[0] = tokens[0].TrimEnd(); + tokens[1] = tokens[1].TrimStart(); + + if (IPAddress.TryParse(tokens[0], out res)) + { + // Is the subnet part a cidr? + if (byte.TryParse(tokens[1], out byte cidr)) + { + ip = new IPNetAddress(res, cidr); + return true; + } + + // Is the subnet in x.y.a.b form? + if (IPAddress.TryParse(tokens[1], out IPAddress mask)) + { + ip = new IPNetAddress(res, MaskToCidr(mask)); + return true; + } + } + } + } + + ip = None; + return false; + } + + /// + /// Parses the string provided, throwing an exception if it is badly formed. + /// + /// String to parse. + /// IPNetAddress object. + public static IPNetAddress Parse(string addr) + { + if (TryParse(addr, out IPNetAddress o)) + { + return o; + } + + throw new ArgumentException("Unable to recognise object :" + addr); + } + + /// + public override bool Contains(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + var altAddress = NetworkAddressOf(address, PrefixLength); + return NetworkAddress.Address.Equals(altAddress.Address) && NetworkAddress.PrefixLength >= altAddress.PrefixLength; + } + + /// + public override bool Contains(IPObject address) + { + if (address is IPHost addressObj && addressObj.HasAddress) + { + foreach (IPAddress addr in addressObj.GetAddresses()) + { + if (Contains(addr)) + { + return true; + } + } + } + else if (address is IPNetAddress netaddrObj) + { + // Have the same network address, but different subnets? + if (NetworkAddress.Address.Equals(netaddrObj.NetworkAddress.Address)) + { + return NetworkAddress.PrefixLength <= netaddrObj.PrefixLength; + } + + var altAddress = NetworkAddressOf(netaddrObj.Address, PrefixLength); + return NetworkAddress.Address.Equals(altAddress.Address); + } + + return false; + } + + /// + public override bool Equals(IPObject? other) + { + if (other is IPNetAddress otherObj && !Address.Equals(IPAddress.None) && !otherObj.Address.Equals(IPAddress.None)) + { + return Address.Equals(otherObj.Address) && + PrefixLength == otherObj.PrefixLength; + } + + return false; + } + + /// + public override bool Equals(IPAddress address) + { + if (address != null && !address.Equals(IPAddress.None) && !Address.Equals(IPAddress.None)) + { + return address.Equals(Address); + } + + return false; + } + + /// + public override string ToString() + { + return ToString(false); + } + + /// + /// Returns a textual representation of this object. + /// + /// Set to true, if the subnet is to be included as part of the address. + /// String representation of this object. + public string ToString(bool shortVersion) + { + if (!Address.Equals(IPAddress.None)) + { + if (Address.Equals(IPAddress.Any)) + { + return "Any IP4 Address"; + } + + if (Address.Equals(IPAddress.IPv6Any)) + { + return "Any IP6 Address"; + } + + if (Address.Equals(IPAddress.Broadcast)) + { + return "Any Address"; + } + + if (shortVersion) + { + return Address.ToString(); + } + + return $"{Address}/{PrefixLength}"; + } + + return string.Empty; + } + + /// + protected override IPObject CalculateNetworkAddress() + { + var value = NetworkAddressOf(_address, PrefixLength); + return new IPNetAddress(value.Address, value.PrefixLength); + } + } +} diff --git a/MediaBrowser.Common/Net/IPObject.cs b/MediaBrowser.Common/Net/IPObject.cs new file mode 100644 index 000000000..a08694c26 --- /dev/null +++ b/MediaBrowser.Common/Net/IPObject.cs @@ -0,0 +1,395 @@ +#nullable enable +using System; +using System.Net; +using System.Net.Sockets; + +namespace MediaBrowser.Common.Net +{ + /// + /// Base network object class. + /// + public abstract class IPObject : IEquatable + { + /// + /// IPv6 Loopback address. + /// + protected static readonly byte[] Ipv6Loopback = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }; + + /// + /// IPv4 Loopback address. + /// + protected static readonly byte[] Ipv4Loopback = { 127, 0, 0, 1 }; + + /// + /// The network address of this object. + /// + private IPObject? _networkAddress; + + /// + /// Gets or sets the user defined functions that need storage in this object. + /// + public int Tag { get; set; } + + /// + /// Gets or sets the object's IP address. + /// + public abstract IPAddress Address { get; set; } + + /// + /// Gets the object's network address. + /// + public IPObject NetworkAddress + { + get + { + if (_networkAddress == null) + { + _networkAddress = CalculateNetworkAddress(); + } + + return _networkAddress; + } + } + + /// + /// Gets or sets the object's IP address. + /// + public abstract byte PrefixLength { get; set; } + + /// + /// Gets the AddressFamily of this object. + /// + public AddressFamily AddressFamily + { + get + { + // Keep terms separate as Address performs other functions in inherited objects. + IPAddress address = Address; + return address.Equals(IPAddress.None) ? AddressFamily.Unspecified : address.AddressFamily; + } + } + + /// + /// Returns the network address of an object. + /// + /// IP Address to convert. + /// Subnet prefix. + /// IPAddress. + public static (IPAddress Address, byte PrefixLength) NetworkAddressOf(IPAddress address, byte prefixLength) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + if (IsLoopback(address)) + { + return (Address: address, PrefixLength: prefixLength); + } + + byte[] addressBytes = address.GetAddressBytes(); + + int div = prefixLength / 8; + int mod = prefixLength % 8; + if (mod != 0) + { + mod = 8 - mod; + addressBytes[div] = (byte)((int)addressBytes[div] >> mod << mod); + div++; + } + + for (int octet = div; octet < addressBytes.Length; octet++) + { + addressBytes[octet] = 0; + } + + return (Address: new IPAddress(addressBytes), PrefixLength: prefixLength); + } + + /// + /// Tests to see if the ip address is a Loopback address. + /// + /// Value to test. + /// True if it is. + public static bool IsLoopback(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (!address.Equals(IPAddress.None)) + { + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + return address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback); + } + + return false; + } + + /// + /// Tests to see if the ip address is an IP6 address. + /// + /// Value to test. + /// True if it is. + public static bool IsIP6(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + return !address.Equals(IPAddress.None) && (address.AddressFamily == AddressFamily.InterNetworkV6); + } + + /// + /// Tests to see if the address in the private address range. + /// + /// Object to test. + /// True if it contains a private address. + public static bool IsPrivateAddressRange(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (!address.Equals(IPAddress.None)) + { + if (address.AddressFamily == AddressFamily.InterNetwork) + { + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + byte[] octet = address.GetAddressBytes(); + + return (octet[0] == 10) || + (octet[0] == 172 && octet[1] >= 16 && octet[1] <= 31) || // RFC1918 + (octet[0] == 192 && octet[1] == 168) || // RFC1918 + (octet[0] == 127); // RFC1122 + } + else + { + byte[] octet = address.GetAddressBytes(); + uint word = (uint)(octet[0] << 8) + octet[1]; + + return (word >= 0xfe80 && word <= 0xfebf) || // fe80::/10 :Local link. + (word >= 0xfc00 && word <= 0xfdff); // fc00::/7 :Unique local address. + } + } + + return false; + } + + /// + /// Returns true if the IPAddress contains an IP6 Local link address. + /// + /// IPAddress object to check. + /// True if it is a local link address. + /// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress + /// it appears that the IPAddress.IsIPv6LinkLocal is out of date. + /// + public static bool IsIPv6LinkLocal(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + if (address.AddressFamily != AddressFamily.InterNetworkV6) + { + return false; + } + + byte[] octet = address.GetAddressBytes(); + uint word = (uint)(octet[0] << 8) + octet[1]; + + return word >= 0xfe80 && word <= 0xfebf; // fe80::/10 :Local link. + } + + /// + /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only. + /// + /// Subnet mask in CIDR notation. + /// IPv4 or IPv6 family. + /// String value of the subnet mask in dotted decimal notation. + public static IPAddress CidrToMask(byte cidr, AddressFamily family) + { + uint addr = 0xFFFFFFFF << (family == AddressFamily.InterNetwork ? 32 : 128 - cidr); + addr = + ((addr & 0xff000000) >> 24) | + ((addr & 0x00ff0000) >> 8) | + ((addr & 0x0000ff00) << 8) | + ((addr & 0x000000ff) << 24); + return new IPAddress(addr); + } + + /// + /// Convert a mask to a CIDR. IPv4 only. + /// https://stackoverflow.com/questions/36954345/get-cidr-from-netmask. + /// + /// Subnet mask. + /// Byte CIDR representing the mask. + public static byte MaskToCidr(IPAddress mask) + { + if (mask == null) + { + throw new ArgumentNullException(nameof(mask)); + } + + byte cidrnet = 0; + if (!mask.Equals(IPAddress.Any)) + { + byte[] bytes = mask.GetAddressBytes(); + + var zeroed = false; + for (var i = 0; i < bytes.Length; i++) + { + for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1) + { + if (zeroed) + { + // Invalid netmask. + return (byte)~cidrnet; + } + + if ((v & 0x80) == 0) + { + zeroed = true; + } + else + { + cidrnet++; + } + } + } + } + + return cidrnet; + } + + /// + /// Tests to see if this object is a Loopback address. + /// + /// True if it is. + public virtual bool IsLoopback() + { + return IsLoopback(Address); + } + + /// + /// Removes all addresses of a specific type from this object. + /// + /// Type of address to remove. + public virtual void Remove(AddressFamily family) + { + // This method only peforms a function in the IPHost implementation of IPObject. + } + + /// + /// Tests to see if this object is an IPv6 address. + /// + /// True if it is. + public virtual bool IsIP6() + { + return IsIP6(Address); + } + + /// + /// Returns true if this IP address is in the RFC private address range. + /// + /// True this object has a private address. + public virtual bool IsPrivateAddressRange() + { + return IsPrivateAddressRange(Address); + } + + /// + /// Compares this to the object passed as a parameter. + /// + /// Object to compare to. + /// Equality result. + public virtual bool Equals(IPAddress ip) + { + if (ip != null) + { + if (ip.IsIPv4MappedToIPv6) + { + ip = ip.MapToIPv4(); + } + + return !Address.Equals(IPAddress.None) && Address.Equals(ip); + } + + return false; + } + + /// + /// Compares this to the object passed as a parameter. + /// + /// Object to compare to. + /// Equality result. + public virtual bool Equals(IPObject? other) + { + if (other != null && other is IPObject otherObj) + { + return !Address.Equals(IPAddress.None) && Address.Equals(otherObj.Address); + } + + return false; + } + + /// + /// Compares the address in this object and the address in the object passed as a parameter. + /// + /// Object's IP address to compare to. + /// Comparison result. + public abstract bool Contains(IPObject address); + + /// + /// Compares the address in this object and the address in the object passed as a parameter. + /// + /// Object's IP address to compare to. + /// Comparison result. + public abstract bool Contains(IPAddress address); + + /// + public override int GetHashCode() + { + return Address.GetHashCode(); + } + + /// + public override bool Equals(object obj) + { + return Equals(obj as IPObject); + } + + /// + /// Calculates the network address of this object. + /// + /// Returns the network address of this object. + protected abstract IPObject CalculateNetworkAddress(); + } +} diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs new file mode 100644 index 000000000..6e9cb46dc --- /dev/null +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -0,0 +1,254 @@ +#pragma warning disable CA1062 // Validate arguments of public methods +using System; +using System.Collections; +using System.Collections.Generic; +using System.Net; +using System.Runtime.CompilerServices; +using NetCollection = System.Collections.ObjectModel.Collection; + +namespace MediaBrowser.Common.Net +{ + /// + /// Defines the . + /// + public static class NetworkExtensions + { + /// + /// Add an address to the collection. + /// + /// The . + /// Item to add. + public static void AddItem(this NetCollection source, IPAddress ip) + { + if (!source.ContainsAddress(ip)) + { + source.Add(new IPNetAddress(ip, 32)); + } + } + + /// + /// Add multiple items to the collection. + /// + /// The . + /// Item to add. + /// Return the collection. + public static NetCollection AddRange(this NetCollection destination, IEnumerable source) + { + foreach (var item in source) + { + destination.Add(item); + } + + return destination; + } + + /// + /// Adds a network to the collection. + /// + /// The . + /// Item to add. + public static void AddItem(this NetCollection source, IPObject item) + { + if (!source.ContainsAddress(item)) + { + source.Add(item); + } + } + + /// + /// Converts this object to a string. + /// + /// The . + /// Returns a string representation of this object. + public static string Readable(this NetCollection source) + { + string output = "["; + if (source.Count > 0) + { + foreach (var i in source) + { + output += $"{i},"; + } + + output = output[0..^1]; + } + + return $"{output}]"; + } + + /// + /// Returns true if the collection contains an item with the ip address, + /// or the ip address falls within any of the collection's network ranges. + /// + /// The . + /// The item to look for. + /// True if the collection contains the item. + public static bool ContainsAddress(this NetCollection source, IPAddress item) + { + if (source.Count == 0) + { + return false; + } + + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + if (item.IsIPv4MappedToIPv6) + { + item = item.MapToIPv4(); + } + + foreach (var i in source) + { + if (i.Contains(item)) + { + return true; + } + } + + return false; + } + + /// + /// Returns true if the collection contains an item with the ip address, + /// or the ip address falls within any of the collection's network ranges. + /// + /// The . + /// The item to look for. + /// True if the collection contains the item. + public static bool ContainsAddress(this NetCollection source, IPObject item) + { + if (source.Count == 0) + { + return false; + } + + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + foreach (var i in source) + { + if (i.Contains(item)) + { + return true; + } + } + + return false; + } + + /// + /// Returns a collection containing the subnets of this collection given. + /// + /// The . + /// NetCollection object containing the subnets. + public static NetCollection AsNetworks(this NetCollection source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + NetCollection res = new NetCollection(); + + foreach (IPObject i in source) + { + if (i is IPNetAddress nw) + { + // Add the subnet calculated from the interface address/mask. + var na = nw.NetworkAddress; + na.Tag = i.Tag; + res.Add(na); + } + else + { + // Flatten out IPHost and add all its ip addresses. + foreach (var addr in ((IPHost)i).GetAddresses()) + { + IPNetAddress host = new IPNetAddress(addr) + { + Tag = i.Tag + }; + + res.Add(host); + } + } + } + + return res; + } + + /// + /// Excludes all the items from this list that are found in excludeList. + /// + /// The . + /// Items to exclude. + /// A new collection, with the items excluded. + public static NetCollection Exclude(this NetCollection source, NetCollection excludeList) + { + if (source.Count == 0 || excludeList == null) + { + return new NetCollection(source); + } + + NetCollection results = new NetCollection(); + + bool found; + foreach (var outer in source) + { + found = false; + + foreach (var inner in excludeList) + { + if (outer.Equals(inner)) + { + found = true; + break; + } + } + + if (!found) + { + results.Add(outer); + } + } + + return results; + } + + /// + /// Returns all items that co-exist in this object and target. + /// + /// The . + /// Collection to compare with. + /// A collection containing all the matches. + public static NetCollection Union(this NetCollection source, NetCollection target) + { + if (source.Count == 0) + { + return new NetCollection(); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + NetCollection nc = new NetCollection(); + + foreach (IPObject i in source) + { + if (target.ContainsAddress(i)) + { + nc.Add(i); + } + } + + return nc; + } + } +} diff --git a/MediaBrowser.sln b/MediaBrowser.sln index d460c0ab0..cb204137b 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -68,6 +68,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\NetworkTesting\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -182,6 +184,10 @@ Global {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.Build.0 = Release|Any CPU + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -193,6 +199,7 @@ Global {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index ae175d8c9..3e89d7f60 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -6,7 +6,6 @@ using System.Net; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; -using NetworkCollection; namespace Rssdp.Infrastructure { diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj new file mode 100644 index 000000000..fa18316df --- /dev/null +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj @@ -0,0 +1,28 @@ + + + + + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} + + + + netcoreapp3.1 + false + true + enable + + + + + + + + + + + + + + + + diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs new file mode 100644 index 000000000..9e7e8d3ac --- /dev/null +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs @@ -0,0 +1,425 @@ +using System; +using System.Net; +using Emby.Dlna.PlayTo; +using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Manager; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using Moq; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; +using NetCollection = System.Collections.ObjectModel.Collection; +using XMLProperties = System.Collections.Generic.Dictionary; + +namespace NetworkTesting +{ + public class NetTesting + { + /// + /// Trys to identify the string and return an object of that class. + /// + /// String to parse. + /// IPObject to return. + /// True if the value parsed successfully. + private static bool TryParse(string addr, out IPObject result) + { + if (!string.IsNullOrEmpty(addr)) + { + // Is it an IP address + if (IPNetAddress.TryParse(addr, out IPNetAddress nw)) + { + result = nw; + return true; + } + + if (IPHost.TryParse(addr, out IPHost h)) + { + result = h; + return true; + } + } + + result = IPNetAddress.None; + return false; + } + + + private IConfigurationManager GetMockConfig(NetworkConfiguration conf) + { + var configManager = new Mock + { + CallBase = true + }; + configManager.Setup(x => x.GetConfiguration(It.IsAny())).Returns(conf); + return (IConfigurationManager)configManager.Object; + } + + [Theory] + [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.0/24", "[192.168.1.208/24,200.200.200.200/24]")] + [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")] + [InlineData("192.168.1.208/24,-16,vEthernet1:192.168.1.208/24,-16,vEthernet212;200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")] + public void IgnoreVirtualInterfaces(string interfaces, string lan, string value) + { + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + LocalNetworkSubnets = lan.Split(';') + }; + + NetworkManager.MockNetworkSettings = interfaces; + var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + NetworkManager.MockNetworkSettings = string.Empty; + + Assert.True(string.Equals(nm.GetInternalBindAddresses().ToString(), value, StringComparison.Ordinal)); + } + + [Theory] + [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")] + public void TextIsInNetwork(string network, string value) + { + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + LocalNetworkSubnets = network.Split(',') + }; + + var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + + Assert.True(!nm.IsInLocalNetwork(value)); + } + + [Theory] + [InlineData("127.0.0.1")] + [InlineData("127.0.0.1:123")] + [InlineData("localhost")] + [InlineData("localhost:1345")] + [InlineData("www.google.co.uk")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] + [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")] + [InlineData("fe80::7add:12ff:febb:c67b%16")] + [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")] + [InlineData("192.168.1.2/255.255.255.0")] + [InlineData("192.168.1.2/24")] + + public void TestCollectionCreation(string address) + { + Assert.True(TryParse(address, out _)); + } + + [Theory] + [InlineData("256.128.0.0.0.1")] + [InlineData("127.0.0.1#")] + [InlineData("localhost!")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] + public void TestInvalidCollectionCreation(string address) + { + Assert.False(TryParse(address, out _)); + } + + [Theory] + // Src, IncIP6, incIP4, exIP6, ecIP4, net + [InlineData("127.0.0.1#", + "[]", + "[]", + "[]", + "[]", + "[]")] + [InlineData("[127.0.0.1]", + "[]", + "[]", + "[127.0.0.1/32]", + "[127.0.0.1/32]", + "[]")] + [InlineData("", + "[]", + "[]", + "[]", + "[]", + "[]")] + [InlineData("192.158.1.2/255.255.0.0,192.169.1.2/8", + "[192.158.1.2/16,192.169.1.2/8]", + "[192.158.1.2/16,192.169.1.2/8]", + "[]", + "[]", + "[192.158.0.0/16,192.0.0.0/8]")] + [InlineData("192.158.1.2/16, localhost, fd23:184f:2029:0:3139:7386:67d7:d517, [10.10.10.10]", + "[192.158.1.2/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]", + "[192.158.1.2/16,127.0.0.1/32]", + "[10.10.10.10/32]", + "[10.10.10.10/32]", + "[192.158.0.0/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]")] + public void TestCollections(string settings, string result1, string result2, string result3, string result4, string result5) + { + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + }; + + var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + + // Test included, IP6. + NetCollection nc = nm.CreateIPCollection(settings.Split(","), false); + Assert.True(string.Equals(nc.ToString(), result1, System.StringComparison.OrdinalIgnoreCase)); + + // Text excluded, non IP6. + nc = nm.CreateIPCollection(settings.Split(","), true); + Assert.True(string.Equals(nc?.ToString(), result3, System.StringComparison.OrdinalIgnoreCase)); + + conf.EnableIPV6 = false; + nm.UpdateSettings(conf); + + // Test included, non IP6. + nc = nm.CreateIPCollection(settings.Split(","), false); + Assert.True(string.Equals(nc.ToString(), result2, System.StringComparison.OrdinalIgnoreCase)); + + // Test excluded, including IPv6. + nc = nm.CreateIPCollection(settings.Split(","), true); + Assert.True(string.Equals(nc.ToString(), result4, System.StringComparison.OrdinalIgnoreCase)); + + conf.EnableIPV6 = true; + nm.UpdateSettings(conf); + + // Test network addresses of collection. + nc = nm.CreateIPCollection(settings.Split(","), false); + nc = nc.AsNetworks(); + Assert.True(string.Equals(nc.ToString(), result5, System.StringComparison.OrdinalIgnoreCase)); + } + + [Theory] + [InlineData("127.0.0.1", "fd23:184f:2029:0:3139:7386:67d7:d517/64,fd23:184f:2029:0:c0f0:8a8a:7605:fffa/128,fe80::3139:7386:67d7:d517%16/64,192.168.1.208/24,::1/128,127.0.0.1/8", "[127.0.0.1/32]")] + [InlineData("127.0.0.1", "127.0.0.1/8", "[127.0.0.1/32]")] + public void UnionCheck(string settings, string compare, string result) + { + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + }; + + var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + + NetCollection nc1 = nm.CreateIPCollection(settings.Split(","), false); + NetCollection nc2 = nm.CreateIPCollection(compare.Split(","), false); + + Assert.True(nc1.Union(nc2).ToString() == result); + } + + [Theory] + [InlineData("192.168.5.85/24", "192.168.5.1")] + [InlineData("192.168.5.85/24", "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("127.0.0.1/8", "127.0.0.1")] + public void IpV4SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.True(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("192.168.5.85/24", "192.168.4.254")] + [InlineData("192.168.5.85/24", "191.168.5.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")] + public void IpV4SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.False(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFFF")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:0001:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFF0")] + [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")] + public void IpV6SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.True(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0011:FFFF:FFFF:FFFF:FFFF")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0013:0000:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0013:0001:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0011:FFFF:FFFF:FFFF:FFF0")] + [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0001")] + public void IpV6SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.False(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("10.0.0.0/255.0.0.0", "10.10.10.1/32")] + [InlineData("10.0.0.0/8", "10.10.10.1/32")] + [InlineData("10.0.0.0/255.0.0.0", "10.10.10.1")] + + [InlineData("10.10.0.0/255.255.0.0", "10.10.10.1/32")] + [InlineData("10.10.0.0/16", "10.10.10.1/32")] + [InlineData("10.10.0.0/255.255.0.0", "10.10.10.1")] + + [InlineData("10.10.10.0/255.255.255.0", "10.10.10.1/32")] + [InlineData("10.10.10.0/24", "10.10.10.1/32")] + [InlineData("10.10.10.0/255.255.255.0", "10.10.10.1")] + + public void TestSubnets(string network, string ip) + { + Assert.True(TryParse(network, out IPObject? networkObj)); + Assert.True(TryParse(ip, out IPObject? ipObj)); + +#pragma warning disable CS8602 // Dereference of a possibly null reference. +#pragma warning disable CS8604 // Possible null reference argument. + Assert.True(networkObj.Contains(ipObj)); +#pragma warning restore CS8604 // Possible null reference argument. +#pragma warning restore CS8602 // Dereference of a possibly null reference. + } + + [Theory] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "172.168.1.2/24", "172.168.1.2/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "172.168.1.2/24, 10.10.10.1", "172.168.1.2/24,10.10.10.1/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "192.168.1.2/255.255.255.0, 10.10.10.1", "192.168.1.2/24,10.10.10.1/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "192.168.1.2/24, 100.10.10.1", "192.168.1.2/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "194.168.1.2/24, 100.10.10.1", "")] + + public void TestMatches(string source, string dest, string result) + { + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true + }; + + var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + + // Test included, IP6. + NetCollection ncSource = nm.CreateIPCollection(source.Split(",")); + NetCollection ncDest = nm.CreateIPCollection(dest.Split(",")); + NetCollection ncResult = ncSource.Union(ncDest); + NetCollection resultCollection = nm.CreateIPCollection(result.Split(",")); + Assert.True(ncResult.Equals(resultCollection)); + } + + + [Theory] + [InlineData("10.1.1.1/32", "10.1.1.1")] + [InlineData("192.168.1.254/32", "192.168.1.254/255.255.255.255")] + + public void TestEquals(string source, string dest) + { + Assert.True(IPNetAddress.Parse(source).Equals(IPNetAddress.Parse(dest))); + Assert.True(IPNetAddress.Parse(dest).Equals(IPNetAddress.Parse(source))); + } + + [Theory] + + // Testing bind interfaces. These are set for my system so won't work elsewhere. + // On my system eth16 is internal, eth11 external (Windows defines the indexes). + // + // This test is to replicate how DNLA requests work throughout the system. + + // User on internal network, we're bound internal and external - so result is internal. + [InlineData("192.168.1.1", "eth16,eth11", false, "eth16")] + // User on external network, we're bound internal and external - so result is external. + [InlineData("8.8.8.8", "eth16,eth11", false, "eth11")] + // User on internal network, we're bound internal only - so result is internal. + [InlineData("10.10.10.10", "eth16", false, "eth16")] + // User on internal network, no binding specified - so result is the 1st internal. + [InlineData("192.168.1.1", "", false, "eth16")] + // User on external network, internal binding only - so result is the 1st internal. + [InlineData("jellyfin.org", "eth16", false, "eth16")] + // User on external network, no binding - so result is the 1st external. + [InlineData("jellyfin.org", "", false, "eth11")] + // User assumed to be internal, no binding - so result is the 1st internal. + [InlineData("", "", false, "eth16")] + public void TestBindInterfaces(string source, string bindAddresses, bool ipv6enabled, string result) + { + var conf = new NetworkConfiguration() + { + LocalNetworkAddresses = bindAddresses.Split(','), + EnableIPV6 = ipv6enabled, + EnableIPV4 = true + }; + + NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11"; + var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + NetworkManager.MockNetworkSettings = string.Empty; + + _ = nm.TryParseInterface(result, out NetCollection? resultObj); + + if (resultObj != null) + { + result = ((IPNetAddress)resultObj[0]).ToString(true); + var intf = nm.GetBindInterface(source, out int? _); + + Assert.True(string.Equals(intf, result, System.StringComparison.OrdinalIgnoreCase)); + } + } + + [Theory] + + // Testing bind interfaces. These are set for my system so won't work elsewhere. + // On my system eth16 is internal, eth11 external (Windows defines the indexes). + // + // This test is to replicate how subnet bound ServerPublisherUri work throughout the system. + + // User on internal network, we're bound internal and external - so result is internal override. + [InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")] + + // User on external network, we're bound internal and external - so result is override. + [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] + + // User on internal network, we're bound internal only, but the address isn't in the LAN - so return the override. + [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")] + + // User on internal network, no binding specified - so result is the 1st internal. + [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] + + // User on external network, internal binding only - so asumption is a proxy forward, return external override. + [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] + + // User on external network, no binding - so result is the 1st external which is overriden. + [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "0.0.0.0 = http://helloworld.com", "http://helloworld.com")] + + // User assumed to be internal, no binding - so result is the 1st internal. + [InlineData("", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] + + // User is internal, no binding - so result is the 1st internal, which is then overridden. + [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")] + + public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result) + { + var conf = new NetworkConfiguration() + { + LocalNetworkSubnets = lan.Split(','), + LocalNetworkAddresses = bindAddresses.Split(','), + EnableIPV6 = ipv6enabled, + EnableIPV4 = true, + PublishedServerUriBySubnet = new string[] { publishedServers } + }; + + NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11"; + var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + NetworkManager.MockNetworkSettings = string.Empty; + + if (nm.TryParseInterface(result, out NetCollection? resultObj) && resultObj != null) + { + // Parse out IPAddresses so we can do a string comparison. (Ignore subnet masks). + result = ((IPNetAddress)resultObj[0]).ToString(true); + } + + var intf = nm.GetBindInterface(source, out int? _); + + Assert.True(string.Equals(intf, result, System.StringComparison.OrdinalIgnoreCase)); + } + } +} -- cgit v1.2.3 From f06e4826c764c1214478a741b0f93315a8bba76b Mon Sep 17 00:00:00 2001 From: Greenback Date: Sat, 31 Oct 2020 19:16:28 +0000 Subject: Fixed testing units --- Jellyfin.Networking/Manager/NetworkManager.cs | 55 ++++++++++---------- MediaBrowser.Common/Net/NetworkExtensions.cs | 60 ++++++++++++++-------- .../NetworkTesting/UnitTesting.cs | 16 +++--- 3 files changed, 77 insertions(+), 54 deletions(-) (limited to 'tests') diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 76ac02d79..2ec2a0ba5 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -158,12 +158,15 @@ namespace Jellyfin.Networking.Manager /// /// Items to assign the collection, or null. /// The collection created. - public static NetCollection CreateCollection(IEnumerable? source) + public static NetCollection CreateCollection(IEnumerable? source = null) { var result = new NetCollection(); if (source != null) { - return result.AddRange(source); + foreach (var item in source) + { + result.AddItem(item); + } } return result; @@ -433,7 +436,7 @@ namespace Jellyfin.Networking.Manager if (_bindExclusions.Count > 0) { // Return all the internal interfaces except the ones excluded. - return CreateCollection(_internalInterfaces.Where(p => !_bindExclusions.Contains(p))); + return CreateCollection(_internalInterfaces.Where(p => !_bindExclusions.ContainsAddress(p))); } // No bind address, so return all internal interfaces. @@ -463,7 +466,7 @@ namespace Jellyfin.Networking.Manager } // As private addresses can be redefined by Configuration.LocalNetworkAddresses - return _lanSubnets.Contains(address) && !_excludedSubnets.Contains(address); + return _lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address); } /// @@ -471,7 +474,7 @@ namespace Jellyfin.Networking.Manager { if (IPHost.TryParse(address, out IPHost ep)) { - return _lanSubnets.Contains(ep) && !_excludedSubnets.Contains(ep); + return _lanSubnets.ContainsAddress(ep) && !_excludedSubnets.ContainsAddress(ep); } return false; @@ -559,7 +562,7 @@ namespace Jellyfin.Networking.Manager ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) { - result.Add(iface); + result.AddItem(iface); } } @@ -603,7 +606,7 @@ namespace Jellyfin.Networking.Manager var address = IPNetAddress.Parse(parts[0]); var index = int.Parse(parts[1], CultureInfo.InvariantCulture); address.Tag = index; - _interfaceAddresses.Add(address); + _interfaceAddresses.AddItem(address); _interfaceNames.Add(parts[2], Math.Abs(index)); } } @@ -747,7 +750,7 @@ namespace Jellyfin.Networking.Manager ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) { - col.Add(iface); + col.AddItem(iface); } } } @@ -759,7 +762,7 @@ namespace Jellyfin.Networking.Manager obj.Remove(AddressFamily.InterNetworkV6); if (!obj.IsIP6()) { - col.Add(obj); + col.AddItem(obj); } } else if (!IsIP4Enabled) @@ -768,12 +771,12 @@ namespace Jellyfin.Networking.Manager obj.Remove(AddressFamily.InterNetwork); if (obj.IsIP6()) { - col.Add(obj); + col.AddItem(obj); } } else { - col.Add(obj); + col.AddItem(obj); } } else @@ -956,7 +959,7 @@ namespace Jellyfin.Networking.Manager { _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); // Internal interfaces must be private and not excluded. - _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.Contains(i))); + _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.ContainsAddress(i))); // Subnets are the same as the calculated internal interface. _lanSubnets = new NetCollection(); @@ -964,17 +967,17 @@ namespace Jellyfin.Networking.Manager // We must listen on loopback for LiveTV to function regardless of the settings. if (IsIP6Enabled) { - _lanSubnets.Add(IPNetAddress.IP6Loopback); - _lanSubnets.Add(IPNetAddress.Parse("fc00::/7")); // ULA - _lanSubnets.Add(IPNetAddress.Parse("fe80::/10")); // Site local + _lanSubnets.AddItem(IPNetAddress.IP6Loopback); + _lanSubnets.AddItem(IPNetAddress.Parse("fc00::/7")); // ULA + _lanSubnets.AddItem(IPNetAddress.Parse("fe80::/10")); // Site local } if (IsIP4Enabled) { - _lanSubnets.Add(IPNetAddress.IP4Loopback); - _lanSubnets.Add(IPNetAddress.Parse("10.0.0.0/8")); - _lanSubnets.Add(IPNetAddress.Parse("172.16.0.0/12")); - _lanSubnets.Add(IPNetAddress.Parse("192.168.0.0/16")); + _lanSubnets.AddItem(IPNetAddress.IP4Loopback); + _lanSubnets.AddItem(IPNetAddress.Parse("10.0.0.0/8")); + _lanSubnets.AddItem(IPNetAddress.Parse("172.16.0.0/12")); + _lanSubnets.AddItem(IPNetAddress.Parse("192.168.0.0/16")); } } else @@ -982,16 +985,16 @@ namespace Jellyfin.Networking.Manager // We must listen on loopback for LiveTV to function regardless of the settings. if (IsIP6Enabled) { - _lanSubnets.Add(IPNetAddress.IP6Loopback); + _lanSubnets.AddItem(IPNetAddress.IP6Loopback); } if (IsIP4Enabled) { - _lanSubnets.Add(IPNetAddress.IP4Loopback); + _lanSubnets.AddItem(IPNetAddress.IP4Loopback); } // Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet. - _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i) && !_excludedSubnets.Contains(i) && _lanSubnets.Contains(i))); + _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i))); } _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets); @@ -1049,7 +1052,7 @@ namespace Jellyfin.Networking.Manager nw.Tag *= -1; } - _interfaceAddresses.Add(nw); + _interfaceAddresses.AddItem(nw); // Store interface name so we can use the name in Collections. _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; @@ -1070,7 +1073,7 @@ namespace Jellyfin.Networking.Manager nw.Tag *= -1; } - _interfaceAddresses.Add(nw); + _interfaceAddresses.AddItem(nw); // Store interface name so we can use the name in Collections. _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; @@ -1104,10 +1107,10 @@ namespace Jellyfin.Networking.Manager { _logger.LogError("No interfaces information available. Resolving DNS name."); // Last ditch attempt - use loopback address. - _interfaceAddresses.Add(IPNetAddress.IP4Loopback); + _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback); if (IsIP6Enabled) { - _interfaceAddresses.Add(IPNetAddress.IP6Loopback); + _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback); } } } diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index 6e9cb46dc..39499460e 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; using System.Net; using System.Runtime.CompilerServices; +using System.Text; using NetCollection = System.Collections.ObjectModel.Collection; namespace MediaBrowser.Common.Net @@ -26,22 +27,6 @@ namespace MediaBrowser.Common.Net } } - /// - /// Add multiple items to the collection. - /// - /// The . - /// Item to add. - /// Return the collection. - public static NetCollection AddRange(this NetCollection destination, IEnumerable source) - { - foreach (var item in source) - { - destination.Add(item); - } - - return destination; - } - /// /// Adds a network to the collection. /// @@ -62,6 +47,7 @@ namespace MediaBrowser.Common.Net /// Returns a string representation of this object. public static string Readable(this NetCollection source) { + var sb = new StringBuilder(); string output = "["; if (source.Count > 0) { @@ -141,6 +127,40 @@ namespace MediaBrowser.Common.Net return false; } + /// + /// Compares two NetCollection objects. The order is ignored. + /// + /// The . + /// Item to compare to. + /// True if both are equal. + public static bool Compare(this NetCollection source, NetCollection dest) + { + if (dest == null || source.Count != dest.Count) + { + return false; + } + + foreach (var sourceItem in source) + { + bool found = false; + foreach (var destItem in dest) + { + if (sourceItem.Equals(destItem)) + { + found = true; + break; + } + } + + if (!found) + { + return false; + } + } + + return true; + } + /// /// Returns a collection containing the subnets of this collection given. /// @@ -162,7 +182,7 @@ namespace MediaBrowser.Common.Net // Add the subnet calculated from the interface address/mask. var na = nw.NetworkAddress; na.Tag = i.Tag; - res.Add(na); + res.AddItem(na); } else { @@ -174,7 +194,7 @@ namespace MediaBrowser.Common.Net Tag = i.Tag }; - res.Add(host); + res.AddItem(host); } } } @@ -213,7 +233,7 @@ namespace MediaBrowser.Common.Net if (!found) { - results.Add(outer); + results.AddItem(outer); } } @@ -244,7 +264,7 @@ namespace MediaBrowser.Common.Net { if (target.ContainsAddress(i)) { - nc.Add(i); + nc.AddItem(i); } } diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs index 9e7e8d3ac..fe64d1cfc 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs @@ -71,7 +71,7 @@ namespace NetworkTesting var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; - Assert.True(string.Equals(nm.GetInternalBindAddresses().ToString(), value, StringComparison.Ordinal)); + Assert.True(string.Equals(nm.GetInternalBindAddresses().Readable(), value, StringComparison.Ordinal)); } [Theory] @@ -163,22 +163,22 @@ namespace NetworkTesting // Test included, IP6. NetCollection nc = nm.CreateIPCollection(settings.Split(","), false); - Assert.True(string.Equals(nc.ToString(), result1, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc.Readable(), result1, System.StringComparison.OrdinalIgnoreCase)); // Text excluded, non IP6. nc = nm.CreateIPCollection(settings.Split(","), true); - Assert.True(string.Equals(nc?.ToString(), result3, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc?.Readable(), result3, System.StringComparison.OrdinalIgnoreCase)); conf.EnableIPV6 = false; nm.UpdateSettings(conf); // Test included, non IP6. nc = nm.CreateIPCollection(settings.Split(","), false); - Assert.True(string.Equals(nc.ToString(), result2, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc.Readable(), result2, System.StringComparison.OrdinalIgnoreCase)); // Test excluded, including IPv6. nc = nm.CreateIPCollection(settings.Split(","), true); - Assert.True(string.Equals(nc.ToString(), result4, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc.Readable(), result4, System.StringComparison.OrdinalIgnoreCase)); conf.EnableIPV6 = true; nm.UpdateSettings(conf); @@ -186,7 +186,7 @@ namespace NetworkTesting // Test network addresses of collection. nc = nm.CreateIPCollection(settings.Split(","), false); nc = nc.AsNetworks(); - Assert.True(string.Equals(nc.ToString(), result5, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc.Readable(), result5, System.StringComparison.OrdinalIgnoreCase)); } [Theory] @@ -205,7 +205,7 @@ namespace NetworkTesting NetCollection nc1 = nm.CreateIPCollection(settings.Split(","), false); NetCollection nc2 = nm.CreateIPCollection(compare.Split(","), false); - Assert.True(nc1.Union(nc2).ToString() == result); + Assert.True(nc1.Union(nc2).Readable() == result); } [Theory] @@ -306,7 +306,7 @@ namespace NetworkTesting NetCollection ncDest = nm.CreateIPCollection(dest.Split(",")); NetCollection ncResult = ncSource.Union(ncDest); NetCollection resultCollection = nm.CreateIPCollection(result.Split(",")); - Assert.True(ncResult.Equals(resultCollection)); + Assert.True(ncResult.Compare(resultCollection)); } -- cgit v1.2.3 From 59619b6ea74ab555977fd213f6ee5737897b0fbd Mon Sep 17 00:00:00 2001 From: Stepan Date: Sun, 1 Nov 2020 10:47:31 +0100 Subject: Enable nullable in Emby.Naming --- Emby.Naming/AudioBook/AudioBookFileInfo.cs | 19 +- Emby.Naming/AudioBook/AudioBookInfo.cs | 4 +- Emby.Naming/AudioBook/AudioBookListResolver.cs | 8 +- Emby.Naming/AudioBook/AudioBookResolver.cs | 14 +- Emby.Naming/Common/EpisodeExpression.cs | 4 +- Emby.Naming/Common/NamingOptions.cs | 515 +++++++++------------ Emby.Naming/Emby.Naming.csproj | 1 + Emby.Naming/Subtitles/SubtitleInfo.cs | 9 +- Emby.Naming/Subtitles/SubtitleParser.cs | 10 +- Emby.Naming/TV/EpisodeInfo.cs | 13 +- Emby.Naming/TV/EpisodePathParserResult.cs | 2 +- Emby.Naming/TV/EpisodeResolver.cs | 3 +- Emby.Naming/Video/ExtraResult.cs | 2 +- Emby.Naming/Video/ExtraRule.cs | 8 + Emby.Naming/Video/FileStack.cs | 2 +- Emby.Naming/Video/Format3DParser.cs | 2 +- Emby.Naming/Video/Format3DResult.cs | 2 +- Emby.Naming/Video/Format3DRule.cs | 8 +- Emby.Naming/Video/StubTypeRule.cs | 6 + Emby.Naming/Video/VideoFileInfo.cs | 12 +- Emby.Naming/Video/VideoInfo.cs | 4 +- Emby.Naming/Video/VideoListResolver.cs | 11 +- Emby.Naming/Video/VideoResolver.cs | 6 +- .../Library/LibraryManager.cs | 5 +- MediaBrowser.Common/MediaBrowser.Common.csproj | 4 +- .../AudioBook/AudioBookFileInfoTests.cs | 10 +- .../AudioBook/AudioBookResolverTests.cs | 32 +- 27 files changed, 349 insertions(+), 367 deletions(-) (limited to 'tests') diff --git a/Emby.Naming/AudioBook/AudioBookFileInfo.cs b/Emby.Naming/AudioBook/AudioBookFileInfo.cs index c4863b50a..e5200416e 100644 --- a/Emby.Naming/AudioBook/AudioBookFileInfo.cs +++ b/Emby.Naming/AudioBook/AudioBookFileInfo.cs @@ -7,6 +7,23 @@ namespace Emby.Naming.AudioBook /// public class AudioBookFileInfo : IComparable { + /// + /// Initializes a new instance of the class. + /// + /// Path to audiobook file. + /// File type. + /// Number of part this file represents. + /// Number of chapter this file represents. + /// Indication if we are looking at file or directory. + public AudioBookFileInfo(string path, string container, int? partNumber = default, int? chapterNumber = default, bool isDirectory = default) + { + Path = path; + Container = container; + PartNumber = partNumber; + ChapterNumber = chapterNumber; + IsDirectory = isDirectory; + } + /// /// Gets or sets the path. /// @@ -38,7 +55,7 @@ namespace Emby.Naming.AudioBook public bool IsDirectory { get; set; } /// - public int CompareTo(AudioBookFileInfo other) + public int CompareTo(AudioBookFileInfo? other) { if (ReferenceEquals(this, other)) { diff --git a/Emby.Naming/AudioBook/AudioBookInfo.cs b/Emby.Naming/AudioBook/AudioBookInfo.cs index b0b5bd881..fba11ea72 100644 --- a/Emby.Naming/AudioBook/AudioBookInfo.cs +++ b/Emby.Naming/AudioBook/AudioBookInfo.cs @@ -10,11 +10,13 @@ namespace Emby.Naming.AudioBook /// /// Initializes a new instance of the class. /// - public AudioBookInfo() + /// Name of audiobook. + public AudioBookInfo(string name) { Files = new List(); Extras = new List(); AlternateVersions = new List(); + Name = name; } /// diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs index f4ba11a0d..f350e5a4a 100644 --- a/Emby.Naming/AudioBook/AudioBookListResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using System.Collections.Generic; using System.Linq; using Emby.Naming.Common; @@ -23,7 +24,7 @@ namespace Emby.Naming.AudioBook var audiobookFileInfos = files .Select(i => audioBookResolver.Resolve(i.FullName, i.IsDirectory)) - .Where(i => i != null) + .OfType() .ToList(); // Filter out all extras, otherwise they could cause stacks to not be resolved @@ -36,9 +37,10 @@ namespace Emby.Naming.AudioBook foreach (var stack in stackResult) { - var stackFiles = stack.Files.Select(i => audioBookResolver.Resolve(i, stack.IsDirectoryStack)).ToList(); + var stackFiles = stack.Files.Select(i => audioBookResolver.Resolve(i, stack.IsDirectoryStack)).OfType().ToList(); stackFiles.Sort(); - var info = new AudioBookInfo { Files = stackFiles, Name = stack.Name }; + // TODO nullable discover if name can be empty + var info = new AudioBookInfo(stack.Name ?? string.Empty) { Files = stackFiles }; yield return info; } diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs index 5807d4688..e76cfd744 100644 --- a/Emby.Naming/AudioBook/AudioBookResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookResolver.cs @@ -42,14 +42,12 @@ namespace Emby.Naming.AudioBook var parsingResult = new AudioBookFilePathParser(_options).Parse(path); - return new AudioBookFileInfo - { - Path = path, - Container = container, - ChapterNumber = parsingResult.ChapterNumber, - PartNumber = parsingResult.PartNumber, - IsDirectory = isDirectory - }; + return new AudioBookFileInfo( + path, + container, + chapterNumber: parsingResult.ChapterNumber, + partNumber: parsingResult.PartNumber, + isDirectory: isDirectory ); } } } diff --git a/Emby.Naming/Common/EpisodeExpression.cs b/Emby.Naming/Common/EpisodeExpression.cs index ed6ba8881..00b27541a 100644 --- a/Emby.Naming/Common/EpisodeExpression.cs +++ b/Emby.Naming/Common/EpisodeExpression.cs @@ -8,11 +8,11 @@ namespace Emby.Naming.Common public class EpisodeExpression { private string _expression; - private Regex _regex; + private Regex? _regex; public EpisodeExpression(string expression, bool byDate) { - Expression = expression; + _expression = expression; IsByDate = byDate; DateTimeFormats = Array.Empty(); SupportsAbsoluteEpisodeNumbers = true; diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index fd4244f64..78bb6242d 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -75,56 +75,45 @@ namespace Emby.Naming.Common StubTypes = new[] { - new StubTypeRule - { - StubType = "dvd", - Token = "dvd" - }, - new StubTypeRule - { - StubType = "hddvd", - Token = "hddvd" - }, - new StubTypeRule - { - StubType = "bluray", - Token = "bluray" - }, - new StubTypeRule - { - StubType = "bluray", - Token = "brrip" - }, - new StubTypeRule - { - StubType = "bluray", - Token = "bd25" - }, - new StubTypeRule - { - StubType = "bluray", - Token = "bd50" - }, - new StubTypeRule - { - StubType = "vhs", - Token = "vhs" - }, - new StubTypeRule - { - StubType = "tv", - Token = "HDTV" - }, - new StubTypeRule - { - StubType = "tv", - Token = "PDTV" - }, - new StubTypeRule - { - StubType = "tv", - Token = "DSR" - } + new StubTypeRule( + stubType: "dvd", + token: "dvd"), + + new StubTypeRule( + stubType: "hddvd", + token: "hddvd"), + + new StubTypeRule( + stubType: "bluray", + token: "bluray"), + + new StubTypeRule( + stubType: "bluray", + token: "brrip"), + + new StubTypeRule( + stubType: "bluray", + token: "bd25"), + + new StubTypeRule( + stubType: "bluray", + token: "bd50"), + + new StubTypeRule( + stubType: "vhs", + token: "vhs"), + + new StubTypeRule( + stubType: "tv", + token: "HDTV"), + + new StubTypeRule( + stubType: "tv", + token: "PDTV"), + + new StubTypeRule( + stubType: "tv", + token: "DSR") }; VideoFileStackingExpressions = new[] @@ -381,247 +370,193 @@ namespace Emby.Naming.Common VideoExtraRules = new[] { - new ExtraRule - { - ExtraType = ExtraType.Trailer, - RuleType = ExtraRuleType.Filename, - Token = "trailer", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.Trailer, - RuleType = ExtraRuleType.Suffix, - Token = "-trailer", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.Trailer, - RuleType = ExtraRuleType.Suffix, - Token = ".trailer", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.Trailer, - RuleType = ExtraRuleType.Suffix, - Token = "_trailer", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.Trailer, - RuleType = ExtraRuleType.Suffix, - Token = " trailer", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.Sample, - RuleType = ExtraRuleType.Filename, - Token = "sample", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.Sample, - RuleType = ExtraRuleType.Suffix, - Token = "-sample", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.Sample, - RuleType = ExtraRuleType.Suffix, - Token = ".sample", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.Sample, - RuleType = ExtraRuleType.Suffix, - Token = "_sample", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.Sample, - RuleType = ExtraRuleType.Suffix, - Token = " sample", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.ThemeSong, - RuleType = ExtraRuleType.Filename, - Token = "theme", - MediaType = MediaType.Audio - }, - new ExtraRule - { - ExtraType = ExtraType.Scene, - RuleType = ExtraRuleType.Suffix, - Token = "-scene", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.Clip, - RuleType = ExtraRuleType.Suffix, - Token = "-clip", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.Interview, - RuleType = ExtraRuleType.Suffix, - Token = "-interview", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.BehindTheScenes, - RuleType = ExtraRuleType.Suffix, - Token = "-behindthescenes", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.DeletedScene, - RuleType = ExtraRuleType.Suffix, - Token = "-deleted", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.Clip, - RuleType = ExtraRuleType.Suffix, - Token = "-featurette", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.Clip, - RuleType = ExtraRuleType.Suffix, - Token = "-short", - MediaType = MediaType.Video - }, - new ExtraRule - { - ExtraType = ExtraType.BehindTheScenes, - RuleType = ExtraRuleType.DirectoryName, - Token = "behind the scenes", - MediaType = MediaType.Video, - }, - new ExtraRule - { - ExtraType = ExtraType.DeletedScene, - RuleType = ExtraRuleType.DirectoryName, - Token = "deleted scenes", - MediaType = MediaType.Video, - }, - new ExtraRule - { - ExtraType = ExtraType.Interview, - RuleType = ExtraRuleType.DirectoryName, - Token = "interviews", - MediaType = MediaType.Video, - }, - new ExtraRule - { - ExtraType = ExtraType.Scene, - RuleType = ExtraRuleType.DirectoryName, - Token = "scenes", - MediaType = MediaType.Video, - }, - new ExtraRule - { - ExtraType = ExtraType.Sample, - RuleType = ExtraRuleType.DirectoryName, - Token = "samples", - MediaType = MediaType.Video, - }, - new ExtraRule - { - ExtraType = ExtraType.Clip, - RuleType = ExtraRuleType.DirectoryName, - Token = "shorts", - MediaType = MediaType.Video, - }, - new ExtraRule - { - ExtraType = ExtraType.Clip, - RuleType = ExtraRuleType.DirectoryName, - Token = "featurettes", - MediaType = MediaType.Video, - }, - new ExtraRule - { - ExtraType = ExtraType.Unknown, - RuleType = ExtraRuleType.DirectoryName, - Token = "extras", - MediaType = MediaType.Video, - }, + new ExtraRule( + ExtraType.Trailer, + ExtraRuleType.Filename, + "trailer", + MediaType.Video), + + new ExtraRule( + ExtraType.Trailer, + ExtraRuleType.Suffix, + "-trailer", + MediaType.Video), + + new ExtraRule( + ExtraType.Trailer, + ExtraRuleType.Suffix, + ".trailer", + MediaType.Video), + + new ExtraRule( + ExtraType.Trailer, + ExtraRuleType.Suffix, + "_trailer", + MediaType.Video), + + new ExtraRule( + ExtraType.Trailer, + ExtraRuleType.Suffix, + " trailer", + MediaType.Video), + + new ExtraRule( + ExtraType.Sample, + ExtraRuleType.Filename, + "sample", + MediaType.Video), + + new ExtraRule( + ExtraType.Sample, + ExtraRuleType.Suffix, + "-sample", + MediaType.Video), + + new ExtraRule( + ExtraType.Sample, + ExtraRuleType.Suffix, + ".sample", + MediaType.Video), + + new ExtraRule( + ExtraType.Sample, + ExtraRuleType.Suffix, + "_sample", + MediaType.Video), + + new ExtraRule( + ExtraType.Sample, + ExtraRuleType.Suffix, + " sample", + MediaType.Video), + + new ExtraRule( + ExtraType.ThemeSong, + ExtraRuleType.Filename, + "theme", + MediaType.Audio), + + new ExtraRule( + ExtraType.Scene, + ExtraRuleType.Suffix, + "-scene", + MediaType.Video), + + new ExtraRule( + ExtraType.Clip, + ExtraRuleType.Suffix, + "-clip", + MediaType.Video), + + new ExtraRule( + ExtraType.Interview, + ExtraRuleType.Suffix, + "-interview", + MediaType.Video), + + new ExtraRule( + ExtraType.BehindTheScenes, + ExtraRuleType.Suffix, + "-behindthescenes", + MediaType.Video), + + new ExtraRule( + ExtraType.DeletedScene, + ExtraRuleType.Suffix, + "-deleted", + MediaType.Video), + + new ExtraRule( + ExtraType.Clip, + ExtraRuleType.Suffix, + "-featurette", + MediaType.Video), + + new ExtraRule( + ExtraType.Clip, + ExtraRuleType.Suffix, + "-short", + MediaType.Video), + + new ExtraRule( + ExtraType.BehindTheScenes, + ExtraRuleType.DirectoryName, + "behind the scenes", + MediaType.Video), + + new ExtraRule( + ExtraType.DeletedScene, + ExtraRuleType.DirectoryName, + "deleted scenes", + MediaType.Video), + + new ExtraRule( + ExtraType.Interview, + ExtraRuleType.DirectoryName, + "interviews", + MediaType.Video), + + new ExtraRule( + ExtraType.Scene, + ExtraRuleType.DirectoryName, + "scenes", + MediaType.Video), + + new ExtraRule( + ExtraType.Sample, + ExtraRuleType.DirectoryName, + "samples", + MediaType.Video), + + new ExtraRule( + ExtraType.Clip, + ExtraRuleType.DirectoryName, + "shorts", + MediaType.Video), + + new ExtraRule( + ExtraType.Clip, + ExtraRuleType.DirectoryName, + "featurettes", + MediaType.Video), + + new ExtraRule( + ExtraType.Unknown, + ExtraRuleType.DirectoryName, + "extras", + MediaType.Video), }; Format3DRules = new[] { // Kodi rules: - new Format3DRule - { - PreceedingToken = "3d", - Token = "hsbs" - }, - new Format3DRule - { - PreceedingToken = "3d", - Token = "sbs" - }, - new Format3DRule - { - PreceedingToken = "3d", - Token = "htab" - }, - new Format3DRule - { - PreceedingToken = "3d", - Token = "tab" - }, - // Media Browser rules: - new Format3DRule - { - Token = "fsbs" - }, - new Format3DRule - { - Token = "hsbs" - }, - new Format3DRule - { - Token = "sbs" - }, - new Format3DRule - { - Token = "ftab" - }, - new Format3DRule - { - Token = "htab" - }, - new Format3DRule - { - Token = "tab" - }, - new Format3DRule - { - Token = "sbs3d" - }, - new Format3DRule - { - Token = "mvc" - } + new Format3DRule( + preceedingToken: "3d", + token: "hsbs"), + + new Format3DRule( + preceedingToken: "3d", + token: "sbs"), + + new Format3DRule( + preceedingToken: "3d", + token: "htab"), + + new Format3DRule( + preceedingToken: "3d", + token: "tab"), + + // Media Browser rules: + new Format3DRule("fsbs"), + new Format3DRule("hsbs"), + new Format3DRule("sbs"), + new Format3DRule("ftab"), + new Format3DRule("htab"), + new Format3DRule("tab"), + new Format3DRule("sbs3d"), + new Format3DRule("mvc") }; + AudioBookPartsExpressions = new[] { // Detect specified chapters, like CH 01 @@ -737,15 +672,15 @@ namespace Emby.Naming.Common public ExtraRule[] VideoExtraRules { get; set; } - public Regex[] VideoFileStackingRegexes { get; private set; } + public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty(); - public Regex[] CleanDateTimeRegexes { get; private set; } + public Regex[] CleanDateTimeRegexes { get; private set; } = Array.Empty(); - public Regex[] CleanStringRegexes { get; private set; } + public Regex[] CleanStringRegexes { get; private set; } = Array.Empty(); - public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } + public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty(); - public Regex[] EpisodeMultiPartRegexes { get; private set; } + public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty(); public void Compile() { diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 6857f9952..93770b156 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -14,6 +14,7 @@ true true snupkg + enable diff --git a/Emby.Naming/Subtitles/SubtitleInfo.cs b/Emby.Naming/Subtitles/SubtitleInfo.cs index f39c496b7..2f16fb2df 100644 --- a/Emby.Naming/Subtitles/SubtitleInfo.cs +++ b/Emby.Naming/Subtitles/SubtitleInfo.cs @@ -4,6 +4,13 @@ namespace Emby.Naming.Subtitles { public class SubtitleInfo { + public SubtitleInfo(string path, bool isDefault, bool isForced) + { + Path = path; + IsDefault = isDefault; + IsForced = isForced; + } + /// /// Gets or sets the path. /// @@ -14,7 +21,7 @@ namespace Emby.Naming.Subtitles /// Gets or sets the language. /// /// The language. - public string Language { get; set; } + public string? Language { get; set; } /// /// Gets or sets a value indicating whether this instance is default. diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs index 24e59f90a..c8659e1b2 100644 --- a/Emby.Naming/Subtitles/SubtitleParser.cs +++ b/Emby.Naming/Subtitles/SubtitleParser.cs @@ -31,12 +31,10 @@ namespace Emby.Naming.Subtitles } var flags = GetFlags(path); - var info = new SubtitleInfo - { - Path = path, - IsDefault = _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)), - IsForced = _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)) - }; + var info = new SubtitleInfo( + path, + _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)), + _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase))); var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase) && !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase)) diff --git a/Emby.Naming/TV/EpisodeInfo.cs b/Emby.Naming/TV/EpisodeInfo.cs index 250df4e2d..a9ee82da3 100644 --- a/Emby.Naming/TV/EpisodeInfo.cs +++ b/Emby.Naming/TV/EpisodeInfo.cs @@ -4,6 +4,11 @@ namespace Emby.Naming.TV { public class EpisodeInfo { + public EpisodeInfo(string path) + { + Path = path; + } + /// /// Gets or sets the path. /// @@ -14,19 +19,19 @@ namespace Emby.Naming.TV /// Gets or sets the container. /// /// The container. - public string Container { get; set; } + public string? Container { get; set; } /// /// Gets or sets the name of the series. /// /// The name of the series. - public string SeriesName { get; set; } + public string? SeriesName { get; set; } /// /// Gets or sets the format3 d. /// /// The format3 d. - public string Format3D { get; set; } + public string? Format3D { get; set; } /// /// Gets or sets a value indicating whether [is3 d]. @@ -44,7 +49,7 @@ namespace Emby.Naming.TV /// Gets or sets the type of the stub. /// /// The type of the stub. - public string StubType { get; set; } + public string? StubType { get; set; } public int? SeasonNumber { get; set; } diff --git a/Emby.Naming/TV/EpisodePathParserResult.cs b/Emby.Naming/TV/EpisodePathParserResult.cs index 05f921edc..9c48d07a3 100644 --- a/Emby.Naming/TV/EpisodePathParserResult.cs +++ b/Emby.Naming/TV/EpisodePathParserResult.cs @@ -10,7 +10,7 @@ namespace Emby.Naming.TV public int? EndingEpsiodeNumber { get; set; } - public string SeriesName { get; set; } + public string? SeriesName { get; set; } public bool Success { get; set; } diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs index 6994f69fc..002de2117 100644 --- a/Emby.Naming/TV/EpisodeResolver.cs +++ b/Emby.Naming/TV/EpisodeResolver.cs @@ -54,9 +54,8 @@ namespace Emby.Naming.TV var parsingResult = new EpisodePathParser(_options) .Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo); - return new EpisodeInfo + return new EpisodeInfo(path) { - Path = path, Container = container, IsStub = isStub, EndingEpsiodeNumber = parsingResult.EndingEpsiodeNumber, diff --git a/Emby.Naming/Video/ExtraResult.cs b/Emby.Naming/Video/ExtraResult.cs index 15db32e87..6be7e6052 100644 --- a/Emby.Naming/Video/ExtraResult.cs +++ b/Emby.Naming/Video/ExtraResult.cs @@ -16,6 +16,6 @@ namespace Emby.Naming.Video /// Gets or sets the rule. /// /// The rule. - public ExtraRule Rule { get; set; } + public ExtraRule? Rule { get; set; } } } diff --git a/Emby.Naming/Video/ExtraRule.cs b/Emby.Naming/Video/ExtraRule.cs index 7c9702e24..c018894fd 100644 --- a/Emby.Naming/Video/ExtraRule.cs +++ b/Emby.Naming/Video/ExtraRule.cs @@ -10,6 +10,14 @@ namespace Emby.Naming.Video /// public class ExtraRule { + public ExtraRule(ExtraType extraType, ExtraRuleType ruleType, string token, MediaType mediaType) + { + Token = token; + ExtraType = extraType; + RuleType = ruleType; + MediaType = mediaType; + } + /// /// Gets or sets the token to use for matching against the file path. /// diff --git a/Emby.Naming/Video/FileStack.cs b/Emby.Naming/Video/FileStack.cs index 3ef190b86..b0a22b18b 100644 --- a/Emby.Naming/Video/FileStack.cs +++ b/Emby.Naming/Video/FileStack.cs @@ -13,7 +13,7 @@ namespace Emby.Naming.Video Files = new List(); } - public string Name { get; set; } + public string Name { get; set; } = string.Empty; public List Files { get; set; } diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs index 51c26af86..3a9eaa1a1 100644 --- a/Emby.Naming/Video/Format3DParser.cs +++ b/Emby.Naming/Video/Format3DParser.cs @@ -57,7 +57,7 @@ namespace Emby.Naming.Video else { var foundPrefix = false; - string format = null; + string? format = null; foreach (var flag in videoFlags) { diff --git a/Emby.Naming/Video/Format3DResult.cs b/Emby.Naming/Video/Format3DResult.cs index fa0e9d3b8..36dc1c12b 100644 --- a/Emby.Naming/Video/Format3DResult.cs +++ b/Emby.Naming/Video/Format3DResult.cs @@ -21,7 +21,7 @@ namespace Emby.Naming.Video /// Gets or sets the format3 d. /// /// The format3 d. - public string Format3D { get; set; } + public string? Format3D { get; set; } /// /// Gets or sets the tokens. diff --git a/Emby.Naming/Video/Format3DRule.cs b/Emby.Naming/Video/Format3DRule.cs index 310ec84e8..a35f0d9d9 100644 --- a/Emby.Naming/Video/Format3DRule.cs +++ b/Emby.Naming/Video/Format3DRule.cs @@ -4,6 +4,12 @@ namespace Emby.Naming.Video { public class Format3DRule { + public Format3DRule(string token, string? preceedingToken = null) + { + Token = token; + PreceedingToken = preceedingToken; + } + /// /// Gets or sets the token. /// @@ -14,6 +20,6 @@ namespace Emby.Naming.Video /// Gets or sets the preceeding token. /// /// The preceeding token. - public string PreceedingToken { get; set; } + public string? PreceedingToken { get; set; } } } diff --git a/Emby.Naming/Video/StubTypeRule.cs b/Emby.Naming/Video/StubTypeRule.cs index 8285cb51a..fa42af604 100644 --- a/Emby.Naming/Video/StubTypeRule.cs +++ b/Emby.Naming/Video/StubTypeRule.cs @@ -4,6 +4,12 @@ namespace Emby.Naming.Video { public class StubTypeRule { + public StubTypeRule(string token, string stubType) + { + Token = token; + StubType = stubType; + } + /// /// Gets or sets the token. /// diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs index 11e789b66..12bd8c436 100644 --- a/Emby.Naming/Video/VideoFileInfo.cs +++ b/Emby.Naming/Video/VideoFileInfo.cs @@ -11,19 +11,19 @@ namespace Emby.Naming.Video /// Gets or sets the path. /// /// The path. - public string Path { get; set; } + public string? Path { get; set; } /// /// Gets or sets the container. /// /// The container. - public string Container { get; set; } + public string? Container { get; set; } /// /// Gets or sets the name. /// /// The name. - public string Name { get; set; } + public string? Name { get; set; } /// /// Gets or sets the year. @@ -41,13 +41,13 @@ namespace Emby.Naming.Video /// Gets or sets the extra rule. /// /// The extra rule. - public ExtraRule ExtraRule { get; set; } + public ExtraRule? ExtraRule { get; set; } /// /// Gets or sets the format3 d. /// /// The format3 d. - public string Format3D { get; set; } + public string? Format3D { get; set; } /// /// Gets or sets a value indicating whether [is3 d]. @@ -65,7 +65,7 @@ namespace Emby.Naming.Video /// Gets or sets the type of the stub. /// /// The type of the stub. - public string StubType { get; set; } + public string? StubType { get; set; } /// /// Gets or sets a value indicating whether this instance is a directory. diff --git a/Emby.Naming/Video/VideoInfo.cs b/Emby.Naming/Video/VideoInfo.cs index ea74c40e2..930fdb33f 100644 --- a/Emby.Naming/Video/VideoInfo.cs +++ b/Emby.Naming/Video/VideoInfo.cs @@ -12,7 +12,7 @@ namespace Emby.Naming.Video /// Initializes a new instance of the class. /// /// The name. - public VideoInfo(string name) + public VideoInfo(string? name) { Name = name; @@ -25,7 +25,7 @@ namespace Emby.Naming.Video /// Gets or sets the name. /// /// The name. - public string Name { get; set; } + public string? Name { get; set; } /// /// Gets or sets the year. diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 948fe037b..601c6c0b6 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -26,7 +26,8 @@ namespace Emby.Naming.Video var videoInfos = files .Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory)) - .Where(i => i != null) + // .Where(i => i != null) + .OfType() .ToList(); // Filter out all extras, otherwise they could cause stacks to not be resolved @@ -39,7 +40,7 @@ namespace Emby.Naming.Video .Resolve(nonExtras).ToList(); var remainingFiles = videoInfos - .Where(i => !stackResult.Any(s => s.ContainsFile(i.Path, i.IsDirectory))) + .Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory))) .ToList(); var list = new List(); @@ -48,7 +49,9 @@ namespace Emby.Naming.Video { var info = new VideoInfo(stack.Name) { - Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack)).ToList() + Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack)) + .OfType() + .ToList() }; info.Year = info.Files[0].Year; @@ -203,7 +206,7 @@ namespace Emby.Naming.Video return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2; } - private bool IsEligibleForMultiVersion(string folderName, string testFilename) + private bool IsEligibleForMultiVersion(string folderName, string? testFilename) { testFilename = Path.GetFileNameWithoutExtension(testFilename) ?? string.Empty; diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs index b4aee614b..b9ff90179 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -22,7 +22,7 @@ namespace Emby.Naming.Video /// /// The path. /// VideoFileInfo. - public VideoFileInfo? ResolveDirectory(string path) + public VideoFileInfo? ResolveDirectory(string? path) { return Resolve(path, true); } @@ -32,7 +32,7 @@ namespace Emby.Naming.Video /// /// The path. /// VideoFileInfo. - public VideoFileInfo? ResolveFile(string path) + public VideoFileInfo? ResolveFile(string? path) { return Resolve(path, false); } @@ -45,7 +45,7 @@ namespace Emby.Naming.Video /// Whether or not the name should be parsed for info. /// VideoFileInfo. /// path is null. - public VideoFileInfo? Resolve(string path, bool isDirectory, bool parseName = true) + public VideoFileInfo? Resolve(string? path, bool isDirectory, bool parseName = true) { if (string.IsNullOrEmpty(path)) { diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 00282b71a..e121e9eaf 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2470,9 +2470,10 @@ namespace Emby.Server.Implementations.Library var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; + // TODO nullable - what are we trying to do there with empty episodeInfo? var episodeInfo = episode.IsFileProtocol - ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo() - : new Naming.TV.EpisodeInfo(); + ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo(episode.Path) + : new Naming.TV.EpisodeInfo(episode.Path); try { diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index e716a6610..777136f8b 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -1,4 +1,4 @@ - + @@ -20,7 +20,7 @@ - + diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs index a214bc57c..cf21f964e 100644 --- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs +++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs @@ -1,4 +1,4 @@ -using Emby.Naming.AudioBook; +using Emby.Naming.AudioBook; using Xunit; namespace Jellyfin.Naming.Tests.AudioBook @@ -8,22 +8,22 @@ namespace Jellyfin.Naming.Tests.AudioBook [Fact] public void CompareTo_Same_Success() { - var info = new AudioBookFileInfo(); + var info = new AudioBookFileInfo(string.Empty, string.Empty); Assert.Equal(0, info.CompareTo(info)); } [Fact] public void CompareTo_Null_Success() { - var info = new AudioBookFileInfo(); + var info = new AudioBookFileInfo(string.Empty, string.Empty); Assert.Equal(1, info.CompareTo(null)); } [Fact] public void CompareTo_Empty_Success() { - var info1 = new AudioBookFileInfo(); - var info2 = new AudioBookFileInfo(); + var info1 = new AudioBookFileInfo(string.Empty, string.Empty); + var info2 = new AudioBookFileInfo(string.Empty, string.Empty); Assert.Equal(0, info1.CompareTo(info2)); } } diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs index 673289436..2708d80bb 100644 --- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Emby.Naming.AudioBook; using Emby.Naming.Common; @@ -14,30 +14,24 @@ namespace Jellyfin.Naming.Tests.AudioBook { yield return new object[] { - new AudioBookFileInfo() - { - Path = @"/server/AudioBooks/Larry Potter/Larry Potter.mp3", - Container = "mp3", - } + new AudioBookFileInfo( + @"/server/AudioBooks/Larry Potter/Larry Potter.mp3", + "mp3") }; yield return new object[] { - new AudioBookFileInfo() - { - Path = @"/server/AudioBooks/Berry Potter/Chapter 1 .ogg", - Container = "ogg", - ChapterNumber = 1 - } + new AudioBookFileInfo( + @"/server/AudioBooks/Berry Potter/Chapter 1 .ogg", + "ogg", + chapterNumber: 1) }; yield return new object[] { - new AudioBookFileInfo() - { - Path = @"/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3", - Container = "mp3", - ChapterNumber = 2, - PartNumber = 3 - } + new AudioBookFileInfo( + @"/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3", + "mp3", + chapterNumber: 2, + partNumber: 3) }; } -- cgit v1.2.3 From 60b49e67eafd356d1276f43de1a3f1f2fe52fe3f Mon Sep 17 00:00:00 2001 From: Stepan Date: Sun, 1 Nov 2020 11:19:22 +0100 Subject: Re-Sharper inspection issues --- Emby.Naming/AudioBook/AudioBookListResolver.cs | 1 - Emby.Naming/AudioBook/AudioBookResolver.cs | 2 +- Emby.Naming/Common/NamingOptions.cs | 12 +++++++----- Emby.Naming/Emby.Naming.csproj | 2 +- Emby.Naming/TV/EpisodeInfo.cs | 2 +- Emby.Naming/TV/EpisodePathParser.cs | 8 ++++---- Emby.Naming/TV/EpisodePathParserResult.cs | 2 +- Emby.Naming/TV/EpisodeResolver.cs | 2 +- Emby.Naming/TV/SeasonPathParser.cs | 12 ++++++------ Emby.Naming/Video/ExtraRuleType.cs | 2 +- Emby.Naming/Video/FlagParser.cs | 4 ++-- Emby.Naming/Video/Format3DParser.cs | 14 +++++++------- Emby.Naming/Video/Format3DRule.cs | 10 +++++----- Emby.Naming/Video/StubResult.cs | 19 ------------------- Emby.Naming/Video/VideoListResolver.cs | 2 +- Emby.Server.Implementations/Library/LibraryManager.cs | 4 ++-- tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs | 2 +- 17 files changed, 41 insertions(+), 59 deletions(-) delete mode 100644 Emby.Naming/Video/StubResult.cs (limited to 'tests') diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs index f350e5a4a..179a3bc07 100644 --- a/Emby.Naming/AudioBook/AudioBookListResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs @@ -1,6 +1,5 @@ #pragma warning disable CS1591 -using System; using System.Collections.Generic; using System.Linq; using Emby.Naming.Common; diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs index e76cfd744..56442fc4e 100644 --- a/Emby.Naming/AudioBook/AudioBookResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookResolver.cs @@ -47,7 +47,7 @@ namespace Emby.Naming.AudioBook container, chapterNumber: parsingResult.ChapterNumber, partNumber: parsingResult.PartNumber, - isDirectory: isDirectory ); + isDirectory: isDirectory); } } } diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 78bb6242d..537de63d5 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -6,6 +6,8 @@ using System.Text.RegularExpressions; using Emby.Naming.Video; using MediaBrowser.Model.Entities; +// ReSharper disable StringLiteralTypo + namespace Emby.Naming.Common { public class NamingOptions @@ -531,19 +533,19 @@ namespace Emby.Naming.Common { // Kodi rules: new Format3DRule( - preceedingToken: "3d", + precedingToken: "3d", token: "hsbs"), new Format3DRule( - preceedingToken: "3d", + precedingToken: "3d", token: "sbs"), new Format3DRule( - preceedingToken: "3d", + precedingToken: "3d", token: "htab"), new Format3DRule( - preceedingToken: "3d", + precedingToken: "3d", token: "tab"), // Media Browser rules: @@ -608,7 +610,7 @@ namespace Emby.Naming.Common ".mxf" }); - MultipleEpisodeExpressions = new string[] + MultipleEpisodeExpressions = new[] { @".*(\\|\/)[sS]?(?[0-9]{1,4})[xX](?[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?[0-9]{1,4})[xX](?[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?[0-9]{1,3}))+[^\\\/]*$", diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 93770b156..b7fd0c545 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -39,7 +39,7 @@ - + diff --git a/Emby.Naming/TV/EpisodeInfo.cs b/Emby.Naming/TV/EpisodeInfo.cs index a9ee82da3..e01c81062 100644 --- a/Emby.Naming/TV/EpisodeInfo.cs +++ b/Emby.Naming/TV/EpisodeInfo.cs @@ -55,7 +55,7 @@ namespace Emby.Naming.TV public int? EpisodeNumber { get; set; } - public int? EndingEpsiodeNumber { get; set; } + public int? EndingEpisodeNumber { get; set; } public int? Year { get; set; } diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs index a6af689c7..866d8adc0 100644 --- a/Emby.Naming/TV/EpisodePathParser.cs +++ b/Emby.Naming/TV/EpisodePathParser.cs @@ -146,7 +146,7 @@ namespace Emby.Naming.TV { if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) { - result.EndingEpsiodeNumber = num; + result.EndingEpisodeNumber = num; } } } @@ -217,13 +217,13 @@ namespace Emby.Naming.TV info.SeriesName = result.SeriesName; } - if (!info.EndingEpsiodeNumber.HasValue && info.EpisodeNumber.HasValue) + if (!info.EndingEpisodeNumber.HasValue && info.EpisodeNumber.HasValue) { - info.EndingEpsiodeNumber = result.EndingEpsiodeNumber; + info.EndingEpisodeNumber = result.EndingEpisodeNumber; } if (!string.IsNullOrEmpty(info.SeriesName) - && (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue)) + && (!info.EpisodeNumber.HasValue || info.EndingEpisodeNumber.HasValue)) { break; } diff --git a/Emby.Naming/TV/EpisodePathParserResult.cs b/Emby.Naming/TV/EpisodePathParserResult.cs index 9c48d07a3..5fa0b6f0b 100644 --- a/Emby.Naming/TV/EpisodePathParserResult.cs +++ b/Emby.Naming/TV/EpisodePathParserResult.cs @@ -8,7 +8,7 @@ namespace Emby.Naming.TV public int? EpisodeNumber { get; set; } - public int? EndingEpsiodeNumber { get; set; } + public int? EndingEpisodeNumber { get; set; } public string? SeriesName { get; set; } diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs index 002de2117..5f02c553d 100644 --- a/Emby.Naming/TV/EpisodeResolver.cs +++ b/Emby.Naming/TV/EpisodeResolver.cs @@ -58,7 +58,7 @@ namespace Emby.Naming.TV { Container = container, IsStub = isStub, - EndingEpsiodeNumber = parsingResult.EndingEpsiodeNumber, + EndingEpisodeNumber = parsingResult.EndingEpisodeNumber, EpisodeNumber = parsingResult.EpisodeNumber, SeasonNumber = parsingResult.SeasonNumber, SeriesName = parsingResult.SeriesName, diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index d2e324dda..142680f0c 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -101,9 +101,9 @@ namespace Emby.Naming.TV } var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries); - for (int i = 0; i < parts.Length; i++) + foreach (var part in parts) { - if (TryGetSeasonNumberFromPart(parts[i], out int seasonNumber)) + if (TryGetSeasonNumberFromPart(part, out int seasonNumber)) { return (seasonNumber, true); } @@ -139,7 +139,7 @@ namespace Emby.Naming.TV var numericStart = -1; var length = 0; - var hasOpenParenth = false; + var hasOpenParenthesis = false; var isSeasonFolder = true; // Find out where the numbers start, and then keep going until they end @@ -147,7 +147,7 @@ namespace Emby.Naming.TV { if (char.IsNumber(path[i])) { - if (!hasOpenParenth) + if (!hasOpenParenthesis) { if (numericStart == -1) { @@ -167,11 +167,11 @@ namespace Emby.Naming.TV var currentChar = path[i]; if (currentChar == '(') { - hasOpenParenth = true; + hasOpenParenthesis = true; } else if (currentChar == ')') { - hasOpenParenth = false; + hasOpenParenthesis = false; } } diff --git a/Emby.Naming/Video/ExtraRuleType.cs b/Emby.Naming/Video/ExtraRuleType.cs index e89876f4a..98114c7e8 100644 --- a/Emby.Naming/Video/ExtraRuleType.cs +++ b/Emby.Naming/Video/ExtraRuleType.cs @@ -22,6 +22,6 @@ namespace Emby.Naming.Video /// /// Match against the name of the directory containing the file. /// - DirectoryName = 3, + DirectoryName = 3 } } diff --git a/Emby.Naming/Video/FlagParser.cs b/Emby.Naming/Video/FlagParser.cs index a8bd9d5c5..27ca1abf1 100644 --- a/Emby.Naming/Video/FlagParser.cs +++ b/Emby.Naming/Video/FlagParser.cs @@ -20,7 +20,7 @@ namespace Emby.Naming.Video return GetFlags(path, _options.VideoFlagDelimiters); } - public string[] GetFlags(string path, char[] delimeters) + public string[] GetFlags(string path, char[] delimiters) { if (string.IsNullOrEmpty(path)) { @@ -31,7 +31,7 @@ namespace Emby.Naming.Video var file = Path.GetFileName(path); - return file.Split(delimeters, StringSplitOptions.RemoveEmptyEntries); + return file.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); } } } diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs index 3a9eaa1a1..fb881f978 100644 --- a/Emby.Naming/Video/Format3DParser.cs +++ b/Emby.Naming/Video/Format3DParser.cs @@ -18,11 +18,11 @@ namespace Emby.Naming.Video public Format3DResult Parse(string path) { int oldLen = _options.VideoFlagDelimiters.Length; - var delimeters = new char[oldLen + 1]; - _options.VideoFlagDelimiters.CopyTo(delimeters, 0); - delimeters[oldLen] = ' '; + var delimiters = new char[oldLen + 1]; + _options.VideoFlagDelimiters.CopyTo(delimiters, 0); + delimiters[oldLen] = ' '; - return Parse(new FlagParser(_options).GetFlags(path, delimeters)); + return Parse(new FlagParser(_options).GetFlags(path, delimiters)); } internal Format3DResult Parse(string[] videoFlags) @@ -44,7 +44,7 @@ namespace Emby.Naming.Video { var result = new Format3DResult(); - if (string.IsNullOrEmpty(rule.PreceedingToken)) + if (string.IsNullOrEmpty(rule.PrecedingToken)) { result.Format3D = new[] { rule.Token }.FirstOrDefault(i => videoFlags.Contains(i, StringComparer.OrdinalIgnoreCase)); result.Is3D = !string.IsNullOrEmpty(result.Format3D); @@ -63,7 +63,7 @@ namespace Emby.Naming.Video { if (foundPrefix) { - result.Tokens.Add(rule.PreceedingToken); + result.Tokens.Add(rule.PrecedingToken); if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase)) { @@ -74,7 +74,7 @@ namespace Emby.Naming.Video break; } - foundPrefix = string.Equals(flag, rule.PreceedingToken, StringComparison.OrdinalIgnoreCase); + foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase); } result.Is3D = foundPrefix && !string.IsNullOrEmpty(format); diff --git a/Emby.Naming/Video/Format3DRule.cs b/Emby.Naming/Video/Format3DRule.cs index a35f0d9d9..7679164b3 100644 --- a/Emby.Naming/Video/Format3DRule.cs +++ b/Emby.Naming/Video/Format3DRule.cs @@ -4,10 +4,10 @@ namespace Emby.Naming.Video { public class Format3DRule { - public Format3DRule(string token, string? preceedingToken = null) + public Format3DRule(string token, string? precedingToken = null) { Token = token; - PreceedingToken = preceedingToken; + PrecedingToken = precedingToken; } /// @@ -17,9 +17,9 @@ namespace Emby.Naming.Video public string Token { get; set; } /// - /// Gets or sets the preceeding token. + /// Gets or sets the preceding token. /// - /// The preceeding token. - public string? PreceedingToken { get; set; } + /// The preceding token. + public string? PrecedingToken { get; set; } } } diff --git a/Emby.Naming/Video/StubResult.cs b/Emby.Naming/Video/StubResult.cs deleted file mode 100644 index 1b8e99b0d..000000000 --- a/Emby.Naming/Video/StubResult.cs +++ /dev/null @@ -1,19 +0,0 @@ -#pragma warning disable CS1591 - -namespace Emby.Naming.Video -{ - public struct StubResult - { - /// - /// Gets or sets a value indicating whether this instance is stub. - /// - /// true if this instance is stub; otherwise, false. - public bool IsStub { get; set; } - - /// - /// Gets or sets the type of the stub. - /// - /// The type of the stub. - public string StubType { get; set; } - } -} diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 601c6c0b6..190562cfc 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -136,7 +136,7 @@ namespace Emby.Naming.Video } // If there's only one video, accept all trailers - // Be lenient because people use all kinds of mish mash conventions with trailers + // Be lenient because people use all kinds of mishmash conventions with trailers if (list.Count == 1) { var trailers = remainingFiles diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index e121e9eaf..6f85a2408 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2562,12 +2562,12 @@ namespace Emby.Server.Implementations.Library if (!episode.IndexNumberEnd.HasValue || forceRefresh) { - if (episode.IndexNumberEnd != episodeInfo.EndingEpsiodeNumber) + if (episode.IndexNumberEnd != episodeInfo.EndingEpisodeNumber) { changed = true; } - episode.IndexNumberEnd = episodeInfo.EndingEpsiodeNumber; + episode.IndexNumberEnd = episodeInfo.EndingEpisodeNumber; } if (!episode.ParentIndexNumber.HasValue || forceRefresh) diff --git a/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs index 3513050b6..58ea0bec5 100644 --- a/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs @@ -74,7 +74,7 @@ namespace Jellyfin.Naming.Tests.TV var result = new EpisodePathParser(options) .Parse(filename, false); - Assert.Equal(result.EndingEpsiodeNumber, endingEpisodeNumber); + Assert.Equal(result.EndingEpisodeNumber, endingEpisodeNumber); } } } -- cgit v1.2.3 From f39775dc3a813609454655c31dd5c6a1413cc890 Mon Sep 17 00:00:00 2001 From: Stepan Date: Sun, 1 Nov 2020 17:10:48 +0100 Subject: Written test to finish coverage for AudioBookListResolver & AudioBookResolver and corrected some logical erros / unhandled exception --- Emby.Naming/AudioBook/AudioBookListResolver.cs | 20 +++++++----- Emby.Naming/AudioBook/AudioBookResolver.cs | 3 +- .../AudioBook/AudioBookListResolverTests.cs | 38 +++++++++++++++++++++- .../AudioBook/AudioBookResolverTests.cs | 21 ++++++++++-- 4 files changed, 69 insertions(+), 13 deletions(-) (limited to 'tests') diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs index 0d190c172..795065a6c 100644 --- a/Emby.Naming/AudioBook/AudioBookListResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs @@ -1,6 +1,8 @@ #pragma warning disable CS1591 +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Emby.Naming.Common; using Emby.Naming.Video; @@ -21,25 +23,27 @@ namespace Emby.Naming.AudioBook { var audioBookResolver = new AudioBookResolver(_options); + // File with empty fullname will be sorted out here var audiobookFileInfos = files .Select(i => audioBookResolver.Resolve(i.FullName)) .OfType() .ToList(); - // Filter out all extras, otherwise they could cause stacks to not be resolved - // See the unit test TestStackedWithTrailer - var metadata = audiobookFileInfos - .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = false }); - var stackResult = new StackResolver(_options) .ResolveAudioBooks(audiobookFileInfos); foreach (var stack in stackResult) { - var stackFiles = stack.Files.Select(i => audioBookResolver.Resolve(i)).OfType().ToList(); + var stackFiles = stack.Files + .Select(i => audioBookResolver.Resolve(i)) + .OfType() + .ToList(); + stackFiles.Sort(); - // TODO nullable discover if name can be empty - var info = new AudioBookInfo(stack.Name ?? string.Empty) { Files = stackFiles }; + + // stack.Name can be empty when we have file without folder, but always have some files + var name = string.IsNullOrEmpty(stack.Name) ? stack.Files[0] : stack.Name; + var info = new AudioBookInfo(name) { Files = stackFiles }; yield return info; } diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs index c9e8c76cb..542d6fee5 100644 --- a/Emby.Naming/AudioBook/AudioBookResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookResolver.cs @@ -21,7 +21,8 @@ namespace Emby.Naming.AudioBook { if (path.Length == 0) { - throw new ArgumentException("String can't be empty.", nameof(path)); + // Return null to indicate this path will not be used, instead of stopping whole process with exception + return null; } var extension = Path.GetExtension(path); diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs index 1084e20bd..d912b0e90 100644 --- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Emby.Naming.AudioBook; using Emby.Naming.Common; using MediaBrowser.Model.IO; @@ -82,6 +83,41 @@ namespace Jellyfin.Naming.Tests.AudioBook Assert.Single(result); } + [Fact] + public void TestWithoutFolder() + { + var files = new[] + { + "Harry Potter and the Deathly Hallows trailer.mp3" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + })).ToList(); + + Assert.Single(result); + } + + [Fact] + public void TestEmpty() + { + var files = Array.Empty(); + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + })).ToList(); + + Assert.Empty(result); + } + private AudioBookListResolver GetResolver() { return new AudioBookListResolver(_namingOptions); diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs index 2708d80bb..282da2b93 100644 --- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs @@ -35,6 +35,11 @@ namespace Jellyfin.Naming.Tests.AudioBook }; } + public static IEnumerable GetPathsWithInvalidExtensions() + { + yield return new object[] { @"/server/AudioBooks/Larry Potter/Larry Potter.mp9" }; + } + [Theory] [MemberData(nameof(GetResolveFileTestData))] public void Resolve_ValidFileName_Success(AudioBookFileInfo expectedResult) @@ -46,13 +51,23 @@ namespace Jellyfin.Naming.Tests.AudioBook Assert.Equal(result!.Container, expectedResult.Container); Assert.Equal(result!.ChapterNumber, expectedResult.ChapterNumber); Assert.Equal(result!.PartNumber, expectedResult.PartNumber); - Assert.Equal(result!.IsDirectory, expectedResult.IsDirectory); + } + + [Theory] + [MemberData(nameof(GetPathsWithInvalidExtensions))] + public void Resolve_InvalidExtension(string path) + { + var result = new AudioBookResolver(_namingOptions).Resolve(path); + + Assert.Null(result); } [Fact] - public void Resolve_EmptyFileName_ArgumentException() + public void Resolve_EmptyFileName() { - Assert.Throws(() => new AudioBookResolver(_namingOptions).Resolve(string.Empty)); + var result = new AudioBookResolver(_namingOptions).Resolve(string.Empty); + + Assert.Null(result); } } } -- cgit v1.2.3 From 50a2ef9d8aaf92e8b69ced5ea2fcc8fa185fe675 Mon Sep 17 00:00:00 2001 From: Stepan Date: Sun, 1 Nov 2020 17:41:47 +0100 Subject: Simplify Resolve_InvalidExtension Test and created tests for Alternative Versions parsing & Year Extraction for audiobooks --- .../AudioBook/AudioBookListResolverTests.cs | 58 ++++++++++++++++++++++ .../AudioBook/AudioBookResolverTests.cs | 11 ++-- 2 files changed, 61 insertions(+), 8 deletions(-) (limited to 'tests') diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs index d912b0e90..c4b061b4e 100644 --- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs @@ -43,6 +43,64 @@ namespace Jellyfin.Naming.Tests.AudioBook Assert.Equal("Batman", result[1].Name); } + [Fact] + public void TestAlternativeVersions() + { + var files = new[] + { + "Harry Potter and the Deathly Hallows/Chapter 1.ogg", + "Harry Potter and the Deathly Hallows/Chapter 1.mp3", + + "Deadpool.ogg", + "Deadpool.mp3", + + "Batman/Chapter 1.mp3" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + })).ToList(); + + Assert.Equal(3, result[0].Files.Count); + Assert.NotEmpty(result[0].AlternateVersions); + Assert.NotEmpty(result[1].AlternateVersions); + Assert.Empty(result[2].AlternateVersions); + } + + [Fact] + public void TestYearExtraction() + { + var files = new[] + { + "Harry Potter and the Deathly Hallows (2007)/Chapter 1.ogg", + "Harry Potter and the Deathly Hallows (2007)/Chapter 2.mp3", + + "Batman (2020).ogg", + + "Batman(2021).mp3", + + "Batman.mp3" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + })).ToList(); + + Assert.Equal(3, result[0].Files.Count); + Assert.Equal(2007, result[0].Year); + Assert.Equal(2020, result[1].Year); + Assert.Equal(2021, result[2].Year); + Assert.Null(result[2].Year); + } + [Fact] public void TestWithMetadata() { diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs index 282da2b93..5e9d12970 100644 --- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs @@ -35,10 +35,6 @@ namespace Jellyfin.Naming.Tests.AudioBook }; } - public static IEnumerable GetPathsWithInvalidExtensions() - { - yield return new object[] { @"/server/AudioBooks/Larry Potter/Larry Potter.mp9" }; - } [Theory] [MemberData(nameof(GetResolveFileTestData))] @@ -53,11 +49,10 @@ namespace Jellyfin.Naming.Tests.AudioBook Assert.Equal(result!.PartNumber, expectedResult.PartNumber); } - [Theory] - [MemberData(nameof(GetPathsWithInvalidExtensions))] - public void Resolve_InvalidExtension(string path) + [Fact] + public void Resolve_InvalidExtension() { - var result = new AudioBookResolver(_namingOptions).Resolve(path); + var result = new AudioBookResolver(_namingOptions).Resolve(@"/server/AudioBooks/Larry Potter/Larry Potter.mp9"); Assert.Null(result); } -- cgit v1.2.3 From e41e832495765ec057ed5cc86eb8b3019e3f71c8 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 1 Nov 2020 10:52:32 -0700 Subject: Dependency catch up --- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- MediaBrowser.Providers/MediaBrowser.Providers.csproj | 2 +- tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj | 6 +++--- .../Jellyfin.Server.Implementations.Tests.csproj | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) (limited to 'tests') diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 07f08ec72..a64d2e1cd 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -43,7 +43,7 @@ - + diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 24400eae5..9465fe42c 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -19,7 +19,7 @@ - + diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index e7993d2e7..0236f2ac1 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -13,9 +13,9 @@ - - - + + + diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 6e5a90802..05323490e 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -14,8 +14,8 @@ - - + + -- cgit v1.2.3 From 1e7177568887d0f808660454e5eb7ca7ebcd6998 Mon Sep 17 00:00:00 2001 From: Stepan Date: Mon, 2 Nov 2020 20:03:12 +0100 Subject: Add Name and Year parsing for audiobooks --- Emby.Naming/AudioBook/AudioBookInfo.cs | 4 +- Emby.Naming/AudioBook/AudioBookListResolver.cs | 6 +- Emby.Naming/AudioBook/AudioBookNameParser.cs | 59 ++++++++++++++++ Emby.Naming/AudioBook/AudioBookNameParserResult.cs | 12 ++++ Emby.Naming/Common/NamingOptions.cs | 9 +++ Emby.Naming/Video/StackResolver.cs | 20 ++++-- .../AudioBook/AudioBookListResolverTests.cs | 78 +++++++++++++++++----- .../AudioBook/AudioBookResolverTests.cs | 1 - 8 files changed, 163 insertions(+), 26 deletions(-) create mode 100644 Emby.Naming/AudioBook/AudioBookNameParser.cs create mode 100644 Emby.Naming/AudioBook/AudioBookNameParserResult.cs (limited to 'tests') diff --git a/Emby.Naming/AudioBook/AudioBookInfo.cs b/Emby.Naming/AudioBook/AudioBookInfo.cs index fba11ea72..353a0f4a0 100644 --- a/Emby.Naming/AudioBook/AudioBookInfo.cs +++ b/Emby.Naming/AudioBook/AudioBookInfo.cs @@ -11,12 +11,14 @@ namespace Emby.Naming.AudioBook /// Initializes a new instance of the class. /// /// Name of audiobook. - public AudioBookInfo(string name) + /// Year of audiobook release. + public AudioBookInfo(string name, int? year) { Files = new List(); Extras = new List(); AlternateVersions = new List(); Name = name; + Year = year; } /// diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs index 795065a6c..86ba2eeea 100644 --- a/Emby.Naming/AudioBook/AudioBookListResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs @@ -41,9 +41,9 @@ namespace Emby.Naming.AudioBook stackFiles.Sort(); - // stack.Name can be empty when we have file without folder, but always have some files - var name = string.IsNullOrEmpty(stack.Name) ? stack.Files[0] : stack.Name; - var info = new AudioBookInfo(name) { Files = stackFiles }; + var result = new AudioBookNameParser(_options).Parse(stack.Name); + + var info = new AudioBookInfo(result.Name, result.Year) { Files = stackFiles }; yield return info; } diff --git a/Emby.Naming/AudioBook/AudioBookNameParser.cs b/Emby.Naming/AudioBook/AudioBookNameParser.cs new file mode 100644 index 000000000..c48db93b3 --- /dev/null +++ b/Emby.Naming/AudioBook/AudioBookNameParser.cs @@ -0,0 +1,59 @@ +#nullable enable +#pragma warning disable CS1591 + +using System.Globalization; +using System.IO; +using System.Text.RegularExpressions; +using Emby.Naming.Common; + +namespace Emby.Naming.AudioBook +{ + public class AudioBookNameParser + { + private readonly NamingOptions _options; + + public AudioBookNameParser(NamingOptions options) + { + _options = options; + } + + public AudioBookNameParserResult Parse(string name) + { + AudioBookNameParserResult result = default; + foreach (var expression in _options.AudioBookNamesExpressions) + { + var match = new Regex(expression, RegexOptions.IgnoreCase).Match(name); + if (match.Success) + { + if (result.Name == null) + { + var value = match.Groups["name"]; + if (value.Success) + { + result.Name = value.Value; + } + } + + if (!result.Year.HasValue) + { + var value = match.Groups["year"]; + if (value.Success) + { + if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + { + result.Year = intValue; + } + } + } + } + } + + if (string.IsNullOrEmpty(result.Name)) + { + result.Name = name; + } + + return result; + } + } +} diff --git a/Emby.Naming/AudioBook/AudioBookNameParserResult.cs b/Emby.Naming/AudioBook/AudioBookNameParserResult.cs new file mode 100644 index 000000000..b28e259dd --- /dev/null +++ b/Emby.Naming/AudioBook/AudioBookNameParserResult.cs @@ -0,0 +1,12 @@ +#nullable enable +#pragma warning disable CS1591 + +namespace Emby.Naming.AudioBook +{ + public struct AudioBookNameParserResult + { + public string Name { get; set; } + + public int? Year { get; set; } + } +} diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 537de63d5..5bf232451 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -575,6 +575,13 @@ namespace Emby.Naming.Common @"dis(?:c|k)[\s_-]?(?[0-9]+)" }; + AudioBookNamesExpressions = new[] + { + // Detect year usually in brackets after name Batman (2020) + @"^(?.+?)\s*\(\s*(?\d{4})\s*\)\s*$", + @"^\s*(?.+?)\s*$" + }; + var extensions = VideoFileExtensions.ToList(); extensions.AddRange(new[] @@ -658,6 +665,8 @@ namespace Emby.Naming.Common public string[] AudioBookPartsExpressions { get; set; } + public string[] AudioBookNamesExpressions { get; set; } + public StubTypeRule[] StubTypes { get; set; } public char[] VideoFlagDelimiters { get; set; } diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs index ce3152739..e11b4063c 100644 --- a/Emby.Naming/Video/StackResolver.cs +++ b/Emby.Naming/Video/StackResolver.cs @@ -36,13 +36,25 @@ namespace Emby.Naming.Video foreach (var directory in groupedDirectoryFiles) { - var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false }; - foreach (var file in directory) + if (string.IsNullOrEmpty(directory.Key)) { - stack.Files.Add(file.Path); + foreach (var file in directory) + { + var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false }; + stack.Files.Add(file.Path); + yield return stack; + } } + else + { + var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false }; + foreach (var file in directory) + { + stack.Files.Add(file.Path); + } - yield return stack; + yield return stack; + } } } diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs index c4b061b4e..91492d46c 100644 --- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Emby.Naming.AudioBook; using Emby.Naming.Common; @@ -72,33 +73,69 @@ namespace Jellyfin.Naming.Tests.AudioBook } [Fact] - public void TestYearExtraction() + public void TestNameYearExtraction() { - var files = new[] + var data = new[] { - "Harry Potter and the Deathly Hallows (2007)/Chapter 1.ogg", - "Harry Potter and the Deathly Hallows (2007)/Chapter 2.mp3", - - "Batman (2020).ogg", - - "Batman(2021).mp3", - - "Batman.mp3" + new NameYearPath + { + Name = "Harry Potter and the Deathly Hallows", + Path = "Harry Potter and the Deathly Hallows (2007)/Chapter 1.ogg", + Year = 2007 + }, + new NameYearPath + { + Name = "Batman", + Path = "Batman (2020).ogg", + Year = 2020 + }, + new NameYearPath + { + Name = "Batman", + Path = "Batman( 2021 ).mp3", + Year = 2021 + }, + new NameYearPath + { + Name = "Batman(*2021*)", + Path = "Batman(*2021*).mp3", + Year = null + }, + new NameYearPath + { + Name = "Batman", + Path = "Batman.mp3", + Year = null + }, + new NameYearPath + { + Name = "+ Batman .", + Path = " + Batman . .mp3", + Year = null + }, + new NameYearPath + { + Name = " ", + Path = " .mp3", + Year = null + } }; var resolver = GetResolver(); - var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + var result = resolver.Resolve(data.Select(i => new FileSystemMetadata { IsDirectory = false, - FullName = i + FullName = i.Path })).ToList(); - Assert.Equal(3, result[0].Files.Count); - Assert.Equal(2007, result[0].Year); - Assert.Equal(2020, result[1].Year); - Assert.Equal(2021, result[2].Year); - Assert.Null(result[2].Year); + Assert.Equal(data.Length, result.Count); + + for (int i = 0; i < data.Length; i++) + { + Assert.Equal(data[i].Name, result[i].Name); + Assert.Equal(data[i].Year, result[i].Year); + } } [Fact] @@ -180,5 +217,12 @@ namespace Jellyfin.Naming.Tests.AudioBook { return new AudioBookListResolver(_namingOptions); } + + internal struct NameYearPath + { + public string Name; + public string Path; + public int? Year; + } } } diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs index 5e9d12970..b3257ace3 100644 --- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs @@ -35,7 +35,6 @@ namespace Jellyfin.Naming.Tests.AudioBook }; } - [Theory] [MemberData(nameof(GetResolveFileTestData))] public void Resolve_ValidFileName_Success(AudioBookFileInfo expectedResult) -- cgit v1.2.3 From 7b6363b09a3aabba3dcd285fd70d2eda8f1ea889 Mon Sep 17 00:00:00 2001 From: Stepan Date: Mon, 2 Nov 2020 23:07:46 +0100 Subject: Update test for detecting audiobooks extras and alternative files --- .../AudioBook/AudioBookListResolverTests.cs | 59 +++++++++++++++++++--- 1 file changed, 51 insertions(+), 8 deletions(-) (limited to 'tests') diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs index 91492d46c..a24699962 100644 --- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs @@ -20,11 +20,20 @@ namespace Jellyfin.Naming.Tests.AudioBook { "Harry Potter and the Deathly Hallows/Part 1.mp3", "Harry Potter and the Deathly Hallows/Part 2.mp3", - "Harry Potter and the Deathly Hallows/book.nfo", + "Harry Potter and the Deathly Hallows/Extra.mp3", "Batman/Chapter 1.mp3", "Batman/Chapter 2.mp3", "Batman/Chapter 3.mp3", + + "Ready Player One (2020)/Ready Player One.mp3", + "Ready Player One (2020)/extra.mp3", + + "Badman/audiobook.mp3", + "Badman/extra.mp3", + + "Superman (2020)/book.mp3", + "Superman (2020)/extra.mp3" }; var resolver = GetResolver(); @@ -35,13 +44,21 @@ namespace Jellyfin.Naming.Tests.AudioBook FullName = i })).ToList(); - Assert.Equal(2, result[0].Files.Count); - // Assert.Empty(result[0].Extras); FIXME: AudioBookListResolver should resolve extra files properly + Assert.Equal(4, result[0].Files.Count); + Assert.Single(result[0].Extras); Assert.Equal("Harry Potter and the Deathly Hallows", result[0].Name); Assert.Equal(3, result[1].Files.Count); Assert.Empty(result[1].Extras); Assert.Equal("Batman", result[1].Name); + + Assert.Equal(2, result[2].Files.Count); + Assert.Single(result[2].Extras); + Assert.Equal("Badman", result[2].Name); + + Assert.Equal(2, result[3].Files.Count); + Assert.Single(result[3].Extras); + Assert.Equal("Superman", result[3].Name); } [Fact] @@ -52,10 +69,19 @@ namespace Jellyfin.Naming.Tests.AudioBook "Harry Potter and the Deathly Hallows/Chapter 1.ogg", "Harry Potter and the Deathly Hallows/Chapter 1.mp3", - "Deadpool.ogg", + "Aqua-man/book.mp3", + "Deadpool.mp3", + "Deadpool [HQ].mp3", + + "Superman/book.mp3", + "Superman/audiobook.mp3", + "Superman/Superman.mp3", + "Superman/Superman [HQ].mp3", + "Superman/extra.mp3", - "Batman/Chapter 1.mp3" + "Batman/ Chapter 1 .mp3", + "Batman/Chapter 1[loss-less].mp3" }; var resolver = GetResolver(); @@ -66,10 +92,27 @@ namespace Jellyfin.Naming.Tests.AudioBook FullName = i })).ToList(); - Assert.Equal(3, result[0].Files.Count); - Assert.NotEmpty(result[0].AlternateVersions); - Assert.NotEmpty(result[1].AlternateVersions); + Assert.Equal(6, result[0].Files.Count); + // HP - Same name so we don't care which file is alternative + Assert.Single(result[0].AlternateVersions); + // Aqua-man + Assert.Empty(result[1].AlternateVersions); + // DP Assert.Empty(result[2].AlternateVersions); + // DP HQ (directory missing so we do not group deadpools together) + Assert.Empty(result[3].AlternateVersions); + // Superman + // Priority: + // 1. Name + // 2. audiobook + // 3. book + // 4. Names with modifiers + Assert.Equal(3, result[4].AlternateVersions.Count); + Assert.Equal("Superman/audiobook.mp3", result[4].AlternateVersions[0].Path); + Assert.Equal("Superman/book.mp3", result[4].AlternateVersions[1].Path); + Assert.Equal("Superman/Superman [HQ].mp3", result[4].AlternateVersions[2].Path); + // Batman + Assert.Single(result[5].AlternateVersions); } [Fact] -- cgit v1.2.3 From c060ed1a1853eaa28ae2f88f6b301c23cf326725 Mon Sep 17 00:00:00 2001 From: Stepan Date: Tue, 3 Nov 2020 16:24:04 +0100 Subject: Added resolving of alternative files and extras for audibooks. --- Emby.Naming/AudioBook/AudioBookInfo.cs | 11 ++- Emby.Naming/AudioBook/AudioBookListResolver.cs | 93 +++++++++++++++++++++- Emby.Naming/AudioBook/AudioBookNameParser.cs | 1 - Emby.Naming/AudioBook/AudioBookResolver.cs | 2 +- Emby.Naming/Common/NamingOptions.cs | 4 +- .../AudioBook/AudioBookListResolverTests.cs | 48 +++++------ 6 files changed, 126 insertions(+), 33 deletions(-) (limited to 'tests') diff --git a/Emby.Naming/AudioBook/AudioBookInfo.cs b/Emby.Naming/AudioBook/AudioBookInfo.cs index 353a0f4a0..adf403ab6 100644 --- a/Emby.Naming/AudioBook/AudioBookInfo.cs +++ b/Emby.Naming/AudioBook/AudioBookInfo.cs @@ -12,13 +12,16 @@ namespace Emby.Naming.AudioBook /// /// Name of audiobook. /// Year of audiobook release. - public AudioBookInfo(string name, int? year) + /// List of files composing the actual audiobook. + /// List of extra files. + /// Alternative version of files. + public AudioBookInfo(string name, int? year, List? files, List? extras, List? alternateVersions) { - Files = new List(); - Extras = new List(); - AlternateVersions = new List(); Name = name; Year = year; + Files = files ?? new List(); + Extras = extras ?? new List(); + AlternateVersions = alternateVersions ?? new List(); } /// diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs index 86ba2eeea..e8908aa37 100644 --- a/Emby.Naming/AudioBook/AudioBookListResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs @@ -41,12 +41,101 @@ namespace Emby.Naming.AudioBook stackFiles.Sort(); - var result = new AudioBookNameParser(_options).Parse(stack.Name); + var nameParserResult = new AudioBookNameParser(_options).Parse(stack.Name); - var info = new AudioBookInfo(result.Name, result.Year) { Files = stackFiles }; + FindExtraAndAlternativeFiles(ref stackFiles, out var extras, out var alternativeVersions, nameParserResult); + + var info = new AudioBookInfo( + nameParserResult.Name, + nameParserResult.Year, + stackFiles, + extras, + alternativeVersions); yield return info; } } + + private void FindExtraAndAlternativeFiles(ref List stackFiles, out List extras, out List alternativeVersions, AudioBookNameParserResult nameParserResult) + { + extras = new List(); + alternativeVersions = new List(); + + var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null); + var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber }); + + foreach (var group in groupedBy) + { + if (group.Key.ChapterNumber == null && group.Key.PartNumber == null) + { + if (group.Count() > 1 || haveChaptersOrPages) + { + var ex = new List(); + var alt = new List(); + + foreach (var audioFile in group) + { + var name = Path.GetFileNameWithoutExtension(audioFile.Path); + if (name == "audiobook" || + name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) || + name.Contains(nameParserResult.Name.Replace(" ", "."), StringComparison.OrdinalIgnoreCase)) + { + alt.Add(audioFile); + } + else + { + ex.Add(audioFile); + } + } + + if (ex.Count > 0) + { + var extra = ex + .OrderBy(x => x.Container) + .ThenBy(x => x.Path) + .ToList(); + + stackFiles = stackFiles.Except(extra).ToList(); + extras.AddRange(extra); + } + + if (alt.Count > 0) + { + var alternatives = alt + .OrderBy(x => x.Container) + .ThenBy(x => x.Path) + .ToList(); + + var main = FindMainAudioBookFile(alternatives, nameParserResult.Name); + alternatives.Remove(main); + stackFiles = stackFiles.Except(alternatives).ToList(); + alternativeVersions.AddRange(alternatives); + } + } + } + else if (group.Count() > 1) + { + var alternatives = group + .OrderBy(x => x.Container) + .ThenBy(x => x.Path) + .Skip(1) + .ToList(); + + stackFiles = stackFiles.Except(alternatives).ToList(); + alternativeVersions.AddRange(alternatives); + } + } + } + + private AudioBookFileInfo FindMainAudioBookFile(List files, string name) + { + var main = files.Find(x => Path.GetFileNameWithoutExtension(x.Path) == name); + main ??= files.FirstOrDefault(x => Path.GetFileNameWithoutExtension(x.Path) == "audiobook"); + main ??= files.OrderBy(x => x.Container) + .ThenBy(x => x.Path) + .First(); + + return main; + } } } diff --git a/Emby.Naming/AudioBook/AudioBookNameParser.cs b/Emby.Naming/AudioBook/AudioBookNameParser.cs index c48db93b3..7c8616124 100644 --- a/Emby.Naming/AudioBook/AudioBookNameParser.cs +++ b/Emby.Naming/AudioBook/AudioBookNameParser.cs @@ -2,7 +2,6 @@ #pragma warning disable CS1591 using System.Globalization; -using System.IO; using System.Text.RegularExpressions; using Emby.Naming.Common; diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs index 542d6fee5..c7b3b2d2d 100644 --- a/Emby.Naming/AudioBook/AudioBookResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookResolver.cs @@ -19,7 +19,7 @@ namespace Emby.Naming.AudioBook public AudioBookFileInfo? Resolve(string path) { - if (path.Length == 0) + if (path.Length == 0 || Path.GetFileNameWithoutExtension(path).Length == 0) { // Return null to indicate this path will not be used, instead of stopping whole process with exception return null; diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 5bf232451..d2f07817a 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -568,7 +568,7 @@ namespace Emby.Naming.Common // Chapter is often beginning of filename "^(?[0-9]+)", // Part if often ending of filename - "(?[0-9]+)$", + @"(?[0-9]+)$", // Sometimes named as 0001_005 (chapter_part) "(?[0-9]+)_(?[0-9]+)", // Some audiobooks are ripped from cd's, and will be named by disk number. @@ -579,7 +579,7 @@ namespace Emby.Naming.Common { // Detect year usually in brackets after name Batman (2020) @"^(?.+?)\s*\(\s*(?\d{4})\s*\)\s*$", - @"^\s*(?.+?)\s*$" + @"^\s*(?[^ ].*?)\s*$" }; var extensions = VideoFileExtensions.ToList(); diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs index a24699962..e5768b620 100644 --- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs @@ -26,14 +26,16 @@ namespace Jellyfin.Naming.Tests.AudioBook "Batman/Chapter 2.mp3", "Batman/Chapter 3.mp3", - "Ready Player One (2020)/Ready Player One.mp3", - "Ready Player One (2020)/extra.mp3", - "Badman/audiobook.mp3", "Badman/extra.mp3", - "Superman (2020)/book.mp3", - "Superman (2020)/extra.mp3" + "Superman (2020)/Part 1.mp3", + "Superman (2020)/extra.mp3", + + "Ready Player One (2020)/audiobook.mp3", + "Ready Player One (2020)/extra.mp3", + + ".mp3" }; var resolver = GetResolver(); @@ -44,7 +46,9 @@ namespace Jellyfin.Naming.Tests.AudioBook FullName = i })).ToList(); - Assert.Equal(4, result[0].Files.Count); + Assert.Equal(5, result.Count); + + Assert.Equal(2, result[0].Files.Count); Assert.Single(result[0].Extras); Assert.Equal("Harry Potter and the Deathly Hallows", result[0].Name); @@ -52,13 +56,17 @@ namespace Jellyfin.Naming.Tests.AudioBook Assert.Empty(result[1].Extras); Assert.Equal("Batman", result[1].Name); - Assert.Equal(2, result[2].Files.Count); + Assert.Single(result[2].Files); Assert.Single(result[2].Extras); Assert.Equal("Badman", result[2].Name); - Assert.Equal(2, result[3].Files.Count); + Assert.Single(result[3].Files); Assert.Single(result[3].Extras); Assert.Equal("Superman", result[3].Name); + + Assert.Single(result[4].Files); + Assert.Single(result[4].Extras); + Assert.Equal("Ready Player One", result[4].Name); } [Fact] @@ -69,12 +77,9 @@ namespace Jellyfin.Naming.Tests.AudioBook "Harry Potter and the Deathly Hallows/Chapter 1.ogg", "Harry Potter and the Deathly Hallows/Chapter 1.mp3", - "Aqua-man/book.mp3", - "Deadpool.mp3", "Deadpool [HQ].mp3", - "Superman/book.mp3", "Superman/audiobook.mp3", "Superman/Superman.mp3", "Superman/Superman [HQ].mp3", @@ -92,27 +97,24 @@ namespace Jellyfin.Naming.Tests.AudioBook FullName = i })).ToList(); - Assert.Equal(6, result[0].Files.Count); + Assert.Equal(5, result.Count); // HP - Same name so we don't care which file is alternative Assert.Single(result[0].AlternateVersions); - // Aqua-man - Assert.Empty(result[1].AlternateVersions); // DP - Assert.Empty(result[2].AlternateVersions); + Assert.Empty(result[1].AlternateVersions); // DP HQ (directory missing so we do not group deadpools together) - Assert.Empty(result[3].AlternateVersions); + Assert.Empty(result[2].AlternateVersions); // Superman // Priority: // 1. Name // 2. audiobook - // 3. book - // 4. Names with modifiers - Assert.Equal(3, result[4].AlternateVersions.Count); - Assert.Equal("Superman/audiobook.mp3", result[4].AlternateVersions[0].Path); - Assert.Equal("Superman/book.mp3", result[4].AlternateVersions[1].Path); - Assert.Equal("Superman/Superman [HQ].mp3", result[4].AlternateVersions[2].Path); + // 3. Names with modifiers + Assert.Equal(2, result[3].AlternateVersions.Count); + var paths = result[3].AlternateVersions.Select(x => x.Path).ToList(); + Assert.Contains("Superman/audiobook.mp3", paths); + Assert.Contains("Superman/Superman [HQ].mp3", paths); // Batman - Assert.Single(result[5].AlternateVersions); + Assert.Single(result[4].AlternateVersions); } [Fact] -- cgit v1.2.3 From aef1fe62c216612f3f42b2e19496730b56b155ce Mon Sep 17 00:00:00 2001 From: Stepan Date: Tue, 3 Nov 2020 16:25:33 +0100 Subject: Complete test coverage for Emby.Naming.Subtitles --- tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs index d11809de1..515209890 100644 --- a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs +++ b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using Emby.Naming.Common; using Emby.Naming.Subtitles; using Xunit; @@ -26,6 +26,7 @@ namespace Jellyfin.Naming.Tests.Subtitles Assert.Equal(language, result?.Language, true); Assert.Equal(isDefault, result?.IsDefault); Assert.Equal(isForced, result?.IsForced); + Assert.Equal(input, result?.Path); } [Theory] -- cgit v1.2.3 From 3e62557959b1233311ab5d4e8f578f84438580d9 Mon Sep 17 00:00:00 2001 From: Greenback Date: Wed, 4 Nov 2020 20:29:55 +0000 Subject: amended testing unit. --- Jellyfin.Networking/Manager/NetworkManager.cs | 14 ++++++++------ MediaBrowser.Common/Net/NetworkExtensions.cs | 2 +- .../NetworkTesting/UnitTesting.cs | 14 +++++++------- 3 files changed, 16 insertions(+), 14 deletions(-) (limited to 'tests') diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 2ec2a0ba5..289b1dc72 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -19,6 +19,8 @@ namespace Jellyfin.Networking.Manager { /// /// Class to take care of network interface management. + /// + /// Note: The normal collection methods and properties will not work with NetCollection. . /// public class NetworkManager : INetworkManager, IDisposable { @@ -925,8 +927,8 @@ namespace Jellyfin.Networking.Manager // Read and parse bind addresses and exclusions, removing ones that don't exist. _bindAddresses = CreateIPCollection(lanAddresses).Union(_interfaceAddresses); _bindExclusions = CreateIPCollection(lanAddresses, true).Union(_interfaceAddresses); - _logger.LogInformation("Using bind addresses: {0}", _bindAddresses); - _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions); + _logger.LogInformation("Using bind addresses: {0}", _bindAddresses.AsString()); + _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions.AsString()); } private void InitialiseRemote(NetworkConfiguration config) @@ -997,9 +999,9 @@ namespace Jellyfin.Networking.Manager _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i))); } - _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets); - _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets); - _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets).AsNetworks()); + _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets.AsString()); + _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets.AsString()); + _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets).AsNetworks().AsString()); } } @@ -1090,7 +1092,7 @@ namespace Jellyfin.Networking.Manager } _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count); - _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses); + _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses.AsString()); // If for some reason we don't have an interface info, resolve our DNS name. if (_interfaceAddresses.Count == 0) diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index 39499460e..e801de5eb 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -45,7 +45,7 @@ namespace MediaBrowser.Common.Net /// /// The . /// Returns a string representation of this object. - public static string Readable(this NetCollection source) + public static string AsString(this NetCollection source) { var sb = new StringBuilder(); string output = "["; diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs index fe64d1cfc..c96c7defd 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs @@ -71,7 +71,7 @@ namespace NetworkTesting var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; - Assert.True(string.Equals(nm.GetInternalBindAddresses().Readable(), value, StringComparison.Ordinal)); + Assert.True(string.Equals(nm.GetInternalBindAddresses().AsString(), value, StringComparison.Ordinal)); } [Theory] @@ -163,22 +163,22 @@ namespace NetworkTesting // Test included, IP6. NetCollection nc = nm.CreateIPCollection(settings.Split(","), false); - Assert.True(string.Equals(nc.Readable(), result1, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc?.AsString(), result1, System.StringComparison.OrdinalIgnoreCase)); // Text excluded, non IP6. nc = nm.CreateIPCollection(settings.Split(","), true); - Assert.True(string.Equals(nc?.Readable(), result3, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc?.AsString(), result3, System.StringComparison.OrdinalIgnoreCase)); conf.EnableIPV6 = false; nm.UpdateSettings(conf); // Test included, non IP6. nc = nm.CreateIPCollection(settings.Split(","), false); - Assert.True(string.Equals(nc.Readable(), result2, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc?.AsString(), result2, System.StringComparison.OrdinalIgnoreCase)); // Test excluded, including IPv6. nc = nm.CreateIPCollection(settings.Split(","), true); - Assert.True(string.Equals(nc.Readable(), result4, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc?.AsString(), result4, System.StringComparison.OrdinalIgnoreCase)); conf.EnableIPV6 = true; nm.UpdateSettings(conf); @@ -186,7 +186,7 @@ namespace NetworkTesting // Test network addresses of collection. nc = nm.CreateIPCollection(settings.Split(","), false); nc = nc.AsNetworks(); - Assert.True(string.Equals(nc.Readable(), result5, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc?.AsString(), result5, System.StringComparison.OrdinalIgnoreCase)); } [Theory] @@ -205,7 +205,7 @@ namespace NetworkTesting NetCollection nc1 = nm.CreateIPCollection(settings.Split(","), false); NetCollection nc2 = nm.CreateIPCollection(compare.Split(","), false); - Assert.True(nc1.Union(nc2).Readable() == result); + Assert.True(nc1.Union(nc2).AsString() == result); } [Theory] -- cgit v1.2.3 From c96aa0551db48063dba74196816ffe51a839b44d Mon Sep 17 00:00:00 2001 From: Stepan Date: Thu, 5 Nov 2020 13:25:29 +0100 Subject: Added NamingOptions tests --- .../Common/NamingOptionsTest.cs | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs (limited to 'tests') diff --git a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs new file mode 100644 index 000000000..3892d00f6 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs @@ -0,0 +1,36 @@ +using Emby.Naming.Common; +using Xunit; + +namespace Jellyfin.Naming.Tests.Common +{ + public class NamingOptionsTest + { + [Fact] + public void TestNamingOptionsCompile() + { + var options = new NamingOptions(); + + Assert.NotEmpty(options.VideoFileStackingRegexes); + Assert.NotEmpty(options.CleanDateTimeRegexes); + Assert.NotEmpty(options.CleanStringRegexes); + Assert.NotEmpty(options.EpisodeWithoutSeasonRegexes); + Assert.NotEmpty(options.EpisodeMultiPartRegexes); + } + + [Fact] + public void TestNamingOptionsEpisodeExpressions() + { + var exp = new EpisodeExpression(string.Empty); + + Assert.False(exp.IsOptimistic); + exp.IsOptimistic = true; + Assert.True(exp.IsOptimistic); + + Assert.Equal(string.Empty, exp.Expression); + Assert.NotNull(exp.Regex); + exp.Expression = "test"; + Assert.Equal("test", exp.Expression); + Assert.NotNull(exp.Regex); + } + } +} -- cgit v1.2.3 From 57411503673c07f4130b73c578c06ccfecc4c612 Mon Sep 17 00:00:00 2001 From: Stepan Date: Thu, 5 Nov 2020 14:51:27 +0100 Subject: Enable MultiVersion video tests and added support for naming based on tests 11 & 8 --- Emby.Naming/Common/NamingOptions.cs | 2 +- Emby.Naming/Video/VideoListResolver.cs | 6 ++ .../Video/MultiVersionTests.cs | 79 +++++++++++----------- 3 files changed, 47 insertions(+), 40 deletions(-) (limited to 'tests') diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index d2f07817a..3a7bcb7d7 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -133,7 +133,7 @@ namespace Emby.Naming.Common CleanStrings = new[] { - @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)", + @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)", @"(\[.*\])" }; diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 190562cfc..be9b4959a 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -212,9 +212,15 @@ namespace Emby.Naming.Video if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) { + if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName)) + { + testFilename = cleanName.ToString(); + } + testFilename = testFilename.Substring(folderName.Length).Trim(); return string.IsNullOrEmpty(testFilename) || testFilename[0] == '-' + || testFilename[0] == '_' || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty)); } diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index 4198d69ff..9df6904ef 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using Emby.Naming.Common; using Emby.Naming.Video; using MediaBrowser.Model.IO; @@ -11,8 +12,8 @@ namespace Jellyfin.Naming.Tests.Video private readonly NamingOptions _namingOptions = new NamingOptions(); // FIXME - // [Fact] - private void TestMultiEdition1() + [Fact] + public void TestMultiEdition1() { var files = new[] { @@ -35,8 +36,8 @@ namespace Jellyfin.Naming.Tests.Video } // FIXME - // [Fact] - private void TestMultiEdition2() + [Fact] + public void TestMultiEdition2() { var files = new[] { @@ -81,8 +82,8 @@ namespace Jellyfin.Naming.Tests.Video } // FIXME - // [Fact] - private void TestLetterFolders() + [Fact] + public void TestLetterFolders() { var files = new[] { @@ -109,8 +110,8 @@ namespace Jellyfin.Naming.Tests.Video } // FIXME - // [Fact] - private void TestMultiVersionLimit() + [Fact] + public void TestMultiVersionLimit() { var files = new[] { @@ -138,8 +139,8 @@ namespace Jellyfin.Naming.Tests.Video } // FIXME - // [Fact] - private void TestMultiVersionLimit2() + [Fact] + public void TestMultiVersionLimit2() { var files = new[] { @@ -168,8 +169,8 @@ namespace Jellyfin.Naming.Tests.Video } // FIXME - // [Fact] - private void TestMultiVersion3() + [Fact] + public void TestMultiVersion3() { var files = new[] { @@ -194,8 +195,8 @@ namespace Jellyfin.Naming.Tests.Video } // FIXME - // [Fact] - private void TestMultiVersion4() + [Fact] + public void TestMultiVersion4() { // Test for false positive @@ -221,9 +222,8 @@ namespace Jellyfin.Naming.Tests.Video Assert.Empty(result[0].AlternateVersions); } - // FIXME - // [Fact] - private void TestMultiVersion5() + [Fact] + public void TestMultiVersion5() { var files = new[] { @@ -254,8 +254,8 @@ namespace Jellyfin.Naming.Tests.Video } // FIXME - // [Fact] - private void TestMultiVersion6() + [Fact] + public void TestMultiVersion6() { var files = new[] { @@ -285,9 +285,8 @@ namespace Jellyfin.Naming.Tests.Video Assert.True(result[0].AlternateVersions[5].Is3D); } - // FIXME - // [Fact] - private void TestMultiVersion7() + [Fact] + public void TestMultiVersion7() { var files = new[] { @@ -306,12 +305,9 @@ namespace Jellyfin.Naming.Tests.Video Assert.Equal(2, result.Count); } - // FIXME - // [Fact] - private void TestMultiVersion8() + [Fact] + public void TestMultiVersion8() { - // This is not actually supported yet - var files = new[] { @"/movies/Iron Man/Iron Man.mkv", @@ -339,9 +335,8 @@ namespace Jellyfin.Naming.Tests.Video Assert.True(result[0].AlternateVersions[4].Is3D); } - // FIXME - // [Fact] - private void TestMultiVersion9() + [Fact] + public void TestMultiVersion9() { // Test for false positive @@ -367,9 +362,8 @@ namespace Jellyfin.Naming.Tests.Video Assert.Empty(result[0].AlternateVersions); } - // FIXME - // [Fact] - private void TestMultiVersion10() + [Fact] + public void TestMultiVersion10() { var files = new[] { @@ -390,12 +384,9 @@ namespace Jellyfin.Naming.Tests.Video Assert.Single(result[0].AlternateVersions); } - // FIXME - // [Fact] - private void TestMultiVersion11() + [Fact] + public void TestMultiVersion11() { - // Currently not supported but we should probably handle this. - var files = new[] { @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [1080p] Blu-ray.x264.DTS.mkv", @@ -415,6 +406,16 @@ namespace Jellyfin.Naming.Tests.Video Assert.Single(result[0].AlternateVersions); } + [Fact] + public void TestEmptyList() + { + var resolver = GetResolver(); + + var result = resolver.Resolve(new List()).ToList(); + + Assert.Empty(result); + } + private VideoListResolver GetResolver() { return new VideoListResolver(_namingOptions); -- cgit v1.2.3 From 3466dc558186f435eafae9b9d3fd80621fdbd79f Mon Sep 17 00:00:00 2001 From: Stepan Date: Thu, 5 Nov 2020 16:59:15 +0100 Subject: Finish coverage for Emby.Naming.Video --- Emby.Naming/Common/NamingOptions.cs | 6 +- Emby.Naming/Video/ExtraResolver.cs | 7 +- Emby.Naming/Video/FlagParser.cs | 2 +- Emby.Naming/Video/StackResolver.cs | 8 +- Emby.Naming/Video/StubResolver.cs | 2 +- Emby.Naming/Video/VideoFileInfo.cs | 34 ++- Emby.Naming/Video/VideoResolver.cs | 28 ++- tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs | 25 ++- tests/Jellyfin.Naming.Tests/Video/StubTests.cs | 3 +- .../Video/VideoListResolverTests.cs | 29 ++- .../Video/VideoResolverTests.cs | 235 ++++++++++----------- 11 files changed, 230 insertions(+), 149 deletions(-) (limited to 'tests') diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 3a7bcb7d7..a1b95954e 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -120,9 +120,9 @@ namespace Emby.Naming.Common VideoFileStackingExpressions = new[] { - "(.*?)([ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(.*?)(\\.[^.]+)$", - "(.*?)([ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(.*?)(\\.[^.]+)$", - "(.*?)([ ._-]*[a-d])(.*?)(\\.[^.]+)$" + "(?.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$", + "(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$", + "(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$" }; CleanDateTimes = new[] diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs index fc0424faa..bd78299dc 100644 --- a/Emby.Naming/Video/ExtraResolver.cs +++ b/Emby.Naming/Video/ExtraResolver.cs @@ -45,7 +45,8 @@ namespace Emby.Naming.Video } else { - return result; + // Currently unreachable code if new rule.MediaType is desired add if clause with proper tests + throw new InvalidOperationException(); } if (rule.RuleType == ExtraRuleType.Filename) @@ -70,6 +71,9 @@ namespace Emby.Naming.Video } else if (rule.RuleType == ExtraRuleType.Regex) { + // Currently unreachable code if new rule.MediaType is desired add if clause with proper tests + throw new InvalidOperationException(); + /* var filename = Path.GetFileName(path); var regex = new Regex(rule.Token, RegexOptions.IgnoreCase); @@ -79,6 +83,7 @@ namespace Emby.Naming.Video result.ExtraType = rule.ExtraType; result.Rule = rule; } + */ } else if (rule.RuleType == ExtraRuleType.DirectoryName) { diff --git a/Emby.Naming/Video/FlagParser.cs b/Emby.Naming/Video/FlagParser.cs index 27ca1abf1..6015c41a0 100644 --- a/Emby.Naming/Video/FlagParser.cs +++ b/Emby.Naming/Video/FlagParser.cs @@ -24,7 +24,7 @@ namespace Emby.Naming.Video { if (string.IsNullOrEmpty(path)) { - throw new ArgumentNullException(nameof(path)); + return Array.Empty<string>(); } // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _. diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs index e11b4063c..30b812e21 100644 --- a/Emby.Naming/Video/StackResolver.cs +++ b/Emby.Naming/Video/StackResolver.cs @@ -86,10 +86,10 @@ namespace Emby.Naming.Video if (match1.Success) { - var title1 = match1.Groups[1].Value; - var volume1 = match1.Groups[2].Value; - var ignore1 = match1.Groups[3].Value; - var extension1 = match1.Groups[4].Value; + var title1 = match1.Groups["title"].Value; + var volume1 = match1.Groups["volume"].Value; + var ignore1 = match1.Groups["ignore"].Value; + var extension1 = match1.Groups["extension"].Value; var j = i + 1; while (j < list.Count) diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs index f1b5d7bcc..b0eb92e53 100644 --- a/Emby.Naming/Video/StubResolver.cs +++ b/Emby.Naming/Video/StubResolver.cs @@ -14,7 +14,7 @@ namespace Emby.Naming.Video { stubType = default; - if (path == null) + if (string.IsNullOrEmpty(path)) { return false; } diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs index 12bd8c436..7d7411a56 100644 --- a/Emby.Naming/Video/VideoFileInfo.cs +++ b/Emby.Naming/Video/VideoFileInfo.cs @@ -7,6 +7,35 @@ namespace Emby.Naming.Video /// </summary> public class VideoFileInfo { + /// <summary> + /// Initializes a new instance of the <see cref="VideoFileInfo"/> class. + /// </summary> + /// <param name="name">Name of file.</param> + /// <param name="path">Path to the file.</param> + /// <param name="container">Container type.</param> + /// <param name="year">Year of release.</param> + /// <param name="extraType">Extra type.</param> + /// <param name="extraRule">Extra rule.</param> + /// <param name="format3D">Format 3D.</param> + /// <param name="is3D">Is 3D.</param> + /// <param name="isStub">Is Stub.</param> + /// <param name="stubType">Stub type.</param> + /// <param name="isDirectory">Is directory.</param> + public VideoFileInfo(string name, string? path, string? container, int? year = default, ExtraType? extraType = default, ExtraRule? extraRule = default, string? format3D = default, bool is3D = default, bool isStub = default, string? stubType = default, bool isDirectory = default) + { + Path = path; + Container = container; + Name = name; + Year = year; + ExtraType = extraType; + ExtraRule = extraRule; + Format3D = format3D; + Is3D = is3D; + IsStub = isStub; + StubType = stubType; + IsDirectory = isDirectory; + } + /// <summary> /// Gets or sets the path. /// </summary> @@ -23,7 +52,7 @@ namespace Emby.Naming.Video /// Gets or sets the name. /// </summary> /// <value>The name.</value> - public string? Name { get; set; } + public string Name { get; set; } /// <summary> /// Gets or sets the year. @@ -84,8 +113,7 @@ namespace Emby.Naming.Video /// <inheritdoc /> public override string ToString() { - // Makes debugging easier - return Name ?? base.ToString(); + return "VideoFileInfo(Name: '" + Name + "')"; } } } diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs index b9ff90179..fed567d03 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -49,7 +49,7 @@ namespace Emby.Naming.Video { if (string.IsNullOrEmpty(path)) { - throw new ArgumentNullException(nameof(path)); + return null; } bool isStub = false; @@ -99,20 +99,18 @@ namespace Emby.Naming.Video } } - return new VideoFileInfo - { - Path = path, - Container = container, - IsStub = isStub, - Name = name, - Year = year, - StubType = stubType, - Is3D = format3DResult.Is3D, - Format3D = format3DResult.Format3D, - ExtraType = extraResult.ExtraType, - IsDirectory = isDirectory, - ExtraRule = extraResult.Rule - }; + return new VideoFileInfo( + path: path, + container: container, + isStub: isStub, + name: name, + year: year, + stubType: stubType, + is3D: format3DResult.Is3D, + format3D: format3DResult.Format3D, + extraType: extraResult.ExtraType, + isDirectory: isDirectory, + extraRule: extraResult.Rule); } public bool IsVideoFile(string path) diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs index 8dfb8f859..12a9b023b 100644 --- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs @@ -1,7 +1,9 @@ -using Emby.Naming.Common; +using System; +using Emby.Naming.Common; using Emby.Naming.Video; using MediaBrowser.Model.Entities; using Xunit; +using MediaType = Emby.Naming.Common.MediaType; namespace Jellyfin.Naming.Tests.Video { @@ -93,6 +95,27 @@ namespace Jellyfin.Naming.Tests.Video } } + [Fact] + public void TestExtraInfo_InvalidRuleMediaType() + { + var options = new NamingOptions { VideoExtraRules = new[] { new ExtraRule(ExtraType.Unknown, ExtraRuleType.DirectoryName, " ", MediaType.Photo) } }; + Assert.Throws<InvalidOperationException>(() => GetExtraTypeParser(options).GetExtraInfo("sample.jpg")); + } + + [Fact] + public void TestExtraInfo_InvalidRuleType() + { + var options = new NamingOptions { VideoExtraRules = new[] { new ExtraRule(ExtraType.Unknown, ExtraRuleType.Regex, " ", MediaType.Video) } }; + Assert.Throws<InvalidOperationException>(() => GetExtraTypeParser(options).GetExtraInfo("sample.mp4")); + } + + [Fact] + public void TestFlagsParser() + { + var flags = new FlagParser(_videoOptions).GetFlags(string.Empty); + Assert.Empty(flags); + } + private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions) { return new ExtraResolver(videoOptions); diff --git a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs index 30ba94136..6e759c6d6 100644 --- a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs @@ -1,4 +1,4 @@ -using Emby.Naming.Common; +using Emby.Naming.Common; using Emby.Naming.Video; using Xunit; @@ -23,6 +23,7 @@ namespace Jellyfin.Naming.Tests.Video Test("video.hdtv.disc", true, "tv"); Test("video.pdtv.disc", true, "tv"); Test("video.dsr.disc", true, "tv"); + Test(string.Empty, false, "tv"); } [Fact] diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs index 12c4a50fe..215c7e540 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Emby.Naming.Common; using Emby.Naming.Video; using MediaBrowser.Model.IO; @@ -369,6 +369,26 @@ namespace Jellyfin.Naming.Tests.Video Assert.Single(result); } + [Fact] + public void TestFourRooms() + { + var files = new[] + { + @"Four Rooms - A.avi", + @"Four Rooms - A.mp4" + }; + + var resolver = GetResolver(); + + var result = resolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList()).ToList(); + + Assert.Equal(2, result.Count); + } + [Fact] public void TestMovieTrailer() { @@ -431,6 +451,13 @@ namespace Jellyfin.Naming.Tests.Video Assert.Single(result); } + [Fact] + public void TestDirectoryStack() + { + var stack = new FileStack(); + Assert.False(stack.ContainsFile("XX", true)); + } + private VideoListResolver GetResolver() { return new VideoListResolver(_namingOptions); diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs index 99828b2eb..3bdafa84d 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Linq; using Emby.Naming.Common; using Emby.Naming.Video; using MediaBrowser.Model.Entities; @@ -14,165 +15,135 @@ namespace Jellyfin.Naming.Tests.Video { yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/7 Psychos.mkv/7 Psychos.mkv", - Container = "mkv", - Name = "7 Psychos" - } + new VideoFileInfo( + path: @"/server/Movies/7 Psychos.mkv/7 Psychos.mkv", + container: "mkv", + name: "7 Psychos") }; yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv", - Container = "mkv", - Name = "3 days to kill", - Year = 2005 - } + new VideoFileInfo( + path: @"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv", + container: "mkv", + name: "3 days to kill", + year: 2005) }; yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/American Psycho/American.Psycho.mkv", - Container = "mkv", - Name = "American.Psycho", - } + new VideoFileInfo( + path: @"/server/Movies/American Psycho/American.Psycho.mkv", + container: "mkv", + name: "American.Psycho") }; yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv", - Container = "mkv", - Name = "brave", - Year = 2006, - Is3D = true, - Format3D = "sbs", - } + new VideoFileInfo( + path: @"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv", + container: "mkv", + name: "brave", + year: 2006, + is3D: true, + format3D: "sbs") }; yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv", - Container = "mkv", - Name = "300", - Year = 2006 - } + new VideoFileInfo( + path: @"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv", + container: "mkv", + name: "300", + year: 2006) }; yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv", - Container = "mkv", - Name = "300", - Year = 2006, - Is3D = true, - Format3D = "sbs", - } + new VideoFileInfo( + path: @"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv", + container: "mkv", + name: "300", + year: 2006, + is3D: true, + format3D: "sbs") }; yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc", - Container = "disc", - Name = "brave", - Year = 2006, - IsStub = true, - StubType = "bluray", - } + new VideoFileInfo( + path: @"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc", + container: "disc", + name: "brave", + year: 2006, + isStub: true, + stubType: "bluray") }; yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc", - Container = "disc", - Name = "300", - Year = 2006, - IsStub = true, - StubType = "bluray", - } + new VideoFileInfo( + path: @"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc", + container: "disc", + name: "300", + year: 2006, + isStub: true, + stubType: "bluray") }; yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/Brave (2007)/Brave (2006).bluray.disc", - Container = "disc", - Name = "Brave", - Year = 2006, - IsStub = true, - StubType = "bluray", - } + new VideoFileInfo( + path: @"/server/Movies/Brave (2007)/Brave (2006).bluray.disc", + container: "disc", + name: "Brave", + year: 2006, + isStub: true, + stubType: "bluray") }; yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/300 (2007)/300 (2006).bluray.disc", - Container = "disc", - Name = "300", - Year = 2006, - IsStub = true, - StubType = "bluray", - } + new VideoFileInfo( + path: @"/server/Movies/300 (2007)/300 (2006).bluray.disc", + container: "disc", + name: "300", + year: 2006, + isStub: true, + stubType: "bluray") }; yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/300 (2007)/300 (2006)-trailer.mkv", - Container = "mkv", - Name = "300", - Year = 2006, - ExtraType = ExtraType.Trailer, - } + new VideoFileInfo( + path: @"/server/Movies/300 (2007)/300 (2006)-trailer.mkv", + container: "mkv", + name: "300", + year: 2006, + extraType: ExtraType.Trailer) }; yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv", - Container = "mkv", - Name = "Brave", - Year = 2006, - ExtraType = ExtraType.Trailer, - } + new VideoFileInfo( + path: @"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv", + container: "mkv", + name: "Brave", + year: 2006, + extraType: ExtraType.Trailer) }; yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/300 (2007)/300 (2006).mkv", - Container = "mkv", - Name = "300", - Year = 2006 - } + new VideoFileInfo( + path: @"/server/Movies/300 (2007)/300 (2006).mkv", + container: "mkv", + name: "300", + year: 2006) }; yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv", - Container = "mkv", - Name = "Bad Boys", - Year = 1995, - } + new VideoFileInfo( + path: @"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv", + container: "mkv", + name: "Bad Boys", + year: 1995) }; yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/Brave (2007)/Brave (2006).mkv", - Container = "mkv", - Name = "Brave", - Year = 2006, - } + new VideoFileInfo( + path: @"/server/Movies/Brave (2007)/Brave (2006).mkv", + container: "mkv", + name: "Brave", + year: 2006) }; } @@ -194,6 +165,34 @@ namespace Jellyfin.Naming.Tests.Video Assert.Equal(result?.StubType, expectedResult.StubType); Assert.Equal(result?.IsDirectory, expectedResult.IsDirectory); Assert.Equal(result?.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension); + Assert.Equal(result?.ToString(), expectedResult.ToString()); + } + + [Fact] + public void ResolveFile_EmptyPath() + { + var result = new VideoResolver(_namingOptions).ResolveFile(string.Empty); + + Assert.Null(result); + } + + [Fact] + public void ResolveDirectoryTest() + { + var paths = new[] + { + @"/Server/Iron Man", + @"Batman", + string.Empty + }; + + var resolver = new VideoResolver(_namingOptions); + var results = paths.Select(path => resolver.ResolveDirectory(path)).ToList(); + + Assert.Equal(3, results.Count); + Assert.NotNull(results[0]); + Assert.NotNull(results[1]); + Assert.Null(results[2]); } } } -- cgit v1.2.3 From f22e0800e272b9f0aac198243a44196421af5dff Mon Sep 17 00:00:00 2001 From: Stepan <ste.martinek+git@gmail.com> Date: Sat, 7 Nov 2020 11:02:12 +0100 Subject: Episode parsing coverage --- Emby.Naming/Common/NamingOptions.cs | 2 +- Emby.Naming/TV/EpisodePathParser.cs | 7 +- .../TV/EpisodePathParserTest.cs | 99 ++++++++++++++++------ 3 files changed, 77 insertions(+), 31 deletions(-) (limited to 'tests') diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 4325cf236..471491d22 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -282,7 +282,7 @@ namespace Emby.Naming.Common // /server/anything_102.mp4 // /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv // /server/anything_1996.11.14.mp4 - new EpisodeExpression(@"[\\\\/\\._ -](?<seriesname>(?![0-9]+[0-9][0-9])([^\\\/])*)[\\\\/\\._ -](?<seasonnumber>[0-9]+)(?<epnumber>[0-9][0-9](?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([\\._ -][^\\\\/]*)$") + new EpisodeExpression(@"[\\/._ -](?<seriesname>(?![0-9]+[0-9][0-9])([^\\\/_])*)[\\\/._ -](?<seasonnumber>[0-9]+)(?<epnumber>[0-9][0-9](?:(?:[a-i]|\.[1-9])(?![0-9]))?)([._ -][^\\\/]*)$") { IsOptimistic = true, IsNamed = true, diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs index 866d8adc0..d9cc8172b 100644 --- a/Emby.Naming/TV/EpisodePathParser.cs +++ b/Emby.Naming/TV/EpisodePathParser.cs @@ -186,7 +186,7 @@ namespace Emby.Naming.TV private void FillAdditional(string path, EpisodePathParserResult info) { - var expressions = _options.MultipleEpisodeExpressions.ToList(); + var expressions = _options.MultipleEpisodeExpressions.Where(i => i.IsNamed).ToList(); if (string.IsNullOrEmpty(info.SeriesName)) { @@ -200,11 +200,6 @@ namespace Emby.Naming.TV { foreach (var i in expressions) { - if (!i.IsNamed) - { - continue; - } - var result = Parse(path, i); if (!result.Success) diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs index 03aeb7f76..57f382b38 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs @@ -1,4 +1,4 @@ -using Emby.Naming.Common; +using Emby.Naming.Common; using Emby.Naming.TV; using Xunit; @@ -7,43 +7,94 @@ namespace Jellyfin.Naming.Tests.TV public class EpisodePathParserTest { [Theory] - [InlineData("/media/Foo/Foo-S01E01", "Foo", 1, 1)] - [InlineData("/media/Foo - S04E011", "Foo", 4, 11)] - [InlineData("/media/Foo/Foo s01x01", "Foo", 1, 1)] - [InlineData("/media/Foo (2019)/Season 4/Foo (2019).S04E03", "Foo (2019)", 4, 3)] - [InlineData("D:\\media\\Foo\\Foo-S01E01", "Foo", 1, 1)] - [InlineData("D:\\media\\Foo - S04E011", "Foo", 4, 11)] - [InlineData("D:\\media\\Foo\\Foo s01x01", "Foo", 1, 1)] - [InlineData("D:\\media\\Foo (2019)\\Season 4\\Foo (2019).S04E03", "Foo (2019)", 4, 3)] - [InlineData("/Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", "Elementary", 2, 3)] - [InlineData("/Season 1/seriesname S01E02 blah.avi", "seriesname", 1, 2)] - [InlineData("/Running Man/Running Man S2017E368.mkv", "Running Man", 2017, 368)] - [InlineData("/Season 1/seriesname 01x02 blah.avi", "seriesname", 1, 2)] - [InlineData("/Season 25/The Simpsons.S25E09.Steal this episode.mp4", "The Simpsons", 25, 9)] - [InlineData("/Season 1/seriesname S01x02 blah.avi", "seriesname", 1, 2)] - [InlineData("/Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", "Elementary", 2, 3)] - [InlineData("/Season 1/seriesname S01xE02 blah.avi", "seriesname", 1, 2)] - [InlineData("/Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", "Elementary", 2, 3)] - [InlineData("/Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", "Elementary", 2, 3)] - [InlineData("/Season 02/Elementary - 02x03-E15 - Ep Name.mp4", "Elementary", 2, 3)] - [InlineData("/Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", "Elementary", 1, 23)] - [InlineData("/The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", "The Wonder Years", 4, 7)] + [InlineData("/media/Foo/Foo-S01E01", true, "Foo", 1, 1)] + [InlineData("/media/Foo - S04E011", true, "Foo", 4, 11)] + [InlineData("/media/Foo/Foo s01x01", true, "Foo", 1, 1)] + [InlineData("/media/Foo (2019)/Season 4/Foo (2019).S04E03", true, "Foo (2019)", 4, 3)] + [InlineData("D:\\media\\Foo\\Foo-S01E01", true, "Foo", 1, 1)] + [InlineData("D:\\media\\Foo - S04E011", true, "Foo", 4, 11)] + [InlineData("D:\\media\\Foo\\Foo s01x01", true, "Foo", 1, 1)] + [InlineData("D:\\media\\Foo (2019)\\Season 4\\Foo (2019).S04E03", true, "Foo (2019)", 4, 3)] + [InlineData("/Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", false, "Elementary", 2, 3)] + [InlineData("/Season 1/seriesname S01E02 blah.avi", false, "seriesname", 1, 2)] + [InlineData("/Running Man/Running Man S2017E368.mkv", false, "Running Man", 2017, 368)] + [InlineData("/Season 1/seriesname 01x02 blah.avi", false, "seriesname", 1, 2)] + [InlineData("/Season 25/The Simpsons.S25E09.Steal this episode.mp4", false, "The Simpsons", 25, 9)] + [InlineData("/Season 1/seriesname S01x02 blah.avi", false, "seriesname", 1, 2)] + [InlineData("/Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", false, "Elementary", 2, 3)] + [InlineData("/Season 1/seriesname S01xE02 blah.avi", false, "seriesname", 1, 2)] + [InlineData("/Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", false, "Elementary", 2, 3)] + [InlineData("/Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", false, "Elementary", 2, 3)] + [InlineData("/Season 02/Elementary - 02x03-E15 - Ep Name.mp4", false, "Elementary", 2, 3)] + [InlineData("/Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", false, "Elementary", 1, 23)] + [InlineData("/The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", false, "The Wonder Years", 4, 7)] // TODO: [InlineData("/Castle Rock 2x01 Que el rio siga su curso [WEB-DL HULU 1080p h264 Dual DD5.1 Subs].mkv", "Castle Rock", 2, 1)] // TODO: [InlineData("/After Life 1x06 Episodio 6 [WEB-DL NF 1080p h264 Dual DD 5.1 Sub].mkv", "After Life", 1, 6)] // TODO: [InlineData("/Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", "Uchuu Senkan Yamoto 2199", 4, 3)] // TODO: [InlineData("The Daily Show/The Daily Show 25x22 - [WEBDL-720p][AAC 2.0][x264] Noah Baumbach-TBS.mkv", "The Daily Show", 25, 22)] // TODO: [InlineData("Watchmen (2019)/Watchmen 1x03 [WEBDL-720p][EAC3 5.1][h264][-TBS] - She Was Killed by Space Junk.mkv", "Watchmen (2019)", 1, 3)] // TODO: [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", "The Legend of Condor Heroes 2017", 1, 7)] - public void ParseEpisodesCorrectly(string path, string name, int season, int episode) + public void ParseEpisodesCorrectly(string path, bool isDirectory, string name, int season, int episode) { NamingOptions o = new NamingOptions(); EpisodePathParser p = new EpisodePathParser(o); - var res = p.Parse(path, false); + var res = p.Parse(path, isDirectory); Assert.True(res.Success); Assert.Equal(name, res.SeriesName); Assert.Equal(season, res.SeasonNumber); Assert.Equal(episode, res.EpisodeNumber); } + + [Theory] + [InlineData("/test/01-03.avi", true, true)] + public void EpisodePathParserTest_DifferentExpressionsParameters(string path, bool? isNamed, bool? isOptimistic) + { + NamingOptions o = new NamingOptions(); + EpisodePathParser p = new EpisodePathParser(o); + var res = p.Parse(path, false, isNamed, isOptimistic); + + Assert.True(res.Success); + } + + [Fact] + public void EpisodePathParserTest_FalsePositivePixelRate() + { + NamingOptions o = new NamingOptions(); + EpisodePathParser p = new EpisodePathParser(o); + var res = p.Parse("Series Special (1920x1080).mkv", false); + + Assert.False(res.Success); + } + + [Fact] + public void EpisodeResolverTest_WrongExtension() + { + var res = new EpisodeResolver(new NamingOptions()).Resolve("test.mp3", false); + Assert.Null(res); + } + + [Fact] + public void EpisodeResolverTest_WrongExtensionStub() + { + var res = new EpisodeResolver(new NamingOptions()).Resolve("dvd.disc", false); + Assert.NotNull(res); + Assert.True(res!.IsStub); + } + + [Fact] + public void EpisodePathParserTest_EmptyDateParsers() + { + NamingOptions o = new NamingOptions() + { + EpisodeExpressions = new[] { new EpisodeExpression("(([0-9]{4})-([0-9]{2})-([0-9]{2}) [0-9]{2}:[0-9]{2}:[0-9]{2})", true) } + }; + o.Compile(); + + EpisodePathParser p = new EpisodePathParser(o); + var res = p.Parse("ABC_2019_10_21 11:00:00", false); + + Assert.True(res.Success); + } } } -- cgit v1.2.3 From 3d1076ae42433314835f4277ff42cd5ef1e6c016 Mon Sep 17 00:00:00 2001 From: Stepan <ste.martinek+git@gmail.com> Date: Sat, 7 Nov 2020 12:30:22 +0100 Subject: Rest of tests for Emby.Naming code coverage --- .../Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs | 38 ++++++++++++---------- .../Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs | 27 +++++++++++++-- 2 files changed, 45 insertions(+), 20 deletions(-) (limited to 'tests') diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs index 078f940b2..b7b5b54ec 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs @@ -1,4 +1,4 @@ -using Emby.Naming.TV; +using Emby.Naming.TV; using Xunit; namespace Jellyfin.Naming.Tests.TV @@ -6,26 +6,30 @@ namespace Jellyfin.Naming.Tests.TV public class SeasonFolderTests { [Theory] - [InlineData(@"/Drive/Season 1", 1)] - [InlineData(@"/Drive/Season 2", 2)] - [InlineData(@"/Drive/Season 02", 2)] - [InlineData(@"/Drive/Seinfeld/S02", 2)] - [InlineData(@"/Drive/Seinfeld/2", 2)] - [InlineData(@"/Drive/Season 2009", 2009)] - [InlineData(@"/Drive/Season1", 1)] - [InlineData(@"The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4)] - [InlineData(@"/Drive/Season 7 (2016)", 7)] - [InlineData(@"/Drive/Staffel 7 (2016)", 7)] - [InlineData(@"/Drive/Stagione 7 (2016)", 7)] - [InlineData(@"/Drive/Season (8)", null)] - [InlineData(@"/Drive/3.Staffel", 3)] - [InlineData(@"/Drive/s06e05", null)] - [InlineData(@"/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null)] - public void GetSeasonNumberFromPathTest(string path, int? seasonNumber) + [InlineData(@"/Drive/Season 1", 1, true)] + [InlineData(@"/Drive/Season 2", 2, true)] + [InlineData(@"/Drive/Season 02", 2, true)] + [InlineData(@"/Drive/Seinfeld/S02", 2, true)] + [InlineData(@"/Drive/Seinfeld/2", 2, true)] + [InlineData(@"/Drive/Season 2009", 2009, true)] + [InlineData(@"/Drive/Season1", 1, true)] + [InlineData(@"The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)] + [InlineData(@"/Drive/Season 7 (2016)", 7, false)] + [InlineData(@"/Drive/Staffel 7 (2016)", 7, false)] + [InlineData(@"/Drive/Stagione 7 (2016)", 7, false)] + [InlineData(@"/Drive/Season (8)", null, false)] + [InlineData(@"/Drive/3.Staffel", 3, false)] + [InlineData(@"/Drive/s06e05", null, false)] + [InlineData(@"/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)] + [InlineData(@"/Drive/extras", 0, true)] + [InlineData(@"/Drive/specials", 0, true)] + public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory) { var result = SeasonPathParser.Parse(path, true, true); + Assert.Equal(result.SeasonNumber != null, result.Success); Assert.Equal(result.SeasonNumber, seasonNumber); + Assert.Equal(isSeasonDirectory, result.IsSeasonFolder); } } } diff --git a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs index 40b41b9f3..89579c037 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs @@ -1,4 +1,5 @@ -using Emby.Naming.Common; +using System.IO; +using Emby.Naming.Common; using Emby.Naming.TV; using Xunit; @@ -15,7 +16,6 @@ namespace Jellyfin.Naming.Tests.TV [InlineData("/server/The Walking Dead 4x01.mp4", "The Walking Dead", 4, 1)] [InlineData("/server/the_simpsons-s02e01_18536.mp4", "the_simpsons", 2, 1)] [InlineData("/server/Temp/S01E02 foo.mp4", "", 1, 2)] - [InlineData("Series/4-12 - The Woman.mp4", "", 4, 12)] [InlineData("Series/4x12 - The Woman.mp4", "", 4, 12)] [InlineData("Series/LA X, Pt. 1_s06e32.mp4", "LA X, Pt. 1", 6, 32)] [InlineData("[Baz-Bar]Foo - [1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)] @@ -24,16 +24,37 @@ namespace Jellyfin.Naming.Tests.TV // TODO: [InlineData("[Baz-Bar]Foo - 01 - 12[1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)] // TODO: [InlineData("E:\\Anime\\Yahari Ore no Seishun Love Comedy wa Machigatteiru\\Yahari Ore no Seishun Love Comedy wa Machigatteiru. Zoku\\Oregairu Zoku 11 - Hayama Hayato Always Renconds to Everyone's Expectations..mkv", "Yahari Ore no Seishun Love Comedy wa Machigatteiru", null, 11)] // TODO: [InlineData(@"/Library/Series/The Grand Tour (2016)/Season 1/S01E01 The Holy Trinity.mkv", "The Grand Tour", 1, 1)] - public void Test(string path, string seriesName, int? seasonNumber, int? episodeNumber) + public void TestSimple(string path, string seriesName, int? seasonNumber, int? episodeNumber) + { + Test(path, seriesName, seasonNumber, episodeNumber, null); + } + + [Theory] + [InlineData("Series/4-12 - The Woman.mp4", "", 4, 12, 12)] + public void TestWithPossibleEpisodeEnd(string path, string seriesName, int? seasonNumber, int? episodeNumber, int? episodeEndNumber) + { + Test(path, seriesName, seasonNumber, episodeNumber, episodeEndNumber); + } + + private void Test(string path, string seriesName, int? seasonNumber, int? episodeNumber, int? episodeEndNumber) { var options = new NamingOptions(); var result = new EpisodeResolver(options) .Resolve(path, false); + Assert.NotNull(result); Assert.Equal(seasonNumber, result?.SeasonNumber); Assert.Equal(episodeNumber, result?.EpisodeNumber); Assert.Equal(seriesName, result?.SeriesName, true); + Assert.Equal(path, result?.Path); + Assert.Equal(Path.GetExtension(path).Substring(1), result?.Container); + Assert.Null(result?.Format3D); + Assert.False(result?.Is3D); + Assert.False(result?.IsStub); + Assert.Null(result?.StubType); + Assert.Equal(episodeEndNumber, result?.EndingEpisodeNumber); + Assert.False(result?.IsByDate); } } } -- cgit v1.2.3 From e78c63c4dc819867acddc5a15a7d7c02f7aa9b30 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Sun, 8 Nov 2020 16:10:33 +0100 Subject: Remove OriginalAuthenticationInfo and add IsAuthenticated property --- .../HttpServer/Security/AuthService.cs | 5 +++-- .../HttpServer/Security/AuthorizationContext.cs | 25 +++++++++++----------- Jellyfin.Api/Auth/CustomAuthenticationHandler.cs | 2 +- MediaBrowser.Controller/Net/AuthorizationInfo.cs | 5 +++++ .../Auth/CustomAuthenticationHandlerTests.cs | 5 +++-- 5 files changed, 24 insertions(+), 18 deletions(-) (limited to 'tests') diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 7d53e886f..df7a034e8 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Http; @@ -19,9 +20,9 @@ namespace Emby.Server.Implementations.HttpServer.Security public AuthorizationInfo Authenticate(HttpRequest request) { var auth = _authorizationContext.GetAuthorizationInfo(request); - if (auth == null) + if (!auth.IsAuthenticated) { - throw new SecurityException("Unauthenticated request."); + throw new AuthenticationException("Invalid token."); } if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false) diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index de7e7bf3b..e733c9092 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -36,8 +36,7 @@ namespace Emby.Server.Implementations.HttpServer.Security public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext) { var auth = GetAuthorizationDictionary(requestContext); - var (authInfo, _) = - GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query); + var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query); return authInfo; } @@ -49,19 +48,13 @@ namespace Emby.Server.Implementations.HttpServer.Security private AuthorizationInfo GetAuthorization(HttpContext httpReq) { var auth = GetAuthorizationDictionary(httpReq); - var (authInfo, originalAuthInfo) = - GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query); - - if (originalAuthInfo != null) - { - httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo; - } + var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query); httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo; return authInfo; } - private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary( + private AuthorizationInfo GetAuthorizationInfoFromDictionary( in Dictionary<string, string> auth, in IHeaderDictionary headers, in IQueryCollection queryString) @@ -108,13 +101,14 @@ namespace Emby.Server.Implementations.HttpServer.Security Device = device, DeviceId = deviceId, Version = version, - Token = token + Token = token, + IsAuthenticated = false }; if (string.IsNullOrWhiteSpace(token)) { // Request doesn't contain a token. - return (null, null); + return authInfo; } var result = _authRepo.Get(new AuthenticationInfoQuery @@ -122,6 +116,11 @@ namespace Emby.Server.Implementations.HttpServer.Security AccessToken = token }); + if (result.Items.Count > 0) + { + authInfo.IsAuthenticated = true; + } + var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null; if (originalAuthenticationInfo != null) @@ -197,7 +196,7 @@ namespace Emby.Server.Implementations.HttpServer.Security } } - return (authInfo, originalAuthenticationInfo); + return authInfo; } /// <summary> diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index e8cc38907..27a1f61be 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -1,10 +1,10 @@ using System.Globalization; -using System.Security.Authentication; using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs index 5c642edff..0194c596f 100644 --- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs +++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs @@ -53,5 +53,10 @@ namespace MediaBrowser.Controller.Net /// Gets or sets the user making the request. /// </summary> public User User { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the token is authenticated. + /// </summary> + public bool IsAuthenticated { get; set; } } } diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs index 33534abd2..a46d94457 100644 --- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Auth; using Jellyfin.Api.Constants; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; @@ -68,14 +69,14 @@ namespace Jellyfin.Api.Tests.Auth } [Fact] - public async Task HandleAuthenticateAsyncShouldFailOnSecurityException() + public async Task HandleAuthenticateAsyncShouldFailOnAuthenticationException() { var errorMessage = _fixture.Create<string>(); _jellyfinAuthServiceMock.Setup( a => a.Authenticate( It.IsAny<HttpRequest>())) - .Throws(new SecurityException(errorMessage)); + .Throws(new AuthenticationException(errorMessage)); var authenticateResult = await _sut.AuthenticateAsync(); -- cgit v1.2.3 From e15891df5109388254119e877c2d9e6e7114ffbb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2020 12:00:39 +0000 Subject: Bump Microsoft.NET.Test.Sdk from 16.7.1 to 16.8.0 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.7.1 to 16.8.0. - [Release notes](https://github.com/microsoft/vstest/releases) - [Commits](https://github.com/microsoft/vstest/compare/v16.7.1...v16.8.0) Signed-off-by: dependabot[bot] <support@github.com> --- tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj | 2 +- tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj | 2 +- tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj | 2 +- tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj | 2 +- tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj | 2 +- .../Jellyfin.Server.Implementations.Tests.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) (limited to 'tests') diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 0236f2ac1..ce61f5684 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -18,7 +18,7 @@ <PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.9" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index e3f87d29b..67dc8286a 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -13,7 +13,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index 5de02a29b..30e84842a 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -13,7 +13,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index 3ac60819b..4fd0d5342 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -19,7 +19,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index 37d0a9929..0d240fd65 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -13,7 +13,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 05323490e..db1f2956e 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -16,7 +16,7 @@ <ItemGroup> <PackageReference Include="AutoFixture" Version="4.14.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> <PackageReference Include="Moq" Version="4.14.7" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> -- cgit v1.2.3 From 83629ab6f24ee1a8991dae2b8a55d24c93832ad5 Mon Sep 17 00:00:00 2001 From: crobibero <cody@robibe.ro> Date: Tue, 10 Nov 2020 09:52:34 -0700 Subject: Update packages to net5 --- DvdLib/DvdLib.csproj | 2 +- Emby.Dlna/Emby.Dlna.csproj | 2 +- Emby.Drawing/Emby.Drawing.csproj | 2 +- Emby.Naming/Emby.Naming.csproj | 2 +- Emby.Notifications/Emby.Notifications.csproj | 2 +- Emby.Photos/Emby.Photos.csproj | 2 +- .../Emby.Server.Implementations.csproj | 12 ++++++------ Jellyfin.Api/Jellyfin.Api.csproj | 6 +++--- Jellyfin.Data/Jellyfin.Data.csproj | 6 +++--- Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj | 2 +- .../Jellyfin.Server.Implementations.csproj | 8 ++++---- Jellyfin.Server/Jellyfin.Server.csproj | 10 +++++----- MediaBrowser.Common/MediaBrowser.Common.csproj | 6 +++--- MediaBrowser.Controller/MediaBrowser.Controller.csproj | 6 +++--- MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj | 2 +- MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj | 4 ++-- MediaBrowser.Model/Extensions/StringHelper.cs | 6 ------ MediaBrowser.Model/MediaBrowser.Model.csproj | 8 ++++---- MediaBrowser.Providers/MediaBrowser.Providers.csproj | 8 ++++---- MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj | 2 +- RSSDP/RSSDP.csproj | 2 +- tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj | 6 +++--- tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj | 2 +- .../Jellyfin.Controller.Tests.csproj | 2 +- .../Jellyfin.MediaEncoding.Tests.csproj | 2 +- tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj | 2 +- .../Jellyfin.Server.Implementations.Tests.csproj | 2 +- 27 files changed, 55 insertions(+), 61 deletions(-) (limited to 'tests') diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj index 64d041cb0..7bbd9acf8 100644 --- a/DvdLib/DvdLib.csproj +++ b/DvdLib/DvdLib.csproj @@ -10,7 +10,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index 6ed49944c..bd30cc1e1 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -17,7 +17,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj index 092f8580a..7d479a5c6 100644 --- a/Emby.Drawing/Emby.Drawing.csproj +++ b/Emby.Drawing/Emby.Drawing.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 6857f9952..80800840e 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj index 1d430a5e5..16ee918c4 100644 --- a/Emby.Notifications/Emby.Notifications.csproj +++ b/Emby.Notifications/Emby.Notifications.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index dbe01257f..62e33e6c4 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -19,7 +19,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index c762aa0b8..bcddea281 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -32,13 +32,13 @@ <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" /> - <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.9" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" /> - <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.9" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" /> <PackageReference Include="Mono.Nat" Version="3.0.0" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" /> - <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" /> + <PackageReference Include="ServiceStack.Text.Core" Version="5.10.0" /> <PackageReference Include="sharpcompress" Version="0.26.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" /> <PackageReference Include="DotNet.Glob" Version="3.1.0" /> @@ -49,7 +49,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors> diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index da0852ceb..2836f7b0a 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> @@ -14,9 +14,9 @@ <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" /> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.9" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" /> - <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.9" /> + <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" /> <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.6.3" /> </ItemGroup> diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 5038988f9..9ae129d07 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> @@ -41,8 +41,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.9" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.9" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0" /> </ItemGroup> <ItemGroup> diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index c11ac5fb3..466a12e67 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index c52be3b8a..e663798da 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netcoreapp3.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> @@ -24,12 +24,12 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="System.Linq.Async" Version="4.1.1" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.9"> + <PackageReference Include="System.Linq.Async" Version="5.0.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.9"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 3558f144c..03d06fdff 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -8,7 +8,7 @@ <PropertyGroup> <AssemblyName>jellyfin</AssemblyName> <OutputType>Exe</OutputType> - <TargetFramework>netcoreapp3.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> @@ -38,10 +38,10 @@ <ItemGroup> <PackageReference Include="CommandLineParser" Version="2.8.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.9" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.9" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.9" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.9" /> + <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.0" /> <PackageReference Include="prometheus-net" Version="4.0.0" /> <PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index e716a6610..b67a54983 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -18,8 +18,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.9" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/> <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" /> </ItemGroup> @@ -29,7 +29,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 4374317d6..9acc98dce 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -14,8 +14,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.9" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/> </ItemGroup> @@ -29,7 +29,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors> diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index 529e7065c..3ce9ff4cc 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -11,7 +11,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 6ead93e09..7bb2a7d03 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> @@ -25,7 +25,7 @@ <ItemGroup> <PackageReference Include="BDInfo" Version="0.7.6.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" /> - <PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.1" /> + <PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" /> <PackageReference Include="UTF.Unknown" Version="2.3.0" /> </ItemGroup> diff --git a/MediaBrowser.Model/Extensions/StringHelper.cs b/MediaBrowser.Model/Extensions/StringHelper.cs index 8ffa3c4ba..2d9a6c4db 100644 --- a/MediaBrowser.Model/Extensions/StringHelper.cs +++ b/MediaBrowser.Model/Extensions/StringHelper.cs @@ -22,11 +22,6 @@ namespace MediaBrowser.Model.Extensions return str; } -#if NETSTANDARD2_0 - char[] a = str.ToCharArray(); - a[0] = char.ToUpperInvariant(a[0]); - return new string(a); -#else return string.Create( str.Length, str, @@ -38,7 +33,6 @@ namespace MediaBrowser.Model.Extensions chars[i] = buf[i]; } }); -#endif } } } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 253ee7e79..b86187f9b 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -14,7 +14,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors> @@ -32,11 +32,11 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.9" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" /> <PackageReference Include="System.Globalization" Version="4.3.0" /> - <PackageReference Include="System.Text.Json" Version="5.0.0-preview.8.20407.11" /> + <PackageReference Include="System.Text.Json" Version="5.0.0" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 9465fe42c..fd3f9f4c7 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -16,9 +16,9 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" /> - <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.9" /> - <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.9" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> <PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" /> <PackageReference Include="PlaylistsNET" Version="1.1.2" /> <PackageReference Include="TMDbLib" Version="1.7.3-alpha" /> @@ -26,7 +26,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors> diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index 45fd9add9..87d1e9464 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -15,7 +15,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj index 664663bd7..d0962e82c 100644 --- a/RSSDP/RSSDP.csproj +++ b/RSSDP/RSSDP.csproj @@ -10,7 +10,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index ce61f5684..5bf322f07 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>netcoreapp3.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <IsPackable>false</IsPackable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> @@ -16,8 +16,8 @@ <PackageReference Include="AutoFixture" Version="4.14.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.9" /> - <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.9" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index 67dc8286a..e8eca6760 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>netcoreapp3.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <IsPackable>false</IsPackable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index 30e84842a..6e3fac43d 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>netcoreapp3.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <IsPackable>false</IsPackable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index 4fd0d5342..e88de3811 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>netcoreapp3.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <IsPackable>false</IsPackable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index 0d240fd65..567cf34ef 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>netcoreapp3.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <IsPackable>false</IsPackable> <Nullable>enable</Nullable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index db1f2956e..b960fda72 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>netcoreapp3.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <IsPackable>false</IsPackable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> -- cgit v1.2.3 From 158eff62d75db2d5e6e6f09fbe6e03eac607fc56 Mon Sep 17 00:00:00 2001 From: Stepan <ste.martinek+git@gmail.com> Date: Tue, 10 Nov 2020 19:23:10 +0100 Subject: Xml-doc part2 --- Emby.Naming/Common/MediaType.cs | 3 + Emby.Naming/Common/NamingOptions.cs | 87 ++++++++++++++++++++++ Emby.Naming/Subtitles/SubtitleInfo.cs | 9 +++ Emby.Naming/Subtitles/SubtitleParser.cs | 14 +++- Emby.Naming/TV/EpisodeResolver.cs | 17 +++++ Emby.Naming/TV/SeasonPathParser.cs | 10 +++ Emby.Naming/TV/SeasonPathParserResult.cs | 7 ++ Emby.Naming/Video/ExtraResolver.cs | 12 +++ Emby.Naming/Video/ExtraResult.cs | 3 + Emby.Naming/Video/ExtraRule.cs | 7 ++ Emby.Naming/Video/FileStack.cs | 21 ++++++ Emby.Naming/Video/FlagParser.cs | 18 +++++ Emby.Naming/Video/Format3DParser.cs | 12 +++ Emby.Naming/Video/Format3DResult.cs | 6 ++ Emby.Naming/Video/Format3DRule.cs | 8 ++ Emby.Naming/Video/StackResolver.cs | 27 +++++++ Emby.Naming/Video/StubResolver.cs | 10 +++ Emby.Naming/Video/StubTypeRule.cs | 8 ++ Emby.Naming/Video/VideoListResolver.cs | 13 ++++ Emby.Naming/Video/VideoResolver.cs | 29 ++++++++ .../Subtitles/SubtitleParserTests.cs | 7 +- 21 files changed, 321 insertions(+), 7 deletions(-) (limited to 'tests') diff --git a/Emby.Naming/Common/MediaType.cs b/Emby.Naming/Common/MediaType.cs index 1231b1887..dc9784c6d 100644 --- a/Emby.Naming/Common/MediaType.cs +++ b/Emby.Naming/Common/MediaType.cs @@ -1,5 +1,8 @@ namespace Emby.Naming.Common { + /// <summary> + /// Type of audiovisual media. + /// </summary> public enum MediaType { /// <summary> diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 0f02c03cb..035d1b228 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -8,8 +8,14 @@ using MediaBrowser.Model.Entities; namespace Emby.Naming.Common { + /// <summary> + /// Big ugly class containing lot of different naming options that should be split and injected instead of passes everywhere. + /// </summary> public class NamingOptions { + /// <summary> + /// Initializes a new instance of the <see cref="NamingOptions"/> class. + /// </summary> public NamingOptions() { VideoFileExtensions = new[] @@ -644,58 +650,139 @@ namespace Emby.Naming.Common Compile(); } + /// <summary> + /// Gets or sets list of audio file extensions. + /// </summary> public string[] AudioFileExtensions { get; set; } + /// <summary> + /// Gets or sets list of album stacking prefixes. + /// </summary> public string[] AlbumStackingPrefixes { get; set; } + /// <summary> + /// Gets or sets list of subtitle file extensions. + /// </summary> public string[] SubtitleFileExtensions { get; set; } + /// <summary> + /// Gets or sets list of subtitles flag delimiters. + /// </summary> public char[] SubtitleFlagDelimiters { get; set; } + /// <summary> + /// Gets or sets list of subtitle forced flags. + /// </summary> public string[] SubtitleForcedFlags { get; set; } + /// <summary> + /// Gets or sets list of subtitle default flags. + /// </summary> public string[] SubtitleDefaultFlags { get; set; } + /// <summary> + /// Gets or sets list of episode regular expressions. + /// </summary> public EpisodeExpression[] EpisodeExpressions { get; set; } + /// <summary> + /// Gets or sets list of raw episode without season regular expressions strings. + /// </summary> public string[] EpisodeWithoutSeasonExpressions { get; set; } + /// <summary> + /// Gets or sets list of raw multi-part episodes regular expressions strings. + /// </summary> public string[] EpisodeMultiPartExpressions { get; set; } + /// <summary> + /// Gets or sets list of video file extensions. + /// </summary> public string[] VideoFileExtensions { get; set; } + /// <summary> + /// Gets or sets list of video stub file extensions. + /// </summary> public string[] StubFileExtensions { get; set; } + /// <summary> + /// Gets or sets list of raw audiobook parts regular expressions strings. + /// </summary> public string[] AudioBookPartsExpressions { get; set; } + /// <summary> + /// Gets or sets list of raw audiobook names regular expressions strings. + /// </summary> public string[] AudioBookNamesExpressions { get; set; } + /// <summary> + /// Gets or sets list of stub type rules. + /// </summary> public StubTypeRule[] StubTypes { get; set; } + /// <summary> + /// Gets or sets list of video flag delimiters. + /// </summary> public char[] VideoFlagDelimiters { get; set; } + /// <summary> + /// Gets or sets list of 3D Format rules. + /// </summary> public Format3DRule[] Format3DRules { get; set; } + /// <summary> + /// Gets or sets list of raw video file-stacking expressions strings. + /// </summary> public string[] VideoFileStackingExpressions { get; set; } + /// <summary> + /// Gets or sets list of raw clean DateTimes regular expressions strings. + /// </summary> public string[] CleanDateTimes { get; set; } + /// <summary> + /// Gets or sets list of raw clean strings regular expressions strings. + /// </summary> public string[] CleanStrings { get; set; } + /// <summary> + /// Gets or sets list of multi-episode regular expressions. + /// </summary> public EpisodeExpression[] MultipleEpisodeExpressions { get; set; } + /// <summary> + /// Gets or sets list of extra rules for videos. + /// </summary> public ExtraRule[] VideoExtraRules { get; set; } + /// <summary> + /// Gets list of video file-stack regular expressions. + /// </summary> public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty<Regex>(); + /// <summary> + /// Gets list of clean datetime regular expressions. + /// </summary> public Regex[] CleanDateTimeRegexes { get; private set; } = Array.Empty<Regex>(); + /// <summary> + /// Gets list of clean string regular expressions. + /// </summary> public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>(); + /// <summary> + /// Gets list of episode without season regular expressions. + /// </summary> public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty<Regex>(); + /// <summary> + /// Gets list of multi-part episode regular expressions. + /// </summary> public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty<Regex>(); + /// <summary> + /// Compiles raw regex strings into regexes. + /// </summary> public void Compile() { VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray(); diff --git a/Emby.Naming/Subtitles/SubtitleInfo.cs b/Emby.Naming/Subtitles/SubtitleInfo.cs index 62cc3ead1..1fb2e0dc8 100644 --- a/Emby.Naming/Subtitles/SubtitleInfo.cs +++ b/Emby.Naming/Subtitles/SubtitleInfo.cs @@ -1,7 +1,16 @@ namespace Emby.Naming.Subtitles { + /// <summary> + /// Class holding information about subtitle. + /// </summary> public class SubtitleInfo { + /// <summary> + /// Initializes a new instance of the <see cref="SubtitleInfo"/> class. + /// </summary> + /// <param name="path">Path to file.</param> + /// <param name="isDefault">Is subtitle default.</param> + /// <param name="isForced">Is subtitle forced.</param> public SubtitleInfo(string path, bool isDefault, bool isForced) { Path = path; diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs index 476a83cf3..e87245251 100644 --- a/Emby.Naming/Subtitles/SubtitleParser.cs +++ b/Emby.Naming/Subtitles/SubtitleParser.cs @@ -5,20 +5,32 @@ using Emby.Naming.Common; namespace Emby.Naming.Subtitles { + /// <summary> + /// Subtitle Parser class. + /// </summary> public class SubtitleParser { private readonly NamingOptions _options; + /// <summary> + /// Initializes a new instance of the <see cref="SubtitleParser"/> class. + /// </summary> + /// <param name="options"><see cref="NamingOptions"/> object containing SubtitleFileExtensions, SubtitleDefaultFlags, SubtitleForcedFlags and SubtitleFlagDelimiters.</param> public SubtitleParser(NamingOptions options) { _options = options; } + /// <summary> + /// Parse file to determine if is subtitle and <see cref="SubtitleInfo"/>. + /// </summary> + /// <param name="path">Path to file.</param> + /// <returns>Returns null or <see cref="SubtitleInfo"/> object if parsing is successful.</returns> public SubtitleInfo? ParseFile(string path) { if (path.Length == 0) { - throw new ArgumentException("File path can't be empty.", nameof(path)); + return null; } var extension = Path.GetExtension(path); diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs index 26dd6915b..f7df58786 100644 --- a/Emby.Naming/TV/EpisodeResolver.cs +++ b/Emby.Naming/TV/EpisodeResolver.cs @@ -6,15 +6,32 @@ using Emby.Naming.Video; namespace Emby.Naming.TV { + /// <summary> + /// Used to resolve information about episode from path. + /// </summary> public class EpisodeResolver { private readonly NamingOptions _options; + /// <summary> + /// Initializes a new instance of the <see cref="EpisodeResolver"/> class. + /// </summary> + /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param> public EpisodeResolver(NamingOptions options) { _options = options; } + /// <summary> + /// Resolve information about episode from path. + /// </summary> + /// <param name="path">Path.</param> + /// <param name="isDirectory">Is path for a directory or file.</param> + /// <param name="isNamed">Do we want to use IsNamed expressions.</param> + /// <param name="isOptimistic">Do we want to use Optimistic expressions.</param> + /// <param name="supportsAbsoluteNumbers">Do we want to use expressions supporting absolute episode numbers.</param> + /// <param name="fillExtendedInfo">Should we attempt to retrieve extended information.</param> + /// <returns>Returns null or <see cref="EpisodeInfo"/> object if successful.</returns> public EpisodeInfo? Resolve( string path, bool isDirectory, diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index cf99097bc..d11c7c99e 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -4,6 +4,9 @@ using System.IO; namespace Emby.Naming.TV { + /// <summary> + /// Class to parse season paths. + /// </summary> public static class SeasonPathParser { /// <summary> @@ -21,6 +24,13 @@ namespace Emby.Naming.TV "stagione" }; + /// <summary> + /// Attempts to parse season number from path. + /// </summary> + /// <param name="path">Path to season.</param> + /// <param name="supportSpecialAliases">Support special aliases when parsing.</param> + /// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param> + /// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns> public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders) { var result = new SeasonPathParserResult(); diff --git a/Emby.Naming/TV/SeasonPathParserResult.cs b/Emby.Naming/TV/SeasonPathParserResult.cs index f52f941a7..b4b6f236a 100644 --- a/Emby.Naming/TV/SeasonPathParserResult.cs +++ b/Emby.Naming/TV/SeasonPathParserResult.cs @@ -1,5 +1,8 @@ namespace Emby.Naming.TV { + /// <summary> + /// Data object to pass result of <see cref="SeasonPathParser"/>. + /// </summary> public class SeasonPathParserResult { /// <summary> @@ -14,6 +17,10 @@ namespace Emby.Naming.TV /// <value><c>true</c> if success; otherwise, <c>false</c>.</value> public bool Success { get; set; } + /// <summary> + /// Gets or sets a value indicating whether "Is season folder". + /// Seems redundant and barely used. + /// </summary> public bool IsSeasonFolder { get; set; } } } diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs index 98ea342ac..dd934d91b 100644 --- a/Emby.Naming/Video/ExtraResolver.cs +++ b/Emby.Naming/Video/ExtraResolver.cs @@ -6,15 +6,27 @@ using Emby.Naming.Common; namespace Emby.Naming.Video { + /// <summary> + /// Resolve if file is extra for video. + /// </summary> public class ExtraResolver { private readonly NamingOptions _options; + /// <summary> + /// Initializes a new instance of the <see cref="ExtraResolver"/> class. + /// </summary> + /// <param name="options"><see cref="NamingOptions"/> object containing VideoExtraRules and passed to <see cref="AudioFileParser"/> and <see cref="VideoResolver"/>.</param> public ExtraResolver(NamingOptions options) { _options = options; } + /// <summary> + /// Attempts to resolve if file is extra. + /// </summary> + /// <param name="path">Path to file.</param> + /// <returns>Returns <see cref="ExtraResult"/> object.</returns> public ExtraResult GetExtraInfo(string path) { return _options.VideoExtraRules diff --git a/Emby.Naming/Video/ExtraResult.cs b/Emby.Naming/Video/ExtraResult.cs index f3b8d2a2f..243fc2b41 100644 --- a/Emby.Naming/Video/ExtraResult.cs +++ b/Emby.Naming/Video/ExtraResult.cs @@ -2,6 +2,9 @@ using MediaBrowser.Model.Entities; namespace Emby.Naming.Video { + /// <summary> + /// Holder object for passing results from ExtraResolver. + /// </summary> public class ExtraResult { /// <summary> diff --git a/Emby.Naming/Video/ExtraRule.cs b/Emby.Naming/Video/ExtraRule.cs index a93474bc6..e267ac55f 100644 --- a/Emby.Naming/Video/ExtraRule.cs +++ b/Emby.Naming/Video/ExtraRule.cs @@ -8,6 +8,13 @@ namespace Emby.Naming.Video /// </summary> public class ExtraRule { + /// <summary> + /// Initializes a new instance of the <see cref="ExtraRule"/> class. + /// </summary> + /// <param name="extraType">Type of extra.</param> + /// <param name="ruleType">Type of rule.</param> + /// <param name="token">Token.</param> + /// <param name="mediaType">Media type.</param> public ExtraRule(ExtraType extraType, ExtraRuleType ruleType, string token, MediaType mediaType) { Token = token; diff --git a/Emby.Naming/Video/FileStack.cs b/Emby.Naming/Video/FileStack.cs index 75620e961..6519db57c 100644 --- a/Emby.Naming/Video/FileStack.cs +++ b/Emby.Naming/Video/FileStack.cs @@ -4,19 +4,40 @@ using System.Linq; namespace Emby.Naming.Video { + /// <summary> + /// Object holding list of files paths with additional information. + /// </summary> public class FileStack { + /// <summary> + /// Initializes a new instance of the <see cref="FileStack"/> class. + /// </summary> public FileStack() { Files = new List<string>(); } + /// <summary> + /// Gets or sets name of file stack. + /// </summary> public string Name { get; set; } = string.Empty; + /// <summary> + /// Gets or sets list of paths in stack. + /// </summary> public List<string> Files { get; set; } + /// <summary> + /// Gets or sets a value indicating whether stack is directory stack. + /// </summary> public bool IsDirectoryStack { get; set; } + /// <summary> + /// Helper function to determine if path is in the stack. + /// </summary> + /// <param name="file">Path of desired file.</param> + /// <param name="isDirectory">Requested type of stack.</param> + /// <returns>True if file is in the stack.</returns> public bool ContainsFile(string file, bool isDirectory) { if (IsDirectoryStack == isDirectory) diff --git a/Emby.Naming/Video/FlagParser.cs b/Emby.Naming/Video/FlagParser.cs index cd15b4666..439de1813 100644 --- a/Emby.Naming/Video/FlagParser.cs +++ b/Emby.Naming/Video/FlagParser.cs @@ -4,20 +4,38 @@ using Emby.Naming.Common; namespace Emby.Naming.Video { + /// <summary> + /// Parses list of flags from filename based on delimiters. + /// </summary> public class FlagParser { private readonly NamingOptions _options; + /// <summary> + /// Initializes a new instance of the <see cref="FlagParser"/> class. + /// </summary> + /// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters.</param> public FlagParser(NamingOptions options) { _options = options; } + /// <summary> + /// Calls GetFlags function with _options.VideoFlagDelimiters parameter. + /// </summary> + /// <param name="path">Path to file.</param> + /// <returns>List of found flags.</returns> public string[] GetFlags(string path) { return GetFlags(path, _options.VideoFlagDelimiters); } + /// <summary> + /// Parses flags from filename based on delimiters. + /// </summary> + /// <param name="path">Path to file.</param> + /// <param name="delimiters">Delimiters used to extract flags.</param> + /// <returns>List of found flags.</returns> public string[] GetFlags(string path, char[] delimiters) { if (string.IsNullOrEmpty(path)) diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs index 73ad36af4..4fd5d78ba 100644 --- a/Emby.Naming/Video/Format3DParser.cs +++ b/Emby.Naming/Video/Format3DParser.cs @@ -4,15 +4,27 @@ using Emby.Naming.Common; namespace Emby.Naming.Video { + /// <summary> + /// Parste 3D format related flags. + /// </summary> public class Format3DParser { private readonly NamingOptions _options; + /// <summary> + /// Initializes a new instance of the <see cref="Format3DParser"/> class. + /// </summary> + /// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters and passes options to <see cref="FlagParser"/>.</param> public Format3DParser(NamingOptions options) { _options = options; } + /// <summary> + /// Parse 3D format related flags. + /// </summary> + /// <param name="path">Path to file.</param> + /// <returns>Returns <see cref="Format3DResult"/> object.</returns> public Format3DResult Parse(string path) { int oldLen = _options.VideoFlagDelimiters.Length; diff --git a/Emby.Naming/Video/Format3DResult.cs b/Emby.Naming/Video/Format3DResult.cs index 539060c98..ac935f203 100644 --- a/Emby.Naming/Video/Format3DResult.cs +++ b/Emby.Naming/Video/Format3DResult.cs @@ -2,8 +2,14 @@ using System.Collections.Generic; namespace Emby.Naming.Video { + /// <summary> + /// Helper object to return data from <see cref="Format3DParser"/>. + /// </summary> public class Format3DResult { + /// <summary> + /// Initializes a new instance of the <see cref="Format3DResult"/> class. + /// </summary> public Format3DResult() { Tokens = new List<string>(); diff --git a/Emby.Naming/Video/Format3DRule.cs b/Emby.Naming/Video/Format3DRule.cs index bee5c109e..e562691df 100644 --- a/Emby.Naming/Video/Format3DRule.cs +++ b/Emby.Naming/Video/Format3DRule.cs @@ -1,7 +1,15 @@ namespace Emby.Naming.Video { + /// <summary> + /// Data holder class for 3D format rule. + /// </summary> public class Format3DRule { + /// <summary> + /// Initializes a new instance of the <see cref="Format3DRule"/> class. + /// </summary> + /// <param name="token">Token.</param> + /// <param name="precedingToken">Token present before current token.</param> public Format3DRule(string token, string? precedingToken = null) { Token = token; diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs index d6de468cc..550c42961 100644 --- a/Emby.Naming/Video/StackResolver.cs +++ b/Emby.Naming/Video/StackResolver.cs @@ -9,25 +9,47 @@ using MediaBrowser.Model.IO; namespace Emby.Naming.Video { + /// <summary> + /// Resolve <see cref="FileStack"/> from list of paths. + /// </summary> public class StackResolver { private readonly NamingOptions _options; + /// <summary> + /// Initializes a new instance of the <see cref="StackResolver"/> class. + /// </summary> + /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileStackingRegexes and passes options to <see cref="VideoResolver"/>.</param> public StackResolver(NamingOptions options) { _options = options; } + /// <summary> + /// Resolves only directories from paths. + /// </summary> + /// <param name="files">List of paths.</param> + /// <returns>Enumerable <see cref="FileStack"/> of directories.</returns> public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files) { return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true })); } + /// <summary> + /// Resolves only files from paths. + /// </summary> + /// <param name="files">List of paths.</param> + /// <returns>Enumerable <see cref="FileStack"/> of files.</returns> public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files) { return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false })); } + /// <summary> + /// Resolves audiobooks from paths. + /// </summary> + /// <param name="files">List of paths.</param> + /// <returns>Enumerable <see cref="FileStack"/> of directories.</returns> public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files) { var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path)); @@ -56,6 +78,11 @@ namespace Emby.Naming.Video } } + /// <summary> + /// Resolves videos from paths. + /// </summary> + /// <param name="files">List of paths.</param> + /// <returns>Enumerable <see cref="FileStack"/> of videos.</returns> public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files) { var resolver = new VideoResolver(_options); diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs index 6241a46b0..079987fe8 100644 --- a/Emby.Naming/Video/StubResolver.cs +++ b/Emby.Naming/Video/StubResolver.cs @@ -5,8 +5,18 @@ using Emby.Naming.Common; namespace Emby.Naming.Video { + /// <summary> + /// Resolve if file is stub (.disc). + /// </summary> public static class StubResolver { + /// <summary> + /// Tries to resolve if file is stub (.disc). + /// </summary> + /// <param name="path">Path to file.</param> + /// <param name="options">NamingOptions containing StubFileExtensions and StubTypes.</param> + /// <param name="stubType">Stub type.</param> + /// <returns>True if file is a stub.</returns> public static bool TryResolveFile(string path, NamingOptions options, out string? stubType) { stubType = default; diff --git a/Emby.Naming/Video/StubTypeRule.cs b/Emby.Naming/Video/StubTypeRule.cs index df2d3c7d2..dfb3ac013 100644 --- a/Emby.Naming/Video/StubTypeRule.cs +++ b/Emby.Naming/Video/StubTypeRule.cs @@ -1,7 +1,15 @@ namespace Emby.Naming.Video { + /// <summary> + /// Data class holding information about Stub type rule. + /// </summary> public class StubTypeRule { + /// <summary> + /// Initializes a new instance of the <see cref="StubTypeRule"/> class. + /// </summary> + /// <param name="token">Token.</param> + /// <param name="stubType">Stub type.</param> public StubTypeRule(string token, string stubType) { Token = token; diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index dda322521..ee0e4d465 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -9,15 +9,28 @@ using MediaBrowser.Model.IO; namespace Emby.Naming.Video { + /// <summary> + /// Resolves alternative versions and extras from list of video files. + /// </summary> public class VideoListResolver { private readonly NamingOptions _options; + /// <summary> + /// Initializes a new instance of the <see cref="VideoListResolver"/> class. + /// </summary> + /// <param name="options"><see cref="NamingOptions"/> object containing CleanStringRegexes and VideoFlagDelimiters and passes options to <see cref="StackResolver"/> and <see cref="VideoResolver"/>.</param> public VideoListResolver(NamingOptions options) { _options = options; } + /// <summary> + /// Resolves alternative versions and extras from list of video files. + /// </summary> + /// <param name="files">List of related video files.</param> + /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param> + /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files togeather when related.</returns> public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true) { var videoResolver = new VideoResolver(_options); diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs index 31b47cdf1..d7165d8d7 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -5,10 +5,18 @@ using Emby.Naming.Common; namespace Emby.Naming.Video { + /// <summary> + /// Resolves <see cref="VideoFileInfo"/> from file path. + /// </summary> public class VideoResolver { private readonly NamingOptions _options; + /// <summary> + /// Initializes a new instance of the <see cref="VideoResolver"/> class. + /// </summary> + /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions, StubFileExtensions, CleanStringRegexes and CleanDateTimeRegexes + /// and passes options in <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="ExtraResolver"/>.</param> public VideoResolver(NamingOptions options) { _options = options; @@ -110,23 +118,44 @@ namespace Emby.Naming.Video extraRule: extraResult.Rule); } + /// <summary> + /// Determines if path is video file based on extension. + /// </summary> + /// <param name="path">Path to file.</param> + /// <returns>True if is video file.</returns> public bool IsVideoFile(string path) { var extension = Path.GetExtension(path) ?? string.Empty; return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); } + /// <summary> + /// Determines if path is video file stub based on extension. + /// </summary> + /// <param name="path">Path to file.</param> + /// <returns>True if is video file stub.</returns> public bool IsStubFile(string path) { var extension = Path.GetExtension(path) ?? string.Empty; return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); } + /// <summary> + /// Tries to clean name of clutter. + /// </summary> + /// <param name="name">Raw name.</param> + /// <param name="newName">Clean name.</param> + /// <returns>True if cleaning of name was successful.</returns> public bool TryCleanString(string name, out ReadOnlySpan<char> newName) { return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName); } + /// <summary> + /// Tries to get name and year from raw name. + /// </summary> + /// <param name="name">Raw name.</param> + /// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns> public CleanDateTimeResult CleanDateTime(string name) { return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes); diff --git a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs index 515209890..f3abacb4f 100644 --- a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs +++ b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs @@ -31,17 +31,12 @@ namespace Jellyfin.Naming.Tests.Subtitles [Theory] [InlineData("The Skin I Live In (2011).mp4")] + [InlineData("")] public void SubtitleParser_InvalidFileName_ReturnsNull(string input) { var parser = new SubtitleParser(_namingOptions); Assert.Null(parser.ParseFile(input)); } - - [Fact] - public void SubtitleParser_EmptyFileName_ThrowsArgumentException() - { - Assert.Throws<ArgumentException>(() => new SubtitleParser(_namingOptions).ParseFile(string.Empty)); - } } } -- cgit v1.2.3 From 195a6261c49f2f1376794a149ddec0a56cbe230a Mon Sep 17 00:00:00 2001 From: Stepan <ste.martinek+git@gmail.com> Date: Tue, 10 Nov 2020 19:28:03 +0100 Subject: Dummy test case explanation. --- tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'tests') diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs index 57f382b38..12fc19bc4 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs @@ -82,6 +82,10 @@ namespace Jellyfin.Naming.Tests.TV Assert.True(res!.IsStub); } + /* + * EpisodePathParser.cs:130 is currently unreachable, but the piece of code is useful and could be reached with addition of new EpisodeExpressions. + * In order to preserve it but achieve 100% code coverage the test case below with made up expressions and filename is used. + */ [Fact] public void EpisodePathParserTest_EmptyDateParsers() { -- cgit v1.2.3 From 3bca1181b3c91115e693dc83ba1f5fdd6ee38f7c Mon Sep 17 00:00:00 2001 From: Stepan <ste.martinek+git@gmail.com> Date: Thu, 12 Nov 2020 13:16:33 +0100 Subject: Taken suggestions from code review and created test for ExtraRuleType.Regex instead of throwing exception there. --- Emby.Naming/AudioBook/AudioBookListResolver.cs | 5 +++-- Emby.Naming/Video/ExtraResolver.cs | 10 +--------- Emby.Naming/Video/VideoListResolver.cs | 4 ++-- tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs | 4 +++- tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs | 14 +++++--------- 5 files changed, 14 insertions(+), 23 deletions(-) (limited to 'tests') diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs index 54c1fe5bd..95817efc3 100644 --- a/Emby.Naming/AudioBook/AudioBookListResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs @@ -73,6 +73,7 @@ namespace Emby.Naming.AudioBook var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null); var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber }); + var nameWithReplacedDots = nameParserResult.Name.Replace(" ", "."); foreach (var group in groupedBy) { @@ -86,9 +87,9 @@ namespace Emby.Naming.AudioBook foreach (var audioFile in group) { var name = Path.GetFileNameWithoutExtension(audioFile.Path); - if (name == "audiobook" || + if (name.Equals("audiobook") || name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) || - name.Contains(nameParserResult.Name.Replace(" ", "."), StringComparison.OrdinalIgnoreCase)) + name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase)) { alt.Add(audioFile); } diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs index dd934d91b..1d3b36a1a 100644 --- a/Emby.Naming/Video/ExtraResolver.cs +++ b/Emby.Naming/Video/ExtraResolver.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using Emby.Naming.Audio; using Emby.Naming.Common; @@ -52,11 +53,6 @@ namespace Emby.Naming.Video return result; } } - else - { - // Currently unreachable code if new rule.MediaType is desired add if clause with proper tests - throw new InvalidOperationException(); - } if (rule.RuleType == ExtraRuleType.Filename) { @@ -80,9 +76,6 @@ namespace Emby.Naming.Video } else if (rule.RuleType == ExtraRuleType.Regex) { - // Currently unreachable code if new rule.MediaType is desired add if clause with proper tests - throw new InvalidOperationException(); - /* var filename = Path.GetFileName(path); var regex = new Regex(rule.Token, RegexOptions.IgnoreCase); @@ -92,7 +85,6 @@ namespace Emby.Naming.Video result.ExtraType = rule.ExtraType; result.Rule = rule; } - */ } else if (rule.RuleType == ExtraRuleType.DirectoryName) { diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 1e18c4452..2fd2d3e8b 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -230,8 +230,8 @@ namespace Emby.Naming.Video testFilename = testFilename.Substring(folderName.Length).Trim(); return string.IsNullOrEmpty(testFilename) - || testFilename[0] == '-' - || testFilename[0].Equals( '_') + || testFilename[0].Equals('-') + || testFilename[0].Equals('_') || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty)); } diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs index ed971eed7..15cb5c72f 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using Emby.Naming.Common; using Emby.Naming.Video; using Xunit; @@ -51,6 +51,8 @@ namespace Jellyfin.Naming.Tests.Video [InlineData("My Movie 2013-12-09", "My Movie 2013-12-09", null)] [InlineData("My Movie 20131209", "My Movie 20131209", null)] [InlineData("My Movie 2013-12-09 2013", "My Movie 2013-12-09", 2013)] + [InlineData(null, null, null)] + [InlineData("", "", null)] public void CleanDateTimeTest(string input, string expectedName, int? expectedYear) { input = Path.GetFileName(input); diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs index 12a9b023b..d34f65409 100644 --- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs @@ -95,18 +95,14 @@ namespace Jellyfin.Naming.Tests.Video } } - [Fact] - public void TestExtraInfo_InvalidRuleMediaType() - { - var options = new NamingOptions { VideoExtraRules = new[] { new ExtraRule(ExtraType.Unknown, ExtraRuleType.DirectoryName, " ", MediaType.Photo) } }; - Assert.Throws<InvalidOperationException>(() => GetExtraTypeParser(options).GetExtraInfo("sample.jpg")); - } - [Fact] public void TestExtraInfo_InvalidRuleType() { - var options = new NamingOptions { VideoExtraRules = new[] { new ExtraRule(ExtraType.Unknown, ExtraRuleType.Regex, " ", MediaType.Video) } }; - Assert.Throws<InvalidOperationException>(() => GetExtraTypeParser(options).GetExtraInfo("sample.mp4")); + var rule = new ExtraRule(ExtraType.Unknown, ExtraRuleType.Regex, @"([eE]x(tra)?\.\w+)", MediaType.Video); + var options = new NamingOptions { VideoExtraRules = new[] { rule } }; + var res = GetExtraTypeParser(options).GetExtraInfo("extra.mp4"); + + Assert.Equal(rule, res.Rule); } [Fact] -- cgit v1.2.3 From 0c23b8cadf8d4163b0db664a8b2cfb717181eb02 Mon Sep 17 00:00:00 2001 From: crobibero <cody@robibe.ro> Date: Thu, 12 Nov 2020 08:06:25 -0700 Subject: Don't throw exception when converting values using binder or JsonConverter --- .../ModelBinders/CommaDelimitedArrayModelBinder.cs | 65 ++++++++++++++++------ .../Converters/JsonCommaDelimitedArrayConverter.cs | 27 ++++++++- .../CommaDelimitedArrayModelBinderTests.cs | 33 +++++------ 3 files changed, 89 insertions(+), 36 deletions(-) (limited to 'tests') diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs index 4f012cab2..fd9f724b1 100644 --- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs +++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging; namespace Jellyfin.Api.ModelBinders { @@ -11,6 +13,17 @@ namespace Jellyfin.Api.ModelBinders /// </summary> public class CommaDelimitedArrayModelBinder : IModelBinder { + private readonly ILogger<CommaDelimitedArrayModelBinder> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinder"/> class. + /// </summary> + /// <param name="logger">Instance of the <see cref="ILogger{CommaDelimitedArrayModelBinder}"/> interface.</param> + public CommaDelimitedArrayModelBinder(ILogger<CommaDelimitedArrayModelBinder> logger) + { + _logger = logger; + } + /// <inheritdoc/> public Task BindModelAsync(ModelBindingContext bindingContext) { @@ -20,16 +33,8 @@ namespace Jellyfin.Api.ModelBinders if (valueProviderResult.Length > 1) { - var result = Array.CreateInstance(elementType, valueProviderResult.Length); - - for (int i = 0; i < valueProviderResult.Length; i++) - { - var value = converter.ConvertFromString(valueProviderResult.Values[i].Trim()); - - result.SetValue(value, i); - } - - bindingContext.Result = ModelBindingResult.Success(result); + var typedValues = GetParsedResult(valueProviderResult.Values, elementType, converter); + bindingContext.Result = ModelBindingResult.Success(typedValues); } else { @@ -37,13 +42,8 @@ namespace Jellyfin.Api.ModelBinders if (value != null) { - var values = Array.ConvertAll( - value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries), - x => converter.ConvertFromString(x?.Trim())); - - var typedValues = Array.CreateInstance(elementType, values.Length); - values.CopyTo(typedValues, 0); - + var splitValues = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + var typedValues = GetParsedResult(splitValues, elementType, converter); bindingContext.Result = ModelBindingResult.Success(typedValues); } else @@ -55,5 +55,36 @@ namespace Jellyfin.Api.ModelBinders return Task.CompletedTask; } + + private Array GetParsedResult(IReadOnlyList<string> values, Type elementType, TypeConverter converter) + { + var parsedValues = new object?[values.Count]; + var convertedCount = 0; + for (var i = 0; i < values.Count; i++) + { + try + { + parsedValues[i] = converter.ConvertFromString(values[i]?.Trim()); + convertedCount++; + } + catch (FormatException e) + { + _logger.LogWarning(e, "Error converting value."); + } + } + + var typedValues = Array.CreateInstance(elementType, convertedCount); + var typedValueIndex = 0; + for (var i = 0; i < parsedValues.Length; i++) + { + if (parsedValues[i] != null) + { + typedValues.SetValue(parsedValues[i], typedValueIndex); + typedValueIndex++; + } + } + + return typedValues; + } } } diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs index bf7048c37..b24a49761 100644 --- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs @@ -32,13 +32,34 @@ namespace MediaBrowser.Common.Json.Converters return Array.Empty<T>(); } - var entries = new T[stringEntries.Length]; + var parsedValues = new object[stringEntries.Length]; + var convertedCount = 0; for (var i = 0; i < stringEntries.Length; i++) { - entries[i] = (T)_typeConverter.ConvertFrom(stringEntries[i].Trim()); + try + { + parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim()); + convertedCount++; + } + catch (FormatException) + { + // TODO log when upgraded to .Net5 + // _logger.LogWarning(e, "Error converting value."); + } } - return entries; + var typedValues = new T[convertedCount]; + var typedValueIndex = 0; + for (var i = 0; i < stringEntries.Length; i++) + { + if (parsedValues[i] != null) + { + typedValues.SetValue(parsedValues[i], typedValueIndex); + typedValueIndex++; + } + } + + return typedValues; } return JsonSerializer.Deserialize<T[]>(ref reader, options); diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs index 89c7d62f7..82c7f6427 100644 --- a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs +++ b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Jellyfin.Api.ModelBinders; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Primitives; using Moq; using Xunit; @@ -21,7 +22,7 @@ namespace Jellyfin.Api.Tests.ModelBinders var queryParamString = "lol,xd"; var queryParamType = typeof(string[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), @@ -46,7 +47,7 @@ namespace Jellyfin.Api.Tests.ModelBinders var queryParamString = "42,0"; var queryParamType = typeof(int[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), @@ -71,7 +72,7 @@ namespace Jellyfin.Api.Tests.ModelBinders var queryParamString = "How,Much"; var queryParamType = typeof(TestType[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), @@ -96,7 +97,7 @@ namespace Jellyfin.Api.Tests.ModelBinders var queryParamString = "How,,Much"; var queryParamType = typeof(TestType[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), @@ -122,7 +123,7 @@ namespace Jellyfin.Api.Tests.ModelBinders var queryParamString2 = "Much"; var queryParamType = typeof(TestType[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), @@ -150,7 +151,7 @@ namespace Jellyfin.Api.Tests.ModelBinders var queryParamValues = Array.Empty<TestType>(); var queryParamType = typeof(TestType[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), @@ -172,13 +173,13 @@ namespace Jellyfin.Api.Tests.ModelBinders } [Fact] - public async Task BindModelAsync_ThrowsIfCommaDelimitedEnumArrayQueryIsInvalid() + public async Task BindModelAsync_EnumArrayQuery_BindValidOnly() { var queryParamName = "test"; var queryParamString = "🔥,😢"; var queryParamType = typeof(TestType[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), @@ -189,20 +190,20 @@ namespace Jellyfin.Api.Tests.ModelBinders bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); bindingContextMock.SetupProperty(b => b.Result); - Func<Task> act = async () => await modelBinder.BindModelAsync(bindingContextMock.Object); - - await Assert.ThrowsAsync<FormatException>(act); + await modelBinder.BindModelAsync(bindingContextMock.Object); + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Empty((TestType[])bindingContextMock.Object.Result.Model); } [Fact] - public async Task BindModelAsync_ThrowsIfCommaDelimitedEnumArrayQueryIsInvalid2() + public async Task BindModelAsync_EnumArrayQuery_BindValidOnly_2() { var queryParamName = "test"; var queryParamString1 = "How"; var queryParamString2 = "😱"; var queryParamType = typeof(TestType[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), @@ -217,9 +218,9 @@ namespace Jellyfin.Api.Tests.ModelBinders bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); bindingContextMock.SetupProperty(b => b.Result); - Func<Task> act = async () => await modelBinder.BindModelAsync(bindingContextMock.Object); - - await Assert.ThrowsAsync<FormatException>(act); + await modelBinder.BindModelAsync(bindingContextMock.Object); + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Single((TestType[])bindingContextMock.Object.Result.Model); } } } -- cgit v1.2.3 From 445eec7f5f8710179a425b3b6afa3d4d54ce03da Mon Sep 17 00:00:00 2001 From: crobibero <cody@robibe.ro> Date: Fri, 13 Nov 2020 09:21:22 -0700 Subject: Fix nullability errors in Jellyfin.Api.Tests --- .../Auth/CustomAuthenticationHandlerTests.cs | 8 ++++---- tests/Jellyfin.Api.Tests/BrandingServiceTests.cs | 4 ++-- .../CommaDelimitedArrayModelBinderTests.cs | 24 +++++++++++----------- tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs | 2 +- tests/Jellyfin.Api.Tests/TestHelpers.cs | 2 +- 5 files changed, 20 insertions(+), 20 deletions(-) (limited to 'tests') diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs index a46d94457..90c491666 100644 --- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -81,7 +81,7 @@ namespace Jellyfin.Api.Tests.Auth var authenticateResult = await _sut.AuthenticateAsync(); Assert.False(authenticateResult.Succeeded); - Assert.Equal(errorMessage, authenticateResult.Failure.Message); + Assert.Equal(errorMessage, authenticateResult.Failure?.Message); } [Fact] @@ -100,7 +100,7 @@ namespace Jellyfin.Api.Tests.Auth var authorizationInfo = SetupUser(); var authenticateResult = await _sut.AuthenticateAsync(); - Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username)); + Assert.True(authenticateResult.Principal?.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username)); } [Theory] @@ -112,7 +112,7 @@ namespace Jellyfin.Api.Tests.Auth var authenticateResult = await _sut.AuthenticateAsync(); var expectedRole = authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User; - Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole)); + Assert.True(authenticateResult.Principal?.HasClaim(ClaimTypes.Role, expectedRole)); } [Fact] @@ -121,7 +121,7 @@ namespace Jellyfin.Api.Tests.Auth SetupUser(); var authenticatedResult = await _sut.AuthenticateAsync(); - Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme); + Assert.Equal(_scheme.Name, authenticatedResult.Ticket?.AuthenticationScheme); } private AuthorizationInfo SetupUser(bool isAdmin = false) diff --git a/tests/Jellyfin.Api.Tests/BrandingServiceTests.cs b/tests/Jellyfin.Api.Tests/BrandingServiceTests.cs index 6fc287420..1cbe94c5b 100644 --- a/tests/Jellyfin.Api.Tests/BrandingServiceTests.cs +++ b/tests/Jellyfin.Api.Tests/BrandingServiceTests.cs @@ -25,7 +25,7 @@ namespace Jellyfin.Api.Tests // Assert response.EnsureSuccessStatusCode(); - Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString()); + Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString()); var responseBody = await response.Content.ReadAsStreamAsync(); _ = await JsonSerializer.DeserializeAsync<BrandingOptions>(responseBody); } @@ -43,7 +43,7 @@ namespace Jellyfin.Api.Tests // Assert response.EnsureSuccessStatusCode(); - Assert.Equal("text/css; charset=utf-8", response.Content.Headers.ContentType.ToString()); + Assert.Equal("text/css; charset=utf-8", response.Content.Headers.ContentType?.ToString()); } } } diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs index 89c7d62f7..6f3bd9132 100644 --- a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs +++ b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs @@ -17,7 +17,7 @@ namespace Jellyfin.Api.Tests.ModelBinders public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedStringArrayQuery() { var queryParamName = "test"; - var queryParamValues = new[] { "lol", "xd" }; + IReadOnlyList<string> queryParamValues = new[] { "lol", "xd" }; var queryParamString = "lol,xd"; var queryParamType = typeof(string[]); @@ -35,14 +35,14 @@ namespace Jellyfin.Api.Tests.ModelBinders await modelBinder.BindModelAsync(bindingContextMock.Object); Assert.True(bindingContextMock.Object.Result.IsModelSet); - Assert.Equal((string[])bindingContextMock.Object.Result.Model, queryParamValues); + Assert.Equal((IReadOnlyList<string>?)bindingContextMock.Object?.Result.Model, queryParamValues); } [Fact] public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedIntArrayQuery() { var queryParamName = "test"; - var queryParamValues = new[] { 42, 0 }; + IReadOnlyList<int> queryParamValues = new[] { 42, 0 }; var queryParamString = "42,0"; var queryParamType = typeof(int[]); @@ -60,14 +60,14 @@ namespace Jellyfin.Api.Tests.ModelBinders await modelBinder.BindModelAsync(bindingContextMock.Object); Assert.True(bindingContextMock.Object.Result.IsModelSet); - Assert.Equal((int[])bindingContextMock.Object.Result.Model, queryParamValues); + Assert.Equal((IReadOnlyList<int>?)bindingContextMock.Object.Result.Model, queryParamValues); } [Fact] public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQuery() { var queryParamName = "test"; - var queryParamValues = new[] { TestType.How, TestType.Much }; + IReadOnlyList<TestType> queryParamValues = new[] { TestType.How, TestType.Much }; var queryParamString = "How,Much"; var queryParamType = typeof(TestType[]); @@ -85,14 +85,14 @@ namespace Jellyfin.Api.Tests.ModelBinders await modelBinder.BindModelAsync(bindingContextMock.Object); Assert.True(bindingContextMock.Object.Result.IsModelSet); - Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues); + Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues); } [Fact] public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQueryWithDoubleCommas() { var queryParamName = "test"; - var queryParamValues = new[] { TestType.How, TestType.Much }; + IReadOnlyList<TestType> queryParamValues = new[] { TestType.How, TestType.Much }; var queryParamString = "How,,Much"; var queryParamType = typeof(TestType[]); @@ -110,14 +110,14 @@ namespace Jellyfin.Api.Tests.ModelBinders await modelBinder.BindModelAsync(bindingContextMock.Object); Assert.True(bindingContextMock.Object.Result.IsModelSet); - Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues); + Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues); } [Fact] public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery() { var queryParamName = "test"; - var queryParamValues = new[] { TestType.How, TestType.Much }; + IReadOnlyList<TestType> queryParamValues = new[] { TestType.How, TestType.Much }; var queryParamString1 = "How"; var queryParamString2 = "Much"; var queryParamType = typeof(TestType[]); @@ -140,14 +140,14 @@ namespace Jellyfin.Api.Tests.ModelBinders await modelBinder.BindModelAsync(bindingContextMock.Object); Assert.True(bindingContextMock.Object.Result.IsModelSet); - Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues); + Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues); } [Fact] public async Task BindModelAsync_CorrectlyBindsEmptyEnumArrayQuery() { var queryParamName = "test"; - var queryParamValues = Array.Empty<TestType>(); + IReadOnlyList<TestType> queryParamValues = Array.Empty<TestType>(); var queryParamType = typeof(TestType[]); var modelBinder = new CommaDelimitedArrayModelBinder(); @@ -168,7 +168,7 @@ namespace Jellyfin.Api.Tests.ModelBinders await modelBinder.BindModelAsync(bindingContextMock.Object); Assert.True(bindingContextMock.Object.Result.IsModelSet); - Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues); + Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues); } [Fact] diff --git a/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs b/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs index 3a85b5514..03ab56d1f 100644 --- a/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs +++ b/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs @@ -30,7 +30,7 @@ namespace Jellyfin.Api.Tests // Assert response.EnsureSuccessStatusCode(); - Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString()); + Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString()); // Write out for publishing var responseBody = await response.Content.ReadAsStringAsync(); diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs index c4ce39885..f27cdf7b6 100644 --- a/tests/Jellyfin.Api.Tests/TestHelpers.cs +++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs @@ -60,7 +60,7 @@ namespace Jellyfin.Api.Tests .Returns(user); httpContextAccessorMock - .Setup(h => h.HttpContext.Connection.RemoteIpAddress) + .Setup(h => h.HttpContext!.Connection.RemoteIpAddress) .Returns(new IPAddress(0)); return new ClaimsPrincipal(identity); -- cgit v1.2.3 From e8b98265b0d0e66fe7ef6b8a0acb28725b86ebe6 Mon Sep 17 00:00:00 2001 From: crobibero <cody@robibe.ro> Date: Fri, 13 Nov 2020 10:40:09 -0700 Subject: Upgrade Jellyfin.Model.Tests to net5.0 --- tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj index e93385136..64d51e063 100644 --- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj +++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netcoreapp3.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <IsPackable>false</IsPackable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> -- cgit v1.2.3 From 056c44010b437b0f718cb10b1ed66a7f38d61874 Mon Sep 17 00:00:00 2001 From: crobibero <cody@robibe.ro> Date: Sun, 15 Nov 2020 12:19:07 -0700 Subject: Fix tests --- .../ModelBinders/CommaDelimitedArrayModelBinderTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs index 94f2800d4..3ae6ae5bd 100644 --- a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs +++ b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs @@ -177,7 +177,7 @@ namespace Jellyfin.Api.Tests.ModelBinders { var queryParamName = "test"; var queryParamString = "🔥,😢"; - var queryParamType = typeof(TestType[]); + var queryParamType = typeof(IReadOnlyList<TestType>); var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var valueProvider = new QueryStringValueProvider( @@ -192,7 +192,7 @@ namespace Jellyfin.Api.Tests.ModelBinders await modelBinder.BindModelAsync(bindingContextMock.Object); Assert.True(bindingContextMock.Object.Result.IsModelSet); - Assert.Empty((TestType[])bindingContextMock.Object.Result.Model); + Assert.Empty((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model); } [Fact] @@ -201,7 +201,7 @@ namespace Jellyfin.Api.Tests.ModelBinders var queryParamName = "test"; var queryParamString1 = "How"; var queryParamString2 = "😱"; - var queryParamType = typeof(TestType[]); + var queryParamType = typeof(IReadOnlyList<TestType>); var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); @@ -220,7 +220,7 @@ namespace Jellyfin.Api.Tests.ModelBinders await modelBinder.BindModelAsync(bindingContextMock.Object); Assert.True(bindingContextMock.Object.Result.IsModelSet); - Assert.Single((TestType[])bindingContextMock.Object.Result.Model); + Assert.Single((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model); } } } -- cgit v1.2.3 From cfb7523ea8461ee4cb19ea8bb8511196d336d0e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 15 Nov 2020 20:47:21 +0000 Subject: Bump Moq from 4.14.7 to 4.15.1 Bumps [Moq](https://github.com/moq/moq4) from 4.14.7 to 4.15.1. - [Release notes](https://github.com/moq/moq4/releases) - [Changelog](https://github.com/moq/moq4/blob/master/CHANGELOG.md) - [Commits](https://github.com/moq/moq4/commits) Signed-off-by: dependabot[bot] <support@github.com> --- tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj | 2 +- .../Jellyfin.Server.Implementations.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 5bf322f07..14eed30e0 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -22,7 +22,7 @@ <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> - <PackageReference Include="Moq" Version="4.14.7" /> + <PackageReference Include="Moq" Version="4.15.1" /> </ItemGroup> <!-- Code Analyzers --> diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index b960fda72..547f80ed9 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -17,7 +17,7 @@ <PackageReference Include="AutoFixture" Version="4.14.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> - <PackageReference Include="Moq" Version="4.14.7" /> + <PackageReference Include="Moq" Version="4.15.1" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> -- cgit v1.2.3 From 979de240cb758acc7866095e6a50ed5ccc6f1d38 Mon Sep 17 00:00:00 2001 From: Greenback <jimcartlidge@yahoo.co.uk> Date: Mon, 16 Nov 2020 17:30:27 +0000 Subject: Updated tests to .Net5 --- Emby.Dlna/Main/DlnaEntryPoint.cs | 3 - Jellyfin.Networking/Jellyfin.Networking.csproj | 2 +- .../Jellyfin.Networking.Tests.csproj | 21 +++-- .../NetworkTesting/UnitTesting.cs | 92 ++++++++++++++++++---- 4 files changed, 93 insertions(+), 25 deletions(-) (limited to 'tests') diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 463ed8d67..fb4454a34 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -259,8 +259,6 @@ namespace Emby.Dlna.Main private void RegisterServerEndpoints() { - var addresses = await _appHost.GetLocalIpAddresses().ConfigureAwait(false); - var udn = CreateUuid(_appHost.SystemId); var descriptorUri = "/dlna/" + udn + "/description.xml"; @@ -292,7 +290,6 @@ namespace Emby.Dlna.Main _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); - var descriptorUri = "/dlna/" + udn + "/description.xml"; var uri = new Uri(_appHost.GetSmartApiUrl(address.Address) + descriptorUri); var device = new SsdpRootDevice diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index 1747a1dc7..cbda74361 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj index fa18316df..703d43210 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj @@ -6,23 +6,34 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>netcoreapp3.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <IsPackable>false</IsPackable> - <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> - <PackageReference Include="coverlet.collector" Version="1.2.1" /> + <PackageReference Include="coverlet.collector" Version="1.3.0" /> <PackageReference Include="Moq" Version="4.14.5" /> </ItemGroup> + <!-- Code Analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> <ItemGroup> <ProjectReference Include="..\..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" /> <ProjectReference Include="..\..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> </ItemGroup> - + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../../jellyfin-tests.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> + <DefineConstants>DEBUG</DefineConstants> + </PropertyGroup> </Project> diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs index c96c7defd..03b733a9a 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs @@ -43,8 +43,7 @@ namespace NetworkTesting return false; } - - private IConfigurationManager GetMockConfig(NetworkConfiguration conf) + private static IConfigurationManager GetMockConfig(NetworkConfiguration conf) { var configManager = new Mock<IConfigurationManager> { @@ -64,11 +63,11 @@ namespace NetworkTesting { EnableIPV6 = true, EnableIPV4 = true, - LocalNetworkSubnets = lan.Split(';') + LocalNetworkSubnets = lan?.Split(';') ?? throw new ArgumentNullException(nameof(lan)) }; NetworkManager.MockNetworkSettings = interfaces; - var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; Assert.True(string.Equals(nm.GetInternalBindAddresses().AsString(), value, StringComparison.Ordinal)); @@ -78,6 +77,11 @@ namespace NetworkTesting [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")] public void TextIsInNetwork(string network, string value) { + if (network == null) + { + throw new ArgumentNullException(nameof(network)); + } + var conf = new NetworkConfiguration() { EnableIPV6 = true, @@ -85,7 +89,7 @@ namespace NetworkTesting LocalNetworkSubnets = network.Split(',') }; - var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); Assert.True(!nm.IsInLocalNetwork(value)); } @@ -153,16 +157,21 @@ namespace NetworkTesting "[192.158.0.0/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]")] public void TestCollections(string settings, string result1, string result2, string result3, string result4, string result5) { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + var conf = new NetworkConfiguration() { EnableIPV6 = true, EnableIPV4 = true, }; - var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); // Test included, IP6. - NetCollection nc = nm.CreateIPCollection(settings.Split(","), false); + NetCollection nc = nm.CreateIPCollection(settings.Split(","), false); Assert.True(string.Equals(nc?.AsString(), result1, System.StringComparison.OrdinalIgnoreCase)); // Text excluded, non IP6. @@ -194,13 +203,29 @@ namespace NetworkTesting [InlineData("127.0.0.1", "127.0.0.1/8", "[127.0.0.1/32]")] public void UnionCheck(string settings, string compare, string result) { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (compare == null) + { + throw new ArgumentNullException(nameof(compare)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + var conf = new NetworkConfiguration() { EnableIPV6 = true, EnableIPV4 = true, }; - var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); NetCollection nc1 = nm.CreateIPCollection(settings.Split(","), false); NetCollection nc2 = nm.CreateIPCollection(compare.Split(","), false); @@ -276,12 +301,7 @@ namespace NetworkTesting { Assert.True(TryParse(network, out IPObject? networkObj)); Assert.True(TryParse(ip, out IPObject? ipObj)); - -#pragma warning disable CS8602 // Dereference of a possibly null reference. -#pragma warning disable CS8604 // Possible null reference argument. Assert.True(networkObj.Contains(ipObj)); -#pragma warning restore CS8604 // Possible null reference argument. -#pragma warning restore CS8602 // Dereference of a possibly null reference. } [Theory] @@ -293,13 +313,28 @@ namespace NetworkTesting public void TestMatches(string source, string dest, string result) { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (dest == null) + { + throw new ArgumentNullException(nameof(dest)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + var conf = new NetworkConfiguration() { EnableIPV6 = true, EnableIPV4 = true }; - var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); // Test included, IP6. NetCollection ncSource = nm.CreateIPCollection(source.Split(",")); @@ -343,6 +378,21 @@ namespace NetworkTesting [InlineData("", "", false, "eth16")] public void TestBindInterfaces(string source, string bindAddresses, bool ipv6enabled, string result) { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (bindAddresses == null) + { + throw new ArgumentNullException(nameof(bindAddresses)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + var conf = new NetworkConfiguration() { LocalNetworkAddresses = bindAddresses.Split(','), @@ -351,7 +401,7 @@ namespace NetworkTesting }; NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11"; - var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; _ = nm.TryParseInterface(result, out NetCollection? resultObj); @@ -398,6 +448,16 @@ namespace NetworkTesting public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result) { + if (lan == null) + { + throw new ArgumentNullException(nameof(lan)); + } + + if (bindAddresses == null) + { + throw new ArgumentNullException(nameof(bindAddresses)); + } + var conf = new NetworkConfiguration() { LocalNetworkSubnets = lan.Split(','), @@ -408,7 +468,7 @@ namespace NetworkTesting }; NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11"; - var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; if (nm.TryParseInterface(result, out NetCollection? resultObj) && resultObj != null) -- cgit v1.2.3 From 3cc0dd7e12380a7e4a0ef86591890ece8421b14c Mon Sep 17 00:00:00 2001 From: crobibero <cody@robibe.ro> Date: Mon, 16 Nov 2020 20:29:46 -0700 Subject: Reduce RequestHelpers.Split usage and remove RequestHelpers.GetGuids usage. --- .../Channels/ChannelManager.cs | 2 +- .../Data/SqliteItemRepository.cs | 8 +- .../Library/LibraryManager.cs | 2 +- .../Playlists/PlaylistManager.cs | 6 +- Jellyfin.Api/Controllers/ArtistsController.cs | 100 +++++---- Jellyfin.Api/Controllers/CollectionController.cs | 17 +- Jellyfin.Api/Controllers/GenresController.cs | 10 +- Jellyfin.Api/Controllers/ItemsController.cs | 103 +++++----- Jellyfin.Api/Controllers/LibraryController.cs | 13 +- Jellyfin.Api/Controllers/LiveTvController.cs | 28 ++- Jellyfin.Api/Controllers/MusicGenresController.cs | 10 +- Jellyfin.Api/Controllers/PersonsController.cs | 8 +- Jellyfin.Api/Controllers/PlaylistsController.cs | 13 +- Jellyfin.Api/Controllers/SearchController.cs | 13 +- Jellyfin.Api/Controllers/SessionController.cs | 8 +- Jellyfin.Api/Controllers/StudiosController.cs | 13 +- Jellyfin.Api/Controllers/SuggestionsController.cs | 9 +- Jellyfin.Api/Controllers/TrailersController.cs | 38 ++-- .../Controllers/UniversalAudioController.cs | 8 +- Jellyfin.Api/Controllers/UserLibraryController.cs | 4 +- Jellyfin.Api/Controllers/UserViewsController.cs | 7 +- Jellyfin.Api/Controllers/VideosController.cs | 5 +- Jellyfin.Api/Controllers/YearsController.cs | 18 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 43 ---- .../ModelBinders/PipeDelimitedArrayModelBinder.cs | 90 ++++++++ Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs | 9 +- .../Models/PlaylistDtos/CreatePlaylistDto.cs | 6 +- .../Converters/JsonPipeDelimitedArrayConverter.cs | 74 +++++++ .../JsonPipeDelimitedArrayConverterFactory.cs | 28 +++ MediaBrowser.Common/MediaBrowser.Common.csproj | 2 +- MediaBrowser.Controller/Entities/Folder.cs | 6 +- .../Entities/InternalItemsQuery.cs | 6 +- .../Entities/UserViewBuilder.cs | 4 +- .../Playlists/IPlaylistManager.cs | 2 +- .../Playlists/PlaylistCreationRequest.cs | 8 +- .../PipeDelimitedArrayModelBinderTests.cs | 226 +++++++++++++++++++++ 36 files changed, 659 insertions(+), 288 deletions(-) create mode 100644 Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs create mode 100644 MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs create mode 100644 MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs create mode 100644 tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs (limited to 'tests') diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 19045b72b..3d97a6ca8 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -634,7 +634,7 @@ namespace Emby.Server.Implementations.Channels { var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray(); - if (query.ChannelIds.Length > 0) + if (query.ChannelIds.Count > 0) { // Avoid implicitly captured closure var ids = query.ChannelIds; diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 638c7a9b4..7e01bd4b6 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -3611,12 +3611,12 @@ namespace Emby.Server.Implementations.Data whereClauses.Add($"type in ({inClause})"); } - if (query.ChannelIds.Length == 1) + if (query.ChannelIds.Count == 1) { whereClauses.Add("ChannelId=@ChannelId"); statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); } - else if (query.ChannelIds.Length > 1) + else if (query.ChannelIds.Count > 1) { var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); whereClauses.Add($"ChannelId in ({inClause})"); @@ -4076,7 +4076,7 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(clause); } - if (query.GenreIds.Length > 0) + if (query.GenreIds.Count > 0) { var clauses = new List<string>(); var index = 0; @@ -4097,7 +4097,7 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(clause); } - if (query.Genres.Length > 0) + if (query.Genres.Count > 0) { var clauses = new List<string>(); var index = 0; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 8ab5e4aef..03bf1c874 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1503,7 +1503,7 @@ namespace Emby.Server.Implementations.Library { if (query.AncestorIds.Length == 0 && query.ParentId.Equals(Guid.Empty) && - query.ChannelIds.Length == 0 && + query.ChannelIds.Count == 0 && query.TopParentIds.Length == 0 && string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) && string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) && diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index d3b64fb31..932f721ab 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -150,7 +150,7 @@ namespace Emby.Server.Implementations.Playlists await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None) .ConfigureAwait(false); - if (options.ItemIdList.Length > 0) + if (options.ItemIdList.Count > 0) { await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false) { @@ -184,7 +184,7 @@ namespace Emby.Server.Implementations.Playlists return Playlist.GetPlaylistItems(playlistMediaType, items, user, options); } - public Task AddToPlaylistAsync(Guid playlistId, ICollection<Guid> itemIds, Guid userId) + public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId) { var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId); @@ -194,7 +194,7 @@ namespace Emby.Server.Implementations.Playlists }); } - private async Task AddToPlaylistInternal(Guid playlistId, ICollection<Guid> newItemIds, User user, DtoOptions options) + private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options) { // Retrieve the existing playlist var playlist = _libraryManager.GetItemById(playlistId) as Playlist diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 9bad206e0..0123bb8fa 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -89,24 +89,24 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery] string? mediaTypes, - [FromQuery] string? genres, - [FromQuery] string? genreIds, - [FromQuery] string? officialRatings, - [FromQuery] string? tags, - [FromQuery] string? years, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] string? person, - [FromQuery] string? personIds, - [FromQuery] string? personTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, [FromQuery] string? studios, - [FromQuery] string? studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, [FromQuery] Guid? userId, [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, @@ -131,30 +131,26 @@ namespace Jellyfin.Api.Controllers parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); } - var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true); - var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true); - var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true); - var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypesArr, - IncludeItemTypes = includeItemTypesArr, - MediaTypes = mediaTypesArr, + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, + MediaTypes = mediaTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, NameLessThan = nameLessThan, NameStartsWith = nameStartsWith, NameStartsWithOrGreater = nameStartsWithOrGreater, - Tags = RequestHelpers.Split(tags, '|', true), - OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), - Genres = RequestHelpers.Split(genres, '|', true), - GenreIds = RequestHelpers.GetGuids(genreIds), - StudioIds = RequestHelpers.GetGuids(studioIds), + Tags = tags, + OfficialRatings = officialRatings, + Genres = genres, + GenreIds = genreIds, + StudioIds = studioIds, Person = person, - PersonIds = RequestHelpers.GetGuids(personIds), - PersonTypes = RequestHelpers.Split(personTypes, ',', true), - Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), + PersonIds = personIds, + PersonTypes = personTypes, + Years = years, MinCommunityRating = minCommunityRating, DtoOptions = dtoOptions, SearchTerm = searchTerm, @@ -230,7 +226,7 @@ namespace Jellyfin.Api.Controllers var (baseItem, itemCounts) = i; var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); - if (!string.IsNullOrWhiteSpace(includeItemTypes)) + if (includeItemTypes.Length != 0) { dto.ChildCount = itemCounts.ItemCount; dto.ProgramCount = itemCounts.ProgramCount; @@ -297,24 +293,24 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery] string? mediaTypes, - [FromQuery] string? genres, - [FromQuery] string? genreIds, - [FromQuery] string? officialRatings, - [FromQuery] string? tags, - [FromQuery] string? years, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] string? person, - [FromQuery] string? personIds, - [FromQuery] string? personTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, [FromQuery] string? studios, - [FromQuery] string? studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, [FromQuery] Guid? userId, [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, @@ -339,30 +335,26 @@ namespace Jellyfin.Api.Controllers parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); } - var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true); - var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true); - var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true); - var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypesArr, - IncludeItemTypes = includeItemTypesArr, - MediaTypes = mediaTypesArr, + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, + MediaTypes = mediaTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, NameLessThan = nameLessThan, NameStartsWith = nameStartsWith, NameStartsWithOrGreater = nameStartsWithOrGreater, - Tags = RequestHelpers.Split(tags, '|', true), - OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), - Genres = RequestHelpers.Split(genres, '|', true), - GenreIds = RequestHelpers.GetGuids(genreIds), - StudioIds = RequestHelpers.GetGuids(studioIds), + Tags = tags, + OfficialRatings = officialRatings, + Genres = genres, + GenreIds = genreIds, + StudioIds = studioIds, Person = person, - PersonIds = RequestHelpers.GetGuids(personIds), - PersonTypes = RequestHelpers.Split(personTypes, ',', true), - Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), + PersonIds = personIds, + PersonTypes = personTypes, + Years = years, MinCommunityRating = minCommunityRating, DtoOptions = dtoOptions, SearchTerm = searchTerm, @@ -438,7 +430,7 @@ namespace Jellyfin.Api.Controllers var (baseItem, itemCounts) = i; var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); - if (!string.IsNullOrWhiteSpace(includeItemTypes)) + if (includeItemTypes.Length != 0) { dto.ChildCount = itemCounts.ItemCount; dto.ProgramCount = itemCounts.ProgramCount; diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index eae06b767..2a342c2cb 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.Helpers; +using Jellyfin.Api.ModelBinders; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Net; @@ -54,7 +55,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<CollectionCreationResult>> CreateCollection( [FromQuery] string? name, - [FromQuery] string? ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids, [FromQuery] Guid? parentId, [FromQuery] bool isLocked = false) { @@ -65,7 +66,7 @@ namespace Jellyfin.Api.Controllers IsLocked = isLocked, Name = name, ParentId = parentId, - ItemIdList = RequestHelpers.Split(ids, ',', true), + ItemIdList = ids, UserIds = new[] { userId } }).ConfigureAwait(false); @@ -88,9 +89,11 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("{collectionId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids) + public async Task<ActionResult> AddToCollection( + [FromRoute, Required] Guid collectionId, + [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) { - await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(true); + await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true); return NoContent(); } @@ -103,9 +106,11 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpDelete("{collectionId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids) + public async Task<ActionResult> RemoveFromCollection( + [FromRoute, Required] Guid collectionId, + [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) { - await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(false); + await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false); return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 9c222135d..2dd504770 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -74,8 +74,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery] bool? isFavorite, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), - IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, @@ -133,7 +133,7 @@ namespace Jellyfin.Api.Controllers result = _libraryManager.GetGenres(query); } - var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes); + var shouldIncludeItemTypes = includeItemTypes.Length != 0; return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index d8d371ebc..cff06aafe 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -173,7 +173,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? hasImdbId, [FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTvdbId, - [FromQuery] string? excludeItemIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool? recursive, @@ -181,34 +181,34 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? sortOrder, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery] string? mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery] string? sortBy, [FromQuery] bool? isPlayed, - [FromQuery] string? genres, - [FromQuery] string? officialRatings, - [FromQuery] string? tags, - [FromQuery] string? years, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] string? person, - [FromQuery] string? personIds, - [FromQuery] string? personTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, [FromQuery] string? studios, [FromQuery] string? artists, - [FromQuery] string? excludeArtistIds, - [FromQuery] string? artistIds, - [FromQuery] string? albumArtistIds, - [FromQuery] string? contributingArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds, [FromQuery] string? albums, - [FromQuery] string? albumIds, - [FromQuery] string? ids, - [FromQuery] string? videoTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes, [FromQuery] string? minOfficialRating, [FromQuery] bool? isLocked, [FromQuery] bool? isPlaceHolder, @@ -223,8 +223,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery] string? studioIds, - [FromQuery] string? genreIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { @@ -238,8 +238,9 @@ namespace Jellyfin.Api.Controllers .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - if (string.Equals(includeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase)) + if (includeItemTypes.Length == 1 + && (includeItemTypes[0].Equals("Playlist", StringComparison.OrdinalIgnoreCase) + || includeItemTypes[0].Equals("BoxSet", StringComparison.OrdinalIgnoreCase))) { parentId = null; } @@ -262,7 +263,7 @@ namespace Jellyfin.Api.Controllers && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) { recursive = true; - includeItemTypes = "Playlist"; + includeItemTypes = new[] { "Playlist" }; } bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id) @@ -291,14 +292,14 @@ namespace Jellyfin.Api.Controllers return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}."); } - if ((recursive.HasValue && recursive.Value) || !string.IsNullOrEmpty(ids) || !(item is UserRootFolder)) + if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || !(item is UserRootFolder)) { var query = new InternalItemsQuery(user!) { IsPlayed = isPlayed, - MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), - IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), - ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), + MediaTypes = mediaTypes, + IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = excludeItemTypes, Recursive = recursive ?? false, OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), IsFavorite = isFavorite, @@ -330,28 +331,28 @@ namespace Jellyfin.Api.Controllers HasTrailer = hasTrailer, IsHD = isHd, Is4K = is4K, - Tags = RequestHelpers.Split(tags, '|', true), - OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), - Genres = RequestHelpers.Split(genres, '|', true), - ArtistIds = RequestHelpers.GetGuids(artistIds), - AlbumArtistIds = RequestHelpers.GetGuids(albumArtistIds), - ContributingArtistIds = RequestHelpers.GetGuids(contributingArtistIds), - GenreIds = RequestHelpers.GetGuids(genreIds), - StudioIds = RequestHelpers.GetGuids(studioIds), + Tags = tags, + OfficialRatings = officialRatings, + Genres = genres, + ArtistIds = artistIds, + AlbumArtistIds = albumArtistIds, + ContributingArtistIds = contributingArtistIds, + GenreIds = genreIds, + StudioIds = studioIds, Person = person, - PersonIds = RequestHelpers.GetGuids(personIds), - PersonTypes = RequestHelpers.Split(personTypes, ',', true), - Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), + PersonIds = personIds, + PersonTypes = personTypes, + Years = years, ImageTypes = imageTypes, - VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(), + VideoTypes = videoTypes, AdjacentTo = adjacentTo, - ItemIds = RequestHelpers.GetGuids(ids), + ItemIds = ids, MinCommunityRating = minCommunityRating, MinCriticRating = minCriticRating, ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId), ParentIndexNumber = parentIndexNumber, EnableTotalRecordCount = enableTotalRecordCount, - ExcludeItemIds = RequestHelpers.GetGuids(excludeItemIds), + ExcludeItemIds = excludeItemIds, DtoOptions = dtoOptions, SearchTerm = searchTerm, MinDateLastSaved = minDateLastSaved?.ToUniversalTime(), @@ -360,7 +361,7 @@ namespace Jellyfin.Api.Controllers MaxPremiereDate = maxPremiereDate?.ToUniversalTime(), }; - if (!string.IsNullOrWhiteSpace(ids) || !string.IsNullOrWhiteSpace(searchTerm)) + if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm)) { query.CollapseBoxSetItems = false; } @@ -449,14 +450,14 @@ namespace Jellyfin.Api.Controllers } // ExcludeArtistIds - if (!string.IsNullOrWhiteSpace(excludeArtistIds)) + if (excludeArtistIds.Length != 0) { - query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds); + query.ExcludeArtistIds = excludeArtistIds; } - if (!string.IsNullOrWhiteSpace(albumIds)) + if (albumIds.Length != 0) { - query.AlbumIds = RequestHelpers.GetGuids(albumIds); + query.AlbumIds = albumIds; } // Albums @@ -533,12 +534,12 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { @@ -569,13 +570,13 @@ namespace Jellyfin.Api.Controllers ParentId = parentIdGuid, Recursive = true, DtoOptions = dtoOptions, - MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), + MediaTypes = mediaTypes, IsVirtualItem = false, CollapseBoxSetItems = false, EnableTotalRecordCount = enableTotalRecordCount, AncestorIds = ancestorIds, - IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), - ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), + IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = excludeItemTypes, SearchTerm = searchTerm }); diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 1b115d800..3ff77e8e0 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -362,15 +362,14 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public ActionResult DeleteItems([FromQuery] string? ids) + public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids) { - if (string.IsNullOrEmpty(ids)) + if (ids.Length == 0) { return NoContent(); } - var itemIds = RequestHelpers.Split(ids, ',', true); - foreach (var i in itemIds) + foreach (var i in ids) { var item = _libraryManager.GetItemById(i); var auth = _authContext.GetAuthorizationInfo(Request); @@ -691,7 +690,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems( [FromRoute, Required] Guid itemId, - [FromQuery] string? excludeArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, [FromQuery] Guid? userId, [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) @@ -753,9 +752,9 @@ namespace Jellyfin.Api.Controllers }; // ExcludeArtistIds - if (!string.IsNullOrEmpty(excludeArtistIds)) + if (excludeArtistIds.Length != 0) { - query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds); + query.ExcludeArtistIds = excludeArtistIds; } List<BaseItem> itemsResult = _libraryManager.GetItemList(query); diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 29c0f1df4..eb8b42b34 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -150,7 +150,7 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, - [FromQuery] string? sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery] SortOrder? sortOrder, [FromQuery] bool enableFavoriteSorting = false, [FromQuery] bool addCurrentProgram = true) @@ -175,7 +175,7 @@ namespace Jellyfin.Api.Controllers IsNews = isNews, IsKids = isKids, IsSports = isSports, - SortBy = RequestHelpers.Split(sortBy, ',', true), + SortBy = sortBy, SortOrder = sortOrder ?? SortOrder.Ascending, AddCurrentProgram = addCurrentProgram }, @@ -539,7 +539,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms( - [FromQuery] string? channelIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds, [FromQuery] Guid? userId, [FromQuery] DateTime? minStartDate, [FromQuery] bool? hasAired, @@ -556,8 +556,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery] string? sortBy, [FromQuery] string? sortOrder, - [FromQuery] string? genres, - [FromQuery] string? genreIds, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -573,8 +573,7 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ChannelIds = RequestHelpers.Split(channelIds, ',', true) - .Select(i => new Guid(i)).ToArray(), + ChannelIds = channelIds, HasAired = hasAired, IsAiring = isAiring, EnableTotalRecordCount = enableTotalRecordCount, @@ -591,8 +590,8 @@ namespace Jellyfin.Api.Controllers IsKids = isKids, IsSports = isSports, SeriesTimerId = seriesTimerId, - Genres = RequestHelpers.Split(genres, '|', true), - GenreIds = RequestHelpers.GetGuids(genreIds) + Genres = genres, + GenreIds = genreIds }; if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty)) @@ -628,8 +627,7 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ChannelIds = RequestHelpers.Split(body.ChannelIds, ',', true) - .Select(i => new Guid(i)).ToArray(), + ChannelIds = body.ChannelIds, HasAired = body.HasAired, IsAiring = body.IsAiring, EnableTotalRecordCount = body.EnableTotalRecordCount, @@ -646,8 +644,8 @@ namespace Jellyfin.Api.Controllers IsKids = body.IsKids, IsSports = body.IsSports, SeriesTimerId = body.SeriesTimerId, - Genres = RequestHelpers.Split(body.Genres, '|', true), - GenreIds = RequestHelpers.GetGuids(body.GenreIds) + Genres = body.Genres, + GenreIds = body.GenreIds }; if (!body.LibrarySeriesId.Equals(Guid.Empty)) @@ -703,7 +701,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] string? genreIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, [FromQuery] bool enableTotalRecordCount = true) @@ -723,7 +721,7 @@ namespace Jellyfin.Api.Controllers IsNews = isNews, IsSports = isSports, EnableTotalRecordCount = enableTotalRecordCount, - GenreIds = RequestHelpers.GetGuids(genreIds) + GenreIds = genreIds }; var dtoOptions = new DtoOptions { Fields = fields } diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 229d9ff02..8c6104302 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -74,8 +74,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery] bool? isFavorite, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), - IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, @@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers var result = _libraryManager.GetMusicGenres(query); - var shouldIncludeItemTypes = !string.IsNullOrWhiteSpace(includeItemTypes); + var shouldIncludeItemTypes = includeItemTypes.Length != 0; return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); } diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 6ac3e6417..9dc79b388 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -77,8 +77,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] string? excludePersonTypes, - [FromQuery] string? personTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, [FromQuery] string? appearsInItemId, [FromQuery] Guid? userId, [FromQuery] bool? enableImages = true) @@ -97,8 +97,8 @@ namespace Jellyfin.Api.Controllers var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite); var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery { - PersonTypes = RequestHelpers.Split(personTypes, ',', true), - ExcludePersonTypes = RequestHelpers.Split(excludePersonTypes, ',', true), + PersonTypes = personTypes, + ExcludePersonTypes = excludePersonTypes, NameContains = searchTerm, User = user, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 4b3d8d3d3..bc47ecbd1 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -63,11 +63,10 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist( [FromBody, Required] CreatePlaylistDto createPlaylistRequest) { - Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids); var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest { Name = createPlaylistRequest.Name, - ItemIdList = idGuidArray, + ItemIdList = createPlaylistRequest.Ids, UserId = createPlaylistRequest.UserId, MediaType = createPlaylistRequest.MediaType }).ConfigureAwait(false); @@ -87,10 +86,10 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> AddToPlaylist( [FromRoute, Required] Guid playlistId, - [FromQuery] string? ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, [FromQuery] Guid? userId) { - await _playlistManager.AddToPlaylistAsync(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty).ConfigureAwait(false); + await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false); return NoContent(); } @@ -122,9 +121,11 @@ namespace Jellyfin.Api.Controllers /// <returns>An <see cref="NoContentResult"/> on success.</returns> [HttpDelete("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task<ActionResult> RemoveFromPlaylist([FromRoute, Required] string playlistId, [FromQuery] string? entryIds) + public async Task<ActionResult> RemoveFromPlaylist( + [FromRoute, Required] string playlistId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds) { - await _playlistManager.RemoveFromPlaylistAsync(playlistId, RequestHelpers.Split(entryIds, ',', true)).ConfigureAwait(false); + await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index e75f0d06b..076fe58f1 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; +using Jellyfin.Api.ModelBinders; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -82,9 +83,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery] Guid? userId, [FromQuery, Required] string searchTerm, - [FromQuery] string? includeItemTypes, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery] string? parentId, [FromQuery] bool? isMovie, [FromQuery] bool? isSeries, @@ -108,9 +109,9 @@ namespace Jellyfin.Api.Controllers IncludeStudios = includeStudios, StartIndex = startIndex, UserId = userId ?? Guid.Empty, - IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), - ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), - MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), + IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = excludeItemTypes, + MediaTypes = mediaTypes, ParentId = parentId, IsKids = isKids, diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index e506ac7bf..6c9b9050e 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -160,12 +160,12 @@ namespace Jellyfin.Api.Controllers public ActionResult Play( [FromRoute, Required] string sessionId, [FromQuery, Required] PlayCommand playCommand, - [FromQuery, Required] string itemIds, + [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds, [FromQuery] long? startPositionTicks) { var playRequest = new PlayRequest { - ItemIds = RequestHelpers.GetGuids(itemIds), + ItemIds = itemIds, StartPositionTicks = startPositionTicks, PlayCommand = playCommand }; @@ -378,7 +378,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostCapabilities( [FromQuery] string? id, - [FromQuery] string? playableMediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, [FromQuery] bool supportsMediaControl = false, [FromQuery] bool supportsSync = false, @@ -391,7 +391,7 @@ namespace Jellyfin.Api.Controllers _sessionManager.ReportCapabilities(id, new ClientCapabilities { - PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true), + PlayableMediaTypes = playableMediaTypes, SupportedCommands = supportedCommands, SupportsMediaControl = supportsMediaControl, SupportsSync = supportsSync, diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index 27dcd51bc..af28b4f59 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -73,8 +73,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery] bool? isFavorite, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, @@ -94,13 +94,10 @@ namespace Jellyfin.Api.Controllers var parentItem = _libraryManager.GetParentItem(parentId, userId); - var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true); - var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true); - var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypesArr, - IncludeItemTypes = includeItemTypesArr, + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, @@ -125,7 +122,7 @@ namespace Jellyfin.Api.Controllers } var result = _libraryManager.GetStudios(query); - var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes); + var shouldIncludeItemTypes = includeItemTypes.Length != 0; return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); } diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index ad64adfba..69292186e 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -4,6 +4,7 @@ using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; +using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -58,8 +59,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetSuggestions( [FromRoute, Required] Guid userId, - [FromQuery] string? mediaType, - [FromQuery] string? type, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] type, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool enableTotalRecordCount = false) @@ -70,8 +71,8 @@ namespace Jellyfin.Api.Controllers var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), - MediaTypes = RequestHelpers.Split(mediaType!, ',', true), - IncludeItemTypes = RequestHelpers.Split(type!, ',', true), + MediaTypes = mediaType, + IncludeItemTypes = type, IsVirtualItem = false, StartIndex = startIndex, Limit = limit, diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index d78adcbcd..f09a6a91a 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -139,7 +139,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? hasImdbId, [FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTvdbId, - [FromQuery] string? excludeItemIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool? recursive, @@ -147,33 +147,33 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? sortOrder, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery] string? mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery] string? sortBy, [FromQuery] bool? isPlayed, - [FromQuery] string? genres, - [FromQuery] string? officialRatings, - [FromQuery] string? tags, - [FromQuery] string? years, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] string? person, - [FromQuery] string? personIds, - [FromQuery] string? personTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, [FromQuery] string? studios, [FromQuery] string? artists, - [FromQuery] string? excludeArtistIds, - [FromQuery] string? artistIds, - [FromQuery] string? albumArtistIds, - [FromQuery] string? contributingArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds, [FromQuery] string? albums, - [FromQuery] string? albumIds, - [FromQuery] string? ids, - [FromQuery] string? videoTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes, [FromQuery] string? minOfficialRating, [FromQuery] bool? isLocked, [FromQuery] bool? isPlaceHolder, @@ -188,12 +188,12 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery] string? studioIds, - [FromQuery] string? genreIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { - var includeItemTypes = "Trailer"; + var includeItemTypes = new[] { "Trailer" }; return _itemsController .GetItems( diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index e10f1fe91..5141aebca 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; +using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Devices; @@ -96,7 +97,7 @@ namespace Jellyfin.Api.Controllers [ProducesAudioFile] public async Task<ActionResult> GetUniversalAudioStream( [FromRoute, Required] Guid itemId, - [FromQuery] string? container, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, [FromQuery] Guid? userId, @@ -258,7 +259,7 @@ namespace Jellyfin.Api.Controllers } private DeviceProfile GetDeviceProfile( - string? container, + string[] containers, string? transcodingContainer, string? audioCodec, string? transcodingProtocol, @@ -270,7 +271,6 @@ namespace Jellyfin.Api.Controllers { var deviceProfile = new DeviceProfile(); - var containers = RequestHelpers.Split(container, ',', true); int len = containers.Length; var directPlayProfiles = new DirectPlayProfile[len]; for (int i = 0; i < len; i++) @@ -327,7 +327,7 @@ namespace Jellyfin.Api.Controllers if (conditions.Count > 0) { // codec profile - codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = container, Conditions = conditions.ToArray() }); + codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = string.Join(',', containers), Conditions = conditions.ToArray() }); } deviceProfile.CodecProfiles = codecProfiles.ToArray(); diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index cfd851129..a7fb4f116 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -269,7 +269,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery] bool? isPlayed, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, @@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers new LatestItemsQuery { GroupItems = groupItems, - IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), + IncludeItemTypes = includeItemTypes, IsPlayed = isPlayed, Limit = limit, ParentId = parentId ?? Guid.Empty, diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index d575bfc3b..60fd1df01 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; +using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.UserViewDtos; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -67,7 +68,7 @@ namespace Jellyfin.Api.Controllers public ActionResult<QueryResult<BaseItemDto>> GetUserViews( [FromRoute, Required] Guid userId, [FromQuery] bool? includeExternalContent, - [FromQuery] string? presetViews, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews, [FromQuery] bool includeHidden = false) { var query = new UserViewQuery @@ -81,9 +82,9 @@ namespace Jellyfin.Api.Controllers query.IncludeExternalContent = includeExternalContent.Value; } - if (!string.IsNullOrWhiteSpace(presetViews)) + if (presetViews.Length != 0) { - query.PresetViews = RequestHelpers.Split(presetViews, ',', true); + query.PresetViews = presetViews; } var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty; diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 4de7aac71..dd5e70a50 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -10,6 +10,7 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; +using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; @@ -203,9 +204,9 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task<ActionResult> MergeVersions([FromQuery, Required] string itemIds) + public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds) { - var items = RequestHelpers.Split(itemIds, ',', true) + var items = itemIds .Select(i => _libraryManager.GetItemById(i)) .OfType<Video>() .OrderBy(i => i.Id) diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 1b38e399d..9c3ecb4ce 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -73,9 +73,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? sortOrder, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? includeItemTypes, - [FromQuery] string? mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery] string? sortBy, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, @@ -103,19 +103,15 @@ namespace Jellyfin.Api.Controllers IList<BaseItem> items; - var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true); - var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true); - var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true); - var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypesArr, - IncludeItemTypes = includeItemTypesArr, - MediaTypes = mediaTypesArr, + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, + MediaTypes = mediaTypes, DtoOptions = dtoOptions }; - bool Filter(BaseItem i) => FilterItem(i, excludeItemTypesArr, includeItemTypesArr, mediaTypesArr); + bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes); if (parentItem.IsFolder) { diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index f06f038ab..efce11f8a 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -122,49 +122,6 @@ namespace Jellyfin.Api.Helpers return session; } - /// <summary> - /// Get Guid array from string. - /// </summary> - /// <param name="value">String value.</param> - /// <returns>Guid array.</returns> - internal static Guid[] GetGuids(string? value) - { - if (value == null) - { - return Array.Empty<Guid>(); - } - - return Split(value, ',', true) - .Select(i => new Guid(i)) - .ToArray(); - } - - /// <summary> - /// Gets the item fields. - /// </summary> - /// <param name="fields">The fields string.</param> - /// <returns>IEnumerable{ItemFields}.</returns> - internal static ItemFields[] GetItemFields(string? fields) - { - if (string.IsNullOrEmpty(fields)) - { - return Array.Empty<ItemFields>(); - } - - return Split(fields, ',', true) - .Select(v => - { - if (Enum.TryParse(v, true, out ItemFields value)) - { - return (ItemFields?)value; - } - - return null; - }).Where(i => i.HasValue) - .Select(i => i!.Value) - .ToArray(); - } - internal static QueryResult<BaseItemDto> CreateQueryResult( QueryResult<(BaseItem, ItemCounts)> result, DtoOptions dtoOptions, diff --git a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs new file mode 100644 index 000000000..a42e0e4da --- /dev/null +++ b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.ModelBinders +{ + /// <summary> + /// Comma delimited array model binder. + /// Returns an empty array of specified type if there is no query parameter. + /// </summary> + public class PipeDelimitedArrayModelBinder : IModelBinder + { + private readonly ILogger<PipeDelimitedArrayModelBinder> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="PipeDelimitedArrayModelBinder"/> class. + /// </summary> + /// <param name="logger">Instance of the <see cref="ILogger{PipeDelimitedArrayModelBinder}"/> interface.</param> + public PipeDelimitedArrayModelBinder(ILogger<PipeDelimitedArrayModelBinder> logger) + { + _logger = logger; + } + + /// <inheritdoc/> + public Task BindModelAsync(ModelBindingContext bindingContext) + { + var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); + var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0]; + var converter = TypeDescriptor.GetConverter(elementType); + + if (valueProviderResult.Length > 1) + { + var typedValues = GetParsedResult(valueProviderResult.Values, elementType, converter); + bindingContext.Result = ModelBindingResult.Success(typedValues); + } + else + { + var value = valueProviderResult.FirstValue; + + if (value != null) + { + var splitValues = value.Split('|', StringSplitOptions.RemoveEmptyEntries); + var typedValues = GetParsedResult(splitValues, elementType, converter); + bindingContext.Result = ModelBindingResult.Success(typedValues); + } + else + { + var emptyResult = Array.CreateInstance(elementType, 0); + bindingContext.Result = ModelBindingResult.Success(emptyResult); + } + } + + return Task.CompletedTask; + } + + private Array GetParsedResult(IReadOnlyList<string> values, Type elementType, TypeConverter converter) + { + var parsedValues = new object?[values.Count]; + var convertedCount = 0; + for (var i = 0; i < values.Count; i++) + { + try + { + parsedValues[i] = converter.ConvertFromString(values[i].Trim()); + convertedCount++; + } + catch (FormatException e) + { + _logger.LogWarning(e, "Error converting value."); + } + } + + var typedValues = Array.CreateInstance(elementType, convertedCount); + var typedValueIndex = 0; + for (var i = 0; i < parsedValues.Length; i++) + { + if (parsedValues[i] != null) + { + typedValues.SetValue(parsedValues[i], typedValueIndex); + typedValueIndex++; + } + } + + return typedValues; + } + } +} diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs index 5ca4408d1..a47ae926c 100644 --- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs +++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs @@ -16,7 +16,8 @@ namespace Jellyfin.Api.Models.LiveTvDtos /// <summary> /// Gets or sets the channels to return guide information for. /// </summary> - public string? ChannelIds { get; set; } + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + public IReadOnlyList<Guid> ChannelIds { get; set; } = Array.Empty<Guid>(); /// <summary> /// Gets or sets optional. Filter by user id. @@ -115,12 +116,14 @@ namespace Jellyfin.Api.Models.LiveTvDtos /// <summary> /// Gets or sets the genres to return guide information for. /// </summary> - public string? Genres { get; set; } + [JsonConverter(typeof(JsonPipeDelimitedArrayConverterFactory))] + public IReadOnlyList<string> Genres { get; set; } = Array.Empty<string>(); /// <summary> /// Gets or sets the genre ids to return guide information for. /// </summary> - public string? GenreIds { get; set; } + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + public IReadOnlyList<Guid> GenreIds { get; set; } = Array.Empty<Guid>(); /// <summary> /// Gets or sets include image information in output. diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs index 0d67c86f7..d0d6889fc 100644 --- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs +++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using MediaBrowser.Common.Json.Converters; namespace Jellyfin.Api.Models.PlaylistDtos { @@ -15,7 +18,8 @@ namespace Jellyfin.Api.Models.PlaylistDtos /// <summary> /// Gets or sets item ids to add to the playlist. /// </summary> - public string? Ids { get; set; } + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + public IReadOnlyList<Guid> Ids { get; set; } = Array.Empty<Guid>(); /// <summary> /// Gets or sets the user id. diff --git a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs new file mode 100644 index 000000000..0eb6916a5 --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs @@ -0,0 +1,74 @@ +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// <summary> + /// Convert Pipe delimited string to array of type. + /// </summary> + /// <typeparam name="T">Type to convert to.</typeparam> + public class JsonPipeDelimitedArrayConverter<T> : JsonConverter<T[]> + { + private readonly TypeConverter _typeConverter; + + /// <summary> + /// Initializes a new instance of the <see cref="JsonPipeDelimitedArrayConverter{T}"/> class. + /// </summary> + public JsonPipeDelimitedArrayConverter() + { + _typeConverter = TypeDescriptor.GetConverter(typeof(T)); + } + + /// <inheritdoc /> + public override T[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + var stringEntries = reader.GetString()?.Split('|', StringSplitOptions.RemoveEmptyEntries); + if (stringEntries == null || stringEntries.Length == 0) + { + return Array.Empty<T>(); + } + + var parsedValues = new object[stringEntries.Length]; + var convertedCount = 0; + for (var i = 0; i < stringEntries.Length; i++) + { + try + { + parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim()); + convertedCount++; + } + catch (FormatException) + { + // TODO log when upgraded to .Net5 + // _logger.LogWarning(e, "Error converting value."); + } + } + + var typedValues = new T[convertedCount]; + var typedValueIndex = 0; + for (var i = 0; i < stringEntries.Length; i++) + { + if (parsedValues[i] != null) + { + typedValues.SetValue(parsedValues[i], typedValueIndex); + typedValueIndex++; + } + } + + return typedValues; + } + + return JsonSerializer.Deserialize<T[]>(ref reader, options); + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, options); + } + } +} diff --git a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs new file mode 100644 index 000000000..5e77223ef --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs @@ -0,0 +1,28 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// <summary> + /// Json Pipe delimited array converter factory. + /// </summary> + /// <remarks> + /// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow. + /// </remarks> + public class JsonPipeDelimitedArrayConverterFactory : JsonConverterFactory + { + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) + { + return true; + } + + /// <inheritdoc /> + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0]; + return (JsonConverter)Activator.CreateInstance(typeof(JsonPipeDelimitedArrayConverter<>).MakeGenericType(structType)); + } + } +} diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index b67a54983..bb9e0b8c0 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -20,7 +20,7 @@ <ItemGroup> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" /> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" /> </ItemGroup> diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index a76c8a376..ead4392f3 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1067,12 +1067,12 @@ namespace MediaBrowser.Controller.Entities return false; } - if (request.Genres.Length > 0) + if (request.Genres.Count > 0) { return false; } - if (request.GenreIds.Length > 0) + if (request.GenreIds.Count > 0) { return false; } @@ -1177,7 +1177,7 @@ namespace MediaBrowser.Controller.Entities return false; } - if (request.GenreIds.Length > 0) + if (request.GenreIds.Count > 0) { return false; } diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 904752a22..270217356 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -46,7 +46,7 @@ namespace MediaBrowser.Controller.Entities public string[] ExcludeInheritedTags { get; set; } - public string[] Genres { get; set; } + public IReadOnlyList<string> Genres { get; set; } public bool? IsSpecialSeason { get; set; } @@ -116,7 +116,7 @@ namespace MediaBrowser.Controller.Entities public Guid[] StudioIds { get; set; } - public Guid[] GenreIds { get; set; } + public IReadOnlyList<Guid> GenreIds { get; set; } public ImageType[] ImageTypes { get; set; } @@ -162,7 +162,7 @@ namespace MediaBrowser.Controller.Entities public double? MinCommunityRating { get; set; } - public Guid[] ChannelIds { get; set; } + public IReadOnlyList<Guid> ChannelIds { get; set; } public int? ParentIndexNumber { get; set; } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index a262fee15..4e33a6bbd 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -791,7 +791,7 @@ namespace MediaBrowser.Controller.Entities } // Apply genre filter - if (query.Genres.Length > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase))) + if (query.Genres.Count > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase))) { return false; } @@ -822,7 +822,7 @@ namespace MediaBrowser.Controller.Entities } // Apply genre filter - if (query.GenreIds.Length > 0 && !query.GenreIds.Any(id => + if (query.GenreIds.Count > 0 && !query.GenreIds.Any(id => { var genreItem = libraryManager.GetItemById(id); return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase); diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index fbf2c5213..f6c592070 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -31,7 +31,7 @@ namespace MediaBrowser.Controller.Playlists /// <param name="itemIds">The item ids.</param> /// <param name="userId">The user identifier.</param> /// <returns>Task.</returns> - Task AddToPlaylistAsync(Guid playlistId, ICollection<Guid> itemIds, Guid userId); + Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId); /// <summary> /// Removes from playlist. diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs index ef435b21e..e8ee49403 100644 --- a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs +++ b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs @@ -2,6 +2,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Generic; namespace MediaBrowser.Model.Playlists { @@ -9,15 +10,10 @@ namespace MediaBrowser.Model.Playlists { public string Name { get; set; } - public Guid[] ItemIdList { get; set; } + public IReadOnlyList<Guid> ItemIdList { get; set; } = Array.Empty<Guid>(); public string MediaType { get; set; } public Guid UserId { get; set; } - - public PlaylistCreationRequest() - { - ItemIdList = Array.Empty<Guid>(); - } } } diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs new file mode 100644 index 000000000..938d19a15 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Api.ModelBinders; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Primitives; +using Moq; +using Xunit; + +namespace Jellyfin.Api.Tests.ModelBinders +{ + public sealed class PipeDelimitedArrayModelBinderTests + { + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedStringArrayQuery() + { + var queryParamName = "test"; + IReadOnlyList<string> queryParamValues = new[] { "lol", "xd" }; + var queryParamString = "lol|xd"; + var queryParamType = typeof(string[]); + + var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock<ModelBindingContext>(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + await modelBinder.BindModelAsync(bindingContextMock.Object); + + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Equal((IReadOnlyList<string>?)bindingContextMock.Object?.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidDelimitedIntArrayQuery() + { + var queryParamName = "test"; + IReadOnlyList<int> queryParamValues = new[] { 42, 0 }; + var queryParamString = "42|0"; + var queryParamType = typeof(int[]); + + var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock<ModelBindingContext>(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + await modelBinder.BindModelAsync(bindingContextMock.Object); + + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Equal((IReadOnlyList<int>?)bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedEnumArrayQuery() + { + var queryParamName = "test"; + IReadOnlyList<TestType> queryParamValues = new[] { TestType.How, TestType.Much }; + var queryParamString = "How|Much"; + var queryParamType = typeof(TestType[]); + + var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock<ModelBindingContext>(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + await modelBinder.BindModelAsync(bindingContextMock.Object); + + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedEnumArrayQueryWithDoublePipes() + { + var queryParamName = "test"; + IReadOnlyList<TestType> queryParamValues = new[] { TestType.How, TestType.Much }; + var queryParamString = "How||Much"; + var queryParamType = typeof(TestType[]); + + var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock<ModelBindingContext>(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + await modelBinder.BindModelAsync(bindingContextMock.Object); + + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery() + { + var queryParamName = "test"; + IReadOnlyList<TestType> queryParamValues = new[] { TestType.How, TestType.Much }; + var queryParamString1 = "How"; + var queryParamString2 = "Much"; + var queryParamType = typeof(TestType[]); + + var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); + + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary<string, StringValues> + { + { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) }, + }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock<ModelBindingContext>(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + await modelBinder.BindModelAsync(bindingContextMock.Object); + + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsEmptyEnumArrayQuery() + { + var queryParamName = "test"; + IReadOnlyList<TestType> queryParamValues = Array.Empty<TestType>(); + var queryParamType = typeof(TestType[]); + + var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); + + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary<string, StringValues> + { + { queryParamName, new StringValues(value: null) }, + }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock<ModelBindingContext>(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + await modelBinder.BindModelAsync(bindingContextMock.Object); + + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_EnumArrayQuery_BindValidOnly() + { + var queryParamName = "test"; + var queryParamString = "🔥|😢"; + var queryParamType = typeof(IReadOnlyList<TestType>); + + var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock<ModelBindingContext>(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + await modelBinder.BindModelAsync(bindingContextMock.Object); + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Empty((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model); + } + + [Fact] + public async Task BindModelAsync_EnumArrayQuery_BindValidOnly_2() + { + var queryParamName = "test"; + var queryParamString1 = "How"; + var queryParamString2 = "😱"; + var queryParamType = typeof(IReadOnlyList<TestType>); + + var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); + + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary<string, StringValues> + { + { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) }, + }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock<ModelBindingContext>(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + await modelBinder.BindModelAsync(bindingContextMock.Object); + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Single((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model); + } + } +} -- cgit v1.2.3 From f512d5167dd317b5be24b5deb7ec7059a3d588ac Mon Sep 17 00:00:00 2001 From: crobibero <cody@robibe.ro> Date: Fri, 20 Nov 2020 11:27:37 -0700 Subject: Remove broken CleanDateTimes regex --- Emby.Naming/Common/NamingOptions.cs | 4 +--- tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs | 12 +++++------- 2 files changed, 6 insertions(+), 10 deletions(-) (limited to 'tests') diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 316ed9f89..035d1b228 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -132,9 +132,7 @@ namespace Emby.Naming.Common CleanDateTimes = new[] { @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*", - @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*", - @"(.+\w)\W+\p{Ps}(19[0-9]{2}|20[0-9]{2})\p{Pe}", // Prefer year enclosed in parenthesis (){}[] - @"(.+\w)\W+(19[0-9]{2}|20[0-9]{2})(\W|$)", // Secondary year surrounded by non-word chars + @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*" }; CleanStrings = new[] diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs index 7d105b084..b6447a7a6 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs @@ -147,13 +147,11 @@ namespace Jellyfin.Naming.Tests.Video }; yield return new object[] { - new VideoFileInfo() - { - Path = @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4", - Container = "mp4", - Name = "Rain Man", - Year = 1988, - } + new VideoFileInfo( + path: @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4", + container: "mp4", + name: "Rain Man", + year: 1988) }; } -- cgit v1.2.3 From ead881d4870f93cc3cd2876ba3c13256b466da39 Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 16:03:16 +0000 Subject: Update tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs | 2 -- 1 file changed, 2 deletions(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs index 03b733a9a..926dd9599 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs @@ -8,8 +8,6 @@ using MediaBrowser.Common.Net; using Moq; using Microsoft.Extensions.Logging.Abstractions; using Xunit; -using NetCollection = System.Collections.ObjectModel.Collection<MediaBrowser.Common.Net.IPObject>; -using XMLProperties = System.Collections.Generic.Dictionary<string, string>; namespace NetworkTesting { -- cgit v1.2.3 From 56754a2af2bf3a1d10e2b8cbedcb99e693da4431 Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 16:03:25 +0000 Subject: Update tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs index 926dd9599..e5885a4b2 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs @@ -11,7 +11,7 @@ using Xunit; namespace NetworkTesting { - public class NetTesting + public class NetworkParseTests { /// <summary> /// Trys to identify the string and return an object of that class. -- cgit v1.2.3 From 46e6128f6c4194b1f3a478e79c866be363897b4a Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 16:08:19 +0000 Subject: Update tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs index e5885a4b2..b1f0420fb 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs @@ -9,7 +9,7 @@ using Moq; using Microsoft.Extensions.Logging.Abstractions; using Xunit; -namespace NetworkTesting +namespace Jellyfin.Networking.Tests { public class NetworkParseTests { -- cgit v1.2.3 From 7403b9a9e5d2a5647f698e988071adda1598284b Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 16:10:35 +0000 Subject: Update tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs | 1 - 1 file changed, 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs index b1f0420fb..b5d59093f 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs @@ -105,7 +105,6 @@ namespace Jellyfin.Networking.Tests [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")] [InlineData("192.168.1.2/255.255.255.0")] [InlineData("192.168.1.2/24")] - public void TestCollectionCreation(string address) { Assert.True(TryParse(address, out _)); -- cgit v1.2.3 From e41d1773bac5657e2a1787f131435f5edb6c4d70 Mon Sep 17 00:00:00 2001 From: Greenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 16:14:00 +0000 Subject: changes --- Jellyfin.Api/Helpers/ClassMigrationHelper.cs | 2 +- .../NetworkTesting/UnitTesting.cs | 24 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) (limited to 'tests') diff --git a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs index 7184d49d5..a911a3324 100644 --- a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs +++ b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs @@ -1,7 +1,7 @@ using System; using System.Reflection; -namespace Jellyfin.Api.Migrations +namespace Jellyfin.Api.Helpers { /// <summary> /// A static class for copying matching properties from one object to another. diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs index b5d59093f..0bd442f0b 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs @@ -8,13 +8,14 @@ using MediaBrowser.Common.Net; using Moq; using Microsoft.Extensions.Logging.Abstractions; using Xunit; +using System.Collections.ObjectModel; namespace Jellyfin.Networking.Tests { public class NetworkParseTests { /// <summary> - /// Trys to identify the string and return an object of that class. + /// Tries to identify the string and return an object of that class. /// </summary> /// <param name="addr">String to parse.</param> /// <param name="result">IPObject to return.</param> @@ -121,7 +122,6 @@ namespace Jellyfin.Networking.Tests } [Theory] - // Src, IncIP6, incIP4, exIP6, ecIP4, net [InlineData("127.0.0.1#", "[]", "[]", @@ -168,10 +168,10 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); // Test included, IP6. - NetCollection nc = nm.CreateIPCollection(settings.Split(","), false); + Collection<IPObject> nc = nm.CreateIPCollection(settings.Split(","), false); Assert.True(string.Equals(nc?.AsString(), result1, System.StringComparison.OrdinalIgnoreCase)); - // Text excluded, non IP6. + // Test excluded, non IP6. nc = nm.CreateIPCollection(settings.Split(","), true); Assert.True(string.Equals(nc?.AsString(), result3, System.StringComparison.OrdinalIgnoreCase)); @@ -224,8 +224,8 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - NetCollection nc1 = nm.CreateIPCollection(settings.Split(","), false); - NetCollection nc2 = nm.CreateIPCollection(compare.Split(","), false); + Collection<IPObject> nc1 = nm.CreateIPCollection(settings.Split(","), false); + Collection<IPObject> nc2 = nm.CreateIPCollection(compare.Split(","), false); Assert.True(nc1.Union(nc2).AsString() == result); } @@ -334,10 +334,10 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); // Test included, IP6. - NetCollection ncSource = nm.CreateIPCollection(source.Split(",")); - NetCollection ncDest = nm.CreateIPCollection(dest.Split(",")); - NetCollection ncResult = ncSource.Union(ncDest); - NetCollection resultCollection = nm.CreateIPCollection(result.Split(",")); + Collection<IPObject> ncSource = nm.CreateIPCollection(source.Split(",")); + Collection<IPObject> ncDest = nm.CreateIPCollection(dest.Split(",")); + Collection<IPObject> ncResult = ncSource.Union(ncDest); + Collection<IPObject> resultCollection = nm.CreateIPCollection(result.Split(",")); Assert.True(ncResult.Compare(resultCollection)); } @@ -401,7 +401,7 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; - _ = nm.TryParseInterface(result, out NetCollection? resultObj); + _ = nm.TryParseInterface(result, out Collection<IPObject>? resultObj); if (resultObj != null) { @@ -468,7 +468,7 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; - if (nm.TryParseInterface(result, out NetCollection? resultObj) && resultObj != null) + if (nm.TryParseInterface(result, out Collection<IPObject>? resultObj) && resultObj != null) { // Parse out IPAddresses so we can do a string comparison. (Ignore subnet masks). result = ((IPNetAddress)resultObj[0]).ToString(true); -- cgit v1.2.3 From 9b5ae690c1d80fc8466b653f93edf7399753aa14 Mon Sep 17 00:00:00 2001 From: Greenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 16:17:26 +0000 Subject: renamed file. --- Emby.Server.Implementations/ApplicationHost.cs | 1 - .../NetworkTesting/NetworkParseTests.cs | 482 +++++++++++++++++++++ .../NetworkTesting/UnitTesting.cs | 482 --------------------- 3 files changed, 482 insertions(+), 483 deletions(-) create mode 100644 tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs delete mode 100644 tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs (limited to 'tests') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index aff67f163..c695c0231 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -47,7 +47,6 @@ using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; -using Jellyfin.Api.Migrations; using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; using MediaBrowser.Common; diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs new file mode 100644 index 000000000..0bd442f0b --- /dev/null +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs @@ -0,0 +1,482 @@ +using System; +using System.Net; +using Emby.Dlna.PlayTo; +using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Manager; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using Moq; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; +using System.Collections.ObjectModel; + +namespace Jellyfin.Networking.Tests +{ + public class NetworkParseTests + { + /// <summary> + /// Tries to identify the string and return an object of that class. + /// </summary> + /// <param name="addr">String to parse.</param> + /// <param name="result">IPObject to return.</param> + /// <returns>True if the value parsed successfully.</returns> + private static bool TryParse(string addr, out IPObject result) + { + if (!string.IsNullOrEmpty(addr)) + { + // Is it an IP address + if (IPNetAddress.TryParse(addr, out IPNetAddress nw)) + { + result = nw; + return true; + } + + if (IPHost.TryParse(addr, out IPHost h)) + { + result = h; + return true; + } + } + + result = IPNetAddress.None; + return false; + } + + private static IConfigurationManager GetMockConfig(NetworkConfiguration conf) + { + var configManager = new Mock<IConfigurationManager> + { + CallBase = true + }; + configManager.Setup(x => x.GetConfiguration(It.IsAny<string>())).Returns(conf); + return (IConfigurationManager)configManager.Object; + } + + [Theory] + [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.0/24", "[192.168.1.208/24,200.200.200.200/24]")] + [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")] + [InlineData("192.168.1.208/24,-16,vEthernet1:192.168.1.208/24,-16,vEthernet212;200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")] + public void IgnoreVirtualInterfaces(string interfaces, string lan, string value) + { + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + LocalNetworkSubnets = lan?.Split(';') ?? throw new ArgumentNullException(nameof(lan)) + }; + + NetworkManager.MockNetworkSettings = interfaces; + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + NetworkManager.MockNetworkSettings = string.Empty; + + Assert.True(string.Equals(nm.GetInternalBindAddresses().AsString(), value, StringComparison.Ordinal)); + } + + [Theory] + [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")] + public void TextIsInNetwork(string network, string value) + { + if (network == null) + { + throw new ArgumentNullException(nameof(network)); + } + + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + LocalNetworkSubnets = network.Split(',') + }; + + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + + Assert.True(!nm.IsInLocalNetwork(value)); + } + + [Theory] + [InlineData("127.0.0.1")] + [InlineData("127.0.0.1:123")] + [InlineData("localhost")] + [InlineData("localhost:1345")] + [InlineData("www.google.co.uk")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] + [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")] + [InlineData("fe80::7add:12ff:febb:c67b%16")] + [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")] + [InlineData("192.168.1.2/255.255.255.0")] + [InlineData("192.168.1.2/24")] + public void TestCollectionCreation(string address) + { + Assert.True(TryParse(address, out _)); + } + + [Theory] + [InlineData("256.128.0.0.0.1")] + [InlineData("127.0.0.1#")] + [InlineData("localhost!")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] + public void TestInvalidCollectionCreation(string address) + { + Assert.False(TryParse(address, out _)); + } + + [Theory] + [InlineData("127.0.0.1#", + "[]", + "[]", + "[]", + "[]", + "[]")] + [InlineData("[127.0.0.1]", + "[]", + "[]", + "[127.0.0.1/32]", + "[127.0.0.1/32]", + "[]")] + [InlineData("", + "[]", + "[]", + "[]", + "[]", + "[]")] + [InlineData("192.158.1.2/255.255.0.0,192.169.1.2/8", + "[192.158.1.2/16,192.169.1.2/8]", + "[192.158.1.2/16,192.169.1.2/8]", + "[]", + "[]", + "[192.158.0.0/16,192.0.0.0/8]")] + [InlineData("192.158.1.2/16, localhost, fd23:184f:2029:0:3139:7386:67d7:d517, [10.10.10.10]", + "[192.158.1.2/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]", + "[192.158.1.2/16,127.0.0.1/32]", + "[10.10.10.10/32]", + "[10.10.10.10/32]", + "[192.158.0.0/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]")] + public void TestCollections(string settings, string result1, string result2, string result3, string result4, string result5) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + }; + + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + + // Test included, IP6. + Collection<IPObject> nc = nm.CreateIPCollection(settings.Split(","), false); + Assert.True(string.Equals(nc?.AsString(), result1, System.StringComparison.OrdinalIgnoreCase)); + + // Test excluded, non IP6. + nc = nm.CreateIPCollection(settings.Split(","), true); + Assert.True(string.Equals(nc?.AsString(), result3, System.StringComparison.OrdinalIgnoreCase)); + + conf.EnableIPV6 = false; + nm.UpdateSettings(conf); + + // Test included, non IP6. + nc = nm.CreateIPCollection(settings.Split(","), false); + Assert.True(string.Equals(nc?.AsString(), result2, System.StringComparison.OrdinalIgnoreCase)); + + // Test excluded, including IPv6. + nc = nm.CreateIPCollection(settings.Split(","), true); + Assert.True(string.Equals(nc?.AsString(), result4, System.StringComparison.OrdinalIgnoreCase)); + + conf.EnableIPV6 = true; + nm.UpdateSettings(conf); + + // Test network addresses of collection. + nc = nm.CreateIPCollection(settings.Split(","), false); + nc = nc.AsNetworks(); + Assert.True(string.Equals(nc?.AsString(), result5, System.StringComparison.OrdinalIgnoreCase)); + } + + [Theory] + [InlineData("127.0.0.1", "fd23:184f:2029:0:3139:7386:67d7:d517/64,fd23:184f:2029:0:c0f0:8a8a:7605:fffa/128,fe80::3139:7386:67d7:d517%16/64,192.168.1.208/24,::1/128,127.0.0.1/8", "[127.0.0.1/32]")] + [InlineData("127.0.0.1", "127.0.0.1/8", "[127.0.0.1/32]")] + public void UnionCheck(string settings, string compare, string result) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (compare == null) + { + throw new ArgumentNullException(nameof(compare)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + }; + + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + + Collection<IPObject> nc1 = nm.CreateIPCollection(settings.Split(","), false); + Collection<IPObject> nc2 = nm.CreateIPCollection(compare.Split(","), false); + + Assert.True(nc1.Union(nc2).AsString() == result); + } + + [Theory] + [InlineData("192.168.5.85/24", "192.168.5.1")] + [InlineData("192.168.5.85/24", "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("127.0.0.1/8", "127.0.0.1")] + public void IpV4SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.True(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("192.168.5.85/24", "192.168.4.254")] + [InlineData("192.168.5.85/24", "191.168.5.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")] + public void IpV4SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.False(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFFF")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:0001:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFF0")] + [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")] + public void IpV6SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.True(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0011:FFFF:FFFF:FFFF:FFFF")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0013:0000:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0013:0001:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0011:FFFF:FFFF:FFFF:FFF0")] + [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0001")] + public void IpV6SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.False(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("10.0.0.0/255.0.0.0", "10.10.10.1/32")] + [InlineData("10.0.0.0/8", "10.10.10.1/32")] + [InlineData("10.0.0.0/255.0.0.0", "10.10.10.1")] + + [InlineData("10.10.0.0/255.255.0.0", "10.10.10.1/32")] + [InlineData("10.10.0.0/16", "10.10.10.1/32")] + [InlineData("10.10.0.0/255.255.0.0", "10.10.10.1")] + + [InlineData("10.10.10.0/255.255.255.0", "10.10.10.1/32")] + [InlineData("10.10.10.0/24", "10.10.10.1/32")] + [InlineData("10.10.10.0/255.255.255.0", "10.10.10.1")] + + public void TestSubnets(string network, string ip) + { + Assert.True(TryParse(network, out IPObject? networkObj)); + Assert.True(TryParse(ip, out IPObject? ipObj)); + Assert.True(networkObj.Contains(ipObj)); + } + + [Theory] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "172.168.1.2/24", "172.168.1.2/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "172.168.1.2/24, 10.10.10.1", "172.168.1.2/24,10.10.10.1/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "192.168.1.2/255.255.255.0, 10.10.10.1", "192.168.1.2/24,10.10.10.1/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "192.168.1.2/24, 100.10.10.1", "192.168.1.2/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "194.168.1.2/24, 100.10.10.1", "")] + + public void TestMatches(string source, string dest, string result) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (dest == null) + { + throw new ArgumentNullException(nameof(dest)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true + }; + + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + + // Test included, IP6. + Collection<IPObject> ncSource = nm.CreateIPCollection(source.Split(",")); + Collection<IPObject> ncDest = nm.CreateIPCollection(dest.Split(",")); + Collection<IPObject> ncResult = ncSource.Union(ncDest); + Collection<IPObject> resultCollection = nm.CreateIPCollection(result.Split(",")); + Assert.True(ncResult.Compare(resultCollection)); + } + + + [Theory] + [InlineData("10.1.1.1/32", "10.1.1.1")] + [InlineData("192.168.1.254/32", "192.168.1.254/255.255.255.255")] + + public void TestEquals(string source, string dest) + { + Assert.True(IPNetAddress.Parse(source).Equals(IPNetAddress.Parse(dest))); + Assert.True(IPNetAddress.Parse(dest).Equals(IPNetAddress.Parse(source))); + } + + [Theory] + + // Testing bind interfaces. These are set for my system so won't work elsewhere. + // On my system eth16 is internal, eth11 external (Windows defines the indexes). + // + // This test is to replicate how DNLA requests work throughout the system. + + // User on internal network, we're bound internal and external - so result is internal. + [InlineData("192.168.1.1", "eth16,eth11", false, "eth16")] + // User on external network, we're bound internal and external - so result is external. + [InlineData("8.8.8.8", "eth16,eth11", false, "eth11")] + // User on internal network, we're bound internal only - so result is internal. + [InlineData("10.10.10.10", "eth16", false, "eth16")] + // User on internal network, no binding specified - so result is the 1st internal. + [InlineData("192.168.1.1", "", false, "eth16")] + // User on external network, internal binding only - so result is the 1st internal. + [InlineData("jellyfin.org", "eth16", false, "eth16")] + // User on external network, no binding - so result is the 1st external. + [InlineData("jellyfin.org", "", false, "eth11")] + // User assumed to be internal, no binding - so result is the 1st internal. + [InlineData("", "", false, "eth16")] + public void TestBindInterfaces(string source, string bindAddresses, bool ipv6enabled, string result) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (bindAddresses == null) + { + throw new ArgumentNullException(nameof(bindAddresses)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + var conf = new NetworkConfiguration() + { + LocalNetworkAddresses = bindAddresses.Split(','), + EnableIPV6 = ipv6enabled, + EnableIPV4 = true + }; + + NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11"; + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + NetworkManager.MockNetworkSettings = string.Empty; + + _ = nm.TryParseInterface(result, out Collection<IPObject>? resultObj); + + if (resultObj != null) + { + result = ((IPNetAddress)resultObj[0]).ToString(true); + var intf = nm.GetBindInterface(source, out int? _); + + Assert.True(string.Equals(intf, result, System.StringComparison.OrdinalIgnoreCase)); + } + } + + [Theory] + + // Testing bind interfaces. These are set for my system so won't work elsewhere. + // On my system eth16 is internal, eth11 external (Windows defines the indexes). + // + // This test is to replicate how subnet bound ServerPublisherUri work throughout the system. + + // User on internal network, we're bound internal and external - so result is internal override. + [InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")] + + // User on external network, we're bound internal and external - so result is override. + [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] + + // User on internal network, we're bound internal only, but the address isn't in the LAN - so return the override. + [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")] + + // User on internal network, no binding specified - so result is the 1st internal. + [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] + + // User on external network, internal binding only - so asumption is a proxy forward, return external override. + [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] + + // User on external network, no binding - so result is the 1st external which is overriden. + [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "0.0.0.0 = http://helloworld.com", "http://helloworld.com")] + + // User assumed to be internal, no binding - so result is the 1st internal. + [InlineData("", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] + + // User is internal, no binding - so result is the 1st internal, which is then overridden. + [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")] + + public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result) + { + if (lan == null) + { + throw new ArgumentNullException(nameof(lan)); + } + + if (bindAddresses == null) + { + throw new ArgumentNullException(nameof(bindAddresses)); + } + + var conf = new NetworkConfiguration() + { + LocalNetworkSubnets = lan.Split(','), + LocalNetworkAddresses = bindAddresses.Split(','), + EnableIPV6 = ipv6enabled, + EnableIPV4 = true, + PublishedServerUriBySubnet = new string[] { publishedServers } + }; + + NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11"; + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + NetworkManager.MockNetworkSettings = string.Empty; + + if (nm.TryParseInterface(result, out Collection<IPObject>? resultObj) && resultObj != null) + { + // Parse out IPAddresses so we can do a string comparison. (Ignore subnet masks). + result = ((IPNetAddress)resultObj[0]).ToString(true); + } + + var intf = nm.GetBindInterface(source, out int? _); + + Assert.True(string.Equals(intf, result, System.StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs deleted file mode 100644 index 0bd442f0b..000000000 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs +++ /dev/null @@ -1,482 +0,0 @@ -using System; -using System.Net; -using Emby.Dlna.PlayTo; -using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Manager; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using Moq; -using Microsoft.Extensions.Logging.Abstractions; -using Xunit; -using System.Collections.ObjectModel; - -namespace Jellyfin.Networking.Tests -{ - public class NetworkParseTests - { - /// <summary> - /// Tries to identify the string and return an object of that class. - /// </summary> - /// <param name="addr">String to parse.</param> - /// <param name="result">IPObject to return.</param> - /// <returns>True if the value parsed successfully.</returns> - private static bool TryParse(string addr, out IPObject result) - { - if (!string.IsNullOrEmpty(addr)) - { - // Is it an IP address - if (IPNetAddress.TryParse(addr, out IPNetAddress nw)) - { - result = nw; - return true; - } - - if (IPHost.TryParse(addr, out IPHost h)) - { - result = h; - return true; - } - } - - result = IPNetAddress.None; - return false; - } - - private static IConfigurationManager GetMockConfig(NetworkConfiguration conf) - { - var configManager = new Mock<IConfigurationManager> - { - CallBase = true - }; - configManager.Setup(x => x.GetConfiguration(It.IsAny<string>())).Returns(conf); - return (IConfigurationManager)configManager.Object; - } - - [Theory] - [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.0/24", "[192.168.1.208/24,200.200.200.200/24]")] - [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")] - [InlineData("192.168.1.208/24,-16,vEthernet1:192.168.1.208/24,-16,vEthernet212;200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")] - public void IgnoreVirtualInterfaces(string interfaces, string lan, string value) - { - var conf = new NetworkConfiguration() - { - EnableIPV6 = true, - EnableIPV4 = true, - LocalNetworkSubnets = lan?.Split(';') ?? throw new ArgumentNullException(nameof(lan)) - }; - - NetworkManager.MockNetworkSettings = interfaces; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - NetworkManager.MockNetworkSettings = string.Empty; - - Assert.True(string.Equals(nm.GetInternalBindAddresses().AsString(), value, StringComparison.Ordinal)); - } - - [Theory] - [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")] - public void TextIsInNetwork(string network, string value) - { - if (network == null) - { - throw new ArgumentNullException(nameof(network)); - } - - var conf = new NetworkConfiguration() - { - EnableIPV6 = true, - EnableIPV4 = true, - LocalNetworkSubnets = network.Split(',') - }; - - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - - Assert.True(!nm.IsInLocalNetwork(value)); - } - - [Theory] - [InlineData("127.0.0.1")] - [InlineData("127.0.0.1:123")] - [InlineData("localhost")] - [InlineData("localhost:1345")] - [InlineData("www.google.co.uk")] - [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] - [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] - [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")] - [InlineData("fe80::7add:12ff:febb:c67b%16")] - [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")] - [InlineData("192.168.1.2/255.255.255.0")] - [InlineData("192.168.1.2/24")] - public void TestCollectionCreation(string address) - { - Assert.True(TryParse(address, out _)); - } - - [Theory] - [InlineData("256.128.0.0.0.1")] - [InlineData("127.0.0.1#")] - [InlineData("localhost!")] - [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] - public void TestInvalidCollectionCreation(string address) - { - Assert.False(TryParse(address, out _)); - } - - [Theory] - [InlineData("127.0.0.1#", - "[]", - "[]", - "[]", - "[]", - "[]")] - [InlineData("[127.0.0.1]", - "[]", - "[]", - "[127.0.0.1/32]", - "[127.0.0.1/32]", - "[]")] - [InlineData("", - "[]", - "[]", - "[]", - "[]", - "[]")] - [InlineData("192.158.1.2/255.255.0.0,192.169.1.2/8", - "[192.158.1.2/16,192.169.1.2/8]", - "[192.158.1.2/16,192.169.1.2/8]", - "[]", - "[]", - "[192.158.0.0/16,192.0.0.0/8]")] - [InlineData("192.158.1.2/16, localhost, fd23:184f:2029:0:3139:7386:67d7:d517, [10.10.10.10]", - "[192.158.1.2/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]", - "[192.158.1.2/16,127.0.0.1/32]", - "[10.10.10.10/32]", - "[10.10.10.10/32]", - "[192.158.0.0/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]")] - public void TestCollections(string settings, string result1, string result2, string result3, string result4, string result5) - { - if (settings == null) - { - throw new ArgumentNullException(nameof(settings)); - } - - var conf = new NetworkConfiguration() - { - EnableIPV6 = true, - EnableIPV4 = true, - }; - - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - - // Test included, IP6. - Collection<IPObject> nc = nm.CreateIPCollection(settings.Split(","), false); - Assert.True(string.Equals(nc?.AsString(), result1, System.StringComparison.OrdinalIgnoreCase)); - - // Test excluded, non IP6. - nc = nm.CreateIPCollection(settings.Split(","), true); - Assert.True(string.Equals(nc?.AsString(), result3, System.StringComparison.OrdinalIgnoreCase)); - - conf.EnableIPV6 = false; - nm.UpdateSettings(conf); - - // Test included, non IP6. - nc = nm.CreateIPCollection(settings.Split(","), false); - Assert.True(string.Equals(nc?.AsString(), result2, System.StringComparison.OrdinalIgnoreCase)); - - // Test excluded, including IPv6. - nc = nm.CreateIPCollection(settings.Split(","), true); - Assert.True(string.Equals(nc?.AsString(), result4, System.StringComparison.OrdinalIgnoreCase)); - - conf.EnableIPV6 = true; - nm.UpdateSettings(conf); - - // Test network addresses of collection. - nc = nm.CreateIPCollection(settings.Split(","), false); - nc = nc.AsNetworks(); - Assert.True(string.Equals(nc?.AsString(), result5, System.StringComparison.OrdinalIgnoreCase)); - } - - [Theory] - [InlineData("127.0.0.1", "fd23:184f:2029:0:3139:7386:67d7:d517/64,fd23:184f:2029:0:c0f0:8a8a:7605:fffa/128,fe80::3139:7386:67d7:d517%16/64,192.168.1.208/24,::1/128,127.0.0.1/8", "[127.0.0.1/32]")] - [InlineData("127.0.0.1", "127.0.0.1/8", "[127.0.0.1/32]")] - public void UnionCheck(string settings, string compare, string result) - { - if (settings == null) - { - throw new ArgumentNullException(nameof(settings)); - } - - if (compare == null) - { - throw new ArgumentNullException(nameof(compare)); - } - - if (result == null) - { - throw new ArgumentNullException(nameof(result)); - } - - - var conf = new NetworkConfiguration() - { - EnableIPV6 = true, - EnableIPV4 = true, - }; - - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - - Collection<IPObject> nc1 = nm.CreateIPCollection(settings.Split(","), false); - Collection<IPObject> nc2 = nm.CreateIPCollection(compare.Split(","), false); - - Assert.True(nc1.Union(nc2).AsString() == result); - } - - [Theory] - [InlineData("192.168.5.85/24", "192.168.5.1")] - [InlineData("192.168.5.85/24", "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("127.0.0.1/8", "127.0.0.1")] - public void IpV4SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress) - { - var ipAddressObj = IPNetAddress.Parse(netMask); - Assert.True(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); - } - - [Theory] - [InlineData("192.168.5.85/24", "192.168.4.254")] - [InlineData("192.168.5.85/24", "191.168.5.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")] - public void IpV4SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress) - { - var ipAddressObj = IPNetAddress.Parse(netMask); - Assert.False(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); - } - - [Theory] - [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")] - [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFFF")] - [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:0001:0000:0000:0000")] - [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFF0")] - [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")] - public void IpV6SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress) - { - var ipAddressObj = IPNetAddress.Parse(netMask); - Assert.True(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); - } - - [Theory] - [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0011:FFFF:FFFF:FFFF:FFFF")] - [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0013:0000:0000:0000:0000")] - [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0013:0001:0000:0000:0000")] - [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0011:FFFF:FFFF:FFFF:FFF0")] - [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0001")] - public void IpV6SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress) - { - var ipAddressObj = IPNetAddress.Parse(netMask); - Assert.False(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); - } - - [Theory] - [InlineData("10.0.0.0/255.0.0.0", "10.10.10.1/32")] - [InlineData("10.0.0.0/8", "10.10.10.1/32")] - [InlineData("10.0.0.0/255.0.0.0", "10.10.10.1")] - - [InlineData("10.10.0.0/255.255.0.0", "10.10.10.1/32")] - [InlineData("10.10.0.0/16", "10.10.10.1/32")] - [InlineData("10.10.0.0/255.255.0.0", "10.10.10.1")] - - [InlineData("10.10.10.0/255.255.255.0", "10.10.10.1/32")] - [InlineData("10.10.10.0/24", "10.10.10.1/32")] - [InlineData("10.10.10.0/255.255.255.0", "10.10.10.1")] - - public void TestSubnets(string network, string ip) - { - Assert.True(TryParse(network, out IPObject? networkObj)); - Assert.True(TryParse(ip, out IPObject? ipObj)); - Assert.True(networkObj.Contains(ipObj)); - } - - [Theory] - [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "172.168.1.2/24", "172.168.1.2/24")] - [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "172.168.1.2/24, 10.10.10.1", "172.168.1.2/24,10.10.10.1/24")] - [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "192.168.1.2/255.255.255.0, 10.10.10.1", "192.168.1.2/24,10.10.10.1/24")] - [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "192.168.1.2/24, 100.10.10.1", "192.168.1.2/24")] - [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "194.168.1.2/24, 100.10.10.1", "")] - - public void TestMatches(string source, string dest, string result) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (dest == null) - { - throw new ArgumentNullException(nameof(dest)); - } - - if (result == null) - { - throw new ArgumentNullException(nameof(result)); - } - - var conf = new NetworkConfiguration() - { - EnableIPV6 = true, - EnableIPV4 = true - }; - - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - - // Test included, IP6. - Collection<IPObject> ncSource = nm.CreateIPCollection(source.Split(",")); - Collection<IPObject> ncDest = nm.CreateIPCollection(dest.Split(",")); - Collection<IPObject> ncResult = ncSource.Union(ncDest); - Collection<IPObject> resultCollection = nm.CreateIPCollection(result.Split(",")); - Assert.True(ncResult.Compare(resultCollection)); - } - - - [Theory] - [InlineData("10.1.1.1/32", "10.1.1.1")] - [InlineData("192.168.1.254/32", "192.168.1.254/255.255.255.255")] - - public void TestEquals(string source, string dest) - { - Assert.True(IPNetAddress.Parse(source).Equals(IPNetAddress.Parse(dest))); - Assert.True(IPNetAddress.Parse(dest).Equals(IPNetAddress.Parse(source))); - } - - [Theory] - - // Testing bind interfaces. These are set for my system so won't work elsewhere. - // On my system eth16 is internal, eth11 external (Windows defines the indexes). - // - // This test is to replicate how DNLA requests work throughout the system. - - // User on internal network, we're bound internal and external - so result is internal. - [InlineData("192.168.1.1", "eth16,eth11", false, "eth16")] - // User on external network, we're bound internal and external - so result is external. - [InlineData("8.8.8.8", "eth16,eth11", false, "eth11")] - // User on internal network, we're bound internal only - so result is internal. - [InlineData("10.10.10.10", "eth16", false, "eth16")] - // User on internal network, no binding specified - so result is the 1st internal. - [InlineData("192.168.1.1", "", false, "eth16")] - // User on external network, internal binding only - so result is the 1st internal. - [InlineData("jellyfin.org", "eth16", false, "eth16")] - // User on external network, no binding - so result is the 1st external. - [InlineData("jellyfin.org", "", false, "eth11")] - // User assumed to be internal, no binding - so result is the 1st internal. - [InlineData("", "", false, "eth16")] - public void TestBindInterfaces(string source, string bindAddresses, bool ipv6enabled, string result) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (bindAddresses == null) - { - throw new ArgumentNullException(nameof(bindAddresses)); - } - - if (result == null) - { - throw new ArgumentNullException(nameof(result)); - } - - var conf = new NetworkConfiguration() - { - LocalNetworkAddresses = bindAddresses.Split(','), - EnableIPV6 = ipv6enabled, - EnableIPV4 = true - }; - - NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11"; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - NetworkManager.MockNetworkSettings = string.Empty; - - _ = nm.TryParseInterface(result, out Collection<IPObject>? resultObj); - - if (resultObj != null) - { - result = ((IPNetAddress)resultObj[0]).ToString(true); - var intf = nm.GetBindInterface(source, out int? _); - - Assert.True(string.Equals(intf, result, System.StringComparison.OrdinalIgnoreCase)); - } - } - - [Theory] - - // Testing bind interfaces. These are set for my system so won't work elsewhere. - // On my system eth16 is internal, eth11 external (Windows defines the indexes). - // - // This test is to replicate how subnet bound ServerPublisherUri work throughout the system. - - // User on internal network, we're bound internal and external - so result is internal override. - [InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")] - - // User on external network, we're bound internal and external - so result is override. - [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] - - // User on internal network, we're bound internal only, but the address isn't in the LAN - so return the override. - [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")] - - // User on internal network, no binding specified - so result is the 1st internal. - [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] - - // User on external network, internal binding only - so asumption is a proxy forward, return external override. - [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] - - // User on external network, no binding - so result is the 1st external which is overriden. - [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "0.0.0.0 = http://helloworld.com", "http://helloworld.com")] - - // User assumed to be internal, no binding - so result is the 1st internal. - [InlineData("", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] - - // User is internal, no binding - so result is the 1st internal, which is then overridden. - [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")] - - public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result) - { - if (lan == null) - { - throw new ArgumentNullException(nameof(lan)); - } - - if (bindAddresses == null) - { - throw new ArgumentNullException(nameof(bindAddresses)); - } - - var conf = new NetworkConfiguration() - { - LocalNetworkSubnets = lan.Split(','), - LocalNetworkAddresses = bindAddresses.Split(','), - EnableIPV6 = ipv6enabled, - EnableIPV4 = true, - PublishedServerUriBySubnet = new string[] { publishedServers } - }; - - NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11"; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - NetworkManager.MockNetworkSettings = string.Empty; - - if (nm.TryParseInterface(result, out Collection<IPObject>? resultObj) && resultObj != null) - { - // Parse out IPAddresses so we can do a string comparison. (Ignore subnet masks). - result = ((IPNetAddress)resultObj[0]).ToString(true); - } - - var intf = nm.GetBindInterface(source, out int? _); - - Assert.True(string.Equals(intf, result, System.StringComparison.OrdinalIgnoreCase)); - } - } -} -- cgit v1.2.3 From 23fa9533af2edd399e9e6c4021cce7322297e555 Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 20:00:17 +0000 Subject: Update tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs index 0bd442f0b..301dafb51 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs @@ -476,7 +476,7 @@ namespace Jellyfin.Networking.Tests var intf = nm.GetBindInterface(source, out int? _); - Assert.True(string.Equals(intf, result, System.StringComparison.OrdinalIgnoreCase)); + Assert.Equal(intf, result); } } } -- cgit v1.2.3 From ac03516f89e6ea95bafa1bd19968481ef526d4f8 Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 20:00:37 +0000 Subject: Update tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs index 301dafb51..c13053900 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs @@ -90,7 +90,7 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - Assert.True(!nm.IsInLocalNetwork(value)); + Assert.False(nm.IsInLocalNetwork(value)); } [Theory] -- cgit v1.2.3 From 5ac25364db9a90b46a84b7754e3121375ee5d5ba Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 20:01:01 +0000 Subject: Update tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs | 1 - 1 file changed, 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs index c13053900..867120246 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs @@ -1,6 +1,5 @@ using System; using System.Net; -using Emby.Dlna.PlayTo; using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; -- cgit v1.2.3 From 13162184f41029436ab295b8d4b467c6909f71b9 Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 20:01:58 +0000 Subject: Update tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs index 867120246..6adce623f 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs @@ -168,7 +168,7 @@ namespace Jellyfin.Networking.Tests // Test included, IP6. Collection<IPObject> nc = nm.CreateIPCollection(settings.Split(","), false); - Assert.True(string.Equals(nc?.AsString(), result1, System.StringComparison.OrdinalIgnoreCase)); + Assert.Equal(nc.AsString(), result1); // Test excluded, non IP6. nc = nm.CreateIPCollection(settings.Split(","), true); -- cgit v1.2.3 From b4e70328724c5e7a6b7fc52378beffc418578685 Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 20:02:19 +0000 Subject: Update tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs index 6adce623f..b75af5126 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs @@ -407,7 +407,7 @@ namespace Jellyfin.Networking.Tests result = ((IPNetAddress)resultObj[0]).ToString(true); var intf = nm.GetBindInterface(source, out int? _); - Assert.True(string.Equals(intf, result, System.StringComparison.OrdinalIgnoreCase)); + Assert.Equal(intf, result); } } -- cgit v1.2.3 From acb79eb56a5b7f91dd8db3f50095f548310a8ad7 Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 20:02:36 +0000 Subject: Update tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs index b75af5126..945b87a03 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs @@ -172,7 +172,7 @@ namespace Jellyfin.Networking.Tests // Test excluded, non IP6. nc = nm.CreateIPCollection(settings.Split(","), true); - Assert.True(string.Equals(nc?.AsString(), result3, System.StringComparison.OrdinalIgnoreCase)); + Assert.Equal(nc.AsString(), result3); conf.EnableIPV6 = false; nm.UpdateSettings(conf); -- cgit v1.2.3 From c14f468099e9994f6b83b8f1a7710b43833bf25b Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 20:04:02 +0000 Subject: Update tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs index 945b87a03..189396439 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs @@ -183,7 +183,7 @@ namespace Jellyfin.Networking.Tests // Test excluded, including IPv6. nc = nm.CreateIPCollection(settings.Split(","), true); - Assert.True(string.Equals(nc?.AsString(), result4, System.StringComparison.OrdinalIgnoreCase)); + Assert.Equal(nc.AsString(), result4); conf.EnableIPV6 = true; nm.UpdateSettings(conf); -- cgit v1.2.3 From e33200db71934b5d45a88571255428a0b992101e Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 20:04:16 +0000 Subject: Update tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs index 189396439..e8f07726c 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs @@ -191,7 +191,7 @@ namespace Jellyfin.Networking.Tests // Test network addresses of collection. nc = nm.CreateIPCollection(settings.Split(","), false); nc = nc.AsNetworks(); - Assert.True(string.Equals(nc?.AsString(), result5, System.StringComparison.OrdinalIgnoreCase)); + Assert.Equal(nc.AsString(), result5); } [Theory] -- cgit v1.2.3 From c3fa9d00ae392e79d90a2602a573dd56b71033b1 Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 20:04:58 +0000 Subject: Update tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs index e8f07726c..3a8e2e65d 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs @@ -226,7 +226,7 @@ namespace Jellyfin.Networking.Tests Collection<IPObject> nc1 = nm.CreateIPCollection(settings.Split(","), false); Collection<IPObject> nc2 = nm.CreateIPCollection(compare.Split(","), false); - Assert.True(nc1.Union(nc2).AsString() == result); + Assert.Equal(nc1.Union(nc2).AsString(), result); } [Theory] -- cgit v1.2.3 From 3f2c331755f48679d05bc09acf39882d7a574196 Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 20:05:11 +0000 Subject: Update tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs index 3a8e2e65d..78db84d91 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs @@ -68,7 +68,7 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; - Assert.True(string.Equals(nm.GetInternalBindAddresses().AsString(), value, StringComparison.Ordinal)); + Assert.Equal(nm.GetInternalBindAddresses().AsString(), value); } [Theory] -- cgit v1.2.3 From 1e13627a944249dfd94e5a0befd943e726bf0f28 Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 20:05:31 +0000 Subject: Update tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs index 78db84d91..f7fc9ebc6 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs @@ -179,7 +179,7 @@ namespace Jellyfin.Networking.Tests // Test included, non IP6. nc = nm.CreateIPCollection(settings.Split(","), false); - Assert.True(string.Equals(nc?.AsString(), result2, System.StringComparison.OrdinalIgnoreCase)); + Assert.Equal(nc.AsString(), result2); // Test excluded, including IPv6. nc = nm.CreateIPCollection(settings.Split(","), true); -- cgit v1.2.3 From dc1ad3fe2aa1061157b037f1e986b590e4027953 Mon Sep 17 00:00:00 2001 From: Greenback <jimcartlidge@yahoo.co.uk> Date: Sat, 21 Nov 2020 20:31:24 +0000 Subject: Better named tests methods. --- .../NetworkTesting/NetworkParseTests.cs | 56 ++++++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) (limited to 'tests') diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs index f7fc9ebc6..56d11ef52 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs @@ -51,6 +51,12 @@ namespace Jellyfin.Networking.Tests return (IConfigurationManager)configManager.Object; } + /// <summary> + /// Checks the ability to ignore interfaces + /// </summary> + /// <param name="interfaces">Mock network setup, in the format (IP address, interface index, interface name) : .... </param> + /// <param name="lan">LAN addresses.</param> + /// <param name="value">Bind addresses that are excluded.</param> [Theory] [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.0/24", "[192.168.1.208/24,200.200.200.200/24]")] [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")] @@ -71,9 +77,14 @@ namespace Jellyfin.Networking.Tests Assert.Equal(nm.GetInternalBindAddresses().AsString(), value); } + /// <summary> + /// Check that the value given is in the network provided. + /// </summary> + /// <param name="network">Network address.</param> + /// <param name="value">Value to check.</param> [Theory] [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")] - public void TextIsInNetwork(string network, string value) + public void IsInNetwork(string network, string value) { if (network == null) { @@ -92,6 +103,10 @@ namespace Jellyfin.Networking.Tests Assert.False(nm.IsInLocalNetwork(value)); } + /// <summary> + /// Checks IP address formats. + /// </summary> + /// <param name="address"></param> [Theory] [InlineData("127.0.0.1")] [InlineData("127.0.0.1:123")] @@ -105,21 +120,36 @@ namespace Jellyfin.Networking.Tests [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")] [InlineData("192.168.1.2/255.255.255.0")] [InlineData("192.168.1.2/24")] - public void TestCollectionCreation(string address) + public void ValidIPStrings(string address) { Assert.True(TryParse(address, out _)); } + + /// <summary> + /// All should be invalid address strings. + /// </summary> + /// <param name="address">Invalid address strings.</param> [Theory] [InlineData("256.128.0.0.0.1")] [InlineData("127.0.0.1#")] [InlineData("localhost!")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] - public void TestInvalidCollectionCreation(string address) + public void InvalidAddressString(string address) { Assert.False(TryParse(address, out _)); } + + /// <summary> + /// Test collection parsing. + /// </summary> + /// <param name="settings">Collection to parse.</param> + /// <param name="result1">Included addresses from the collection.</param> + /// <param name="result2">Included IP4 addresses from the collection.</param> + /// <param name="result3">Excluded addresses from the collection.</param> + /// <param name="result4">Excluded IP4 addresses from the collection.</param> + /// <param name="result5">Network addresses of the collection.</param> [Theory] [InlineData("127.0.0.1#", "[]", @@ -166,22 +196,22 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - // Test included, IP6. + // Test included. Collection<IPObject> nc = nm.CreateIPCollection(settings.Split(","), false); Assert.Equal(nc.AsString(), result1); - // Test excluded, non IP6. + // Test excluded. nc = nm.CreateIPCollection(settings.Split(","), true); Assert.Equal(nc.AsString(), result3); conf.EnableIPV6 = false; nm.UpdateSettings(conf); - // Test included, non IP6. + // Test IP4 included. nc = nm.CreateIPCollection(settings.Split(","), false); Assert.Equal(nc.AsString(), result2); - // Test excluded, including IPv6. + // Test IP4 excluded. nc = nm.CreateIPCollection(settings.Split(","), true); Assert.Equal(nc.AsString(), result4); @@ -194,6 +224,12 @@ namespace Jellyfin.Networking.Tests Assert.Equal(nc.AsString(), result5); } + /// <summary> + /// Union two collections. + /// </summary> + /// <param name="settings">Source.</param> + /// <param name="compare">Destination.</param> + /// <param name="result">Result.</param> [Theory] [InlineData("127.0.0.1", "fd23:184f:2029:0:3139:7386:67d7:d517/64,fd23:184f:2029:0:c0f0:8a8a:7605:fffa/128,fe80::3139:7386:67d7:d517%16/64,192.168.1.208/24,::1/128,127.0.0.1/8", "[127.0.0.1/32]")] [InlineData("127.0.0.1", "127.0.0.1/8", "[127.0.0.1/32]")] @@ -293,7 +329,7 @@ namespace Jellyfin.Networking.Tests [InlineData("10.10.10.0/24", "10.10.10.1/32")] [InlineData("10.10.10.0/255.255.255.0", "10.10.10.1")] - public void TestSubnets(string network, string ip) + public void TestSubnetContains(string network, string ip) { Assert.True(TryParse(network, out IPObject? networkObj)); Assert.True(TryParse(ip, out IPObject? ipObj)); @@ -307,7 +343,7 @@ namespace Jellyfin.Networking.Tests [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "192.168.1.2/24, 100.10.10.1", "192.168.1.2/24")] [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "194.168.1.2/24, 100.10.10.1", "")] - public void TestMatches(string source, string dest, string result) + public void TestCollectionEquality(string source, string dest, string result) { if (source == null) { @@ -353,7 +389,7 @@ namespace Jellyfin.Networking.Tests [Theory] - // Testing bind interfaces. These are set for my system so won't work elsewhere. + // Testing bind interfaces. // On my system eth16 is internal, eth11 external (Windows defines the indexes). // // This test is to replicate how DNLA requests work throughout the system. -- cgit v1.2.3 From 50e375020a580078ed309e2d59cf124d10b4a2ed Mon Sep 17 00:00:00 2001 From: BaronGreenback <jimcartlidge@yahoo.co.uk> Date: Tue, 24 Nov 2020 05:11:02 +0000 Subject: [Fix] NetworkManager binding to [::] (#4549) * Autodiscovery enable/disable patch * Fixed [::] issue on bind. Altered test. * Update UdpServerEntryPoint.cs * Update Jellyfin.Networking.Tests.csproj * Update Jellyfin.Networking.Tests.csproj * Update INetworkManager.cs --- Jellyfin.Networking/Manager/NetworkManager.cs | 37 ++++++++++++---------- MediaBrowser.Common/Net/INetworkManager.cs | 12 +++---- .../NetworkTesting/NetworkParseTests.cs | 16 ++++++---- 3 files changed, 36 insertions(+), 29 deletions(-) (limited to 'tests') diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 515ae669a..1a5614b7b 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -84,7 +84,7 @@ namespace Jellyfin.Networking.Manager private Collection<IPObject> _internalInterfaces; /// <summary> - /// Flag set when no custom LAN has been defined in the config. + /// Flag set when no custom LAN has been defined in the configuration. /// </summary> private bool _usingPrivateAddresses; @@ -228,7 +228,7 @@ namespace Jellyfin.Networking.Manager } /// <inheritdoc/> - public Collection<IPObject> CreateIPCollection(string[] values, bool bracketed = false) + public Collection<IPObject> CreateIPCollection(string[] values, bool negated = false) { Collection<IPObject> col = new Collection<IPObject>(); if (values == null) @@ -242,21 +242,14 @@ namespace Jellyfin.Networking.Manager try { - if (v.StartsWith('[') && v.EndsWith(']')) + if (v.StartsWith('!')) { - if (bracketed) - { - AddToCollection(col, v[1..^1]); - } - } - else if (v.StartsWith('!')) - { - if (bracketed) + if (negated) { AddToCollection(col, v[1..]); } } - else if (!bracketed) + else if (!negated) { AddToCollection(col, v); } @@ -730,7 +723,7 @@ namespace Jellyfin.Networking.Manager } /// <summary> - /// Parses a string and adds it into the the collection, replacing any interface references. + /// Parses a string and adds it into the collection, replacing any interface references. /// </summary> /// <param name="col"><see cref="Collection{IPObject}"/>Collection.</param> /// <param name="token">String value to parse.</param> @@ -755,7 +748,19 @@ namespace Jellyfin.Networking.Manager } else if (TryParse(token, out IPObject obj)) { - if (!IsIP6Enabled) + // Expand if the ip address is "any". + if ((obj.Address.Equals(IPAddress.Any) && IsIP4Enabled) + || (obj.Address.Equals(IPAddress.IPv6Any) && IsIP6Enabled)) + { + foreach (IPNetAddress iface in _interfaceAddresses) + { + if (obj.AddressFamily == iface.AddressFamily) + { + col.AddItem(iface); + } + } + } + else if (!IsIP6Enabled) { // Remove IP6 addresses from multi-homed IPHosts. obj.Remove(AddressFamily.InterNetworkV6); @@ -872,7 +877,7 @@ namespace Jellyfin.Networking.Manager else { var replacement = parts[1].Trim(); - if (string.Equals(parts[0], "remaining", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(parts[0], "all", StringComparison.OrdinalIgnoreCase)) { _publishedServerUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement; } @@ -956,7 +961,7 @@ namespace Jellyfin.Networking.Manager { _logger.LogDebug("Refreshing LAN information."); - // Get config options. + // Get configuration options. string[] subnets = config.LocalNetworkSubnets; // Create lists from user settings. diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 43562afe3..b6c390d23 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -149,7 +149,7 @@ namespace MediaBrowser.Common.Net /// <summary> /// Returns true if the address is a private address. - /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// The configuration option TrustIP6Interfaces overrides this functions behaviour. /// </summary> /// <param name="address">Address to check.</param> /// <returns>True or False.</returns> @@ -157,7 +157,7 @@ namespace MediaBrowser.Common.Net /// <summary> /// Returns true if the address is part of the user defined LAN. - /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// The configuration option TrustIP6Interfaces overrides this functions behaviour. /// </summary> /// <param name="address">IP to check.</param> /// <returns>True if endpoint is within the LAN range.</returns> @@ -165,7 +165,7 @@ namespace MediaBrowser.Common.Net /// <summary> /// Returns true if the address is part of the user defined LAN. - /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// The configuration option TrustIP6Interfaces overrides this functions behaviour. /// </summary> /// <param name="address">IP to check.</param> /// <returns>True if endpoint is within the LAN range.</returns> @@ -173,7 +173,7 @@ namespace MediaBrowser.Common.Net /// <summary> /// Returns true if the address is part of the user defined LAN. - /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// The configuration option TrustIP6Interfaces overrides this functions behaviour. /// </summary> /// <param name="address">IP to check.</param> /// <returns>True if endpoint is within the LAN range.</returns> @@ -192,9 +192,9 @@ namespace MediaBrowser.Common.Net /// Parses an array of strings into a Collection{IPObject}. /// </summary> /// <param name="values">Values to parse.</param> - /// <param name="bracketed">When true, only include values in []. When false, ignore bracketed values.</param> + /// <param name="negated">When true, only include values beginning with !. When false, ignore ! values.</param> /// <returns>IPCollection object containing the value strings.</returns> - Collection<IPObject> CreateIPCollection(string[] values, bool bracketed = false); + Collection<IPObject> CreateIPCollection(string[] values, bool negated = false); /// <summary> /// Returns all the internal Bind interface addresses. diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs index 56d11ef52..c350685af 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs @@ -135,6 +135,7 @@ namespace Jellyfin.Networking.Tests [InlineData("127.0.0.1#")] [InlineData("localhost!")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] + [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")] public void InvalidAddressString(string address) { Assert.False(TryParse(address, out _)); @@ -157,7 +158,7 @@ namespace Jellyfin.Networking.Tests "[]", "[]", "[]")] - [InlineData("[127.0.0.1]", + [InlineData("!127.0.0.1", "[]", "[]", "[127.0.0.1/32]", @@ -169,18 +170,19 @@ namespace Jellyfin.Networking.Tests "[]", "[]", "[]")] + [InlineData( + "192.158.1.2/16, localhost, fd23:184f:2029:0:3139:7386:67d7:d517, !10.10.10.10", + "[192.158.1.2/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]", + "[192.158.1.2/16,127.0.0.1/32]", + "[10.10.10.10/32]", + "[10.10.10.10/32]", + "[192.158.0.0/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]")] [InlineData("192.158.1.2/255.255.0.0,192.169.1.2/8", "[192.158.1.2/16,192.169.1.2/8]", "[192.158.1.2/16,192.169.1.2/8]", "[]", "[]", "[192.158.0.0/16,192.0.0.0/8]")] - [InlineData("192.158.1.2/16, localhost, fd23:184f:2029:0:3139:7386:67d7:d517, [10.10.10.10]", - "[192.158.1.2/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]", - "[192.158.1.2/16,127.0.0.1/32]", - "[10.10.10.10/32]", - "[10.10.10.10/32]", - "[192.158.0.0/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]")] public void TestCollections(string settings, string result1, string result2, string result3, string result4, string result5) { if (settings == null) -- cgit v1.2.3 From 8c8a71692e2f42e30f95e9ff38cc5442a0d56978 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 25 Nov 2020 23:40:31 +0100 Subject: Remove Hex class as the BCL has one now --- .../LiveTv/Listings/SchedulesDirect.cs | 2 +- .../QuickConnect/QuickConnectManager.cs | 2 +- .../Updates/InstallationManager.cs | 2 +- Jellyfin.Api/Controllers/LiveTvController.cs | 5 +- MediaBrowser.Common/Cryptography/PasswordHash.cs | 10 +-- MediaBrowser.Common/Hex.cs | 94 ---------------------- .../Jellyfin.Common.Benches/HexDecodeBenches.cs | 45 ----------- .../Jellyfin.Common.Benches/HexEncodeBenches.cs | 32 -------- .../Jellyfin.Common.Benches.csproj | 16 ---- benches/Jellyfin.Common.Benches/Program.cs | 14 ---- tests/Jellyfin.Common.Tests/HexTests.cs | 19 ----- tests/Jellyfin.Common.Tests/PasswordHashTests.cs | 5 +- 12 files changed, 13 insertions(+), 233 deletions(-) delete mode 100644 MediaBrowser.Common/Hex.cs delete mode 100644 benches/Jellyfin.Common.Benches/HexDecodeBenches.cs delete mode 100644 benches/Jellyfin.Common.Benches/HexEncodeBenches.cs delete mode 100644 benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj delete mode 100644 benches/Jellyfin.Common.Benches/Program.cs delete mode 100644 tests/Jellyfin.Common.Tests/HexTests.cs (limited to 'tests') diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 5d17ba1de..8c44b244c 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -647,7 +647,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token"); var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>()); - string hashedPassword = Hex.Encode(hashedPasswordBytes); + string hashedPassword = Convert.ToHexString(hashedPasswordBytes); options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json); using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs index 140a67541..7bed06de3 100644 --- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -243,7 +243,7 @@ namespace Emby.Server.Implementations.QuickConnect Span<byte> bytes = stackalloc byte[length]; _rng.GetBytes(bytes); - return Hex.Encode(bytes); + return Convert.ToHexString(bytes); } /// <inheritdoc/> diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index f2c096b8a..ef346dd5d 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -414,7 +414,7 @@ namespace Emby.Server.Implementations.Updates using var md5 = MD5.Create(); cancellationToken.ThrowIfCancellationRequested(); - var hash = Hex.Encode(md5.ComputeHash(stream)); + var hash = Convert.ToHexString(md5.ComputeHash(stream)); if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase)) { _logger.LogError( diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 410f3a340..2c27601e9 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; @@ -17,7 +17,6 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Data.Enums; -using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Dto; @@ -1015,7 +1014,7 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(pw)) { using var sha = SHA1.Create(); - listingsProviderInfo.Password = Hex.Encode(sha.ComputeHash(Encoding.UTF8.GetBytes(pw))); + listingsProviderInfo.Password = Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes(pw))); } return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false); diff --git a/MediaBrowser.Common/Cryptography/PasswordHash.cs b/MediaBrowser.Common/Cryptography/PasswordHash.cs index 3e12536ec..3e2eae1c8 100644 --- a/MediaBrowser.Common/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Common/Cryptography/PasswordHash.cs @@ -101,13 +101,13 @@ namespace MediaBrowser.Common.Cryptography // Check if the string also contains a salt if (splitted.Length - index == 2) { - salt = Hex.Decode(splitted[index++]); - hash = Hex.Decode(splitted[index++]); + salt = Convert.FromHexString(splitted[index++]); + hash = Convert.FromHexString(splitted[index++]); } else { salt = Array.Empty<byte>(); - hash = Hex.Decode(splitted[index++]); + hash = Convert.FromHexString(splitted[index++]); } return new PasswordHash(id, hash, salt, parameters); @@ -144,11 +144,11 @@ namespace MediaBrowser.Common.Cryptography if (_salt.Length != 0) { str.Append('$') - .Append(Hex.Encode(_salt, false)); + .Append(Convert.ToHexString(_salt)); } return str.Append('$') - .Append(Hex.Encode(_hash, false)).ToString(); + .Append(Convert.ToHexString(_hash)).ToString(); } } } diff --git a/MediaBrowser.Common/Hex.cs b/MediaBrowser.Common/Hex.cs deleted file mode 100644 index 559109f74..000000000 --- a/MediaBrowser.Common/Hex.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace MediaBrowser.Common -{ - /// <summary> - /// Encoding and decoding hex strings. - /// </summary> - public static class Hex - { - internal const string HexCharsLower = "0123456789abcdef"; - internal const string HexCharsUpper = "0123456789ABCDEF"; - - internal const int LastHexSymbol = 0x66; // 102: f - - /// <summary> - /// Gets a map from an ASCII char to its hex value shifted, - /// e.g. <c>b</c> -> 11. 0xFF means it's not a hex symbol. - /// </summary> - internal static ReadOnlySpan<byte> HexLookup => new byte[] - { - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f - }; - - /// <summary> - /// Encodes each element of the specified bytes as its hexadecimal string representation. - /// </summary> - /// <param name="bytes">An array of bytes.</param> - /// <param name="lowercase"><c>true</c> to use lowercase hexadecimal characters; otherwise <c>false</c>.</param> - /// <returns><c>bytes</c> as a hex string.</returns> - public static string Encode(ReadOnlySpan<byte> bytes, bool lowercase = true) - { - var hexChars = lowercase ? HexCharsLower : HexCharsUpper; - - // TODO: use string.Create when it's supports spans - // Ref: https://github.com/dotnet/corefx/issues/29120 - char[] s = new char[bytes.Length * 2]; - int j = 0; - for (int i = 0; i < bytes.Length; i++) - { - s[j++] = hexChars[bytes[i] >> 4]; - s[j++] = hexChars[bytes[i] & 0x0f]; - } - - return new string(s); - } - - /// <summary> - /// Decodes a hex string into bytes. - /// </summary> - /// <param name="str">The <see cref="string" />.</param> - /// <returns>The decoded bytes.</returns> - public static byte[] Decode(ReadOnlySpan<char> str) - { - if (str.Length == 0) - { - return Array.Empty<byte>(); - } - - var unHex = HexLookup; - - int byteLen = str.Length / 2; - byte[] bytes = new byte[byteLen]; - int i = 0; - for (int j = 0; j < byteLen; j++) - { - byte a; - byte b; - if (str[i] > LastHexSymbol - || (a = unHex[str[i++]]) == 0xFF - || str[i] > LastHexSymbol - || (b = unHex[str[i++]]) == 0xFF) - { - ThrowArgumentException(nameof(str)); - break; // Unreachable - } - - bytes[j] = (byte)((a * 16) | b); - } - - return bytes; - } - - [DoesNotReturn] - private static void ThrowArgumentException(string paramName) - => throw new ArgumentException("Character is not a hex symbol.", paramName); - } -} diff --git a/benches/Jellyfin.Common.Benches/HexDecodeBenches.cs b/benches/Jellyfin.Common.Benches/HexDecodeBenches.cs deleted file mode 100644 index d9a107b69..000000000 --- a/benches/Jellyfin.Common.Benches/HexDecodeBenches.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Globalization; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Running; -using MediaBrowser.Common; - -namespace Jellyfin.Common.Benches -{ - [MemoryDiagnoser] - public class HexDecodeBenches - { - private string _data; - - [Params(0, 10, 100, 1000, 10000, 1000000)] - public int N { get; set; } - - [GlobalSetup] - public void GlobalSetup() - { - var bytes = new byte[N]; - new Random(42).NextBytes(bytes); - _data = Hex.Encode(bytes); - } - - [Benchmark] - public byte[] Decode() => Hex.Decode(_data); - - [Benchmark] - public byte[] DecodeSubString() => DecodeSubString(_data); - - private static byte[] DecodeSubString(string str) - { - byte[] bytes = new byte[str.Length / 2]; - for (int i = 0; i < str.Length; i += 2) - { - bytes[i / 2] = byte.Parse( - str.Substring(i, 2), - NumberStyles.HexNumber, - CultureInfo.InvariantCulture); - } - - return bytes; - } - } -} diff --git a/benches/Jellyfin.Common.Benches/HexEncodeBenches.cs b/benches/Jellyfin.Common.Benches/HexEncodeBenches.cs deleted file mode 100644 index 7abf93c51..000000000 --- a/benches/Jellyfin.Common.Benches/HexEncodeBenches.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Running; -using MediaBrowser.Common; - -namespace Jellyfin.Common.Benches -{ - [MemoryDiagnoser] - public class HexEncodeBenches - { - private byte[] _data; - - [Params(0, 10, 100, 1000, 10000, 1000000)] - public int N { get; set; } - - [GlobalSetup] - public void GlobalSetup() - { - _data = new byte[N]; - new Random(42).NextBytes(_data); - } - - [Benchmark] - public string HexEncode() => Hex.Encode(_data); - - [Benchmark] - public string BitConverterToString() => BitConverter.ToString(_data); - - [Benchmark] - public string BitConverterToStringWithReplace() => BitConverter.ToString(_data).Replace("-", ""); - } -} diff --git a/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj b/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj deleted file mode 100644 index c564e86e9..000000000 --- a/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj +++ /dev/null @@ -1,16 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <PropertyGroup> - <OutputType>Exe</OutputType> - <TargetFramework>net5.0</TargetFramework> - </PropertyGroup> - - <ItemGroup> - <PackageReference Include="BenchmarkDotNet" Version="0.12.0" /> - </ItemGroup> - - <ItemGroup> - <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" /> - </ItemGroup> - -</Project> diff --git a/benches/Jellyfin.Common.Benches/Program.cs b/benches/Jellyfin.Common.Benches/Program.cs deleted file mode 100644 index b218b0dc1..000000000 --- a/benches/Jellyfin.Common.Benches/Program.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using BenchmarkDotNet.Running; - -namespace Jellyfin.Common.Benches -{ - public static class Program - { - public static void Main(string[] args) - { - _ = BenchmarkRunner.Run<HexEncodeBenches>(); - _ = BenchmarkRunner.Run<HexDecodeBenches>(); - } - } -} diff --git a/tests/Jellyfin.Common.Tests/HexTests.cs b/tests/Jellyfin.Common.Tests/HexTests.cs deleted file mode 100644 index 5b578d38c..000000000 --- a/tests/Jellyfin.Common.Tests/HexTests.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MediaBrowser.Common; -using Xunit; - -namespace Jellyfin.Common.Tests -{ - public class HexTests - { - [Theory] - [InlineData("")] - [InlineData("00")] - [InlineData("01")] - [InlineData("000102030405060708090a0b0c0d0e0f")] - [InlineData("0123456789abcdef")] - public void RoundTripTest(string data) - { - Assert.Equal(data, Hex.Encode(Hex.Decode(data))); - } - } -} diff --git a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs index 46926f4f8..c4422bd10 100644 --- a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs +++ b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs @@ -1,3 +1,4 @@ +using System; using MediaBrowser.Common; using MediaBrowser.Common.Cryptography; using Xunit; @@ -16,8 +17,8 @@ namespace Jellyfin.Common.Tests { var pass = PasswordHash.Parse(passwordHash); Assert.Equal(id, pass.Id); - Assert.Equal(salt, Hex.Encode(pass.Salt, false)); - Assert.Equal(hash, Hex.Encode(pass.Hash, false)); + Assert.Equal(salt, Convert.ToHexString(pass.Salt)); + Assert.Equal(hash, Convert.ToHexString(pass.Hash)); } [Theory] -- cgit v1.2.3