From c9a387943f05cbbd11c5b92d900ff850055ed4f3 Mon Sep 17 00:00:00 2001 From: Neuheit <38368299+Neuheit@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:17:56 -0400 Subject: [PATCH 01/16] Add IPv4 fallback from IPv6 failure. Co-authored-by: BaronGreenback --- Emby.Dlna/Eventing/DlnaEventManager.cs | 2 +- .../HappyEyeballs/HttpClientExtension.cs | 117 ++++++++++++++++++ Jellyfin.Networking/Manager/NetworkManager.cs | 17 ++- Jellyfin.Server/Startup.cs | 20 ++- MediaBrowser.Common/Net/NamedClient.cs | 9 +- 5 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs index d17e238715..d2c0017ec6 100644 --- a/Emby.Dlna/Eventing/DlnaEventManager.cs +++ b/Emby.Dlna/Eventing/DlnaEventManager.cs @@ -165,7 +165,7 @@ namespace Emby.Dlna.Eventing try { - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + using var response = await _httpClientFactory.CreateClient(NamedClient.DirectIp) .SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); } catch (OperationCanceledException) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs new file mode 100644 index 0000000000..12b42cc6e1 --- /dev/null +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -0,0 +1,117 @@ +using System.Diagnostics.Metrics; +using System.IO; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +/* + Copyright (c) 2021 ppy Pty Ltd . + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +namespace Jellyfin.Networking.HappyEyeballs +{ + /// + /// Defines the class. + /// + /// Implementation taken from https://github.com/ppy/osu-framework/pull/4191 . + /// + public static class HttpClientExtension + { + private const int ConnectionEstablishTimeout = 2000; + + /// + /// Gets a value indicating whether the initial IPv6 check has been performed (to determine whether v6 is available or not). + /// + private static bool _hasResolvedIPv6Availability; + + /// + /// Gets or sets a value indicating whether IPv6 should be preferred. Value may change based on runtime failures. + /// + public static bool? UseIPv6 { get; set; } = null; + + /// + /// Implements the httpclient callback method. + /// + /// The instance. + /// The instance. + /// The http steam. + public static async ValueTask OnConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken) + { + // Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), + // let's make IPv4 fallback work in a simple way. This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 + // and expected to be fixed in .NET 6. + if (UseIPv6 == true) + { + try + { + var localToken = cancellationToken; + + if (!_hasResolvedIPv6Availability) + { + // to make things move fast, use a very low timeout for the initial ipv6 attempt. + using var quickFailCts = new CancellationTokenSource(ConnectionEstablishTimeout); + using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, quickFailCts.Token); + + localToken = linkedTokenSource.Token; + } + + return await AttemptConnection(AddressFamily.InterNetworkV6, context, localToken).ConfigureAwait(false); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt. + // Network manager will reset this value in the case of physical network changes / interruptions. + UseIPv6 = false; + } + finally + { + _hasResolvedIPv6Availability = true; + } + } + + // fallback to IPv4. + return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken).ConfigureAwait(false); + } + + private static async ValueTask AttemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken) + { + // The following socket constructor will create a dual-mode socket on systems where IPV6 is available. + var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp) + { + // Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios. + NoDelay = true + }; + + try + { + await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false); + // The stream should take the ownership of the underlying socket, + // closing it when it's disposed. + return new NetworkStream(socket, ownsSocket: true); + } + catch + { + socket.Dispose(); + throw; + } + } + } +} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 9e06cdfe71..631e9cdb8d 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -594,6 +594,7 @@ namespace Jellyfin.Networking.Manager IsIP4Enabled = Socket.OSSupportsIPv4 && config.EnableIPV4; IsIP6Enabled = Socket.OSSupportsIPv6 && config.EnableIPV6; + HappyEyeballs.HttpClientExtension.UseIPv6 = IsIP6Enabled; if (!IsIP6Enabled && !IsIP4Enabled) { @@ -838,9 +839,19 @@ namespace Jellyfin.Networking.Manager try { await Task.Delay(2000).ConfigureAwait(false); - InitialiseInterfaces(); - // Recalculate LAN caches. - InitialiseLAN(_configurationManager.GetNetworkConfiguration()); + + var config = _configurationManager.GetNetworkConfiguration(); + // Have we lost IPv6 capability? + if (IsIP6Enabled && !Socket.OSSupportsIPv6) + { + UpdateSettings(config); + } + else + { + InitialiseInterfaces(); + // Recalculate LAN caches. + InitialiseLAN(config); + } NetworkChanged?.Invoke(this, EventArgs.Empty); } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 1954a5c558..4417bb7226 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -7,6 +7,7 @@ using System.Net.Mime; using System.Text; using Jellyfin.MediaEncoding.Hls.Extensions; using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.HappyEyeballs; using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; using Jellyfin.Server.Infrastructure; @@ -24,6 +25,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; +using Microsoft.VisualBasic; using Prometheus; namespace Jellyfin.Server @@ -79,6 +81,13 @@ namespace Jellyfin.Server var acceptJsonHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json, 1.0); var acceptXmlHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Xml, 0.9); var acceptAnyHeader = new MediaTypeWithQualityHeaderValue("*/*", 0.8); + Func eyeballsHttpClientHandlerDelegate = (_) => new SocketsHttpHandler() + { + AutomaticDecompression = DecompressionMethods.All, + RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8, + ConnectCallback = HttpClientExtension.OnConnect + }; + Func defaultHttpClientHandlerDelegate = (_) => new SocketsHttpHandler() { AutomaticDecompression = DecompressionMethods.All, @@ -93,7 +102,7 @@ namespace Jellyfin.Server c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader); c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader); }) - .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); + .ConfigurePrimaryHttpMessageHandler(eyeballsHttpClientHandlerDelegate); services.AddHttpClient(NamedClient.MusicBrainz, c => { @@ -102,6 +111,15 @@ namespace Jellyfin.Server c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader); c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader); }) + .ConfigurePrimaryHttpMessageHandler(eyeballsHttpClientHandlerDelegate); + + services.AddHttpClient(NamedClient.DirectIp, c => + { + c.DefaultRequestHeaders.UserAgent.Add(productHeader); + c.DefaultRequestHeaders.Accept.Add(acceptJsonHeader); + c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader); + c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader); + }) .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); services.AddHttpClient(NamedClient.Dlna, c => diff --git a/MediaBrowser.Common/Net/NamedClient.cs b/MediaBrowser.Common/Net/NamedClient.cs index a6cacd4f17..9c5544b0ff 100644 --- a/MediaBrowser.Common/Net/NamedClient.cs +++ b/MediaBrowser.Common/Net/NamedClient.cs @@ -1,4 +1,4 @@ -namespace MediaBrowser.Common.Net +namespace MediaBrowser.Common.Net { /// /// Registered http client names. @@ -6,7 +6,7 @@ public static class NamedClient { /// - /// Gets the value for the default named http client. + /// Gets the value for the default named http client which implements happy eyeballs. /// public const string Default = nameof(Default); @@ -19,5 +19,10 @@ /// Gets the value for the DLNA named http client. /// public const string Dlna = nameof(Dlna); + + /// + /// Non happy eyeballs implementation. + /// + public const string DirectIp = nameof(DirectIp); } } From 6c479dfb36d305444bae7d4b3ad78bfb2112549b Mon Sep 17 00:00:00 2001 From: Neuheit <38368299+Neuheit@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:38:28 -0400 Subject: [PATCH 02/16] Ensure IPv6 property is threadsafe. --- .../HappyEyeballs/HttpClientExtension.cs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index 12b42cc6e1..6760869107 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -35,6 +35,11 @@ namespace Jellyfin.Networking.HappyEyeballs { private const int ConnectionEstablishTimeout = 2000; + /// + /// Interlocked doesn't support bool types. + /// + private static int _useIPv6 = 0; + /// /// Gets a value indicating whether the initial IPv6 check has been performed (to determine whether v6 is available or not). /// @@ -43,7 +48,21 @@ namespace Jellyfin.Networking.HappyEyeballs /// /// Gets or sets a value indicating whether IPv6 should be preferred. Value may change based on runtime failures. /// - public static bool? UseIPv6 { get; set; } = null; + public static bool UseIPv6 + { + get => Interlocked.CompareExchange(ref _useIPv6, 1, 1) == 1; + set + { + if (value) + { + Interlocked.CompareExchange(ref _useIPv6, 1, 0); + } + else + { + Interlocked.CompareExchange(ref _useIPv6, 0, 1); + } + } + } /// /// Implements the httpclient callback method. @@ -56,7 +75,7 @@ namespace Jellyfin.Networking.HappyEyeballs // Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), // let's make IPv4 fallback work in a simple way. This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 // and expected to be fixed in .NET 6. - if (UseIPv6 == true) + if (UseIPv6) { try { From 6caabe68ff05f4100db63a3a98d6eeb02347f689 Mon Sep 17 00:00:00 2001 From: Neuheit <38368299+Neuheit@users.noreply.github.com> Date: Mon, 24 Oct 2022 17:17:41 -0400 Subject: [PATCH 03/16] Change method to call IPv6 and IPv4 in parallel. --- .../HappyEyeballs/HttpClientExtension.cs | 119 ++++++------------ 1 file changed, 38 insertions(+), 81 deletions(-) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index 6760869107..049c8dd150 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -1,29 +1,9 @@ -using System.Diagnostics.Metrics; using System.IO; using System.Net.Http; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -/* - Copyright (c) 2021 ppy Pty Ltd . - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ - namespace Jellyfin.Networking.HappyEyeballs { /// @@ -33,36 +13,12 @@ namespace Jellyfin.Networking.HappyEyeballs /// public static class HttpClientExtension { - private const int ConnectionEstablishTimeout = 2000; - /// - /// Interlocked doesn't support bool types. + /// Gets or sets a value indicating whether the client should use IPv6. /// - private static int _useIPv6 = 0; + public static bool UseIPv6 { get; set; } = true; - /// - /// Gets a value indicating whether the initial IPv6 check has been performed (to determine whether v6 is available or not). - /// - private static bool _hasResolvedIPv6Availability; - - /// - /// Gets or sets a value indicating whether IPv6 should be preferred. Value may change based on runtime failures. - /// - public static bool UseIPv6 - { - get => Interlocked.CompareExchange(ref _useIPv6, 1, 1) == 1; - set - { - if (value) - { - Interlocked.CompareExchange(ref _useIPv6, 1, 0); - } - else - { - Interlocked.CompareExchange(ref _useIPv6, 0, 1); - } - } - } + // Implementation taken from https://github.com/rmkerr/corefx/blob/SocketsHttpHandler_Connect_HappyEyeballs/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs /// /// Implements the httpclient callback method. @@ -72,45 +28,46 @@ namespace Jellyfin.Networking.HappyEyeballs /// The http steam. public static async ValueTask OnConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken) { - // Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), - // let's make IPv4 fallback work in a simple way. This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 - // and expected to be fixed in .NET 6. - if (UseIPv6) + if (!UseIPv6) { - try - { - var localToken = cancellationToken; - - if (!_hasResolvedIPv6Availability) - { - // to make things move fast, use a very low timeout for the initial ipv6 attempt. - using var quickFailCts = new CancellationTokenSource(ConnectionEstablishTimeout); - using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, quickFailCts.Token); - - localToken = linkedTokenSource.Token; - } - - return await AttemptConnection(AddressFamily.InterNetworkV6, context, localToken).ConfigureAwait(false); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch -#pragma warning restore CA1031 // Do not catch general exception types - { - // very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt. - // Network manager will reset this value in the case of physical network changes / interruptions. - UseIPv6 = false; - } - finally - { - _hasResolvedIPv6Availability = true; - } + return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken).ConfigureAwait(false); } - // fallback to IPv4. - return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken).ConfigureAwait(false); + using var cancelIPv6 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var tryConnectAsyncIPv6 = AttemptConnection(AddressFamily.InterNetworkV6, context, cancelIPv6.Token); + + if (await Task.WhenAny(tryConnectAsyncIPv6, Task.Delay(200, cancelIPv6.Token)).ConfigureAwait(false) == tryConnectAsyncIPv6 && tryConnectAsyncIPv6.IsCompletedSuccessfully) + { + cancelIPv6.Cancel(); + return tryConnectAsyncIPv6.GetAwaiter().GetResult(); + } + + using var cancelIPv4 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var tryConnectAsyncIPv4 = AttemptConnection(AddressFamily.InterNetwork, context, cancelIPv4.Token); + + if (await Task.WhenAny(tryConnectAsyncIPv6, tryConnectAsyncIPv4).ConfigureAwait(false) == tryConnectAsyncIPv6) + { + if (tryConnectAsyncIPv6.IsCompletedSuccessfully) + { + cancelIPv4.Cancel(); + return tryConnectAsyncIPv6.GetAwaiter().GetResult(); + } + + return tryConnectAsyncIPv4.GetAwaiter().GetResult(); + } + else + { + if (tryConnectAsyncIPv4.IsCompletedSuccessfully) + { + cancelIPv6.Cancel(); + return tryConnectAsyncIPv4.GetAwaiter().GetResult(); + } + + return tryConnectAsyncIPv6.GetAwaiter().GetResult(); + } } - private static async ValueTask AttemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken) + private static async Task AttemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken) { // The following socket constructor will create a dual-mode socket on systems where IPV6 is available. var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp) From 6621121c1b958f0e3726f3b4f1d9c2604e398504 Mon Sep 17 00:00:00 2001 From: Stefan Hendricks <38368299+Neuheit@users.noreply.github.com> Date: Sat, 29 Oct 2022 18:54:11 -0400 Subject: [PATCH 04/16] Add .NET MIT License --- .../HappyEyeballs/HttpClientExtension.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index 049c8dd150..bab02505dc 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -1,3 +1,29 @@ +/* +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + using System.IO; using System.Net.Http; using System.Net.Sockets; @@ -18,8 +44,6 @@ namespace Jellyfin.Networking.HappyEyeballs /// public static bool UseIPv6 { get; set; } = true; - // Implementation taken from https://github.com/rmkerr/corefx/blob/SocketsHttpHandler_Connect_HappyEyeballs/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs - /// /// Implements the httpclient callback method. /// From 9e791a92c3dbf12420ec13e8c0b15f70d4dcba78 Mon Sep 17 00:00:00 2001 From: Neuheit <38368299+Neuheit@users.noreply.github.com> Date: Thu, 8 Dec 2022 00:16:18 -0500 Subject: [PATCH 05/16] Add comments explaining GetAwaiter() --- Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index bab02505dc..0ceaeaf968 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -69,6 +69,9 @@ namespace Jellyfin.Networking.HappyEyeballs using var cancelIPv4 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var tryConnectAsyncIPv4 = AttemptConnection(AddressFamily.InterNetwork, context, cancelIPv4.Token); + //Both connect tasks use GetAwaiter().GetResult() as the appropriate task has already been completed. + //This results in improved exception handling. + //See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details. if (await Task.WhenAny(tryConnectAsyncIPv6, tryConnectAsyncIPv4).ConfigureAwait(false) == tryConnectAsyncIPv6) { if (tryConnectAsyncIPv6.IsCompletedSuccessfully) From a7fc5e6e12e2330344640c92cc4d0afc36c74fe2 Mon Sep 17 00:00:00 2001 From: Neuheit <38368299+Neuheit@users.noreply.github.com> Date: Thu, 8 Dec 2022 00:17:46 -0500 Subject: [PATCH 06/16] Add additional awaiter code comment. --- Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index 0ceaeaf968..add70c9cd6 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -60,6 +60,9 @@ namespace Jellyfin.Networking.HappyEyeballs using var cancelIPv6 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var tryConnectAsyncIPv6 = AttemptConnection(AddressFamily.InterNetworkV6, context, cancelIPv6.Token); + //This connect task uses GetAwaiter().GetResult() as the appropriate task has already been completed. + //This results in improved exception handling. + //See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details. if (await Task.WhenAny(tryConnectAsyncIPv6, Task.Delay(200, cancelIPv6.Token)).ConfigureAwait(false) == tryConnectAsyncIPv6 && tryConnectAsyncIPv6.IsCompletedSuccessfully) { cancelIPv6.Cancel(); @@ -71,7 +74,6 @@ namespace Jellyfin.Networking.HappyEyeballs //Both connect tasks use GetAwaiter().GetResult() as the appropriate task has already been completed. //This results in improved exception handling. - //See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details. if (await Task.WhenAny(tryConnectAsyncIPv6, tryConnectAsyncIPv4).ConfigureAwait(false) == tryConnectAsyncIPv6) { if (tryConnectAsyncIPv6.IsCompletedSuccessfully) From 292c4ebe72d293184f7a60f68f5ad76296539b89 Mon Sep 17 00:00:00 2001 From: Neuheit <38368299+Neuheit@users.noreply.github.com> Date: Thu, 8 Dec 2022 16:15:36 -0500 Subject: [PATCH 07/16] Clarify code comment. Co-authored-by: Claus Vium --- Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index add70c9cd6..ddbf841b07 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -72,8 +72,6 @@ namespace Jellyfin.Networking.HappyEyeballs using var cancelIPv4 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var tryConnectAsyncIPv4 = AttemptConnection(AddressFamily.InterNetwork, context, cancelIPv4.Token); - //Both connect tasks use GetAwaiter().GetResult() as the appropriate task has already been completed. - //This results in improved exception handling. if (await Task.WhenAny(tryConnectAsyncIPv6, tryConnectAsyncIPv4).ConfigureAwait(false) == tryConnectAsyncIPv6) { if (tryConnectAsyncIPv6.IsCompletedSuccessfully) From 93fe47c7cb7a3aa5eb8504edc2fd4f27c4824993 Mon Sep 17 00:00:00 2001 From: Neuheit <38368299+Neuheit@users.noreply.github.com> Date: Thu, 8 Dec 2022 16:16:06 -0500 Subject: [PATCH 08/16] Clarify code comment. Co-authored-by: Claus Vium --- Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index ddbf841b07..59e6956c71 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -60,9 +60,9 @@ namespace Jellyfin.Networking.HappyEyeballs using var cancelIPv6 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var tryConnectAsyncIPv6 = AttemptConnection(AddressFamily.InterNetworkV6, context, cancelIPv6.Token); - //This connect task uses GetAwaiter().GetResult() as the appropriate task has already been completed. - //This results in improved exception handling. - //See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details. + // GetAwaiter().GetResult() is used instead of .Result as this results in improved exception handling. + // The tasks have already been completed. + // See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details. if (await Task.WhenAny(tryConnectAsyncIPv6, Task.Delay(200, cancelIPv6.Token)).ConfigureAwait(false) == tryConnectAsyncIPv6 && tryConnectAsyncIPv6.IsCompletedSuccessfully) { cancelIPv6.Cancel(); From b42a6119f56d3201ac402c834faeae73f52cf992 Mon Sep 17 00:00:00 2001 From: SuperDumbTM Date: Tue, 9 May 2023 05:24:04 +0000 Subject: [PATCH 09/16] Translated using Weblate (Chinese (Traditional, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- Emby.Server.Implementations/Localization/Core/zh-HK.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 610f503ddb..e8b8c2c5fa 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -15,7 +15,7 @@ "Favorites": "我的最愛", "Folders": "資料夾", "Genres": "風格", - "HeaderAlbumArtists": "專輯藝人", + "HeaderAlbumArtists": "專輯歌手", "HeaderContinueWatching": "繼續觀看", "HeaderFavoriteAlbums": "最愛的專輯", "HeaderFavoriteArtists": "最愛的藝人", From b3b75d5bc232a0e912f4ded8e3588e2c347a51de Mon Sep 17 00:00:00 2001 From: garikaib Date: Wed, 10 May 2023 01:01:16 -0400 Subject: [PATCH 10/16] Added translation using Weblate (Shona) --- Emby.Server.Implementations/Localization/Core/sn.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/sn.json diff --git a/Emby.Server.Implementations/Localization/Core/sn.json b/Emby.Server.Implementations/Localization/Core/sn.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/sn.json @@ -0,0 +1 @@ +{} From f0bf5c4998ca2f8bedd61a750dc5228e2787f21e Mon Sep 17 00:00:00 2001 From: garikaib Date: Wed, 10 May 2023 05:10:35 +0000 Subject: [PATCH 11/16] Translated using Weblate (Shona) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sn/ --- .../Localization/Core/sn.json | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/sn.json b/Emby.Server.Implementations/Localization/Core/sn.json index 0967ef424b..74720e7646 100644 --- a/Emby.Server.Implementations/Localization/Core/sn.json +++ b/Emby.Server.Implementations/Localization/Core/sn.json @@ -1 +1,28 @@ -{} +{ + "HeaderAlbumArtists": "Vaimbi vemadambarefu", + "HeaderContinueWatching": "Simudzira kuona", + "HeaderFavoriteSongs": "Nziyo dzaunofarira", + "Albums": "Dambarefu", + "AppDeviceValues": "Apu: {0}, Dhivhaisi: {1}", + "Application": "Purogiramu", + "Artists": "Vaimbi", + "AuthenticationSucceededWithUserName": "apinda", + "Books": "Mabhuku", + "CameraImageUploadedFrom": "Mufananidzo mutsva vabva pakamera {0}", + "Channels": "Machanewo", + "ChapterNameValue": "Chikamu {0}", + "Collections": "Akafanana", + "Default": "Zvakasarudzwa Kare", + "DeviceOfflineWithName": "{0} haasisipo", + "DeviceOnlineWithName": "{0} aripo", + "External": "Zvekunze", + "FailedLoginAttemptWithUserName": "Vatadza kuloga chimboedza kushandisa {0}", + "Favorites": "Zvaunofarira", + "Folders": "Mafoodha", + "Forced": "Zvekumanikidzira", + "Genres": "Mhando", + "HeaderFavoriteAlbums": "Madambarefu aunofarira", + "HeaderFavoriteArtists": "Vaimbi vaunofarira", + "HeaderFavoriteEpisodes": "Maepisodhi aunofarira", + "HeaderFavoriteShows": "Masirisi aunofarira" +} From d5fec4963ee69460a84025c456eb7d928634e765 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 10 May 2023 22:05:27 +0200 Subject: [PATCH 12/16] Fix FirstTimeSetupHandler not failing on invalid user if not in setup mode (#9747) --- .../FirstTimeSetupPolicy/FirstTimeSetupHandler.cs | 12 ++++++++++-- Jellyfin.Api/Controllers/SystemController.cs | 12 ++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs index 28ba258503..688a13bc0b 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs @@ -38,7 +38,15 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy return Task.CompletedTask; } - if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator)) + var contextUser = context.User; + if (requirement.RequireAdmin && !contextUser.IsInRole(UserRoles.Administrator)) + { + context.Fail(); + return Task.CompletedTask; + } + + var userId = contextUser.GetUserId(); + if (userId.Equals(default)) { context.Fail(); return Task.CompletedTask; @@ -50,7 +58,7 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy return Task.CompletedTask; } - var user = _userManager.GetUserById(context.User.GetUserId()); + var user = _userManager.GetUserById(userId); if (user is null) { throw new ResourceNotFoundException(); diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 4ab705f40a..9ed69f4205 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -59,10 +59,12 @@ public class SystemController : BaseJellyfinApiController /// Gets information about the server. /// /// Information retrieved. + /// User does not have permission to retrieve information. /// A with info about the system. [HttpGet("Info")] [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult GetSystemInfo() { return _appHost.GetSystemInfo(Request); @@ -97,10 +99,12 @@ public class SystemController : BaseJellyfinApiController /// Restarts the application. /// /// Server restarted. + /// User does not have permission to restart server. /// No content. Server restarted. [HttpPost("Restart")] [Authorize(Policy = Policies.LocalAccessOrRequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult RestartApplication() { Task.Run(async () => @@ -115,10 +119,12 @@ public class SystemController : BaseJellyfinApiController /// Shuts down the application. /// /// Server shut down. + /// User does not have permission to shutdown server. /// No content. Server shut down. [HttpPost("Shutdown")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult ShutdownApplication() { Task.Run(async () => @@ -133,10 +139,12 @@ public class SystemController : BaseJellyfinApiController /// Gets a list of available server log files. /// /// Information retrieved. + /// User does not have permission to get server logs. /// An array of with the available log files. [HttpGet("Logs")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult GetServerLogs() { IEnumerable files; @@ -170,10 +178,12 @@ public class SystemController : BaseJellyfinApiController /// Gets information about the request endpoint. /// /// Information retrieved. + /// User does not have permission to get endpoint information. /// with information about the endpoint. [HttpGet("Endpoint")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult GetEndpointInfo() { return new EndPointInfo @@ -188,10 +198,12 @@ public class SystemController : BaseJellyfinApiController /// /// The name of the log file to get. /// Log file retrieved. + /// User does not have permission to get log files. /// The log file. [HttpGet("Logs/Log")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesFile(MediaTypeNames.Text.Plain)] public ActionResult GetLogFile([FromQuery, Required] string name) { From bcf92b57220d92242ef6ae82118d469f2401e27f Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 11 May 2023 01:38:54 +0200 Subject: [PATCH 13/16] Fix MigrateRatingLevels (#9461) --- Jellyfin.Server/Migrations/MigrationRunner.cs | 6 +- .../PreStartupRoutines/MigrateRatingLevels.cs | 86 --------------- .../Routines/MigrateRatingLevels.cs | 103 ++++++++++++++++++ 3 files changed, 106 insertions(+), 89 deletions(-) delete mode 100644 Jellyfin.Server/Migrations/PreStartupRoutines/MigrateRatingLevels.cs create mode 100644 Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 866262d22f..abfdcd77d5 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -22,8 +22,7 @@ namespace Jellyfin.Server.Migrations private static readonly Type[] _preStartupMigrationTypes = { typeof(PreStartupRoutines.CreateNetworkConfiguration), - typeof(PreStartupRoutines.MigrateMusicBrainzTimeout), - typeof(PreStartupRoutines.MigrateRatingLevels) + typeof(PreStartupRoutines.MigrateMusicBrainzTimeout) }; /// @@ -41,7 +40,8 @@ namespace Jellyfin.Server.Migrations typeof(Routines.MigrateDisplayPreferencesDb), typeof(Routines.RemoveDownloadImagesInAdvance), typeof(Routines.MigrateAuthenticationDb), - typeof(Routines.FixPlaylistOwner) + typeof(Routines.FixPlaylistOwner), + typeof(Routines.MigrateRatingLevels) }; /// diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateRatingLevels.cs deleted file mode 100644 index 465bbd7fe1..0000000000 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateRatingLevels.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Globalization; -using System.IO; - -using Emby.Server.Implementations; -using MediaBrowser.Controller; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Jellyfin.Server.Migrations.PreStartupRoutines -{ - /// - /// Migrate rating levels to new rating level system. - /// - internal class MigrateRatingLevels : IMigrationRoutine - { - private const string DbFilename = "library.db"; - private readonly ILogger _logger; - private readonly IServerApplicationPaths _applicationPaths; - - public MigrateRatingLevels(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory) - { - _applicationPaths = applicationPaths; - _logger = loggerFactory.CreateLogger(); - } - - /// - public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}"); - - /// - public string Name => "MigrateRatingLevels"; - - /// - public bool PerformOnNewInstall => false; - - /// - public void Perform() - { - var dataPath = _applicationPaths.DataPath; - var dbPath = Path.Combine(dataPath, DbFilename); - using (var connection = SQLite3.Open( - dbPath, - ConnectionFlags.ReadWrite, - null)) - { - // Back up the database before deleting any entries - for (int i = 1; ; i++) - { - var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i); - if (!File.Exists(bakPath)) - { - try - { - File.Copy(dbPath, bakPath); - _logger.LogInformation("Library database backed up to {BackupPath}", bakPath); - break; - } - catch (Exception ex) - { - _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath); - throw; - } - } - } - - // Migrate parental rating levels to new schema - _logger.LogInformation("Migrating parental rating levels."); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating = 'NR'"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE InheritedParentalRatingValue = ''"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE InheritedParentalRatingValue = 0"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 1000 WHERE InheritedParentalRatingValue = 100"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 1000 WHERE InheritedParentalRatingValue = 15"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 18 WHERE InheritedParentalRatingValue = 10"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 18 WHERE InheritedParentalRatingValue = 9"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 16 WHERE InheritedParentalRatingValue = 8"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 7"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 6"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 5"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 7 WHERE InheritedParentalRatingValue = 4"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 6 WHERE InheritedParentalRatingValue = 3"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 6 WHERE InheritedParentalRatingValue = 2"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 0 WHERE InheritedParentalRatingValue = 1"); - } - } - } -} diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs new file mode 100644 index 0000000000..9dee520a50 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -0,0 +1,103 @@ +using System; +using System.Globalization; +using System.IO; + +using Emby.Server.Implementations.Data; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Globalization; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// + /// Migrate rating levels to new rating level system. + /// + internal class MigrateRatingLevels : IMigrationRoutine + { + private const string DbFilename = "library.db"; + private readonly ILogger _logger; + private readonly IServerApplicationPaths _applicationPaths; + private readonly ILocalizationManager _localizationManager; + private readonly IItemRepository _repository; + + public MigrateRatingLevels( + IServerApplicationPaths applicationPaths, + ILoggerFactory loggerFactory, + ILocalizationManager localizationManager, + IItemRepository repository) + { + _applicationPaths = applicationPaths; + _localizationManager = localizationManager; + _repository = repository; + _logger = loggerFactory.CreateLogger(); + } + + /// + public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}"); + + /// + public string Name => "MigrateRatingLevels"; + + /// + public bool PerformOnNewInstall => false; + + /// + public void Perform() + { + var dbPath = Path.Combine(_applicationPaths.DataPath, DbFilename); + + // Back up the database before modifying any entries + for (int i = 1; ; i++) + { + var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i); + if (!File.Exists(bakPath)) + { + try + { + File.Copy(dbPath, bakPath); + _logger.LogInformation("Library database backed up to {BackupPath}", bakPath); + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath); + throw; + } + } + } + + // Migrate parental rating strings to new levels + _logger.LogInformation("Recalculating parental rating levels based on rating string."); + using (var connection = SQLite3.Open( + dbPath, + ConnectionFlags.ReadWrite, + null)) + { + var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems"); + foreach (var entry in queryResult) + { + var ratingString = entry[0].ToString(); + if (string.IsNullOrEmpty(ratingString)) + { + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';"); + } + else + { + var ratingValue = _localizationManager.GetRatingLevel(ratingString).ToString(); + if (string.IsNullOrEmpty(ratingValue)) + { + ratingValue = "NULL"; + } + + var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;"); + statement.TryBind("@Value", ratingValue); + statement.TryBind("@Rating", ratingString); + statement.ExecuteQuery(); + } + } + } + } + } +} From 756ee38d01ae66f1d6abe920d96eabce468a2e2e Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 11 May 2023 01:39:57 +0200 Subject: [PATCH 14/16] Remove ExtendedFileSystemInfo (#9749) --- .../IO/ExtendedFileSystemInfo.cs | 13 ------ .../IO/ManagedFileSystem.cs | 46 +++++-------------- 2 files changed, 11 insertions(+), 48 deletions(-) delete mode 100644 Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs diff --git a/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs b/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs deleted file mode 100644 index 545d73e05f..0000000000 --- a/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs +++ /dev/null @@ -1,13 +0,0 @@ -#pragma warning disable CS1591 - -namespace Emby.Server.Implementations.IO -{ - public class ExtendedFileSystemInfo - { - public bool IsHidden { get; set; } - - public bool IsReadOnly { get; set; } - - public bool Exists { get; set; } - } -} diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 55f384ae8f..1fffdfbfa0 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -267,25 +267,6 @@ namespace Emby.Server.Implementations.IO return result; } - private static ExtendedFileSystemInfo GetExtendedFileSystemInfo(string path) - { - var result = new ExtendedFileSystemInfo(); - - var info = new FileInfo(path); - - if (info.Exists) - { - result.Exists = true; - - var attributes = info.Attributes; - - result.IsHidden = (attributes & FileAttributes.Hidden) == FileAttributes.Hidden; - result.IsReadOnly = (attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly; - } - - return result; - } - /// /// Takes a filename and removes invalid characters. /// @@ -403,19 +384,18 @@ namespace Emby.Server.Implementations.IO return; } - var info = GetExtendedFileSystemInfo(path); + var info = new FileInfo(path); - if (info.Exists && info.IsHidden != isHidden) + if (info.Exists && + ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) != isHidden) { if (isHidden) { - File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.Hidden); + File.SetAttributes(path, info.Attributes | FileAttributes.Hidden); } else { - var attributes = File.GetAttributes(path); - attributes = RemoveAttribute(attributes, FileAttributes.Hidden); - File.SetAttributes(path, attributes); + File.SetAttributes(path, info.Attributes & ~FileAttributes.Hidden); } } } @@ -428,19 +408,20 @@ namespace Emby.Server.Implementations.IO return; } - var info = GetExtendedFileSystemInfo(path); + var info = new FileInfo(path); if (!info.Exists) { return; } - if (info.IsReadOnly == readOnly && info.IsHidden == isHidden) + if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly + && ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden) { return; } - var attributes = File.GetAttributes(path); + var attributes = info.Attributes; if (readOnly) { @@ -448,7 +429,7 @@ namespace Emby.Server.Implementations.IO } else { - attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly); + attributes &= ~FileAttributes.ReadOnly; } if (isHidden) @@ -457,17 +438,12 @@ namespace Emby.Server.Implementations.IO } else { - attributes = RemoveAttribute(attributes, FileAttributes.Hidden); + attributes &= ~FileAttributes.Hidden; } File.SetAttributes(path, attributes); } - private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove) - { - return attributes & ~attributesToRemove; - } - /// /// Swaps the files. /// From ffac062489ec6bd8823823148220e1785ecc44d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 May 2023 17:43:17 -0600 Subject: [PATCH 15/16] chore(deps): update fedora docker tag to v39 (#9482) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- deployment/Dockerfile.fedora.amd64 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 275b379b3d..8d708f9028 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -1,4 +1,4 @@ -FROM fedora:36 +FROM fedora:39 # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist From 11d7c00de9731b3b875ea5bb3afa02e1dfd7b950 Mon Sep 17 00:00:00 2001 From: SenorSmartyPants Date: Wed, 10 May 2023 18:46:55 -0500 Subject: [PATCH 16/16] Fix local JPG primary image for video being overwritten by screen grabber (#9552) --- .../Manager/ItemImageProvider.cs | 42 +++++++++++++++++-- .../Manager/MetadataService.cs | 19 ++++----- .../Manager/ItemImageProviderTests.cs | 6 +-- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index ba2d2db2f7..dab36625e5 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -32,6 +32,7 @@ namespace MediaBrowser.Providers.Manager private readonly ILogger _logger; private readonly IProviderManager _providerManager; private readonly IFileSystem _fileSystem; + private static readonly ImageType[] AllImageTypes = Enum.GetValues(); /// /// Image types that are only one per item. @@ -90,11 +91,12 @@ namespace MediaBrowser.Providers.Manager /// /// The to validate images for. /// The providers to use, must include (s) for local scanning. - /// The directory service for s to use. + /// The refresh options. /// true if changes were made to the item; otherwise false. - public bool ValidateImages(BaseItem item, IEnumerable providers, IDirectoryService directoryService) + public bool ValidateImages(BaseItem item, IEnumerable providers, ImageRefreshOptions refreshOptions) { var hasChanges = false; + IDirectoryService directoryService = refreshOptions?.DirectoryService; if (item is not Photo) { @@ -102,7 +104,7 @@ namespace MediaBrowser.Providers.Manager .SelectMany(i => i.GetImages(item, directoryService)) .ToList(); - if (MergeImages(item, images)) + if (MergeImages(item, images, refreshOptions)) { hasChanges = true; } @@ -381,15 +383,36 @@ namespace MediaBrowser.Providers.Manager item.RemoveImages(images); } + /// + /// Merges a list of images into the provided item, validating existing images and replacing them or adding new images as necessary. + /// + /// The refresh options. + /// List of imageTypes to remove from ReplaceImages. + public void UpdateReplaceImages(ImageRefreshOptions refreshOptions, ICollection dontReplaceImages) + { + if (refreshOptions is not null) + { + if (refreshOptions.ReplaceAllImages) + { + refreshOptions.ReplaceAllImages = false; + refreshOptions.ReplaceImages = AllImageTypes.ToList(); + } + + refreshOptions.ReplaceImages = refreshOptions.ReplaceImages.Except(dontReplaceImages).ToList(); + } + } + /// /// Merges a list of images into the provided item, validating existing images and replacing them or adding new images as necessary. /// /// The to modify. /// The new images to place in item. + /// The refresh options. /// true if changes were made to the item; otherwise false. - public bool MergeImages(BaseItem item, IReadOnlyList images) + public bool MergeImages(BaseItem item, IReadOnlyList images, ImageRefreshOptions refreshOptions) { var changed = item.ValidateImages(); + var foundImageTypes = new List(); for (var i = 0; i < _singularImages.Length; i++) { @@ -399,6 +422,11 @@ namespace MediaBrowser.Providers.Manager if (image is not null) { var currentImage = item.GetImageInfo(type, 0); + // if image file is stored with media, don't replace that later + if (item.ContainingFolderPath is not null && item.ContainingFolderPath.Contains(Path.GetDirectoryName(image.FileInfo.FullName), StringComparison.OrdinalIgnoreCase)) + { + foundImageTypes.Add(type); + } if (currentImage is null || !string.Equals(currentImage.Path, image.FileInfo.FullName, StringComparison.OrdinalIgnoreCase)) { @@ -425,6 +453,12 @@ namespace MediaBrowser.Providers.Manager if (UpdateMultiImages(item, images, ImageType.Backdrop)) { changed = true; + foundImageTypes.Add(ImageType.Backdrop); + } + + if (foundImageTypes.Count > 0) + { + UpdateReplaceImages(refreshOptions, foundImageTypes); } return changed; diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index bcc9b809c2..80f77f7c3a 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -26,8 +26,6 @@ namespace MediaBrowser.Providers.Manager where TItemType : BaseItem, IHasLookupInfo, new() where TIdType : ItemLookupInfo, new() { - private static readonly ImageType[] AllImageTypes = Enum.GetValues(); - protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) { ServerConfigurationManager = serverConfigurationManager; @@ -110,7 +108,7 @@ namespace MediaBrowser.Providers.Manager try { // Always validate images and check for new locally stored ones. - if (ImageProvider.ValidateImages(item, allImageProviders.OfType(), refreshOptions.DirectoryService)) + if (ImageProvider.ValidateImages(item, allImageProviders.OfType(), refreshOptions)) { updateType |= ItemUpdateType.ImageUpdate; } @@ -674,8 +672,7 @@ namespace MediaBrowser.Providers.Manager } var hasLocalMetadata = false; - var replaceImages = AllImageTypes.ToList(); - var localImagesFound = false; + var foundImageTypes = new List(); foreach (var provider in providers.OfType>()) { @@ -703,9 +700,8 @@ namespace MediaBrowser.Providers.Manager await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false); refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; - // remove imagetype that has just been downloaded - replaceImages.Remove(remoteImage.Type); - localImagesFound = true; + // remember imagetype that has just been downloaded + foundImageTypes.Add(remoteImage.Type); } catch (HttpRequestException ex) { @@ -713,13 +709,12 @@ namespace MediaBrowser.Providers.Manager } } - if (localImagesFound) + if (foundImageTypes.Count > 0) { - options.ReplaceAllImages = false; - options.ReplaceImages = replaceImages; + imageService.UpdateReplaceImages(options, foundImageTypes); } - if (imageService.MergeImages(item, localItem.Images)) + if (imageService.MergeImages(item, localItem.Images, options)) { refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; } diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs index 08b343cd89..925e8fa199 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs @@ -94,7 +94,7 @@ namespace Jellyfin.Providers.Tests.Manager public void MergeImages_EmptyItemNewImagesEmpty_NoChange() { var itemImageProvider = GetItemImageProvider(null, null); - var changed = itemImageProvider.MergeImages(new Video(), Array.Empty()); + var changed = itemImageProvider.MergeImages(new Video(), Array.Empty(), new ImageRefreshOptions(Mock.Of())); Assert.False(changed); } @@ -108,7 +108,7 @@ namespace Jellyfin.Providers.Tests.Manager var images = GetImages(imageType, imageCount, false); var itemImageProvider = GetItemImageProvider(null, null); - var changed = itemImageProvider.MergeImages(item, images); + var changed = itemImageProvider.MergeImages(item, images, new ImageRefreshOptions(Mock.Of())); Assert.True(changed); // adds for types that allow multiple, replaces singular type images @@ -151,7 +151,7 @@ namespace Jellyfin.Providers.Tests.Manager var images = GetImages(imageType, imageCount, true); var itemImageProvider = GetItemImageProvider(null, fileSystem); - var changed = itemImageProvider.MergeImages(item, images); + var changed = itemImageProvider.MergeImages(item, images, new ImageRefreshOptions(Mock.Of())); if (updateTime) {