From e9f23c61c937b230b1d3bd7865a083aeb3d51657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20M=C3=BCller?= Date: Tue, 1 Aug 2023 17:11:32 +0200 Subject: [PATCH 01/75] Fix the fLaC/flac HLS issue also for audio-only I moved the first application of the workaround out of the if block so that it also applies to audio-only streams. The workaround was extended likewise. We should first and foremost adhere to the specifications and apply workarounds afterwards for software that doesn't follow them. So I turned around the workaround to first output the fLaC variant and then the alternative flac variant. Fixes: #10066 --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 21 +++++++++++-------- Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 6 ++++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 63667e7e69..dfcccddfc2 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -198,15 +198,15 @@ public class DynamicHlsHelper var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); - if (state.VideoStream is not null && state.VideoRequest is not null) + // Provide a workaround for the case issue between flac and fLaC. + var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString()); + if (!string.IsNullOrEmpty(flacWaPlaylist)) { - // Provide a workaround for the case issue between flac and fLaC. - var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString()); - if (!string.IsNullOrEmpty(flacWaPlaylist)) - { - builder.Append(flacWaPlaylist); - } + builder.Append(flacWaPlaylist); + } + if (state.VideoStream is not null && state.VideoRequest is not null) + { var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); // Provide SDR HEVC entrance for backward compatibility. @@ -775,8 +775,11 @@ public class DynamicHlsHelper return string.Empty; } - var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal); + var newPlaylist = srcPlaylist; + + newPlaylist = newPlaylist.Replace(",fLaC\"", ",flac\"", StringComparison.Ordinal); + newPlaylist = newPlaylist.Replace("\"fLaC\"", "\"flac\"", StringComparison.Ordinal); - return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty; + return string.Equals(srcPlaylist, newPlaylist, StringComparison.Ordinal) ? string.Empty : newPlaylist; } } diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index 9a141a16d9..9b1c52045f 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -5,7 +5,9 @@ using System.Text; namespace Jellyfin.Api.Helpers; /// -/// Hls Codec string helpers. +/// Helpers to generate HLS codec strings according to +/// RFC 6381 section 3.3 +/// and the MP4 Registration Authority. /// public static class HlsCodecStringHelpers { @@ -27,7 +29,7 @@ public static class HlsCodecStringHelpers /// /// Codec name for FLAC. /// - public const string FLAC = "flac"; + public const string FLAC = "fLaC"; /// /// Codec name for ALAC. From 19fb061381dd107d5e0236cf9d8b59b2e2318130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20M=C3=BCller?= Date: Tue, 1 Aug 2023 17:22:42 +0200 Subject: [PATCH 02/75] Correct the HLS Opus codec string Apple doesn't support Opus via HLS yet, but if they ever do, they will definitely expect "Opus" instead of "opus". See https://mp4ra.org/#/codecs Fixes: #10066 --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 28 ++++++++++--------- Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index dfcccddfc2..888b667a68 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -198,11 +198,11 @@ public class DynamicHlsHelper var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); - // Provide a workaround for the case issue between flac and fLaC. - var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString()); - if (!string.IsNullOrEmpty(flacWaPlaylist)) + // Provide a workaround for alternative codec string capitalization. + var alternativeCodecCapitalizationPlaylist = ApplyCodecCapitalizationWorkaround(state, basicPlaylist.ToString()); + if (!string.IsNullOrEmpty(alternativeCodecCapitalizationPlaylist)) { - builder.Append(flacWaPlaylist); + builder.Append(alternativeCodecCapitalizationPlaylist); } if (state.VideoStream is not null && state.VideoRequest is not null) @@ -238,11 +238,11 @@ public class DynamicHlsHelper var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate; var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup); - // Provide a workaround for the case issue between flac and fLaC. - flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString()); - if (!string.IsNullOrEmpty(flacWaPlaylist)) + // Provide a workaround for alternative codec string capitalization. + alternativeCodecCapitalizationPlaylist = ApplyCodecCapitalizationWorkaround(state, sdrPlaylist.ToString()); + if (!string.IsNullOrEmpty(alternativeCodecCapitalizationPlaylist)) { - builder.Append(flacWaPlaylist); + builder.Append(alternativeCodecCapitalizationPlaylist); } // Restore the video codec @@ -275,11 +275,11 @@ public class DynamicHlsHelper var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField); builder.Append(newPlaylist); - // Provide a workaround for the case issue between flac and fLaC. - flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist); - if (!string.IsNullOrEmpty(flacWaPlaylist)) + // Provide a workaround for alternative codec string capitalization. + alternativeCodecCapitalizationPlaylist = ApplyCodecCapitalizationWorkaround(state, newPlaylist); + if (!string.IsNullOrEmpty(alternativeCodecCapitalizationPlaylist)) { - builder.Append(flacWaPlaylist); + builder.Append(alternativeCodecCapitalizationPlaylist); } } } @@ -768,7 +768,7 @@ public class DynamicHlsHelper StringComparison.Ordinal); } - private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist) + private string ApplyCodecCapitalizationWorkaround(StreamState state, string srcPlaylist) { if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)) { @@ -779,6 +779,8 @@ public class DynamicHlsHelper newPlaylist = newPlaylist.Replace(",fLaC\"", ",flac\"", StringComparison.Ordinal); newPlaylist = newPlaylist.Replace("\"fLaC\"", "\"flac\"", StringComparison.Ordinal); + newPlaylist = newPlaylist.Replace(",Opus\"", ",opus\"", StringComparison.Ordinal); + newPlaylist = newPlaylist.Replace("\"Opus\"", "\"opus\"", StringComparison.Ordinal); return string.Equals(srcPlaylist, newPlaylist, StringComparison.Ordinal) ? string.Empty : newPlaylist; } diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index 9b1c52045f..5eec1b0ca6 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -39,7 +39,7 @@ public static class HlsCodecStringHelpers /// /// Codec name for OPUS. /// - public const string OPUS = "opus"; + public const string OPUS = "Opus"; /// /// Gets a MP3 codec string. From 79cff704ff4e00895a8d2b97ecbbea3e2f5f56fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20M=C3=BCller?= Date: Tue, 1 Aug 2023 17:28:49 +0200 Subject: [PATCH 03/75] Allow flac inside mp4 for all HLS audio streams The -strict -2 setting was only added if the encoder was set to 'copy'. If 'flac' is explicitly requested, we also need to set it, so that ffmpeg doesn't abort the conversion. Fixes: #10066 --- .../Controllers/DynamicHlsController.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index ce684e457c..94093f1671 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1704,18 +1704,29 @@ public class DynamicHlsController : BaseJellyfinApiController var audioCodec = _encodingHelper.GetAudioEncoder(state); + // dts, flac, opus and truehd are experimental in mp4 muxer + var strictArgs = string.Empty; + var actualOutputAudioCodec = state.ActualOutputAudioCodec; + if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) + || string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase) + || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase) + || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)) + { + strictArgs = " -strict -2"; + } + if (!state.IsOutputVideo) { if (EncodingHelper.IsCopyCodec(audioCodec)) { var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); - return "-acodec copy -strict -2" + bitStreamArgs; + return "-acodec copy" + bitStreamArgs + strictArgs; } var audioTranscodeParams = string.Empty; - audioTranscodeParams += "-acodec " + audioCodec; + audioTranscodeParams += "-acodec " + audioCodec + strictArgs; var audioBitrate = state.OutputAudioBitrate; var audioChannels = state.OutputAudioChannels; @@ -1747,17 +1758,6 @@ public class DynamicHlsController : BaseJellyfinApiController return audioTranscodeParams; } - // dts, flac, opus and truehd are experimental in mp4 muxer - var strictArgs = string.Empty; - var actualOutputAudioCodec = state.ActualOutputAudioCodec; - if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) - || string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase) - || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase) - || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)) - { - strictArgs = " -strict -2"; - } - if (EncodingHelper.IsCopyCodec(audioCodec)) { var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); From 8dc58e8f04ca891aa8e515c369f14ecbb21d2191 Mon Sep 17 00:00:00 2001 From: null <9310d27e@gmail.com> Date: Sun, 20 Aug 2023 21:18:55 +0400 Subject: [PATCH 04/75] Added handling of FFmpeg:probesize variable --- CONTRIBUTORS.md | 1 + .../MediaEncoding/EncodingHelper.cs | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index b7e7778176..e3af12a497 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -238,3 +238,4 @@ - [Jakob Kukla](https://github.com/jakobkukla) - [Utku Özdemir](https://github.com/utkuozdemir) - [JPUC1143](https://github.com/Jpuc1143/) + - [0x25CBFC4F](https://github.com/0x25CBFC4F) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index d61430b0bc..f8d2dd40fd 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -5678,7 +5678,6 @@ namespace MediaBrowser.Controller.MediaEncoding // Apply -analyzeduration as per the environment variable, // otherwise ffmpeg will break on certain files due to default value is 0. - // The default value of -probesize is more than enough, so leave it as is. var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty; if (state.MediaSource.AnalyzeDurationMs > 0) @@ -5697,6 +5696,22 @@ namespace MediaBrowser.Controller.MediaEncoding inputModifier = inputModifier.Trim(); + // Apply -probesize if configured + var probeSizeArgument = string.Empty; + var ffmpegProbeSize = _config.GetFFmpegProbeSize(); + + if (!string.IsNullOrEmpty(ffmpegProbeSize)) + { + probeSizeArgument = $"-probesize {probeSizeArgument}"; + } + + if (!string.IsNullOrEmpty(probeSizeArgument)) + { + inputModifier += $" {probeSizeArgument}"; + } + + inputModifier = inputModifier.Trim(); + var userAgentParam = GetUserAgentParam(state); if (!string.IsNullOrEmpty(userAgentParam)) From 9d5e6108fb6446834e7d7db12abc10f03a8657f5 Mon Sep 17 00:00:00 2001 From: "Brian J. Murrell" Date: Tue, 12 Sep 2023 07:57:09 -0400 Subject: [PATCH 05/75] Make startup script more portable Make the creation of the startup script more portable and future-proof. --- fedora/jellyfin.spec | 1 + 1 file changed, 1 insertion(+) diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index d9d3d48694..1f7262e660 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -75,6 +75,7 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti %{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir} %{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin %{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin +sed -i -e 's/\/usr\/lib64/%{_libdir}/g' %{buildroot}%{_bindir}/jellyfin # Jellyfin config %{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json From 1635d82345a035c007daffe980cc09de17984813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20M=C3=BCller?= Date: Sat, 16 Sep 2023 12:57:20 +0200 Subject: [PATCH 06/75] Remove workaround for codec capitalization This is not required anymore as Shaka Player now supports the correct codec strings. --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 40 +----------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 03b723ef1b..276a09f41e 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -198,13 +198,6 @@ public class DynamicHlsHelper var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); - // Provide a workaround for alternative codec string capitalization. - var alternativeCodecCapitalizationPlaylist = ApplyCodecCapitalizationWorkaround(state, basicPlaylist.ToString()); - if (!string.IsNullOrEmpty(alternativeCodecCapitalizationPlaylist)) - { - builder.Append(alternativeCodecCapitalizationPlaylist); - } - if (state.VideoStream is not null && state.VideoRequest is not null) { var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); @@ -236,14 +229,7 @@ public class DynamicHlsHelper } var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate; - var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup); - - // Provide a workaround for alternative codec string capitalization. - alternativeCodecCapitalizationPlaylist = ApplyCodecCapitalizationWorkaround(state, sdrPlaylist.ToString()); - if (!string.IsNullOrEmpty(alternativeCodecCapitalizationPlaylist)) - { - builder.Append(alternativeCodecCapitalizationPlaylist); - } + AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup); // Restore the video codec state.OutputVideoCodec = "copy"; @@ -274,13 +260,6 @@ public class DynamicHlsHelper state.VideoStream.Level = originalLevel; var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField); builder.Append(newPlaylist); - - // Provide a workaround for alternative codec string capitalization. - alternativeCodecCapitalizationPlaylist = ApplyCodecCapitalizationWorkaround(state, newPlaylist); - if (!string.IsNullOrEmpty(alternativeCodecCapitalizationPlaylist)) - { - builder.Append(alternativeCodecCapitalizationPlaylist); - } } } @@ -767,21 +746,4 @@ public class DynamicHlsHelper newValue.ToString(), StringComparison.Ordinal); } - - private string ApplyCodecCapitalizationWorkaround(StreamState state, string srcPlaylist) - { - if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)) - { - return string.Empty; - } - - var newPlaylist = srcPlaylist; - - newPlaylist = newPlaylist.Replace(",fLaC\"", ",flac\"", StringComparison.Ordinal); - newPlaylist = newPlaylist.Replace("\"fLaC\"", "\"flac\"", StringComparison.Ordinal); - newPlaylist = newPlaylist.Replace(",Opus\"", ",opus\"", StringComparison.Ordinal); - newPlaylist = newPlaylist.Replace("\"Opus\"", "\"opus\"", StringComparison.Ordinal); - - return string.Equals(srcPlaylist, newPlaylist, StringComparison.Ordinal) ? string.Empty : newPlaylist; - } } From 1d8c3e088be75ce02b811afaeea20307df0487be Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 22 Sep 2023 09:50:29 -0400 Subject: [PATCH 07/75] Don't log unhandled exceptions twice --- Jellyfin.Server/Program.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 6e8b17a737..3a3dd97bd3 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -89,12 +89,6 @@ namespace Jellyfin.Server private static async Task StartApp(StartupOptions options) { _startTimestamp = Stopwatch.GetTimestamp(); - - // Log all uncaught exceptions to std error - static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) => - Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject); - AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole; - ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager @@ -112,8 +106,7 @@ namespace Jellyfin.Server StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths); _logger = _loggerFactory.CreateLogger("Main"); - // Log uncaught exceptions to the logging instead of std error - AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole; + // Use the logging framework for uncaught exceptions instead of std error AppDomain.CurrentDomain.UnhandledException += (_, e) => _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception"); From f778073132f78b3a246a5a665a2c536d05a0278f Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 23 Sep 2023 17:57:08 +0200 Subject: [PATCH 08/75] Downgrade SkiaSharp to prevent segfault (#10264) --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 27851cdebd..6bf960bb71 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,6 +19,7 @@ + @@ -68,11 +69,10 @@ + + - - - From 493de3297a415061f8d6a69ff9f62261c3159a2a Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 22 Sep 2023 21:10:49 -0400 Subject: [PATCH 09/75] Use IHostLifetime to handle restarting and shutting down --- .../ApplicationHost.cs | 124 ++---------------- .../Plugins/PluginManager.cs | 22 ++-- .../Session/SessionManager.cs | 117 ++++++++--------- Jellyfin.Api/Controllers/SystemController.cs | 14 +- Jellyfin.Server/CoreAppHost.cs | 6 - Jellyfin.Server/Program.cs | 70 +--------- MediaBrowser.Common/IApplicationHost.cs | 21 +-- MediaBrowser.Common/Plugins/IPluginManager.cs | 5 - .../Session/ISessionManager.cs | 14 -- .../JellyfinApplicationFactory.cs | 3 +- 10 files changed, 98 insertions(+), 298 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8b13ccadab..86721ace61 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -12,7 +12,6 @@ using System.Linq; using System.Net; using System.Reflection; using System.Security.Cryptography.X509Certificates; -using System.Threading; using System.Threading.Tasks; using Emby.Dlna; using Emby.Dlna.Main; @@ -112,7 +111,7 @@ namespace Emby.Server.Implementations /// /// Class CompositionRoot. /// - public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable + public abstract class ApplicationHost : IServerApplicationHost, IDisposable { /// /// The disposable parts. @@ -127,7 +126,6 @@ namespace Emby.Server.Implementations private readonly IPluginManager _pluginManager; private List _creatingInstances; - private ISessionManager _sessionManager; /// /// Gets or sets all concrete types. @@ -172,6 +170,8 @@ namespace Emby.Server.Implementations ConfigurationManager.Configuration, ApplicationPaths.PluginsPath, ApplicationVersion); + + _disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue); } /// @@ -202,7 +202,10 @@ namespace Emby.Server.Implementations public bool HasPendingRestart { get; private set; } /// - public bool IsShuttingDown { get; private set; } + public bool IsShuttingDown { get; set; } + + /// + public bool ShouldRestart { get; set; } /// /// Gets the logger. @@ -406,11 +409,9 @@ namespace Emby.Server.Implementations /// /// Runs the startup tasks. /// - /// The cancellation token. /// . - public async Task RunStartupTasksAsync(CancellationToken cancellationToken) + public async Task RunStartupTasksAsync() { - cancellationToken.ThrowIfCancellationRequested(); Logger.LogInformation("Running startup tasks"); Resolve().AddTasks(GetExports(false)); @@ -424,8 +425,6 @@ namespace Emby.Server.Implementations var entryPoints = GetExports(); - cancellationToken.ThrowIfCancellationRequested(); - var stopWatch = new Stopwatch(); stopWatch.Start(); @@ -435,8 +434,6 @@ namespace Emby.Server.Implementations Logger.LogInformation("Core startup complete"); CoreStartupHasCompleted = true; - cancellationToken.ThrowIfCancellationRequested(); - stopWatch.Restart(); await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); @@ -633,8 +630,6 @@ namespace Emby.Server.Implementations var localizationManager = (LocalizationManager)Resolve(); await localizationManager.LoadAll().ConfigureAwait(false); - _sessionManager = Resolve(); - SetStaticProperties(); FindParts(); @@ -855,38 +850,6 @@ namespace Emby.Server.Implementations } } - /// - /// Restarts this instance. - /// - public void Restart() - { - if (IsShuttingDown) - { - return; - } - - IsShuttingDown = true; - _pluginManager.UnloadAssemblies(); - - Task.Run(async () => - { - try - { - await _sessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error sending server restart notification"); - } - - Logger.LogInformation("Calling RestartInternal"); - - RestartInternal(); - }); - } - - protected abstract void RestartInternal(); - /// /// Gets the composable part assemblies. /// @@ -1065,30 +1028,6 @@ namespace Emby.Server.Implementations }.ToString().TrimEnd('/'); } - /// - public async Task Shutdown() - { - if (IsShuttingDown) - { - return; - } - - IsShuttingDown = true; - - try - { - await _sessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error sending server shutdown notification"); - } - - ShutdownInternal(); - } - - protected abstract void ShutdownInternal(); - public IEnumerable GetApiPluginAssemblies() { var assemblies = _allConcreteTypes @@ -1152,52 +1091,5 @@ namespace Emby.Server.Implementations _disposed = true; } - - public async ValueTask DisposeAsync() - { - await DisposeAsyncCore().ConfigureAwait(false); - Dispose(false); - GC.SuppressFinalize(this); - } - - /// - /// Used to perform asynchronous cleanup of managed resources or for cascading calls to . - /// - /// A ValueTask. - protected virtual async ValueTask DisposeAsyncCore() - { - var type = GetType(); - - Logger.LogInformation("Disposing {Type}", type.Name); - - foreach (var (part, _) in _disposableParts) - { - var partType = part.GetType(); - if (partType == type) - { - continue; - } - - Logger.LogInformation("Disposing {Type}", partType.Name); - - try - { - part.Dispose(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error disposing {Type}", partType.Name); - } - } - - if (_sessionManager is not null) - { - // used for closing websockets - foreach (var session in _sessionManager.Sessions) - { - await session.DisposeAsync().ConfigureAwait(false); - } - } - } } } diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 1303012e1a..d7189ef0ca 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Data; using System.Globalization; using System.IO; using System.Linq; @@ -11,7 +10,6 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; using Emby.Server.Implementations.Library; -using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Common; @@ -30,7 +28,7 @@ namespace Emby.Server.Implementations.Plugins /// /// Defines the . /// - public class PluginManager : IPluginManager + public sealed class PluginManager : IPluginManager, IDisposable { private const string MetafileName = "meta.json"; @@ -191,15 +189,6 @@ namespace Emby.Server.Implementations.Plugins } } - /// - public void UnloadAssemblies() - { - foreach (var assemblyLoadContext in _assemblyLoadContexts) - { - assemblyLoadContext.Unload(); - } - } - /// /// Creates all the plugin instances. /// @@ -441,6 +430,15 @@ namespace Emby.Server.Implementations.Plugins return SaveManifest(manifest, path); } + /// + public void Dispose() + { + foreach (var assemblyLoadContext in _assemblyLoadContexts) + { + assemblyLoadContext.Unload(); + } + } + /// /// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path. /// If no file is found, no reconciliation occurs. diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index b4a622ccf4..902d46a906 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -36,6 +36,7 @@ using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; @@ -44,7 +45,7 @@ namespace Emby.Server.Implementations.Session /// /// Class SessionManager. /// - public class SessionManager : ISessionManager, IDisposable + public sealed class SessionManager : ISessionManager, IAsyncDisposable { private readonly IUserDataManager _userDataManager; private readonly ILogger _logger; @@ -57,11 +58,9 @@ namespace Emby.Server.Implementations.Session private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerApplicationHost _appHost; private readonly IDeviceManager _deviceManager; - - /// - /// The active connections. - /// - private readonly ConcurrentDictionary _activeConnections = new(StringComparer.OrdinalIgnoreCase); + private readonly CancellationTokenRegistration _shutdownCallback; + private readonly ConcurrentDictionary _activeConnections + = new(StringComparer.OrdinalIgnoreCase); private Timer _idleTimer; @@ -79,7 +78,8 @@ namespace Emby.Server.Implementations.Session IImageProcessor imageProcessor, IServerApplicationHost appHost, IDeviceManager deviceManager, - IMediaSourceManager mediaSourceManager) + IMediaSourceManager mediaSourceManager, + IHostApplicationLifetime hostApplicationLifetime) { _logger = logger; _eventManager = eventManager; @@ -92,6 +92,7 @@ namespace Emby.Server.Implementations.Session _appHost = appHost; _deviceManager = deviceManager; _mediaSourceManager = mediaSourceManager; + _shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping); _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated; } @@ -151,36 +152,6 @@ namespace Emby.Server.Implementations.Session } } - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and optionally managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _idleTimer?.Dispose(); - } - - _idleTimer = null; - - _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated; - - _disposed = true; - } - private void CheckDisposed() { if (_disposed) @@ -1330,32 +1301,6 @@ namespace Emby.Server.Implementations.Session return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken); } - /// - /// Sends the server shutdown notification. - /// - /// The cancellation token. - /// Task. - public Task SendServerShutdownNotification(CancellationToken cancellationToken) - { - CheckDisposed(); - - return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken); - } - - /// - /// Sends the server restart notification. - /// - /// The cancellation token. - /// Task. - public Task SendServerRestartNotification(CancellationToken cancellationToken) - { - CheckDisposed(); - - _logger.LogDebug("Beginning SendServerRestartNotification"); - - return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken); - } - /// /// Adds the additional user. /// @@ -1833,5 +1778,51 @@ namespace Emby.Server.Implementations.Session return SendMessageToSessions(sessions, name, data, cancellationToken); } + + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + foreach (var session in _activeConnections.Values) + { + await session.DisposeAsync().ConfigureAwait(false); + } + + if (_idleTimer is not null) + { + await _idleTimer.DisposeAsync().ConfigureAwait(false); + _idleTimer = null; + } + + await _shutdownCallback.DisposeAsync().ConfigureAwait(false); + + _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated; + _disposed = true; + } + + private async void OnApplicationStopping() + { + _logger.LogInformation("Sending shutdown notifications"); + try + { + var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.ServerShuttingDown; + + await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending server shutdown notifications"); + } + + // Close open websockets to allow Kestrel to shut down cleanly + foreach (var session in _activeConnections.Values) + { + await session.DisposeAsync().ConfigureAwait(false); + } + } } } diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index a29790961e..4cc0f0ced4 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -18,6 +18,7 @@ using MediaBrowser.Model.System; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers; @@ -32,6 +33,7 @@ public class SystemController : BaseJellyfinApiController private readonly IFileSystem _fileSystem; private readonly INetworkManager _network; private readonly ILogger _logger; + private readonly IHostApplicationLifetime _hostApplicationLifetime; /// /// Initializes a new instance of the class. @@ -41,18 +43,21 @@ public class SystemController : BaseJellyfinApiController /// Instance of interface. /// Instance of interface. /// Instance of interface. + /// Instance of interface. public SystemController( IServerConfigurationManager serverConfigurationManager, IServerApplicationHost appHost, IFileSystem fileSystem, INetworkManager network, - ILogger logger) + ILogger logger, + IHostApplicationLifetime hostApplicationLifetime) { _appPaths = serverConfigurationManager.ApplicationPaths; _appHost = appHost; _fileSystem = fileSystem; _network = network; _logger = logger; + _hostApplicationLifetime = hostApplicationLifetime; } /// @@ -110,7 +115,9 @@ public class SystemController : BaseJellyfinApiController Task.Run(async () => { await Task.Delay(100).ConfigureAwait(false); - _appHost.Restart(); + _appHost.ShouldRestart = true; + _appHost.IsShuttingDown = true; + _hostApplicationLifetime.StopApplication(); }); return NoContent(); } @@ -130,7 +137,8 @@ public class SystemController : BaseJellyfinApiController Task.Run(async () => { await Task.Delay(100).ConfigureAwait(false); - await _appHost.Shutdown().ConfigureAwait(false); + _appHost.IsShuttingDown = true; + _hostApplicationLifetime.StopApplication(); }); return NoContent(); } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 0c6315c667..4c116745b8 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -102,9 +102,6 @@ namespace Jellyfin.Server base.RegisterServices(serviceCollection); } - /// - protected override void RestartInternal() => Program.Restart(); - /// protected override IEnumerable GetAssembliesWithPartsInternal() { @@ -114,8 +111,5 @@ namespace Jellyfin.Server // Jellyfin.Server.Implementations yield return typeof(JellyfinDbContext).Assembly; } - - /// - protected override void ShutdownInternal() => Program.Shutdown(); } } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 3a3dd97bd3..f9259d0d92 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; -using System.Threading; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; @@ -42,7 +41,6 @@ namespace Jellyfin.Server public const string LoggingConfigFileSystem = "logging.json"; private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); - private static CancellationTokenSource _tokenSource = new(); private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; @@ -65,27 +63,6 @@ namespace Jellyfin.Server .MapResult(StartApp, ErrorParsingArguments); } - /// - /// Shuts down the application. - /// - internal static void Shutdown() - { - if (!_tokenSource.IsCancellationRequested) - { - _tokenSource.Cancel(); - } - } - - /// - /// Restarts the application. - /// - internal static void Restart() - { - _restartOnShutdown = true; - - Shutdown(); - } - private static async Task StartApp(StartupOptions options) { _startTimestamp = Stopwatch.GetTimestamp(); @@ -110,33 +87,6 @@ namespace Jellyfin.Server AppDomain.CurrentDomain.UnhandledException += (_, e) => _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception"); - // Intercept Ctrl+C and Ctrl+Break - Console.CancelKeyPress += (_, e) => - { - if (_tokenSource.IsCancellationRequested) - { - return; // Already shutting down - } - - e.Cancel = true; - _logger.LogInformation("Ctrl+C, shutting down"); - Environment.ExitCode = 128 + 2; - Shutdown(); - }; - - // Register a SIGTERM handler - AppDomain.CurrentDomain.ProcessExit += (_, _) => - { - if (_tokenSource.IsCancellationRequested) - { - return; // Already shutting down - } - - _logger.LogInformation("Received a SIGTERM signal, shutting down"); - Environment.ExitCode = 128 + 15; - Shutdown(); - }; - _logger.LogInformation( "Jellyfin version: {Version}", Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3)); @@ -166,12 +116,10 @@ namespace Jellyfin.Server do { - _restartOnShutdown = false; await StartServer(appPaths, options, startupConfig).ConfigureAwait(false); if (_restartOnShutdown) { - _tokenSource = new CancellationTokenSource(); _startTimestamp = Stopwatch.GetTimestamp(); } } while (_restartOnShutdown); @@ -179,7 +127,7 @@ namespace Jellyfin.Server private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig) { - var appHost = new CoreAppHost( + using var appHost = new CoreAppHost( appPaths, _loggerFactory, options, @@ -189,6 +137,7 @@ namespace Jellyfin.Server try { host = Host.CreateDefaultBuilder() + .UseConsoleLifetime() .ConfigureServices(services => appHost.Init(services)) .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger)) .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig)) @@ -203,7 +152,7 @@ namespace Jellyfin.Server try { - await host.StartAsync(_tokenSource.Token).ConfigureAwait(false); + await host.StartAsync().ConfigureAwait(false); if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) { @@ -212,22 +161,18 @@ namespace Jellyfin.Server StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger); } } - catch (Exception ex) when (ex is not TaskCanceledException) + catch (Exception) { _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again"); throw; } - await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false); + await appHost.RunStartupTasksAsync().ConfigureAwait(false); _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp)); - // Block main thread until shutdown - await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false); - } - catch (TaskCanceledException) - { - // Don't throw on cancellation + await host.WaitForShutdownAsync().ConfigureAwait(false); + _restartOnShutdown = appHost.ShouldRestart; } catch (Exception ex) { @@ -250,7 +195,6 @@ namespace Jellyfin.Server } } - await appHost.DisposeAsync().ConfigureAwait(false); host?.Dispose(); } } diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 96ee701b38..c1ea6e87cc 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Reflection; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; namespace MediaBrowser.Common @@ -42,10 +41,15 @@ namespace MediaBrowser.Common bool HasPendingRestart { get; } /// - /// Gets a value indicating whether this instance is currently shutting down. + /// Gets or sets a value indicating whether this instance is currently shutting down. /// /// true if this instance is shutting down; otherwise, false. - bool IsShuttingDown { get; } + bool IsShuttingDown { get; set; } + + /// + /// Gets or sets a value indicating whether the application should restart. + /// + bool ShouldRestart { get; set; } /// /// Gets the application version. @@ -87,11 +91,6 @@ namespace MediaBrowser.Common /// void NotifyPendingRestart(); - /// - /// Restarts this instance. - /// - void Restart(); - /// /// Gets the exports. /// @@ -123,12 +122,6 @@ namespace MediaBrowser.Common /// ``0. T Resolve(); - /// - /// Shuts down. - /// - /// A task. - Task Shutdown(); - /// /// Initializes this instance. /// diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs index 1d73de3c95..0ff9719e98 100644 --- a/MediaBrowser.Common/Plugins/IPluginManager.cs +++ b/MediaBrowser.Common/Plugins/IPluginManager.cs @@ -29,11 +29,6 @@ namespace MediaBrowser.Common.Plugins /// An IEnumerable{Assembly}. IEnumerable LoadAssemblies(); - /// - /// Unloads all of the assemblies. - /// - void UnloadAssemblies(); - /// /// Registers the plugin's services with the DI. /// Note: DI is not yet instantiated yet. diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 0c4719a0e5..53df7133b5 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -232,20 +232,6 @@ namespace MediaBrowser.Controller.Session /// Task. Task SendRestartRequiredNotification(CancellationToken cancellationToken); - /// - /// Sends the server shutdown notification. - /// - /// The cancellation token. - /// Task. - Task SendServerShutdownNotification(CancellationToken cancellationToken); - - /// - /// Sends the server restart notification. - /// - /// The cancellation token. - /// Task. - Task SendServerRestartNotification(CancellationToken cancellationToken); - /// /// Adds the additional user. /// diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index 55bc43455f..1c87d11f18 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Concurrent; using System.Globalization; using System.IO; -using System.Threading; using Emby.Server.Implementations; using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; @@ -105,7 +104,7 @@ namespace Jellyfin.Server.Integration.Tests var appHost = (TestAppHost)testServer.Services.GetRequiredService(); appHost.ServiceProvider = testServer.Services; appHost.InitializeServices().GetAwaiter().GetResult(); - appHost.RunStartupTasksAsync(CancellationToken.None).GetAwaiter().GetResult(); + appHost.RunStartupTasksAsync().GetAwaiter().GetResult(); return testServer; } From effa303cb9804f48cac9a74c3c2370ee29fbd477 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Sat, 23 Sep 2023 15:58:03 -0600 Subject: [PATCH 10/75] Add missing LocalAccessOrRequiresElevationHandler (#10268) --- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 3271e08e48..89dbbdd2fe 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -59,6 +59,7 @@ namespace Jellyfin.Server.Extensions serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); return serviceCollection.AddAuthorizationCore(options => { From 0eddc3e6adb65bea097115694469ecc1cd14c845 Mon Sep 17 00:00:00 2001 From: trailfullideal Date: Sun, 24 Sep 2023 11:18:28 -0400 Subject: [PATCH 11/75] Added translation using Weblate (Cherokee) --- Emby.Server.Implementations/Localization/Core/chr.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/chr.json diff --git a/Emby.Server.Implementations/Localization/Core/chr.json b/Emby.Server.Implementations/Localization/Core/chr.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/chr.json @@ -0,0 +1 @@ +{} From 79976b6b48ccbc8ba828051a8751984a969f6fbc Mon Sep 17 00:00:00 2001 From: trailfullideal Date: Sun, 24 Sep 2023 15:14:19 +0000 Subject: [PATCH 12/75] Translated using Weblate (Zulu) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zu/ --- Emby.Server.Implementations/Localization/Core/zu.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zu.json b/Emby.Server.Implementations/Localization/Core/zu.json index b5f4b920f3..aa056d4498 100644 --- a/Emby.Server.Implementations/Localization/Core/zu.json +++ b/Emby.Server.Implementations/Localization/Core/zu.json @@ -25,5 +25,14 @@ "Channels": "Amashaneli", "Books": "Izincwadi", "Artists": "Abadlali", - "Albums": "Ama-albhamu" + "Albums": "Ama-albhamu", + "CameraImageUploadedFrom": "Kulandelayo lwesithonjana sekhamera selithunyelwe kusuka ku {0}", + "HeaderFavoriteArtists": "Abasethi Abathandekayo", + "HeaderFavoriteEpisodes": "Izilimi Ezithandekayo", + "HeaderFavoriteShows": "Izisho Ezithandekayo", + "External": "Kwezifungo", + "FailedLoginAttemptWithUserName": "Ukushayiswa kwesithombe sokungena okungekho {0}", + "HeaderContinueWatching": "Buyela Ukubona", + "HeaderFavoriteAlbums": "Izimpahla Ezithandwayo", + "HeaderAlbumArtists": "Abasethi wenkulumo" } From 3229d3ba0236243fd2116b78376400a76d9d70dd Mon Sep 17 00:00:00 2001 From: trailfullideal Date: Sun, 24 Sep 2023 14:52:43 +0000 Subject: [PATCH 13/75] Translated using Weblate (Assamese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/as/ --- .../Localization/Core/as.json | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/as.json b/Emby.Server.Implementations/Localization/Core/as.json index 0967ef424b..7c7dd26e92 100644 --- a/Emby.Server.Implementations/Localization/Core/as.json +++ b/Emby.Server.Implementations/Localization/Core/as.json @@ -1 +1,43 @@ -{} +{ + "Albums": "এলবাম", + "Application": "আবেদন", + "AppDeviceValues": "এপ্‌: {0}, ডিভাইচ: {1}", + "Artists": "শিল্পী", + "Channels": "চেনেলস", + "Default": "ডিফল্ট", + "AuthenticationSucceededWithUserName": "{0} সফলভাবে প্রমাণিত", + "Books": "পুস্তক", + "Movies": "চলচ্চিত্ৰ", + "CameraImageUploadedFrom": "একটি নতুন ক্যামেরা চিত্র আপলোড করা হয়েছে {0}", + "Collections": "সংগ্রহ", + "HeaderFavoriteShows": "প্রিয় শোসমূহ", + "Latest": "শেহতীয়া", + "MessageApplicationUpdated": "জেলিফিন চাইভাৰ আপডেট কৰা হৈছে", + "MixedContent": "মিশ্ৰিত সমগ্ৰতা", + "NewVersionIsAvailable": "ডাউনলোড কৰিবলৈ জেলিফিন চাইভাৰৰ এটা নতুন সংস্কৰণ উপলব্ধ আছে.", + "NotificationOptionCameraImageUploaded": "কেমেৰাৰ চিত্ৰ আপল'ড কৰা হ'ল", + "External": "বাহ্যিক", + "Favorites": "পছন্দসই", + "Folders": "ফোল্ডাৰ", + "Forced": "বলপূর্বক", + "Genres": "শ্রেণী", + "HeaderAlbumArtists": "অ্যালবাম শিল্পী", + "HeaderContinueWatching": "দেখা চালিয়ে যান", + "FailedLoginAttemptWithUserName": "লগইন ব্যর্থ চেষ্টা কৰা হৈছে থেকে {0}", + "HeaderFavoriteAlbums": "প্রিয় অ্যালবামসমূহ", + "HeaderFavoriteArtists": "প্রিয় শিল্পীসমূহ", + "HeaderFavoriteEpisodes": "প্রিয় পর্বসমূহ", + "HeaderFavoriteSongs": "প্ৰিয় গীত", + "HeaderLiveTV": "প্ৰতিবেদন টিভি", + "HeaderNextUp": "পৰৱৰ্তী অংশ", + "HeaderRecordingGroups": "অলংকৰণ গোষ্ঠীসমূহ", + "HearingImpaired": "শ্ৰবণ অক্ষম", + "HomeVideos": "ঘৰৰ ভিডিঅ'সমূহ", + "Inherit": "উত্তপ্ত কৰা", + "MessageServerConfigurationUpdated": "চাইভাৰ কনফিগাৰেশ্যন আপডেট কৰা হৈছে", + "NotificationOptionApplicationUpdateAvailable": "অ্যাপ্লিকেশ্যন আপডেট উপলব্ধ", + "NotificationOptionApplicationUpdateInstalled": "অ্যাপ্লিকেশ্যন আপডেট ইনষ্টল কৰা হ'ল", + "NotificationOptionAudioPlayback": "অডিঅ' প্লেবেক আৰম্ভ হ'ল", + "NotificationOptionAudioPlaybackStopped": "অডিঅ' প্লেবেক আঁতৰ হ'ল", + "NotificationOptionInstallationFailed": "ইনষ্টলেশ্যন ব্যৰ্থতা" +} From 99cc1ed13ac8ca3da4138500644515941f6390f6 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Mon, 25 Sep 2023 22:56:59 +0800 Subject: [PATCH 14/75] Fix A53 CC SEI breaking H26x_VAAPI hardware encode Signed-off-by: nyanmisaka --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b6e680ab97..c311d3b8ab 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -48,6 +48,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0); private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3); private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1); + private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0); private static readonly string[] _videoProfilesH264 = new[] { @@ -2006,6 +2007,14 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -svtav1-params:0 rc=1:tune=0:film-grain=0:enable-overlays=1:enable-tf=0"; } + /* Access unit too large: 8192 < 20880 error */ + if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) || + string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) && + _mediaEncoder.EncoderVersion >= _minFFmpegVaapiH26xEncA53CcSei) + { + param += " -sei -a53_cc"; + } + return param; } From b17ee80652237102aa87e407648efaddbf032962 Mon Sep 17 00:00:00 2001 From: trailfullideal Date: Sun, 24 Sep 2023 15:22:17 +0000 Subject: [PATCH 15/75] Translated using Weblate (Cherokee) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/chr/ --- .../Localization/Core/chr.json | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/chr.json b/Emby.Server.Implementations/Localization/Core/chr.json index 0967ef424b..85d1f4c881 100644 --- a/Emby.Server.Implementations/Localization/Core/chr.json +++ b/Emby.Server.Implementations/Localization/Core/chr.json @@ -1 +1,52 @@ -{} +{ + "ChapterNameValue": "Didanedi {0}", + "HeaderAlbumArtists": "Didanidanolisgisgi", + "HeaderFavoriteAlbums": "Dvganidi didanidisgisgi", + "HeaderLiveTV": "Anigadi didanidisgosgi", + "HeaderRecordingGroups": "Didanisquodiisgisgi", + "HomeVideos": "Diganadi dinagadisgisgi", + "Inherit": "Anigwe", + "MessageApplicationUpdatedTo": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe anigadi {0}", + "MixedContent": "Ganinidi dininoladisgisgi", + "Movies": "Anidvnisgisgi", + "MusicVideos": "Danodisgisgi didanidisgosgi", + "NotificationOptionAudioPlayback": "Didanidigwe diganuyisgisgi anigadi", + "NotificationOptionInstallationFailed": "Diudvdi anadvnatisgisgi", + "NotificationOptionPluginUninstalled": "Ditsigvhnidv anawvdisgisgi", + "Albums": "Anigawidaniyv", + "Application": "Didanvyi", + "Artists": "Dinidaniyi", + "AuthenticationSucceededWithUserName": "{0} Sesoquonisdi nagadani", + "Books": "Didanedi", + "CameraImageUploadedFrom": "Anigawidaniyv nasgi didagwalanvyi {0}", + "Channels": "Diganadasgi", + "Collections": "Diganadisgi", + "Default": "Dinadi", + "DeviceOfflineWithName": "{0} Aniyvolehvi nasgi", + "External": "Amohdi", + "Favorites": "Nvdayelvdisgi", + "Folders": "Didanididisgi", + "Forced": "Ganedi", + "Genres": "Diganadisgi", + "HeaderContinueWatching": "Uwoditsu asdanidisgisgi", + "HeaderFavoriteArtists": "Dvganidi dinidanolisgisgi", + "HeaderFavoriteEpisodes": "Dvganidi didanidilisgadisgisgi", + "HeaderFavoriteShows": "Dvganidi didanididanolisgisgi)", + "HeaderFavoriteSongs": "Dvganidi danodisgisgi", + "HeaderNextUp": "Anidvli uwodoli", + "HearingImpaired": "Anitsunidi talunidisgisgi", + "ItemAddedWithName": "{0} Dinigwe anididanidisgi", + "Latest": "Uwodoli", + "MessageApplicationUpdated": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe", + "MessageServerConfigurationUpdated": "Sedanidvdi anigadi diganidinonvhi", + "Music": "Danodisgisgi", + "NameSeasonUnknown": "Tsunita anidvdisgi", + "NewVersionIsAvailable": "Danodigwe anigadi Jellyfin Server tsadanidigwe adisdi uwodvdi diganidinonvhi.", + "NotificationOptionApplicationUpdateAvailable": "Disisdi tsadanidigwe udvdi", + "NotificationOptionApplicationUpdateInstalled": "Disisdi tsadanidigwe digawvdi", + "NotificationOptionAudioPlaybackStopped": "Didanidigwe diganuyisgisgi digawvdi", + "NotificationOptionCameraImageUploaded": "Asdayi adininisgisgi diganuyisgisgi", + "NotificationOptionNewLibraryContent": "Danodisgisgi anigadi digawvdi", + "NotificationOptionPluginError": "Ditsigvhnidv anadvnatisgisgi", + "NotificationOptionPluginInstalled": "Ditsigvhnidv digawvdi" +} From 526c918524468cab1044b93ac47cdf9b3fff59bd Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Mon, 25 Sep 2023 18:12:12 +0200 Subject: [PATCH 16/75] CollectionFolder: replace Dictionary + locks with ConcurrentDictionary This should be faster (and still safe I hope) --- .../Entities/CollectionFolder.cs | 42 ++++++------------- 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 095b261c05..f51162f9d2 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -29,7 +30,7 @@ namespace MediaBrowser.Controller.Entities public class CollectionFolder : Folder, ICollectionFolder { private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private static readonly Dictionary _libraryOptions = new Dictionary(); + private static readonly ConcurrentDictionary _libraryOptions = new ConcurrentDictionary(); private bool _requiresRefresh; /// @@ -139,45 +140,26 @@ namespace MediaBrowser.Controller.Entities } public static LibraryOptions GetLibraryOptions(string path) - { - lock (_libraryOptions) - { - if (!_libraryOptions.TryGetValue(path, out var options)) - { - options = LoadLibraryOptions(path); - _libraryOptions[path] = options; - } - - return options; - } - } + => _libraryOptions.GetOrAdd(path, LoadLibraryOptions); public static void SaveLibraryOptions(string path, LibraryOptions options) { - lock (_libraryOptions) - { - _libraryOptions[path] = options; + _libraryOptions[path] = options; - var clone = JsonSerializer.Deserialize(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions); - foreach (var mediaPath in clone.PathInfos) + var clone = JsonSerializer.Deserialize(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions); + foreach (var mediaPath in clone.PathInfos) + { + if (!string.IsNullOrEmpty(mediaPath.Path)) { - if (!string.IsNullOrEmpty(mediaPath.Path)) - { - mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path); - } + mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path); } - - XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path)); } + + XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path)); } public static void OnCollectionFolderChange() - { - lock (_libraryOptions) - { - _libraryOptions.Clear(); - } - } + => _libraryOptions.Clear(); public override bool IsSaveLocalMetadataEnabled() { From 57891e763972705f2db88a46e03e4e9b74d4621b Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 13 Sep 2023 23:36:46 +0200 Subject: [PATCH 17/75] PhotoResolver: change how generated images are detected Backdrops/fanart are generated as (backdrop)|(fanart)[0-9]*.extension Fixes #7830 --- .../Library/Resolvers/PhotoResolver.cs | 27 +++++++++---------- .../Resolvers/IItemResolver.cs | 2 +- .../Resolvers/ItemResolver.cs | 6 ++--- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index 9026160ff0..b77c6b204b 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.IO; @@ -25,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers private readonly NamingOptions _namingOptions; private readonly IDirectoryService _directoryService; - private static readonly HashSet _ignoreFiles = new HashSet(StringComparer.OrdinalIgnoreCase) + private static readonly string[] _ignoreFiles = new[] { "folder", "thumb", @@ -56,7 +54,7 @@ namespace Emby.Server.Implementations.Library.Resolvers /// /// The args. /// Trailer. - protected override Photo Resolve(ItemResolveArgs args) + protected override Photo? Resolve(ItemResolveArgs args) { if (!args.IsDirectory) { @@ -68,10 +66,11 @@ namespace Emby.Server.Implementations.Library.Resolvers { if (IsImageFile(args.Path, _imageProcessor)) { - var filename = Path.GetFileNameWithoutExtension(args.Path); + var filename = Path.GetFileNameWithoutExtension(args.Path.AsSpan()); // Make sure the image doesn't belong to a video file - var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)); + var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path) + ?? throw new InvalidOperationException("Path can't be a root directory.")); foreach (var file in files) { @@ -92,32 +91,32 @@ namespace Emby.Server.Implementations.Library.Resolvers return null; } - internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename) + internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, ReadOnlySpan imageFilename) { return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename); } - internal static bool IsOwnedByResolvedMedia(string file, string imageFilename) + internal static bool IsOwnedByResolvedMedia(ReadOnlySpan file, ReadOnlySpan imageFilename) => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase); internal static bool IsImageFile(string path, IImageProcessor imageProcessor) { ArgumentNullException.ThrowIfNull(path); - var filename = Path.GetFileNameWithoutExtension(path); - - if (_ignoreFiles.Contains(filename)) + var extension = Path.GetExtension(path.AsSpan()).TrimStart('.'); + if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)) { return false; } - if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1)) + var filename = Path.GetFileNameWithoutExtension(path); + + if (_ignoreFiles.Any(i => filename.StartsWith(i, StringComparison.OrdinalIgnoreCase))) { return false; } - string extension = Path.GetExtension(path).TrimStart('.'); - return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase); + return true; } } } diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs index b95d00aa3c..282aa721e8 100644 --- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Resolvers /// /// The args. /// BaseItem. - BaseItem ResolvePath(ItemResolveArgs args); + BaseItem? ResolvePath(ItemResolveArgs args); } public interface IMultiItemResolver diff --git a/MediaBrowser.Controller/Resolvers/ItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs index a6da8384e8..5c9dd6f07c 100644 --- a/MediaBrowser.Controller/Resolvers/ItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs @@ -1,5 +1,3 @@ -#nullable disable - using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Resolvers /// /// The args. /// `0. - protected internal virtual T Resolve(ItemResolveArgs args) + protected internal virtual T? Resolve(ItemResolveArgs args) { return null; } @@ -42,7 +40,7 @@ namespace MediaBrowser.Controller.Resolvers /// /// The args. /// BaseItem. - public BaseItem ResolvePath(ItemResolveArgs args) + public BaseItem? ResolvePath(ItemResolveArgs args) { var item = Resolve(args); From cc15ea7f65da7f32e52c909fbf1d6757c20e9e4e Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 13 Sep 2023 18:12:29 +0200 Subject: [PATCH 18/75] Ignore .zfs folder Maybe helps with #10215 ? --- Emby.Server.Implementations/Library/IgnorePatterns.cs | 4 ++++ .../Library/IgnorePatternsTests.cs | 1 + 2 files changed, 5 insertions(+) diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index 5384c04b3b..cf6fc18456 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -89,6 +89,10 @@ namespace Emby.Server.Implementations.Library // bts sync files "**/*.bts", "**/*.sync", + + // zfs + "**/.zfs/**", + "**/.zfs" }; private static readonly GlobOptions _globOptions = new GlobOptions diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs index 09eb223288..07061cfc77 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs @@ -31,6 +31,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/music/Foo B.A.R./epic.flac", false)] [InlineData("/media/music/Foo B.A.R", false)] [InlineData("/media/music/Foo B.A.R.", false)] + [InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)] public void PathIgnored(string path, bool expected) { Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); From 626f474faaa1f0511f6241fe95a9bb288e50343b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:35:14 -0600 Subject: [PATCH 19/75] Update github/codeql-action action to v2.21.9 (#10290) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7855ee4f79..5dc38e188d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8 + uses: github/codeql-action/init@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8 + uses: github/codeql-action/autobuild@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8 + uses: github/codeql-action/analyze@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 From 3cc10d065ba37d1290c64ca49813ecfc1dab2e8f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Sep 2023 12:30:06 +0000 Subject: [PATCH 20/75] Update dependency Serilog.Sinks.Graylog to v3.1.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6bf960bb71..8cf3eae2a5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -66,7 +66,7 @@ - + From ea56b3edb7739e4c12dd2734da854f724d0fdb1b Mon Sep 17 00:00:00 2001 From: Anand CU Date: Wed, 27 Sep 2023 17:51:43 +0000 Subject: [PATCH 21/75] Translated using Weblate (Kannada) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/kn/ --- .../Localization/Core/kn.json | 122 +++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/kn.json b/Emby.Server.Implementations/Localization/Core/kn.json index 3c8c38ed4f..5e2b3756bb 100644 --- a/Emby.Server.Implementations/Localization/Core/kn.json +++ b/Emby.Server.Implementations/Localization/Core/kn.json @@ -3,5 +3,125 @@ "TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ", "TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.", "TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್‌ಟ್ರಾಕ್ಟರ್", - "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು." + "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು.", + "ValueHasBeenAddedToLibrary": "{0} ಅನ್ನು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಗೆ ಸೇರಿಸಲಾಗಿದೆ", + "ValueSpecialEpisodeName": "ವಿಶೇಷ - {0}", + "TasksLibraryCategory": "ಸಮೊಹ", + "TasksApplicationCategory": "ಅಪ್ಲಿಕೇಶನ್", + "TasksChannelsCategory": "ಇಂಟರ್ನೆಟ್ ಚಾನೆಲ್ಗಳು", + "TaskCleanCache": "ಕ್ಲೀನ್ ಕ್ಯಾಶ ಡೈರೆಕ್ಟರಿ", + "TaskCleanCacheDescription": "ಸಿಸ್ಟಮ್‌ಗೆ ಇನ್ನು ಮುಂದೆ ಅಗತ್ಯವಿಲ್ಲದ ಸಂಗ್ರಹ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.", + "TaskRefreshLibrary": "ಸ್ಕ್ಯಾನ್ ಮೀಡಿಯಾ ಲೈಬ್ರರಿ", + "UserOfflineFromDevice": "{1} ನಿಂದ {0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ", + "Albums": "ಸಂಪುಟ", + "Application": "ಅಪ್ಲಿಕೇಶನ್", + "AppDeviceValues": "ಅಪ್ಲಿಕೇಶನ್: {0}, ಸಾಧನ: {1}", + "Artists": "ಕಲಾವಿದರು", + "AuthenticationSucceededWithUserName": "{0} ಯಶಸ್ವಿಯಾಗಿ ದೃಢೀಕರಿಸಲಾಗಿದೆ", + "Books": "ಪುಸ್ತಕಗಳು", + "ChapterNameValue": "ಅಧ್ಯಾಯ {0}", + "Collections": "ಸಂಗ್ರಹಣೆಗಳು", + "Default": "ಪೂರ್ವನಿಯೋಜಿತ", + "DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ", + "DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ", + "External": "ಹೊರಗಿನ", + "FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ", + "Favorites": "ಮೆಚ್ಚಿನವುಗಳು", + "Folders": "ಫೋಲ್ಡರ್‌ಗಳು", + "Forced": "ಬಲವಂತವಾಗಿ", + "Genres": "ಪ್ರಕಾರಗಳು", + "HeaderContinueWatching": "ನೋಡುವುದನ್ನು ಮುಂದುವರಿಸಿ", + "HeaderFavoriteAlbums": "ಮೆಚ್ಚಿನ ಸಂಪುಟಗಳು", + "HeaderFavoriteArtists": "ಮೆಚ್ಚಿನ ಕಲಾವಿದರು", + "HeaderFavoriteShows": "ಮೆಚ್ಚಿನ ಪ್ರದರ್ಶನಗಳು", + "HeaderFavoriteSongs": "ಮೆಚ್ಚಿನ ಹಾಡುಗಳು", + "HeaderLiveTV": "ನೇರ ದೂರದರ್ಶನ", + "HeaderNextUp": "ಮುಂದೆ", + "HeaderRecordingGroups": "ರೆಕಾರ್ಡಿಂಗ್ ಗುಂಪುಗಳು", + "MessageApplicationUpdated": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ", + "CameraImageUploadedFrom": "ಹೊಸ ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು {0} ನಿಂದ ಅಪ್‌ಲೋಡ್ ಮಾಡಲಾಗಿದೆ", + "Channels": "ಮೂಲಗಳು", + "HeaderAlbumArtists": "ಸಂಪುಟ ಕಲಾವಿದರು", + "HeaderFavoriteEpisodes": "ಮೆಚ್ಚಿನ ಸಂಚಿಕೆಗಳು", + "HearingImpaired": "ಮೂಗ", + "ItemAddedWithName": "{0} ಅನ್ನು ಸಂಕಲನಕ್ಕೆ ಸೇರಿಸಲಾಗಿದೆ", + "MessageApplicationUpdatedTo": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ", + "MessageNamedServerConfigurationUpdatedWithValue": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ವಿಭಾಗ {0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ", + "NewVersionIsAvailable": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್‌ನ ಹೊಸ ಆವೃತ್ತಿಯು ಡೌನ್‌ಲೋಡ್‌ಗೆ ಲಭ್ಯವಿದೆ.", + "NotificationOptionAudioPlayback": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ", + "NotificationOptionCameraImageUploaded": "ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ", + "NotificationOptionPluginUninstalled": "ಪ್ಲಗಿನ್ ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ", + "NotificationOptionUserLockedOut": "ಬಳಕೆದಾರರು ಲಾಕ್ ಔಟ್ ಆಗಿದ್ದಾರೆ", + "NotificationOptionVideoPlaybackStopped": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ", + "PluginUninstalledWithName": "{0} ಅನ್ನು ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ", + "ScheduledTaskFailedWithName": "{0} ವಿಫಲವಾಗಿದೆ", + "ScheduledTaskStartedWithName": "{0} ಪ್ರಾರಂಭವಾಯಿತು", + "ServerNameNeedsToBeRestarted": "{0} ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕಾಗಿದೆ", + "UserCreatedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ರಚಿಸಲಾಗಿದೆ", + "UserLockedOutWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಲಾಕ್ ಮಾಡಲಾಗಿದೆ", + "UserOnlineFromDevice": "{1} ನಿಂದ {0} ಆನ್‌ಲೈನ್‌ನಲ್ಲಿದೆ", + "UserPasswordChangedWithName": "{0} ಬಳಕೆದಾರರಿಗಾಗಿ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ", + "UserPolicyUpdatedWithName": "ಬಳಕೆದಾರರ ನೀತಿಯನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ", + "UserStartedPlayingItemWithValues": "{2} ರಂದು {0} ಆಡುತ್ತಿದೆ {1}", + "UserStoppedPlayingItemWithValues": "{0} ಅವರು {1} ಅನ್ನು {2} ನಲ್ಲಿ ಆಡುವುದನ್ನು ಮುಗಿಸಿದ್ದಾರೆ", + "VersionNumber": "ಆವೃತ್ತಿ {0}", + "TasksMaintenanceCategory": "ನಿರ್ವಹಣೆ", + "TaskCleanActivityLog": "ಕ್ಲೀನ್ ಚಟುವಟಿಕೆ ಲಾಗ್", + "TaskCleanActivityLogDescription": "ಕಾನ್ಫಿಗರ್ ಮಾಡಿದ ವಯಸ್ಸಿಗಿಂತ ಹಳೆಯದಾದ ಚಟುವಟಿಕೆ ಲಾಗ್ ನಮೂದುಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.", + "TaskRefreshChapterImages": "ಅಧ್ಯಾಯ ಚಿತ್ರಗಳನ್ನು ಹೊರತೆಗೆಯಿರಿ", + "TaskRefreshChapterImagesDescription": "ಅಧ್ಯಾಯಗಳನ್ನು ಹೊಂದಿರುವ ವೀಡಿಯೊಗಳಿಗಾಗಿ ಥಂಬ್‌ನೇಲ್‌ಗಳನ್ನು ರಚಿಸುತ್ತದೆ.", + "TaskRefreshLibraryDescription": "ಹೊಸ ಫೈಲ್‌ಗಳಿಗಾಗಿ ನಿಮ್ಮ ಮೀಡಿಯಾ ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮೆಟಾಡೇಟಾವನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.", + "TaskCleanLogsDescription": "{0} ದಿನಗಳಿಗಿಂತ ಹಳೆಯದಾದ ಲಾಗ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.", + "TaskUpdatePluginsDescription": "ಸ್ವಯಂಚಾಲಿತವಾಗಿ ನವೀಕರಿಸಲು ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾದ ಪ್ಲಗಿನ್‌ಗಳಿಗಾಗಿ ನವೀಕರಣಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಸ್ಥಾಪಿಸುತ್ತದೆ.", + "TaskCleanTranscodeDescription": "ಒಂದು ದಿನಕ್ಕಿಂತ ಹಳೆಯದಾದ ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.", + "TaskDownloadMissingSubtitles": "ಕಾಣೆಯಾದ ಉಪಶೀರ್ಷಿಕೆಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ", + "Shows": "ಧಾರವಾಹಿಗಳು", + "Songs": "ಹಾಡುಗಳು", + "StartupEmbyServerIsLoading": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಲೋಡ್ ಆಗುತ್ತಿದೆ. ದಯವಿಟ್ಟು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.", + "UserDeletedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಅಳಿಸಲಾಗಿದೆ", + "UserDownloadingItemWithValues": "{0} ಡೌನ್‌ಲೋಡ್ ಆಗುತ್ತಿದೆ {1}", + "SubtitleDownloadFailureFromForItem": "ಉಪಶೀರ್ಷಿಕೆಗಳು {0} ನಿಂದ {1} ಗಾಗಿ ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ವಿಫಲವಾಗಿವೆ", + "Sync": "ಹೊಂದಿಕೆ", + "System": "ವ್ಯವಸ್ಥೆ", + "TvShows": "ದೂರದರ್ಶನ ಕಾರ್ಯಕ್ರಮಗಳು", + "Undefined": "ವ್ಯಾಖ್ಯಾನಿಸಲಾಗಿಲ್ಲ", + "User": "ಬಳಕೆದಾರ", + "HomeVideos": "ಮುಖಪುಟ ವೀಡಿಯೊಗಳು", + "Inherit": "ಪಾರಂಪರ್ಯವಾಗಿ", + "ItemRemovedWithName": "{0} ಅನ್ನು ಸಂಕಲನದಿಂದ ತೆಗೆದುಹಾಕಲಾಗಿದೆ", + "LabelIpAddressValue": "IP ವಿಳಾಸ: {0}", + "LabelRunningTimeValue": "ಅವಧಿ: {0}", + "Latest": "ಹೊಸದಾದ", + "MessageServerConfigurationUpdated": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ", + "MixedContent": "ಮಿಶ್ರ ವಿಷಯ", + "Movies": "ಚಲನಚಿತ್ರಗಳು", + "Music": "ಸಂಗೀತ", + "MusicVideos": "ಸಂಗೀತ ವೀಡಿಯೊಗಳು", + "NameInstallFailed": "{0} ಸ್ಥಾಪನೆ ವಿಫಲವಾಗಿದೆ", + "NameSeasonNumber": "ಸೀಸನ್ {0}", + "NameSeasonUnknown": "ಸೀಸನ್ ತಿಳಿದಿಲ್ಲ", + "NotificationOptionApplicationUpdateAvailable": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣ ಲಭ್ಯವಿದೆ", + "NotificationOptionApplicationUpdateInstalled": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ", + "NotificationOptionAudioPlaybackStopped": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ", + "NotificationOptionInstallationFailed": "ಸ್ಥಾಪನ ವೈಫಲ್ಯ", + "NotificationOptionNewLibraryContent": "ಹೊಸ ವಿಷಯವನ್ನು ಒಳಗೊಂಡಿದೆ", + "NotificationOptionPluginError": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ", + "NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ", + "NotificationOptionPluginUpdateInstalled": "ಪ್ಲಗಿನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ", + "NotificationOptionServerRestartRequired": "ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ", + "NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ", + "NotificationOptionVideoPlayback": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ", + "Photos": "ಚಿತ್ರಗಳು", + "Playlists": "ಪ್ಲೇಪಟ್ಟಿಗಳು", + "Plugin": "ಪ್ಲಗಿನ್", + "PluginInstalledWithName": "{0} ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ", + "PluginUpdatedWithName": "{0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ", + "ProviderValue": "ಒದಗಿಸುವವರು: {0}", + "TaskCleanLogs": "ಕ್ಲೀನ್ ಲಾಗ್ ಡೈರೆಕ್ಟರಿ", + "TaskRefreshPeople": "ಜನರನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ", + "TaskRefreshPeopleDescription": "ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಯಲ್ಲಿ ನಟರು ಮತ್ತು ನಿರ್ದೇಶಕರಿಗಾಗಿ ಮೆಟಾಡೇಟಾವನ್ನು ನವೀಕರಿಸಿ.", + "TaskUpdatePlugins": "ಪ್ಲಗಿನ್‌ಗಳನ್ನು ನವೀಕರಿಸಿ", + "TaskCleanTranscode": "ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ", + "TaskRefreshChannels": "ಚಾನಲ್‌ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ", + "TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ." } From d0dc080c93006b3cec0e111aa9c2a79c8d68872a Mon Sep 17 00:00:00 2001 From: Thomas Johansen Date: Fri, 29 Sep 2023 14:41:35 +0200 Subject: [PATCH 22/75] I think this is better --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 346e97ae12..90ef937203 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -419,6 +419,8 @@ namespace MediaBrowser.MediaEncoding.Encoder var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; var analyzeDuration = string.Empty; var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty; + var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty; + var extraArgs = string.Empty; if (request.MediaSource.AnalyzeDurationMs > 0) { @@ -429,12 +431,22 @@ namespace MediaBrowser.MediaEncoding.Encoder analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration; } + if (!string.IsNullOrEmpty(analyzeDuration)) + { + extraArgs = analyzeDuration; + } + + if (!string.IsNullOrEmpty(ffmpegProbeSize)) + { + extraArgs += " -probesize " + ffmpegProbeSize; + } + return GetMediaInfoInternal( GetInputArgument(request.MediaSource.Path, request.MediaSource), request.MediaSource.Path, request.MediaSource.Protocol, extractChapters, - analyzeDuration, + extraArgs, request.MediaType == DlnaProfileType.Audio, request.MediaSource.VideoType, cancellationToken); From 3e10ace985f70fdb92dc655d792cfbc34f2c4bd8 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 29 Sep 2023 10:11:01 -0400 Subject: [PATCH 23/75] Update node versions --- Dockerfile | 2 +- Dockerfile.arm | 2 +- Dockerfile.arm64 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index e51d285e12..9be319311e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # https://github.com/multiarch/qemu-user-static#binfmt_misc-register ARG DOTNET_VERSION=7.0 -FROM node:lts-alpine as web-builder +FROM node:20-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ diff --git a/Dockerfile.arm b/Dockerfile.arm index 46a3e9b998..e8ec6398e6 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -5,7 +5,7 @@ ARG DOTNET_VERSION=7.0 -FROM node:lts-alpine as web-builder +FROM node:20-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 4f9d5e1fdc..83137ee895 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -5,7 +5,7 @@ ARG DOTNET_VERSION=7.0 -FROM node:lts-alpine as web-builder +FROM node:20-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ From 59ec06c35c3b958a1778a56334bdf91a2f0ccf3f Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 29 Sep 2023 12:43:49 -0400 Subject: [PATCH 24/75] Clear active sessions on application stopping --- Emby.Server.Implementations/Session/SessionManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 902d46a906..50d3e8e46d 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1823,6 +1823,8 @@ namespace Emby.Server.Implementations.Session { await session.DisposeAsync().ConfigureAwait(false); } + + _activeConnections.Clear(); } } } From 7f8d9ae7c5821c2ba67812eff176eba19823226b Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sun, 1 Oct 2023 05:10:42 +0200 Subject: [PATCH 25/75] fix: use TryGetString to avoid crashing, fixes #10306 (#10308) --- Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index 996f3fe8a6..e1a43bb489 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -73,8 +73,7 @@ namespace Jellyfin.Server.Migrations.Routines var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems"); foreach (var entry in queryResult) { - var ratingString = entry.GetString(0); - if (string.IsNullOrEmpty(ratingString)) + if (!entry.TryGetString(0, out var ratingString) || string.IsNullOrEmpty(ratingString)) { connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';"); } From 35d6c1465301689de098748a43fe12a282fcaec3 Mon Sep 17 00:00:00 2001 From: DavidFair Date: Sun, 1 Oct 2023 04:11:20 +0100 Subject: [PATCH 26/75] Fix sed failing on Docker builds for CentOS/Fedora (#10285) --- fedora/jellyfin.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index 1f7262e660..e783689069 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -75,7 +75,7 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti %{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir} %{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin %{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin -sed -i -e 's/\/usr\/lib64/%{_libdir}/g' %{buildroot}%{_bindir}/jellyfin +sed -i -e 's|/usr/lib64|%{_libdir}|g' %{buildroot}%{_bindir}/jellyfin # Jellyfin config %{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json From 916a40d4ec0abad90d0e26dcbac72d2da34c650f Mon Sep 17 00:00:00 2001 From: MartinWilkerson Date: Sun, 1 Oct 2023 09:51:19 +0100 Subject: [PATCH 27/75] Add shebang to jellyfin.init --- debian/jellyfin.init | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/jellyfin.init b/debian/jellyfin.init index 7f5642bac1..784536d874 100644 --- a/debian/jellyfin.init +++ b/debian/jellyfin.init @@ -1,3 +1,4 @@ +#!/bin/sh ### BEGIN INIT INFO # Provides: Jellyfin Media Server # Required-Start: $local_fs $network From b83217d1d74deaafe3dc1e605c5d4ec572278d46 Mon Sep 17 00:00:00 2001 From: YuLong Yao Date: Mon, 2 Oct 2023 08:55:55 +0800 Subject: [PATCH 28/75] use pcm as ext name when codec is pcm --- Jellyfin.Api/Helpers/StreamingHelpers.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 782cd65685..e55420d116 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -191,6 +191,11 @@ public static class StreamingHelpers state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0; } + if (outputAudioCodec.StartsWith("pcm_", StringComparison.Ordinal)) + { + containerInternal = ".pcm"; + } + state.OutputAudioCodec = outputAudioCodec; state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.'); state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec); From 808e59fdda3f08cdb2baaac8485f575f0c77ecff Mon Sep 17 00:00:00 2001 From: YuLong Yao Date: Mon, 2 Oct 2023 09:03:00 +0800 Subject: [PATCH 29/75] add pcm format when codec is pcm_* --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index c311d3b8ab..4a71f25a78 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -6242,6 +6242,12 @@ namespace MediaBrowser.Controller.MediaEncoding audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state)); } + if (GetAudioEncoder(state).StartsWith("pcm_", StringComparison.Ordinal)) + { + audioTranscodeParams.Add(string.Concat("-f ", GetAudioEncoder(state).Substring(4))); + audioTranscodeParams.Add("-ar " + state.BaseRequest.AudioBitRate); + } + if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase)) { // opus only supports specific sampling rates From 34c4c9f034b316030245d86e23502bca5488f88a Mon Sep 17 00:00:00 2001 From: Hagay Goshen Date: Mon, 2 Oct 2023 13:48:32 +0300 Subject: [PATCH 30/75] allow repeated same tv guide m3u channels , issue 6527 --- .../LiveTv/TunerHosts/M3uParser.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index df9101f48c..341782d9d3 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -94,14 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#')) { var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine); - if (string.IsNullOrWhiteSpace(channel.Id)) - { - channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture); - } - else - { - channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture); - } + channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture); channel.Path = trimmedLine; channels.Add(channel); From 6f464a3ac6253a5280d89f1f51d2aee93ad9a016 Mon Sep 17 00:00:00 2001 From: Oliver Bastholm Date: Sun, 1 Oct 2023 23:57:26 +0000 Subject: [PATCH 31/75] Translated using Weblate (Danish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/da/ --- .../Localization/Core/da.json | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index 1b6eecdcfe..837172a5b9 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -15,13 +15,13 @@ "Favorites": "Favoritter", "Folders": "Mapper", "Genres": "Genrer", - "HeaderAlbumArtists": "Albums kunstnere", + "HeaderAlbumArtists": "Albumkunstnere", "HeaderContinueWatching": "Fortsæt afspilning", - "HeaderFavoriteAlbums": "Favorit albummer", - "HeaderFavoriteArtists": "Favorit kunstnere", - "HeaderFavoriteEpisodes": "Favorit afsnit", - "HeaderFavoriteShows": "Favorit serier", - "HeaderFavoriteSongs": "Favorit sange", + "HeaderFavoriteAlbums": "Favoritalbummer", + "HeaderFavoriteArtists": "Favoritkunstnere", + "HeaderFavoriteEpisodes": "Yndlingsafsnit", + "HeaderFavoriteShows": "Yndlingsserier", + "HeaderFavoriteSongs": "Yndlingssange", "HeaderLiveTV": "Live-TV", "HeaderNextUp": "Næste", "HeaderRecordingGroups": "Optagelsesgrupper", @@ -34,8 +34,8 @@ "Latest": "Seneste", "MessageApplicationUpdated": "Jellyfin Server er blevet opdateret", "MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret", - "MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret", + "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret", + "MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret", "MixedContent": "Blandet indhold", "Movies": "Film", "Music": "Musik", @@ -51,7 +51,7 @@ "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet", "NotificationOptionInstallationFailed": "Installationen mislykkedes", "NotificationOptionNewLibraryContent": "Nyt indhold tilføjet", - "NotificationOptionPluginError": "Plugin fejl", + "NotificationOptionPluginError": "Plugin-fejl", "NotificationOptionPluginInstalled": "Plugin blev installeret", "NotificationOptionPluginUninstalled": "Plugin blev afinstalleret", "NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret", @@ -92,26 +92,26 @@ "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek", "ValueSpecialEpisodeName": "Special - {0}", "VersionNumber": "Version {0}", - "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.", + "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.", "TaskDownloadMissingSubtitles": "Hent manglende undertekster", "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.", "TaskUpdatePlugins": "Opdater Plugins", - "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.", - "TaskCleanLogs": "Ryd Log mappe", - "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.", - "TaskRefreshLibrary": "Scan Medie Bibliotek", - "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.", - "TaskCleanCache": "Ryd Cache mappe", - "TasksChannelsCategory": "Internet Kanaler", + "TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.", + "TaskCleanLogs": "Ryd Log-mappe", + "TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.", + "TaskRefreshLibrary": "Scan Mediebibliotek", + "TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.", + "TaskCleanCache": "Ryd Cache-mappe", + "TasksChannelsCategory": "Internetkanaler", "TasksApplicationCategory": "Applikation", "TasksLibraryCategory": "Bibliotek", "TasksMaintenanceCategory": "Vedligeholdelse", - "TaskRefreshChapterImages": "Udtræk kapitel billeder", - "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.", - "TaskRefreshChannelsDescription": "Opdater internet kanal information.", + "TaskRefreshChapterImages": "Udtræk kapitelbilleder", + "TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.", + "TaskRefreshChannelsDescription": "Opdaterer information for internetkanal.", "TaskRefreshChannels": "Opdater Kanaler", - "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.", - "TaskCleanTranscode": "Tøm Transcode mappen", + "TaskCleanTranscodeDescription": "Fjerner transcode-filer, som er mere end 1 dag gammel.", + "TaskCleanTranscode": "Tøm Transcode-mappen", "TaskRefreshPeople": "Opdater Personer", "TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.", "TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.", @@ -121,8 +121,8 @@ "Default": "Standard", "TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.", "TaskOptimizeDatabase": "Optimér database", - "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.", - "TaskKeyframeExtractor": "Nøglebillede udtræk", + "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.", + "TaskKeyframeExtractor": "Udtræk af nøglebillede", "External": "Ekstern", "HearingImpaired": "Hørehæmmet" } From 5153e9259b9695a2619c1dbd34f11ce21b4068ca Mon Sep 17 00:00:00 2001 From: officialdanielamani Date: Sun, 1 Oct 2023 23:19:21 +0000 Subject: [PATCH 32/75] Translated using Weblate (Malay) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ms/ --- Emby.Server.Implementations/Localization/Core/ms.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index 904443bea1..a07222975b 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -1,5 +1,5 @@ { - "Albums": "Albums", + "Albums": "Album", "AppDeviceValues": "Apl: {0}, Peranti: {1}", "Application": "Aplikasi", "Artists": "Artis-artis", From f746db9a54caef36bb3b32cf22812a78d97c865d Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 2 Oct 2023 15:55:26 -0400 Subject: [PATCH 33/75] Re-add shutdown/restart methods --- .../ApplicationHost.cs | 19 +++++++++++++++ Jellyfin.Api/Controllers/SystemController.cs | 23 +++---------------- MediaBrowser.Common/IApplicationHost.cs | 18 +++++++++++---- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 86721ace61..e2d2719e5d 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -101,6 +101,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; @@ -850,6 +851,24 @@ namespace Emby.Server.Implementations } } + /// + public void Restart() + { + ShouldRestart = true; + Shutdown(); + } + + /// + public void Shutdown() + { + Task.Run(async () => + { + await Task.Delay(100).ConfigureAwait(false); + IsShuttingDown = true; + Resolve().StopApplication(); + }); + } + /// /// Gets the composable part assemblies. /// diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 4cc0f0ced4..42ac4a9b4b 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Net.Mime; -using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using MediaBrowser.Common.Configuration; @@ -18,7 +17,6 @@ using MediaBrowser.Model.System; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers; @@ -33,7 +31,6 @@ public class SystemController : BaseJellyfinApiController private readonly IFileSystem _fileSystem; private readonly INetworkManager _network; private readonly ILogger _logger; - private readonly IHostApplicationLifetime _hostApplicationLifetime; /// /// Initializes a new instance of the class. @@ -43,21 +40,18 @@ public class SystemController : BaseJellyfinApiController /// Instance of interface. /// Instance of interface. /// Instance of interface. - /// Instance of interface. public SystemController( IServerConfigurationManager serverConfigurationManager, IServerApplicationHost appHost, IFileSystem fileSystem, INetworkManager network, - ILogger logger, - IHostApplicationLifetime hostApplicationLifetime) + ILogger logger) { _appPaths = serverConfigurationManager.ApplicationPaths; _appHost = appHost; _fileSystem = fileSystem; _network = network; _logger = logger; - _hostApplicationLifetime = hostApplicationLifetime; } /// @@ -112,13 +106,7 @@ public class SystemController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult RestartApplication() { - Task.Run(async () => - { - await Task.Delay(100).ConfigureAwait(false); - _appHost.ShouldRestart = true; - _appHost.IsShuttingDown = true; - _hostApplicationLifetime.StopApplication(); - }); + _appHost.Restart(); return NoContent(); } @@ -134,12 +122,7 @@ public class SystemController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult ShutdownApplication() { - Task.Run(async () => - { - await Task.Delay(100).ConfigureAwait(false); - _appHost.IsShuttingDown = true; - _hostApplicationLifetime.StopApplication(); - }); + _appHost.Shutdown(); return NoContent(); } diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index c1ea6e87cc..5985d3dd82 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -41,15 +41,15 @@ namespace MediaBrowser.Common bool HasPendingRestart { get; } /// - /// Gets or sets a value indicating whether this instance is currently shutting down. + /// Gets a value indicating whether this instance is currently shutting down. /// /// true if this instance is shutting down; otherwise, false. - bool IsShuttingDown { get; set; } + bool IsShuttingDown { get; } /// - /// Gets or sets a value indicating whether the application should restart. + /// Gets a value indicating whether the application should restart. /// - bool ShouldRestart { get; set; } + bool ShouldRestart { get; } /// /// Gets the application version. @@ -91,6 +91,11 @@ namespace MediaBrowser.Common /// void NotifyPendingRestart(); + /// + /// Restarts this instance. + /// + void Restart(); + /// /// Gets the exports. /// @@ -122,6 +127,11 @@ namespace MediaBrowser.Common /// ``0. T Resolve(); + /// + /// Shuts down. + /// + void Shutdown(); + /// /// Initializes this instance. /// From ab0790271aac8846430c9689dff647eb4ce55078 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 2 Oct 2023 16:54:19 -0400 Subject: [PATCH 34/75] Apply suggestions from code review Co-authored-by: Claus Vium --- Emby.Server.Implementations/ApplicationHost.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index e2d2719e5d..a8b8e5fb94 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -203,10 +203,10 @@ namespace Emby.Server.Implementations public bool HasPendingRestart { get; private set; } /// - public bool IsShuttingDown { get; set; } + public bool IsShuttingDown { get; private set; } /// - public bool ShouldRestart { get; set; } + public bool ShouldRestart { get; private set; } /// /// Gets the logger. From b95040bc5ed7b53ad3e2a92363d50dc3f6ee82c5 Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Mon, 2 Oct 2023 23:00:51 -0400 Subject: [PATCH 35/75] Add We;Na to split whitelist --- MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 3055e6cde3..441a3abd46 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -78,6 +78,7 @@ namespace MediaBrowser.MediaEncoding.Probing "She/Her/Hers", "5/8erl in Ehr'n", "Smith/Kotzen", + "We;Na", }; /// From 2c31ed80314350a1445f9b55426c6744475beff2 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Tue, 3 Oct 2023 15:33:59 +0800 Subject: [PATCH 36/75] Fix JELLYFIN_FFMPEG_OPT is not enabled in fedora ExecStart --- fedora/jellyfin.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fedora/jellyfin.service b/fedora/jellyfin.service index 1b3f8032c6..01accdc0c9 100644 --- a/fedora/jellyfin.service +++ b/fedora/jellyfin.service @@ -8,7 +8,7 @@ EnvironmentFile = /etc/sysconfig/jellyfin User = jellyfin Group = jellyfin WorkingDirectory = /var/lib/jellyfin -ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS +ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS Restart = on-failure TimeoutSec = 15 SuccessExitStatus=0 143 From 1ca9f8b04ba93f2c9d33f278beb4e591d84a2f6a Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 3 Oct 2023 09:26:20 -0400 Subject: [PATCH 37/75] Remove unused fields and parameters --- Jellyfin.Api/Controllers/LiveTvController.cs | 7 +------ Jellyfin.Server.Implementations/Users/UserManager.cs | 10 ++-------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 267ba4afb4..649397d68d 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -23,7 +23,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; @@ -48,7 +47,6 @@ public class LiveTvController : BaseJellyfinApiController private readonly IMediaSourceManager _mediaSourceManager; private readonly IConfigurationManager _configurationManager; private readonly TranscodingJobHelper _transcodingJobHelper; - private readonly ISessionManager _sessionManager; /// /// Initializes a new instance of the class. @@ -61,7 +59,6 @@ public class LiveTvController : BaseJellyfinApiController /// Instance of the interface. /// Instance of the interface. /// Instance of the class. - /// Instance of the interface. public LiveTvController( ILiveTvManager liveTvManager, IUserManager userManager, @@ -70,8 +67,7 @@ public class LiveTvController : BaseJellyfinApiController IDtoService dtoService, IMediaSourceManager mediaSourceManager, IConfigurationManager configurationManager, - TranscodingJobHelper transcodingJobHelper, - ISessionManager sessionManager) + TranscodingJobHelper transcodingJobHelper) { _liveTvManager = liveTvManager; _userManager = userManager; @@ -81,7 +77,6 @@ public class LiveTvController : BaseJellyfinApiController _mediaSourceManager = mediaSourceManager; _configurationManager = configurationManager; _transcodingJobHelper = transcodingJobHelper; - _sessionManager = sessionManager; } /// diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 5010751ddb..94ac4798ca 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -20,7 +20,6 @@ using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Users; using Microsoft.EntityFrameworkCore; @@ -35,7 +34,6 @@ namespace Jellyfin.Server.Implementations.Users { private readonly IDbContextFactory _dbProvider; private readonly IEventManager _eventManager; - private readonly ICryptoProvider _cryptoProvider; private readonly INetworkManager _networkManager; private readonly IApplicationHost _appHost; private readonly IImageProcessor _imageProcessor; @@ -53,7 +51,6 @@ namespace Jellyfin.Server.Implementations.Users /// /// The database provider. /// The event manager. - /// The cryptography provider. /// The network manager. /// The application host. /// The image processor. @@ -61,7 +58,6 @@ namespace Jellyfin.Server.Implementations.Users public UserManager( IDbContextFactory dbProvider, IEventManager eventManager, - ICryptoProvider cryptoProvider, INetworkManager networkManager, IApplicationHost appHost, IImageProcessor imageProcessor, @@ -69,7 +65,6 @@ namespace Jellyfin.Server.Implementations.Users { _dbProvider = dbProvider; _eventManager = eventManager; - _cryptoProvider = cryptoProvider; _networkManager = networkManager; _appHost = appHost; _imageProcessor = imageProcessor; @@ -384,7 +379,7 @@ namespace Jellyfin.Server.Implementations.Users } var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); - var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint) + var authResult = await AuthenticateLocalUser(username, password, user) .ConfigureAwait(false); var authenticationProvider = authResult.AuthenticationProvider; var success = authResult.Success; @@ -787,8 +782,7 @@ namespace Jellyfin.Server.Implementations.Users private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> AuthenticateLocalUser( string username, string password, - User? user, - string remoteEndPoint) + User? user) { bool success = false; IAuthenticationProvider? authenticationProvider = null; From fa26bcde3a7b6c288ae5a528a45f08e64a6a2011 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 3 Oct 2023 09:29:06 -0400 Subject: [PATCH 38/75] Remove unnecessary ToString in RobotsRedirectionMiddleware --- Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs index 8bf626035d..acf3645fdc 100644 --- a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs +++ b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs @@ -33,8 +33,7 @@ public class RobotsRedirectionMiddleware /// The async task. public async Task Invoke(HttpContext httpContext) { - var localPath = httpContext.Request.Path.ToString(); - if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase)) + if (httpContext.Request.Path.Equals("/robots.txt", StringComparison.OrdinalIgnoreCase)) { _logger.LogDebug("Redirecting robots.txt request to web/robots.txt"); httpContext.Response.Redirect("web/robots.txt"); From 78e00578c20d0127b540bc69290972104f12ac84 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 3 Oct 2023 10:25:14 -0400 Subject: [PATCH 39/75] Use DI for IFileSystem --- .../AppBase/BaseConfigurationManager.cs | 20 +++++++------------ .../ApplicationHost.cs | 11 +++++----- .../ServerConfigurationManager.cs | 11 +++++----- .../IO/ManagedFileSystem.cs | 19 ++++++++---------- MediaBrowser.Model/IO/IFileSystem.cs | 2 -- 5 files changed, 26 insertions(+), 37 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index a4deeddb78..a2f38c8c2d 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; @@ -19,14 +18,8 @@ namespace Emby.Server.Implementations.AppBase /// public abstract class BaseConfigurationManager : IConfigurationManager { - private readonly IFileSystem _fileSystem; - - private readonly ConcurrentDictionary _configurations = new ConcurrentDictionary(); - - /// - /// The _configuration sync lock. - /// - private readonly object _configurationSyncLock = new object(); + private readonly ConcurrentDictionary _configurations = new(); + private readonly object _configurationSyncLock = new(); private ConfigurationStore[] _configurationStores = Array.Empty(); private IConfigurationFactory[] _configurationFactories = Array.Empty(); @@ -42,12 +35,13 @@ namespace Emby.Server.Implementations.AppBase /// The application paths. /// The logger factory. /// The XML serializer. - /// The file system. - protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem) + protected BaseConfigurationManager( + IApplicationPaths applicationPaths, + ILoggerFactory loggerFactory, + IXmlSerializer xmlSerializer) { CommonApplicationPaths = applicationPaths; XmlSerializer = xmlSerializer; - _fileSystem = fileSystem; Logger = loggerFactory.CreateLogger(); UpdateCachePath(); @@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.AppBase { var file = Path.Combine(path, Guid.NewGuid().ToString()); File.WriteAllText(file, string.Empty); - _fileSystem.DeleteFile(file); + File.Delete(file); } private string GetConfigurationFile(string key) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 985ab0db5e..8518b13521 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -120,7 +120,6 @@ namespace Emby.Server.Implementations private readonly ConcurrentDictionary _disposableParts = new(); private readonly DeviceId _deviceId; - private readonly IFileSystem _fileSystemManager; private readonly IConfiguration _startupConfig; private readonly IXmlSerializer _xmlSerializer; private readonly IStartupOptions _startupOptions; @@ -153,10 +152,8 @@ namespace Emby.Server.Implementations LoggerFactory = loggerFactory; _startupOptions = options; _startupConfig = startupConfig; - _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger(), applicationPaths); Logger = LoggerFactory.CreateLogger(); - _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler()); _deviceId = new DeviceId(ApplicationPaths, LoggerFactory); ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; @@ -164,7 +161,7 @@ namespace Emby.Server.Implementations ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; _xmlSerializer = new MyXmlSerializer(); - ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); + ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer); _pluginManager = new PluginManager( LoggerFactory.CreateLogger(), this, @@ -507,7 +504,9 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(_pluginManager); serviceCollection.AddSingleton(ApplicationPaths); - serviceCollection.AddSingleton(_fileSystemManager); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(NetManager); @@ -681,7 +680,7 @@ namespace Emby.Server.Implementations BaseItem.ProviderManager = Resolve(); BaseItem.LocalizationManager = Resolve(); BaseItem.ItemRepository = Resolve(); - BaseItem.FileSystem = _fileSystemManager; + BaseItem.FileSystem = Resolve(); BaseItem.UserDataManager = Resolve(); BaseItem.ChannelManager = Resolve(); Video.LiveTvManager = Resolve(); diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index 6b8b1a620f..0ee43ce0a3 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; @@ -22,11 +21,13 @@ namespace Emby.Server.Implementations.Configuration /// Initializes a new instance of the class. /// /// The application paths. - /// The paramref name="loggerFactory" factory. + /// The logger factory. /// The XML serializer. - /// The file system. - public ServerConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem) - : base(applicationPaths, loggerFactory, xmlSerializer, fileSystem) + public ServerConfigurationManager( + IApplicationPaths applicationPaths, + ILoggerFactory loggerFactory, + IXmlSerializer xmlSerializer) + : base(applicationPaths, loggerFactory, xmlSerializer) { UpdateMetadataPath(); } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 3aa5233ed1..18b00ce0b2 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -15,10 +15,6 @@ namespace Emby.Server.Implementations.IO /// public class ManagedFileSystem : IFileSystem { - private readonly ILogger _logger; - - private readonly List _shortcutHandlers = new List(); - private readonly string _tempPath; private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows(); private static readonly char[] _invalidPathCharacters = { @@ -29,23 +25,24 @@ namespace Emby.Server.Implementations.IO (char)31, ':', '*', '?', '\\', '/' }; + private readonly ILogger _logger; + private readonly List _shortcutHandlers; + private readonly string _tempPath; + /// /// Initializes a new instance of the class. /// /// The instance to use. /// The instance to use. + /// the 's to use. public ManagedFileSystem( ILogger logger, - IApplicationPaths applicationPaths) + IApplicationPaths applicationPaths, + IEnumerable shortcutHandlers) { _logger = logger; _tempPath = applicationPaths.TempDirectory; - } - - /// - public virtual void AddShortcutHandler(IShortcutHandler handler) - { - _shortcutHandlers.Add(handler); + _shortcutHandlers = shortcutHandlers.ToList(); } /// diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index 5bdda2b240..ec381d4231 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -10,8 +10,6 @@ namespace MediaBrowser.Model.IO /// public interface IFileSystem { - void AddShortcutHandler(IShortcutHandler handler); - /// /// Determines whether the specified filename is shortcut. /// From 12b51cf2ba537a6f56882277dc856ab39ace94a0 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 3 Oct 2023 10:31:55 -0400 Subject: [PATCH 40/75] Reduce nesting in SessionManager.OnPlaybackStopped --- .../Session/SessionManager.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 50d3e8e46d..e935f7e5e5 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -951,27 +951,27 @@ namespace Emby.Server.Implementations.Session private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed) { - bool playedToCompletion = false; - - if (!playbackFailed) + if (playbackFailed) { - var data = _userDataManager.GetUserData(user, item); - - if (positionTicks.HasValue) - { - playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value); - } - else - { - // If the client isn't able to report this, then we'll just have to make an assumption - data.PlayCount++; - data.Played = item.SupportsPlayedStatus; - data.PlaybackPositionTicks = 0; - playedToCompletion = true; - } + return false; + } - _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None); + var data = _userDataManager.GetUserData(user, item); + bool playedToCompletion; + if (positionTicks.HasValue) + { + playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value); } + else + { + // If the client isn't able to report this, then we'll just have to make an assumption + data.PlayCount++; + data.Played = item.SupportsPlayedStatus; + data.PlaybackPositionTicks = 0; + playedToCompletion = true; + } + + _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None); return playedToCompletion; } From 487d79f8acd5449c8c01d68b0df2caeb979213cc Mon Sep 17 00:00:00 2001 From: Pithaya <19533412+Pithaya@users.noreply.github.com> Date: Wed, 4 Oct 2023 05:16:16 +0200 Subject: [PATCH 41/75] Add book related values to PersonKind (#10325) --- Jellyfin.Data/Enums/PersonKind.cs | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Jellyfin.Data/Enums/PersonKind.cs b/Jellyfin.Data/Enums/PersonKind.cs index 10a8056669..29308789a0 100644 --- a/Jellyfin.Data/Enums/PersonKind.cs +++ b/Jellyfin.Data/Enums/PersonKind.cs @@ -94,4 +94,40 @@ public enum PersonKind /// A person who was the illustrator. /// Illustrator, + + /// + /// A person responsible for drawing the art. + /// + Penciller, + + /// + /// A person responsible for inking the pencil art. + /// + Inker, + + /// + /// A person responsible for applying color to drawings. + /// + Colorist, + + /// + /// A person responsible for drawing text and speech bubbles. + /// + Letterer, + + /// + /// A person responsible for drawing the cover art. + /// + CoverArtist, + + /// + /// A person contributing to a resource by revising or elucidating the content, e.g., adding an introduction, notes, or other critical matter. + /// An editor may also prepare a resource for production, publication, or distribution. + /// + Editor, + + /// + /// A person who renders a text from one language into another. + /// + Translator } From 6f2c165cc3d0000bda5722200971dd619833c349 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 4 Oct 2023 16:06:26 +0200 Subject: [PATCH 42/75] Use Authorization header in integration tests instead of X-Emby-Authorization And ensure the response has a successful status code --- .../Jellyfin.Server.Integration.Tests/AuthHelper.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs index 3dc62afaf8..5ddbd30d1e 100644 --- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs +++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs @@ -15,8 +15,8 @@ namespace Jellyfin.Server.Integration.Tests { public static class AuthHelper { - public const string AuthHeaderName = "X-Emby-Authorization"; - public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server Integration Tests\", DeviceId=\"69420\", Device=\"Apple II\", Version=\"10.8.0\""; + public const string AuthHeaderName = "Authorization"; + public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server%20Integration%20Tests\", DeviceId=\"69420\", Device=\"Apple%20II\", Version=\"10.8.0\""; public static async Task CompleteStartupAsync(HttpClient client) { @@ -27,16 +27,19 @@ namespace Jellyfin.Server.Integration.Tests using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty())); Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode); - using var content = JsonContent.Create( + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/Users/AuthenticateByName"); + httpRequest.Headers.TryAddWithoutValidation(AuthHeaderName, DummyAuthHeader); + httpRequest.Content = JsonContent.Create( new AuthenticateUserByName() { Username = user!.Name, Pw = user.Password, }, options: jsonOptions); - content.Headers.Add("X-Emby-Authorization", DummyAuthHeader); - using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content); + using var authResponse = await client.SendAsync(httpRequest); + authResponse.EnsureSuccessStatusCode(); + var auth = await JsonSerializer.DeserializeAsync( await authResponse.Content.ReadAsStreamAsync(), jsonOptions); From 76c64516a78ca583c13739cedd63dd2b2cc5e05e Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 4 Oct 2023 16:18:14 +0200 Subject: [PATCH 43/75] Simplify some stuff in AuthorizationContext --- .../Security/AuthorizationContext.cs | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index 700e639700..f415d01115 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -49,14 +49,13 @@ namespace Jellyfin.Server.Implementations.Security /// /// Gets the authorization. /// - /// The HTTP req. + /// The HTTP req. /// Dictionary{System.StringSystem.String}. - private async Task GetAuthorization(HttpContext httpReq) + private async Task GetAuthorization(HttpContext httpContext) { - var auth = GetAuthorizationDictionary(httpReq); - var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false); + var authInfo = await GetAuthorizationInfo(httpContext.Request).ConfigureAwait(false); - httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo; + httpContext.Request.HttpContext.Items["AuthorizationInfo"] = authInfo; return authInfo; } @@ -80,7 +79,6 @@ namespace Jellyfin.Server.Implementations.Security auth.TryGetValue("Token", out token); } -#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false. if (string.IsNullOrEmpty(token)) { token = headers["X-Emby-Token"]; @@ -118,7 +116,6 @@ namespace Jellyfin.Server.Implementations.Security // Request doesn't contain a token. return authInfo; } -#pragma warning restore CA1508 authInfo.HasToken = true; var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false); @@ -219,24 +216,7 @@ namespace Jellyfin.Server.Implementations.Security /// /// Gets the auth. /// - /// The HTTP req. - /// Dictionary{System.StringSystem.String}. - private static Dictionary? GetAuthorizationDictionary(HttpContext httpReq) - { - var auth = httpReq.Request.Headers["X-Emby-Authorization"]; - - if (string.IsNullOrEmpty(auth)) - { - auth = httpReq.Request.Headers[HeaderNames.Authorization]; - } - - return auth.Count > 0 ? GetAuthorization(auth[0]) : null; - } - - /// - /// Gets the auth. - /// - /// The HTTP req. + /// The HTTP request. /// Dictionary{System.StringSystem.String}. private static Dictionary? GetAuthorizationDictionary(HttpRequest httpReq) { From 35e09cf203550ca101b85be92e7aa4014902ffd3 Mon Sep 17 00:00:00 2001 From: Piranha Date: Tue, 3 Oct 2023 14:30:03 +0000 Subject: [PATCH 44/75] Translated using Weblate (Spanish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es/ --- Emby.Server.Implementations/Localization/Core/es.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index f5636a0af0..4c56f789d3 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -3,9 +3,9 @@ "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}", "Application": "Aplicación", "Artists": "Artistas", - "AuthenticationSucceededWithUserName": "{0} identificado correctamente", + "AuthenticationSucceededWithUserName": "{0} autenticado correctamente", "Books": "Libros", - "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}", + "CameraImageUploadedFrom": "Se ha subido una nueva imagen por cámara desde {0}", "Channels": "Canales", "ChapterNameValue": "Capítulo {0}", "Collections": "Colecciones", From c9ce246c173339c00b4131f3e8b2c9240584d6ae Mon Sep 17 00:00:00 2001 From: Artnal Date: Tue, 3 Oct 2023 11:29:38 +0000 Subject: [PATCH 45/75] Translated using Weblate (French) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr/ --- Emby.Server.Implementations/Localization/Core/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 4877bcd7a7..a2b429dcdd 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -105,8 +105,8 @@ "TaskRefreshPeople": "Actualiser les acteurs", "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.", "TaskCleanLogs": "Nettoyer le répertoire des journaux", - "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et actualise les métadonnées.", - "TaskRefreshLibrary": "Scanner la médiathèque", + "TaskRefreshLibraryDescription": "Analyser sa médiathèque pour trouver les nouveaux fichiers et actualiser les métadonnées.", + "TaskRefreshLibrary": "Analyser la médiathèque", "TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.", "TaskRefreshChapterImages": "Extraire les images de chapitre", "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.", From 6f7413812fb4654d1b1ca065a177a1bb19408386 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Wed, 4 Oct 2023 14:34:53 -0400 Subject: [PATCH 46/75] Add SystemManager service --- .../ApplicationHost.cs | 80 +------------ Emby.Server.Implementations/SystemManager.cs | 108 ++++++++++++++++++ Jellyfin.Api/Controllers/SystemController.cs | 47 ++++---- MediaBrowser.Common/IApplicationHost.cs | 24 +--- .../IServerApplicationHost.cs | 12 -- MediaBrowser.Controller/ISystemManager.cs | 34 ++++++ 6 files changed, 173 insertions(+), 132 deletions(-) create mode 100644 Emby.Server.Implementations/SystemManager.cs create mode 100644 MediaBrowser.Controller/ISystemManager.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8518b13521..3253ea0267 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -101,7 +101,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; @@ -133,7 +132,7 @@ namespace Emby.Server.Implementations /// All concrete types. private Type[] _allConcreteTypes; - private bool _disposed = false; + private bool _disposed; /// /// Initializes a new instance of the class. @@ -184,26 +183,16 @@ namespace Emby.Server.Implementations public bool CoreStartupHasCompleted { get; private set; } - public virtual bool CanLaunchWebBrowser => Environment.UserInteractive - && !_startupOptions.IsService - && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()); - /// /// Gets the singleton instance. /// public INetworkManager NetManager { get; private set; } - /// - /// Gets a value indicating whether this instance has changes that require the entire application to restart. - /// - /// true if this instance has pending application restart; otherwise, false. - public bool HasPendingRestart { get; private set; } - /// - public bool IsShuttingDown { get; private set; } + public bool HasPendingRestart { get; private set; } /// - public bool ShouldRestart { get; private set; } + public bool ShouldRestart { get; set; } /// /// Gets the logger. @@ -507,6 +496,8 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddScoped(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(NetManager); @@ -850,24 +841,6 @@ namespace Emby.Server.Implementations } } - /// - public void Restart() - { - ShouldRestart = true; - Shutdown(); - } - - /// - public void Shutdown() - { - Task.Run(async () => - { - await Task.Delay(100).ConfigureAwait(false); - IsShuttingDown = true; - Resolve().StopApplication(); - }); - } - /// /// Gets the composable part assemblies. /// @@ -923,49 +896,6 @@ namespace Emby.Server.Implementations protected abstract IEnumerable GetAssembliesWithPartsInternal(); - /// - /// Gets the system status. - /// - /// Where this request originated. - /// SystemInfo. - public SystemInfo GetSystemInfo(HttpRequest request) - { - return new SystemInfo - { - HasPendingRestart = HasPendingRestart, - IsShuttingDown = IsShuttingDown, - Version = ApplicationVersionString, - WebSocketPortNumber = HttpPort, - CompletedInstallations = Resolve().CompletedInstallations.ToArray(), - Id = SystemId, - ProgramDataPath = ApplicationPaths.ProgramDataPath, - WebPath = ApplicationPaths.WebPath, - LogPath = ApplicationPaths.LogDirectoryPath, - ItemsByNamePath = ApplicationPaths.InternalMetadataPath, - InternalMetadataPath = ApplicationPaths.InternalMetadataPath, - CachePath = ApplicationPaths.CachePath, - CanLaunchWebBrowser = CanLaunchWebBrowser, - TranscodingTempPath = ConfigurationManager.GetTranscodePath(), - ServerName = FriendlyName, - LocalAddress = GetSmartApiUrl(request), - SupportsLibraryMonitor = true, - PackageName = _startupOptions.PackageName - }; - } - - public PublicSystemInfo GetPublicSystemInfo(HttpRequest request) - { - return new PublicSystemInfo - { - Version = ApplicationVersionString, - ProductName = ApplicationProductName, - Id = SystemId, - ServerName = FriendlyName, - LocalAddress = GetSmartApiUrl(request), - StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted - }; - } - /// public string GetSmartApiUrl(IPAddress remoteAddr) { diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs new file mode 100644 index 0000000000..5e9c424e9a --- /dev/null +++ b/Emby.Server.Implementations/SystemManager.cs @@ -0,0 +1,108 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Updates; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.System; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; + +namespace Emby.Server.Implementations; + +/// +public class SystemManager : ISystemManager +{ + private readonly IHostApplicationLifetime _applicationLifetime; + private readonly IServerApplicationHost _applicationHost; + private readonly IServerApplicationPaths _applicationPaths; + private readonly IServerConfigurationManager _configurationManager; + private readonly IStartupOptions _startupOptions; + private readonly IInstallationManager _installationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of . + /// Instance of . + /// Instance of . + /// Instance of . + /// Instance of . + /// Instance of . + public SystemManager( + IHostApplicationLifetime applicationLifetime, + IServerApplicationHost applicationHost, + IServerApplicationPaths applicationPaths, + IServerConfigurationManager configurationManager, + IStartupOptions startupOptions, + IInstallationManager installationManager) + { + _applicationLifetime = applicationLifetime; + _applicationHost = applicationHost; + _applicationPaths = applicationPaths; + _configurationManager = configurationManager; + _startupOptions = startupOptions; + _installationManager = installationManager; + } + + private bool CanLaunchWebBrowser => Environment.UserInteractive + && !_startupOptions.IsService + && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()); + + /// + public SystemInfo GetSystemInfo(HttpRequest request) + { + return new SystemInfo + { + HasPendingRestart = _applicationHost.HasPendingRestart, + IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested, + Version = _applicationHost.ApplicationVersionString, + WebSocketPortNumber = _applicationHost.HttpPort, + CompletedInstallations = _installationManager.CompletedInstallations.ToArray(), + Id = _applicationHost.SystemId, + ProgramDataPath = _applicationPaths.ProgramDataPath, + WebPath = _applicationPaths.WebPath, + LogPath = _applicationPaths.LogDirectoryPath, + ItemsByNamePath = _applicationPaths.InternalMetadataPath, + InternalMetadataPath = _applicationPaths.InternalMetadataPath, + CachePath = _applicationPaths.CachePath, + CanLaunchWebBrowser = CanLaunchWebBrowser, + TranscodingTempPath = _configurationManager.GetTranscodePath(), + ServerName = _applicationHost.FriendlyName, + LocalAddress = _applicationHost.GetSmartApiUrl(request), + SupportsLibraryMonitor = true, + PackageName = _startupOptions.PackageName + }; + } + + /// + public PublicSystemInfo GetPublicSystemInfo(HttpRequest request) + { + return new PublicSystemInfo + { + Version = _applicationHost.ApplicationVersionString, + ProductName = _applicationHost.Name, + Id = _applicationHost.SystemId, + ServerName = _applicationHost.FriendlyName, + LocalAddress = _applicationHost.GetSmartApiUrl(request), + StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted + }; + } + + /// + public void Restart() => ShutdownInternal(true); + + /// + public void Shutdown() => ShutdownInternal(false); + + private void ShutdownInternal(bool restart) + { + Task.Run(async () => + { + await Task.Delay(100).ConfigureAwait(false); + _applicationHost.ShouldRestart = restart; + _applicationLifetime.StopApplication(); + }); + } +} diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 42ac4a9b4b..11095a97f0 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -10,7 +10,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.System; @@ -26,32 +25,36 @@ namespace Jellyfin.Api.Controllers; /// public class SystemController : BaseJellyfinApiController { + private readonly ILogger _logger; private readonly IServerApplicationHost _appHost; private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; - private readonly INetworkManager _network; - private readonly ILogger _logger; + private readonly INetworkManager _networkManager; + private readonly ISystemManager _systemManager; /// /// Initializes a new instance of the class. /// - /// Instance of interface. + /// Instance of interface. + /// Instance of interface. /// Instance of interface. /// Instance of interface. - /// Instance of interface. - /// Instance of interface. + /// Instance of interface. + /// Instance of interface. public SystemController( - IServerConfigurationManager serverConfigurationManager, + ILogger logger, IServerApplicationHost appHost, + IServerApplicationPaths appPaths, IFileSystem fileSystem, - INetworkManager network, - ILogger logger) + INetworkManager networkManager, + ISystemManager systemManager) { - _appPaths = serverConfigurationManager.ApplicationPaths; + _logger = logger; _appHost = appHost; + _appPaths = appPaths; _fileSystem = fileSystem; - _network = network; - _logger = logger; + _networkManager = networkManager; + _systemManager = systemManager; } /// @@ -65,9 +68,7 @@ public class SystemController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult GetSystemInfo() - { - return _appHost.GetSystemInfo(Request); - } + => _systemManager.GetSystemInfo(Request); /// /// Gets public information about the server. @@ -77,9 +78,7 @@ public class SystemController : BaseJellyfinApiController [HttpGet("Info/Public")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetPublicSystemInfo() - { - return _appHost.GetPublicSystemInfo(Request); - } + => _systemManager.GetPublicSystemInfo(Request); /// /// Pings the system. @@ -90,9 +89,7 @@ public class SystemController : BaseJellyfinApiController [HttpPost("Ping", Name = "PostPingSystem")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult PingSystem() - { - return _appHost.Name; - } + => _appHost.Name; /// /// Restarts the application. @@ -106,7 +103,7 @@ public class SystemController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult RestartApplication() { - _appHost.Restart(); + _systemManager.Restart(); return NoContent(); } @@ -122,7 +119,7 @@ public class SystemController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult ShutdownApplication() { - _appHost.Shutdown(); + _systemManager.Shutdown(); return NoContent(); } @@ -180,7 +177,7 @@ public class SystemController : BaseJellyfinApiController return new EndPointInfo { IsLocal = HttpContext.IsLocal(), - IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP()) + IsInNetwork = _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP()) }; } @@ -218,7 +215,7 @@ public class SystemController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetWakeOnLanInfo() { - var result = _network.GetMacAddresses() + var result = _networkManager.GetMacAddresses() .Select(i => new WakeOnLanInfo(i)); return Ok(result); } diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 5985d3dd82..23795c6be8 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -35,21 +35,15 @@ namespace MediaBrowser.Common string SystemId { get; } /// - /// Gets a value indicating whether this instance has pending kernel reload. + /// Gets a value indicating whether this instance has pending changes requiring a restart. /// - /// true if this instance has pending kernel reload; otherwise, false. + /// true if this instance has a pending restart; otherwise, false. bool HasPendingRestart { get; } /// - /// Gets a value indicating whether this instance is currently shutting down. + /// Gets or sets a value indicating whether the application should restart. /// - /// true if this instance is shutting down; otherwise, false. - bool IsShuttingDown { get; } - - /// - /// Gets a value indicating whether the application should restart. - /// - bool ShouldRestart { get; } + bool ShouldRestart { get; set; } /// /// Gets the application version. @@ -91,11 +85,6 @@ namespace MediaBrowser.Common /// void NotifyPendingRestart(); - /// - /// Restarts this instance. - /// - void Restart(); - /// /// Gets the exports. /// @@ -127,11 +116,6 @@ namespace MediaBrowser.Common /// ``0. T Resolve(); - /// - /// Shuts down. - /// - void Shutdown(); - /// /// Initializes this instance. /// diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 45ac5c3a85..e9c4d9e19a 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -4,7 +4,6 @@ using System.Net; using MediaBrowser.Common; -using MediaBrowser.Model.System; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller @@ -16,8 +15,6 @@ namespace MediaBrowser.Controller { bool CoreStartupHasCompleted { get; } - bool CanLaunchWebBrowser { get; } - /// /// Gets the HTTP server port. /// @@ -41,15 +38,6 @@ namespace MediaBrowser.Controller /// The name of the friendly. string FriendlyName { get; } - /// - /// Gets the system info. - /// - /// The HTTP request. - /// SystemInfo. - SystemInfo GetSystemInfo(HttpRequest request); - - PublicSystemInfo GetPublicSystemInfo(HttpRequest request); - /// /// Gets a URL specific for the request. /// diff --git a/MediaBrowser.Controller/ISystemManager.cs b/MediaBrowser.Controller/ISystemManager.cs new file mode 100644 index 0000000000..ef3034d2f5 --- /dev/null +++ b/MediaBrowser.Controller/ISystemManager.cs @@ -0,0 +1,34 @@ +using MediaBrowser.Model.System; +using Microsoft.AspNetCore.Http; + +namespace MediaBrowser.Controller; + +/// +/// A service for managing the application instance. +/// +public interface ISystemManager +{ + /// + /// Gets the system info. + /// + /// The HTTP request. + /// The . + SystemInfo GetSystemInfo(HttpRequest request); + + /// + /// Gets the public system info. + /// + /// The HTTP request. + /// The . + PublicSystemInfo GetPublicSystemInfo(HttpRequest request); + + /// + /// Starts the application restart process. + /// + void Restart(); + + /// + /// Starts the application shutdown process. + /// + void Shutdown(); +} From cb8a8c3ef441dd048536a4bcd81d4f5ddf8485b2 Mon Sep 17 00:00:00 2001 From: Leo Date: Thu, 5 Oct 2023 11:19:52 +0800 Subject: [PATCH 47/75] fix: use movie.nfo first when .nfo also exists (jellyfin/jellyfin#1558) --- MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index 82e1dc860d..8fa22fad94 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -60,13 +60,13 @@ namespace MediaBrowser.XbmcMetadata.Savers } else { - yield return Path.ChangeExtension(item.Path, ".nfo"); - // only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie) if (!item.IsInMixedFolder && item.ItemType == typeof(Movie)) { yield return Path.Combine(item.ContainingFolderPath, "movie.nfo"); } + + yield return Path.ChangeExtension(item.Path, ".nfo"); } } From 130c035d4fe7e9c195d0f3f9bd8a216cc36e896e Mon Sep 17 00:00:00 2001 From: Jacob Slusser Date: Thu, 5 Oct 2023 05:02:10 -0700 Subject: [PATCH 48/75] #10333 Updates issue stale action to fix issues with not running (#10334) --- .github/workflows/repo-stale.yaml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index c753c1600a..b2e8a572b2 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -2,20 +2,21 @@ name: Stale Check on: schedule: - - cron: '30 1 * * *' + - cron: '30 */12 * * *' workflow_dispatch: permissions: issues: write pull-requests: write + actions: write jobs: issues: - name: Check issues + name: Check for stale issues runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 + - uses: actions/stale@v8 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} days-before-stale: 120 @@ -26,11 +27,11 @@ jobs: exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed stale-issue-label: stale stale-issue-message: |- - This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments. - - If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label. + This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs. - This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html). + If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact). + close-issue-message: |- + This issue was closed due to inactivity. prs-conflicts: name: Check PRs with merge conflicts From 7dd4c1223105edd926605bd59c20ba7591b085e2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 12:02:38 +0000 Subject: [PATCH 49/75] Pin actions/stale action to 1160a22 --- .github/workflows/repo-stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index b2e8a572b2..4eb0cf0996 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@v8 + - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} days-before-stale: 120 From b87765bacec36aba3ee37ebc034458f36c637ffe Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 5 Oct 2023 18:21:43 +0200 Subject: [PATCH 50/75] Update Jellyfin.Server.Implementations/Security/AuthorizationContext.cs Co-authored-by: Patrick Barron --- .../Security/AuthorizationContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index f415d01115..77f8f7071b 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -49,7 +49,7 @@ namespace Jellyfin.Server.Implementations.Security /// /// Gets the authorization. /// - /// The HTTP req. + /// The HTTP context. /// Dictionary{System.StringSystem.String}. private async Task GetAuthorization(HttpContext httpContext) { From 852f1dc0c1e3178955e2d1b2791b1e45f3f956b3 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 5 Oct 2023 23:16:17 +0200 Subject: [PATCH 51/75] Don't create non existent persons in LibraryManager.GetPerson return null instead. GetStudio, GetGenre, GetMusicGenre, GetYear, GetArtist still create a new one when the requested one doesn't exist Fixes #3901 --- .../Library/LibraryManager.cs | 27 ++++++++++--------- .../Controllers/PersonsControllerTests.cs | 26 ++++++++++++++++++ 2 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index b0a4a4151a..bdbf5f703c 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -46,7 +46,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using EpisodeInfo = Emby.Naming.TV.EpisodeInfo; @@ -839,19 +838,12 @@ namespace Emby.Server.Implementations.Library { var path = Person.GetPath(name); var id = GetItemByNameId(path); - if (GetItemById(id) is not Person item) + if (GetItemById(id) is Person item) { - item = new Person - { - Name = name, - Id = id, - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - Path = path - }; + return item; } - return item; + return null; } /// @@ -2900,9 +2892,18 @@ namespace Emby.Server.Implementations.Library var saveEntity = false; var personEntity = GetPerson(person.Name); - // if PresentationUniqueKey is empty it's likely a new item. - if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey)) + if (personEntity is null) { + var path = Person.GetPath(person.Name); + personEntity = new Person() + { + Name = person.Name, + Id = GetItemByNameId(path), + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + Path = path + }; + personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey(); saveEntity = true; } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs new file mode 100644 index 0000000000..38c64547c2 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs @@ -0,0 +1,26 @@ +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public class PersonsControllerTests : IClassFixture +{ + private readonly JellyfinApplicationFactory _factory; + private static string? _accessToken; + + public PersonsControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task GetPerson_DoesntExist_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); + + using var response = await client.GetAsync($"Persons/DoesntExist"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} From efc4c305a912eb92904289fa4a176db120047fba Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 5 Oct 2023 23:29:31 +0200 Subject: [PATCH 52/75] Use CryptoStream to convert stream from base64 Should be way more efficient --- Jellyfin.Api/Controllers/ImageController.cs | 43 ++++++++----------- .../Controllers/SubtitleController.cs | 8 ++-- MediaBrowser.Providers/Manager/ImageSaver.cs | 6 ++- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 3c5f18af55..7b10ea170f 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net.Mime; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; @@ -78,6 +79,9 @@ public class ImageController : BaseJellyfinApiController _appPaths = appPaths; } + private static Stream GetFromBase64Stream(Stream inputStream) + => new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read); + /// /// Sets the user image. /// @@ -116,8 +120,8 @@ public class ImageController : BaseJellyfinApiController return BadRequest("Incorrect ContentType."); } - var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - await using (memoryStream.ConfigureAwait(false)) + var stream = GetFromBase64Stream(Request.Body); + await using (stream.ConfigureAwait(false)) { // Handle image/png; charset=utf-8 var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); @@ -130,7 +134,7 @@ public class ImageController : BaseJellyfinApiController user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension)); await _providerManager - .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) + .SaveImage(stream, mimeType, user.ProfileImage.Path) .ConfigureAwait(false); await _userManager.UpdateUserAsync(user).ConfigureAwait(false); @@ -176,8 +180,8 @@ public class ImageController : BaseJellyfinApiController return BadRequest("Incorrect ContentType."); } - var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - await using (memoryStream.ConfigureAwait(false)) + var stream = GetFromBase64Stream(Request.Body); + await using (stream.ConfigureAwait(false)) { // Handle image/png; charset=utf-8 var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); @@ -190,7 +194,7 @@ public class ImageController : BaseJellyfinApiController user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension)); await _providerManager - .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) + .SaveImage(stream, mimeType, user.ProfileImage.Path) .ConfigureAwait(false); await _userManager.UpdateUserAsync(user).ConfigureAwait(false); @@ -372,12 +376,12 @@ public class ImageController : BaseJellyfinApiController return BadRequest("Incorrect ContentType."); } - var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - await using (memoryStream.ConfigureAwait(false)) + var stream = GetFromBase64Stream(Request.Body); + await using (stream.ConfigureAwait(false)) { // Handle image/png; charset=utf-8 var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); - await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); + await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); return NoContent(); @@ -416,12 +420,12 @@ public class ImageController : BaseJellyfinApiController return BadRequest("Incorrect ContentType."); } - var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - await using (memoryStream.ConfigureAwait(false)) + var stream = GetFromBase64Stream(Request.Body); + await using (stream.ConfigureAwait(false)) { // Handle image/png; charset=utf-8 var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); - await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); + await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); return NoContent(); @@ -1792,8 +1796,8 @@ public class ImageController : BaseJellyfinApiController return BadRequest("Incorrect ContentType."); } - var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - await using (memoryStream.ConfigureAwait(false)) + var stream = GetFromBase64Stream(Request.Body); + await using (stream.ConfigureAwait(false)) { var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension); var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); @@ -1803,7 +1807,7 @@ public class ImageController : BaseJellyfinApiController var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); await using (fs.ConfigureAwait(false)) { - await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false); + await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false); } return NoContent(); @@ -1833,15 +1837,6 @@ public class ImageController : BaseJellyfinApiController return NoContent(); } - private static async Task GetMemoryStream(Stream inputStream) - { - using var reader = new StreamReader(inputStream); - var text = await reader.ReadToEndAsync().ConfigureAwait(false); - - var bytes = Convert.FromBase64String(text); - return new MemoryStream(bytes, 0, bytes.Length, false, true); - } - private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex) { int? width = null; diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 7d02550b68..fb89e96108 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net.Mime; +using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -405,9 +406,8 @@ public class SubtitleController : BaseJellyfinApiController [FromBody, Required] UploadSubtitleDto body) { var video = (Video)_libraryManager.GetItemById(itemId); - var data = Convert.FromBase64String(body.Data); - var memoryStream = new MemoryStream(data, 0, data.Length, false, true); - await using (memoryStream.ConfigureAwait(false)) + var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read); + await using (stream.ConfigureAwait(false)) { await _subtitleManager.UploadSubtitle( video, @@ -417,7 +417,7 @@ public class SubtitleController : BaseJellyfinApiController Language = body.Language, IsForced = body.IsForced, IsHearingImpaired = body.IsHearingImpaired, - Stream = memoryStream + Stream = stream }).ConfigureAwait(false); _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index e7c2cd2558..d827168314 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -263,7 +263,11 @@ namespace MediaBrowser.Providers.Manager var fileStreamOptions = AsyncFile.WriteOptions; fileStreamOptions.Mode = FileMode.Create; - fileStreamOptions.PreallocationSize = source.Length; + if (source.CanSeek) + { + fileStreamOptions.PreallocationSize = source.Length; + } + var fs = new FileStream(path, fileStreamOptions); await using (fs.ConfigureAwait(false)) { From 33b3331c72a811acb1caa03cfa5c3e492df56c50 Mon Sep 17 00:00:00 2001 From: fei long Date: Fri, 6 Oct 2023 06:26:52 +0800 Subject: [PATCH 53/75] change Substring to AsSpan Co-authored-by: Bond-009 --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 4a71f25a78..72511ec4da 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -6244,7 +6244,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (GetAudioEncoder(state).StartsWith("pcm_", StringComparison.Ordinal)) { - audioTranscodeParams.Add(string.Concat("-f ", GetAudioEncoder(state).Substring(4))); + audioTranscodeParams.Add(string.Concat("-f ", GetAudioEncoder(state).AsSpan(4))); audioTranscodeParams.Add("-ar " + state.BaseRequest.AudioBitRate); } From b176beb88e22a36cc056439ac2a4df4fbe68f2c1 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Fri, 6 Oct 2023 00:40:09 +0200 Subject: [PATCH 54/75] Reduce string allocations Some simple changes to reduce the number of allocated strings --- Emby.Dlna/DlnaManager.cs | 2 +- .../ExternalFiles/ExternalPathParser.cs | 2 +- Emby.Naming/Video/StubResolver.cs | 7 ++-- Emby.Photos/PhotoProvider.cs | 2 +- .../IO/ManagedFileSystem.cs | 6 ++-- .../Library/LibraryManager.cs | 4 +-- .../Library/Resolvers/Audio/AudioResolver.cs | 6 ++-- .../Library/Resolvers/BaseVideoResolver.cs | 2 +- .../Library/Resolvers/Books/BookResolver.cs | 9 +++-- .../MediaEncoder/EncodingManager.cs | 2 +- .../Playlists/PlaylistManager.cs | 16 ++++----- .../Updates/InstallationManager.cs | 3 +- .../Controllers/DynamicHlsController.cs | 4 +-- .../Controllers/HlsSegmentController.cs | 9 ++--- Jellyfin.Api/Helpers/StreamingHelpers.cs | 10 +++--- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 2 +- .../MediaEncoding/EncodingHelper.cs | 18 +++++----- .../Attachments/AttachmentExtractor.cs | 36 +++---------------- .../Encoder/MediaEncoder.cs | 6 ++-- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 2 +- .../StripCollageBuilder.cs | 12 +++---- 21 files changed, 63 insertions(+), 97 deletions(-) diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index 99b3e6e7ef..d67cb67b54 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -228,7 +228,7 @@ namespace Emby.Dlna try { return _fileSystem.GetFilePaths(path) - .Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase)) + .Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase)) .Select(i => ParseProfileFile(i, type)) .Where(i => i is not null) .ToList()!; // We just filtered out all the nulls diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs index 9531296711..4080ba10d3 100644 --- a/Emby.Naming/ExternalFiles/ExternalPathParser.cs +++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs @@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles return null; } - var extension = Path.GetExtension(path); + var extension = Path.GetExtension(path.AsSpan()); if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) && !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))) { diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs index f7ba606e3e..4b9df19b08 100644 --- a/Emby.Naming/Video/StubResolver.cs +++ b/Emby.Naming/Video/StubResolver.cs @@ -26,19 +26,18 @@ namespace Emby.Naming.Video return false; } - var extension = Path.GetExtension(path); + var extension = Path.GetExtension(path.AsSpan()); if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { return false; } - path = Path.GetFileNameWithoutExtension(path); - var token = Path.GetExtension(path).TrimStart('.'); + var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.'); foreach (var rule in options.StubTypes) { - if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase)) + if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)) { stubType = rule.StubType; return true; diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs index f54066c57f..27329a7f2f 100644 --- a/Emby.Photos/PhotoProvider.cs +++ b/Emby.Photos/PhotoProvider.cs @@ -61,7 +61,7 @@ namespace Emby.Photos item.SetImagePath(ImageType.Primary, item.Path); // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs - if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase)) + if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase)) { try { diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 18b00ce0b2..4178936ce2 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -103,15 +103,17 @@ namespace Emby.Server.Implementations.IO return filePath; } + var filePathSpan = filePath.AsSpan(); + // relative path if (firstChar == '\\') { - filePath = filePath.Substring(1); + filePathSpan = filePathSpan.Slice(1); } try { - return Path.GetFullPath(Path.Combine(folderPath, filePath)); + return Path.GetFullPath(Path.Join(folderPath, filePathSpan)); } catch (ArgumentException) { diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index b0a4a4151a..845e7e3512 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1162,7 +1162,7 @@ namespace Emby.Server.Implementations.Library Name = Path.GetFileName(dir), Locations = _fileSystem.GetFilePaths(dir, false) - .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) + .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase)) .Select(i => { try @@ -3135,7 +3135,7 @@ namespace Emby.Server.Implementations.Library } var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true) - .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) + .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase)) .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrEmpty(shortcut)) diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index a74f824752..862f144e68 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -94,9 +94,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (AudioFileParser.IsAudioFile(args.Path, _namingOptions)) { - var extension = Path.GetExtension(args.Path); + var extension = Path.GetExtension(args.Path.AsSpan()); - if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase)) + if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase)) { // if audio file exists of same name, return null return null; @@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (item is not null) { - item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase); + item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase); item.IsInMixedFolder = true; } diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 381796d0e3..779cfd5be4 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers return false; } - return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase)); + return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase)); } /// diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 042422c6f4..73861ff599 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -32,9 +32,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books return GetBook(args); } - var extension = Path.GetExtension(args.Path); + var extension = Path.GetExtension(args.Path.AsSpan()); - if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) + if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { // It's a book return new Book @@ -51,12 +51,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books { var bookFiles = args.FileSystemChildren.Where(f => { - var fileExtension = Path.GetExtension(f.FullName) - ?? string.Empty; + var fileExtension = Path.GetExtension(f.FullName.AsSpan()); return _validExtensions.Contains( fileExtension, - StringComparer.OrdinalIgnoreCase); + StringComparison.OrdinalIgnoreCase); }).ToList(); // Don't return a Book if there is more (or less) than one document in the directory diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 7732e32d0a..896f47923f 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -222,7 +222,7 @@ namespace Emby.Server.Implementations.MediaEncoder { var deadImages = images .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase) - .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) + .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase)) .ToList(); foreach (var image in deadImages) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 0cb450cdf3..649c499247 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -327,9 +327,9 @@ namespace Emby.Server.Implementations.Playlists // this is probably best done as a metadata provider // saving a file over itself will require some work to prevent this from happening when not needed var playlistPath = item.Path; - var extension = Path.GetExtension(playlistPath); + var extension = Path.GetExtension(playlistPath.AsSpan()); - if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase)) + if (extension.Equals(".wpl", StringComparison.OrdinalIgnoreCase)) { var playlist = new WplPlaylist(); foreach (var child in item.GetLinkedChildren()) @@ -362,8 +362,7 @@ namespace Emby.Server.Implementations.Playlists string text = new WplContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase)) { var playlist = new ZplPlaylist(); foreach (var child in item.GetLinkedChildren()) @@ -396,8 +395,7 @@ namespace Emby.Server.Implementations.Playlists string text = new ZplContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".m3u", StringComparison.OrdinalIgnoreCase)) { var playlist = new M3uPlaylist { @@ -428,8 +426,7 @@ namespace Emby.Server.Implementations.Playlists string text = new M3uContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase)) { var playlist = new M3uPlaylist(); playlist.IsExtended = true; @@ -458,8 +455,7 @@ namespace Emby.Server.Implementations.Playlists string text = new M3uContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase)) { var playlist = new PlsPlaylist(); foreach (var child in item.GetLinkedChildren()) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 6c198b6f99..77d385ba1c 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -504,8 +504,7 @@ namespace Emby.Server.Implementations.Updates private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken) { - var extension = Path.GetExtension(package.SourceUrl); - if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase)) + if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase)) { _logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl); return; diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 065a4ce5c6..4026a38f8d 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -2041,9 +2041,9 @@ public class DynamicHlsController : BaseJellyfinApiController return null; } - var playlistFilename = Path.GetFileNameWithoutExtension(playlist); + var playlistFilename = Path.GetFileNameWithoutExtension(playlist.AsSpan()); - var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length); + var indexString = Path.GetFileNameWithoutExtension(file.Name.AsSpan()).Slice(playlistFilename.Length); return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture); } diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index d7cec865e1..6eedfd8c7f 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -59,7 +59,7 @@ public class HlsSegmentController : BaseJellyfinApiController public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId) { // TODO: Deprecate with new iOS app - var file = segmentId + Path.GetExtension(Request.Path); + var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan())); var transcodePath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodePath, file)); var fileDir = Path.GetDirectoryName(file); @@ -85,11 +85,12 @@ public class HlsSegmentController : BaseJellyfinApiController [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId) { - var file = playlistId + Path.GetExtension(Request.Path); + var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan())); var transcodePath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodePath, file)); var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8") + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) + || Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase)) { return BadRequest("Invalid segment."); } @@ -138,7 +139,7 @@ public class HlsSegmentController : BaseJellyfinApiController [FromRoute, Required] string segmentId, [FromRoute, Required] string segmentContainer) { - var file = segmentId + Path.GetExtension(Request.Path); + var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan())); var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file)); diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 782cd65685..d15fe4fab2 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -243,7 +243,7 @@ public static class StreamingHelpers ? GetOutputFileExtension(state, mediaSource) : ("." + state.OutputContainer); - state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId); + state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId); return state; } @@ -418,11 +418,11 @@ public static class StreamingHelpers /// System.String. private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource) { - var ext = Path.GetExtension(state.RequestedUrl); + var ext = Path.GetExtension(state.RequestedUrl.AsSpan()); - if (!string.IsNullOrEmpty(ext)) + if (ext.IsEmpty) { - return ext; + return null; } // Try to infer based on the desired video codec @@ -504,7 +504,7 @@ public static class StreamingHelpers /// The device id. /// The play session id. /// The complete file path, including the folder, for the transcoding file. - private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId) + private static string GetOutputFilePath(StreamState state, string? outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId) { var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}"; diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 73ebb396d0..c16a586d60 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -538,7 +538,7 @@ public class TranscodingJobHelper : IDisposable await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); } - if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase)) + if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase)) { string subtitlePath = state.SubtitleStream.Path; string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal)); diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index c311d3b8ab..3be7a4252b 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -548,25 +548,25 @@ namespace MediaBrowser.Controller.MediaEncoding /// System.Nullable{VideoCodecs}. public string InferVideoCodec(string url) { - var ext = Path.GetExtension(url); + var ext = Path.GetExtension(url.AsSpan()); - if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".asf", StringComparison.OrdinalIgnoreCase)) { return "wmv"; } - if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".webm", StringComparison.OrdinalIgnoreCase)) { // TODO: this may not always mean VP8, as the codec ages return "vp8"; } - if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".ogg", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ogv", StringComparison.OrdinalIgnoreCase)) { return "theora"; } - if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".m3u8", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ts", StringComparison.OrdinalIgnoreCase)) { return "h264"; } @@ -1080,10 +1080,10 @@ namespace MediaBrowser.Controller.MediaEncoding && state.SubtitleStream.IsExternal) { var subtitlePath = state.SubtitleStream.Path; - var subtitleExtension = Path.GetExtension(subtitlePath); + var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan()); - if (string.Equals(subtitleExtension, ".sub", StringComparison.OrdinalIgnoreCase) - || string.Equals(subtitleExtension, ".sup", StringComparison.OrdinalIgnoreCase)) + if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase) + || subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase)) { var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); if (File.Exists(idxFile)) @@ -6033,7 +6033,7 @@ namespace MediaBrowser.Controller.MediaEncoding var format = string.Empty; var keyFrame = string.Empty; - if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase) + if (Path.GetExtension(outputPath.AsSpan()).Equals(".mp4", StringComparison.OrdinalIgnoreCase) && state.BaseRequest.Context == EncodingContext.Streaming) { // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 989e386a51..0ec0c84d41 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -1,4 +1,3 @@ -#nullable disable #pragma warning disable CS1591 using System; @@ -23,7 +22,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Attachments { - public class AttachmentExtractor : IAttachmentExtractor, IDisposable + public sealed class AttachmentExtractor : IAttachmentExtractor { private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; @@ -34,8 +33,6 @@ namespace MediaBrowser.MediaEncoding.Attachments private readonly ConcurrentDictionary _semaphoreLocks = new ConcurrentDictionary(); - private bool _disposed = false; - public AttachmentExtractor( ILogger logger, IApplicationPaths appPaths, @@ -296,7 +293,7 @@ namespace MediaBrowser.MediaEncoding.Attachments ArgumentException.ThrowIfNullOrEmpty(outputPath); - Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputPath))); var processArgs = string.Format( CultureInfo.InvariantCulture, @@ -391,33 +388,8 @@ namespace MediaBrowser.MediaEncoding.Attachments filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture); } - var prefix = filename.Substring(0, 1); - return Path.Combine(_appPaths.DataPath, "attachments", prefix, filename); - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - } - - _disposed = true; + var prefix = filename.AsSpan(0, 1); + return Path.Join(_appPaths.DataPath, "attachments", prefix, filename); } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 346e97ae12..59c3505616 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -316,10 +316,8 @@ namespace MediaBrowser.MediaEncoding.Encoder { var files = _fileSystem.GetFilePaths(path, recursive); - var excludeExtensions = new[] { ".c" }; - - return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase) - && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); + return files.FirstOrDefault(i => Path.GetFileNameWithoutExtension(i.AsSpan()).Equals(filename, StringComparison.OrdinalIgnoreCase) + && !Path.GetExtension(i.AsSpan()).Equals(".c", StringComparison.OrdinalIgnoreCase)); } catch (Exception) { diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 126c0503e0..416f602179 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -188,7 +188,7 @@ public class SkiaEncoder : IImageEncoder return path; } - var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path)); + var tempPath = Path.Combine(_appPaths.TempDirectory, string.Concat(Guid.NewGuid().ToString(), Path.GetExtension(path.AsSpan()))); var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid."); Directory.CreateDirectory(directory); File.Copy(path, tempPath, true); diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index 6dff7aa9b4..4aff26c16b 100644 --- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -38,25 +38,25 @@ public partial class StripCollageBuilder { ArgumentNullException.ThrowIfNull(outputPath); - var ext = Path.GetExtension(outputPath); + var ext = Path.GetExtension(outputPath.AsSpan()); - if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase) - || string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".jpg", StringComparison.OrdinalIgnoreCase) + || ext.Equals(".jpeg", StringComparison.OrdinalIgnoreCase)) { return SKEncodedImageFormat.Jpeg; } - if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".webp", StringComparison.OrdinalIgnoreCase)) { return SKEncodedImageFormat.Webp; } - if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".gif", StringComparison.OrdinalIgnoreCase)) { return SKEncodedImageFormat.Gif; } - if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".bmp", StringComparison.OrdinalIgnoreCase)) { return SKEncodedImageFormat.Bmp; } From e9e1fd214b608007735b2a2a3ed722d0e4937da8 Mon Sep 17 00:00:00 2001 From: "[ ]" Date: Thu, 5 Oct 2023 07:08:18 +0000 Subject: [PATCH 55/75] Translated using Weblate (Pirate (pr)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pr/ --- Emby.Server.Implementations/Localization/Core/pr.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json index d2446a7fdc..26dc5ce82f 100644 --- a/Emby.Server.Implementations/Localization/Core/pr.json +++ b/Emby.Server.Implementations/Localization/Core/pr.json @@ -29,5 +29,8 @@ "Forced": "Pressed", "External": "Outboard", "HeaderFavoriteEpisodes": "Treasured Tales", - "HeaderFavoriteShows": "Treasured Tales" + "HeaderFavoriteShows": "Treasured Tales", + "ChapterNameValue": "Piece {0}", + "HeaderFavoriteSongs": "Treasured Chimes", + "HeaderNextUp": "Incoming" } From a18b3fbe70429f55852f0aebeda4e02e2abdef77 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Fri, 6 Oct 2023 10:49:20 +0200 Subject: [PATCH 56/75] simplify the if --- .../MediaEncoding/EncodingHelper.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index f8d2dd40fd..449ea64899 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -5697,21 +5697,13 @@ namespace MediaBrowser.Controller.MediaEncoding inputModifier = inputModifier.Trim(); // Apply -probesize if configured - var probeSizeArgument = string.Empty; var ffmpegProbeSize = _config.GetFFmpegProbeSize(); if (!string.IsNullOrEmpty(ffmpegProbeSize)) { - probeSizeArgument = $"-probesize {probeSizeArgument}"; + inputModifier += $" -probesize {ffmpegProbeSize}"; } - if (!string.IsNullOrEmpty(probeSizeArgument)) - { - inputModifier += $" {probeSizeArgument}"; - } - - inputModifier = inputModifier.Trim(); - var userAgentParam = GetUserAgentParam(state); if (!string.IsNullOrEmpty(userAgentParam)) From ffb3df9e7725a067934e1ce22483007209f17010 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 08:50:50 -0600 Subject: [PATCH 57/75] Update github/codeql-action action to v2.22.0 (#10351) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5dc38e188d..8e764b0194 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 + uses: github/codeql-action/init@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 + uses: github/codeql-action/autobuild@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 + uses: github/codeql-action/analyze@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0 From bdca4ed322a6eaa1fa1810e3187aa7948a574c60 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 6 Oct 2023 12:46:35 -0400 Subject: [PATCH 58/75] Add XmlReader.GetPersonFromXmlNode --- .../Extensions/XmlReaderExtensions.cs | 107 ++++++++++++++++ .../Parsers/BaseItemXmlParser.cs | 100 +-------------- .../Parsers/BaseNfoParser.cs | 119 +----------------- 3 files changed, 116 insertions(+), 210 deletions(-) create mode 100644 MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs new file mode 100644 index 0000000000..cd7db91dd4 --- /dev/null +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -0,0 +1,107 @@ +using System; +using System.Globalization; +using System.Xml; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Extensions; + +/// +/// Provides extension methods for to parse 's. +/// +public static class XmlReaderExtensions +{ + /// + /// Parses a from the xml node. + /// + /// The . + /// A , or null if none is found. + public static PersonInfo? GetPersonFromXmlNode(this XmlReader reader) + { + ArgumentNullException.ThrowIfNull(reader); + + if (reader.IsEmptyElement) + { + reader.Read(); + return null; + } + + var name = string.Empty; + var type = PersonKind.Actor; // If type is not specified assume actor + var role = string.Empty; + int? sortOrder = null; + string? imageUrl = null; + + using var subtree = reader.ReadSubtree(); + subtree.MoveToContent(); + subtree.Read(); + + while (subtree is { EOF: false, ReadState: ReadState.Interactive }) + { + if (subtree.NodeType != XmlNodeType.Element) + { + subtree.Read(); + continue; + } + + switch (subtree.Name) + { + case "name": + case "Name": + name = subtree.ReadElementContentAsString(); + break; + case "role": + case "Role": + var roleValue = subtree.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(roleValue)) + { + role = roleValue; + } + + break; + case "type": + case "Type": + Enum.TryParse(subtree.ReadElementContentAsString(), true, out type); + break; + case "order": + case "sortorder": + case "SortOrder": + if (int.TryParse( + subtree.ReadElementContentAsString(), + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var intVal)) + { + sortOrder = intVal; + } + + break; + case "thumb": + var thumb = subtree.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(thumb)) + { + imageUrl = thumb; + } + + break; + default: + subtree.Skip(); + break; + } + } + + if (string.IsNullOrWhiteSpace(name)) + { + return null; + } + + return new PersonInfo + { + Name = name.Trim(), + Role = role, + Type = type, + SortOrder = sortOrder, + ImageUrl = imageUrl + }; + } +} diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index cb369d8377..df4d40a100 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -9,6 +9,7 @@ using System.Xml; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -929,29 +930,13 @@ namespace MediaBrowser.LocalMetadata.Parsers { case "Person": case "Actor": - { - if (reader.IsEmptyElement) + var person = reader.GetPersonFromXmlNode(); + if (person is not null) { - reader.Read(); - continue; - } - - using (var subtree = reader.ReadSubtree()) - { - foreach (var person in GetPersonsFromXmlNode(subtree)) - { - if (string.IsNullOrWhiteSpace(person.Name)) - { - continue; - } - - item.AddPerson(person); - } + item.AddPerson(person); } break; - } - default: reader.Skip(); break; @@ -1041,83 +1026,6 @@ namespace MediaBrowser.LocalMetadata.Parsers } } - /// - /// Gets the persons from XML node. - /// - /// The reader. - /// IEnumerable{PersonInfo}. - private IEnumerable GetPersonsFromXmlNode(XmlReader reader) - { - var name = string.Empty; - var type = PersonKind.Actor; // If type is not specified assume actor - var role = string.Empty; - int? sortOrder = null; - - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Name": - name = reader.ReadElementContentAsString(); - break; - - case "Type": - { - var val = reader.ReadElementContentAsString(); - _ = Enum.TryParse(val, true, out type); - - break; - } - - case "Role": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - role = val; - } - - break; - } - - case "SortOrder": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal)) - { - sortOrder = intVal; - } - } - - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - - var personInfo = new PersonInfo { Name = name.Trim(), Role = role, Type = type, SortOrder = sortOrder }; - - return new[] { personInfo }; - } - /// /// Get linked child. /// diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 5b68924acb..e11378c786 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -13,6 +13,7 @@ using MediaBrowser.Common.Providers; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -581,27 +582,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "actor": + var person = reader.GetPersonFromXmlNode(); + if (person is not null) { - if (!reader.IsEmptyElement) - { - using (var subtree = reader.ReadSubtree()) - { - var person = GetPersonFromXmlNode(subtree); - - if (!string.IsNullOrWhiteSpace(person.Name)) - { - itemResult.AddPerson(person); - } - } - } - else - { - reader.Read(); - } - - break; + itemResult.AddPerson(person); } + break; case "trailer": { var val = reader.ReadElementContentAsString(); @@ -1196,102 +1183,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } - /// - /// Gets the persons from a XML node. - /// - /// The . - /// IEnumerable{PersonInfo}. - private PersonInfo GetPersonFromXmlNode(XmlReader reader) - { - var name = string.Empty; - var type = PersonKind.Actor; // If type is not specified assume actor - var role = string.Empty; - int? sortOrder = null; - string? imageUrl = null; - - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "name": - name = reader.ReadElementContentAsString(); - break; - - case "role": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - role = val; - } - - break; - } - - case "type": - { - var val = reader.ReadElementContentAsString(); - if (!Enum.TryParse(val, true, out type)) - { - type = PersonKind.Actor; - } - - break; - } - - case "order": - case "sortorder": - { - var val = reader.ReadElementContentAsString(); - - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal)) - { - sortOrder = intVal; - } - - break; - } - - case "thumb": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - imageUrl = val; - } - - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - - return new PersonInfo - { - Name = name.Trim(), - Role = role, - Type = type, - SortOrder = sortOrder, - ImageUrl = imageUrl - }; - } - internal XmlReaderSettings GetXmlReaderSettings() => new XmlReaderSettings() { From 1a6ec2c74062e28552089a8b85dc5d45224d4e86 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 6 Oct 2023 13:40:08 -0400 Subject: [PATCH 59/75] Add GetStringArray and GetPersonArray to XmlReaderExtensions --- .../Extensions/XmlReaderExtensions.cs | 38 +++++++ .../Parsers/BaseItemXmlParser.cs | 100 ++---------------- .../Parsers/BaseNfoParser.cs | 48 ++------- 3 files changed, 53 insertions(+), 133 deletions(-) diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index cd7db91dd4..8e4b7c8594 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Xml; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; @@ -104,4 +106,40 @@ public static class XmlReaderExtensions ImageUrl = imageUrl }; } + + /// + /// Used to split names of comma or pipe delimited genres and people. + /// + /// The . + /// IEnumerable{System.String}. + public static IEnumerable GetStringArray(this XmlReader reader) + { + ArgumentNullException.ThrowIfNull(reader); + var value = reader.ReadElementContentAsString(); + + // Only split by comma if there is no pipe in the string + // We have to be careful to not split names like Matthew, Jr. + var separator = !value.Contains('|', StringComparison.Ordinal) + && !value.Contains(';', StringComparison.Ordinal) + ? new[] { ',' } + : new[] { '|', ';' }; + + foreach (var part in value.Trim().Trim(separator).Split(separator)) + { + if (!string.IsNullOrWhiteSpace(part)) + { + yield return part.Trim(); + } + } + } + + /// + /// Parses a array from the xml node. + /// + /// The . + /// The . + /// The . + public static IEnumerable GetPersonArray(this XmlReader reader, PersonKind personKind) + => reader.GetStringArray() + .Select(part => new PersonInfo { Name = part, Type = personKind }); } diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index df4d40a100..d62862be40 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -354,93 +354,40 @@ namespace MediaBrowser.LocalMetadata.Parsers } case "Network": - { - foreach (var name in SplitNames(reader.ReadElementContentAsString())) + foreach (var name in reader.GetStringArray()) { - if (string.IsNullOrWhiteSpace(name)) - { - continue; - } - item.AddStudio(name); } break; - } - case "Director": - { - foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director })) + foreach (var director in reader.GetPersonArray(PersonKind.Director)) { - if (string.IsNullOrWhiteSpace(p.Name)) - { - continue; - } - - itemResult.AddPerson(p); + itemResult.AddPerson(director); } break; - } - case "Writer": - { - foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer })) + foreach (var writer in reader.GetPersonArray(PersonKind.Writer)) { - if (string.IsNullOrWhiteSpace(p.Name)) - { - continue; - } - - itemResult.AddPerson(p); + itemResult.AddPerson(writer); } break; - } - case "Actors": - { - var actors = reader.ReadInnerXml(); - - if (actors.Contains('<', StringComparison.Ordinal)) + foreach (var actor in reader.GetPersonArray(PersonKind.Actor)) { - // This is one of the mis-named "Actors" full nodes created by MB2 - // Create a reader and pass it to the persons node processor - using var xmlReader = XmlReader.Create(new StringReader($"{actors}")); - FetchDataFromPersonsNode(xmlReader, itemResult); - } - else - { - // Old-style piped string - foreach (var p in SplitNames(actors).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Actor })) - { - if (string.IsNullOrWhiteSpace(p.Name)) - { - continue; - } - - itemResult.AddPerson(p); - } + itemResult.AddPerson(actor); } break; - } - case "GuestStars": - { - foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.GuestStar })) + foreach (var guestStar in reader.GetPersonArray(PersonKind.GuestStar)) { - if (string.IsNullOrWhiteSpace(p.Name)) - { - continue; - } - - itemResult.AddPerson(p); + itemResult.AddPerson(guestStar); } break; - } - case "Trailer": { var val = reader.ReadElementContentAsString(); @@ -1129,34 +1076,5 @@ namespace MediaBrowser.LocalMetadata.Parsers return null; } - - /// - /// Used to split names of comma or pipe delimited genres and people. - /// - /// The value. - /// IEnumerable{System.String}. - private IEnumerable SplitNames(string value) - { - // Only split by comma if there is no pipe in the string - // We have to be careful to not split names like Matthew, Jr. - var separator = !value.Contains('|', StringComparison.Ordinal) - && !value.Contains(';', StringComparison.Ordinal) ? new[] { ',' } : new[] { '|', ';' }; - - value = value.Trim().Trim(separator); - - return string.IsNullOrWhiteSpace(value) ? Array.Empty() : Split(value, separator, StringSplitOptions.RemoveEmptyEntries); - } - - /// - /// Provides an additional overload for string.split. - /// - /// The val. - /// The separators. - /// The options. - /// System.String[][]. - private string[] Split(string val, char[] separators, StringSplitOptions options) - { - return val.Split(separators, options); - } } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index e11378c786..797ce59e05 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -527,21 +527,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "director": + foreach (var director in reader.GetPersonArray(PersonKind.Director)) { - var val = reader.ReadElementContentAsString(); - foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director })) - { - if (string.IsNullOrWhiteSpace(p.Name)) - { - continue; - } - - itemResult.AddPerson(p); - } - - break; + itemResult.AddPerson(director); } + break; case "credits": { var val = reader.ReadElementContentAsString(); @@ -566,21 +557,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "writer": + foreach (var writer in reader.GetPersonArray(PersonKind.Writer)) { - var val = reader.ReadElementContentAsString(); - foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer })) - { - if (string.IsNullOrWhiteSpace(p.Name)) - { - continue; - } - - itemResult.AddPerson(p); - } - - break; + itemResult.AddPerson(writer); } + break; case "actor": var person = reader.GetPersonFromXmlNode(); if (person is not null) @@ -1192,24 +1174,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers IgnoreComments = true }; - /// - /// Used to split names of comma or pipe delimited genres and people. - /// - /// The value. - /// IEnumerable{System.String}. - private IEnumerable SplitNames(string value) - { - // Only split by comma if there is no pipe in the string - // We have to be careful to not split names like Matthew, Jr. - var separator = !value.Contains('|', StringComparison.Ordinal) && !value.Contains(';', StringComparison.Ordinal) - ? new[] { ',' } - : new[] { '|', ';' }; - - value = value.Trim().Trim(separator); - - return string.IsNullOrWhiteSpace(value) ? Array.Empty() : value.Split(separator, StringSplitOptions.RemoveEmptyEntries); - } - /// /// Parses the from the NFO aspect property. /// From 99832642cebcce3f93b49004a1dd10def3fb227d Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 6 Oct 2023 14:18:56 -0400 Subject: [PATCH 60/75] Add TryParseDateTime and TryParseDateTimeExact to XmlReaderExtensions --- .../Extensions/XmlReaderExtensions.cs | 47 ++++++++++++ .../Parsers/BaseItemXmlParser.cs | 43 ++--------- .../Parsers/BaseNfoParser.cs | 72 +++++-------------- 3 files changed, 73 insertions(+), 89 deletions(-) diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index 8e4b7c8594..cb239e3145 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Xml; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; +using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Extensions; @@ -13,6 +14,52 @@ namespace MediaBrowser.Controller.Extensions; /// public static class XmlReaderExtensions { + /// + /// Parses a from the current node. + /// + /// The . + /// The to use on failure. + /// The parsed . + /// A value indicating whether the parsing succeeded. + public static bool TryReadDateTime(this XmlReader reader, ILogger logger, out DateTime value) + { + ArgumentNullException.ThrowIfNull(reader); + ArgumentNullException.ThrowIfNull(logger); + + var text = reader.ReadElementContentAsString(); + if (DateTime.TryParse( + text, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out value)) + { + return true; + } + + logger.LogWarning("Invalid date: {Date}", text); + return false; + } + + /// + /// Parses a from the current node. + /// + /// The . + /// The date format string. + /// The parsed . + /// A value indicating whether the parsing succeeded. + public static bool TryReadDateTimeExact(this XmlReader reader, string formatString, out DateTime value) + { + ArgumentNullException.ThrowIfNull(reader); + ArgumentNullException.ThrowIfNull(formatString); + + return DateTime.TryParseExact( + reader.ReadElementContentAsString(), + formatString, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out value); + } + /// /// Parses a from the xml node. /// diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index d62862be40..b77b2b43e4 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -129,26 +129,13 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { - // DateCreated case "Added": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) + if (reader.TryReadDateTime(Logger, out var dateCreated)) { - if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var added)) - { - item.DateCreated = added; - } - else - { - Logger.LogWarning("Invalid Added value found: {Value}", val); - } + item.DateCreated = dateCreated; } break; - } - case "OriginalTitle": { var val = reader.ReadElementContentAsString(); @@ -465,37 +452,21 @@ namespace MediaBrowser.LocalMetadata.Parsers case "BirthDate": case "PremiereDate": case "FirstAired": - { - var firstAired = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(firstAired)) + if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var firstAired)) { - if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850) - { - item.PremiereDate = airDate; - item.ProductionYear = airDate.Year; - } + item.PremiereDate = firstAired; + item.ProductionYear = firstAired.Year; } break; - } - case "DeathDate": case "EndDate": - { - var firstAired = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(firstAired)) + if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var endDate)) { - if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850) - { - item.EndDate = airDate; - } + item.EndDate = endDate; } break; - } - case "CollectionNumber": var tmdbCollection = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(tmdbCollection)) diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 797ce59e05..6e02add775 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -268,23 +268,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers switch (reader.Name) { - // DateCreated case "dateadded": + if (reader.TryReadDateTime(Logger, out var dateCreated)) { - var val = reader.ReadElementContentAsString(); - - if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added)) - { - item.DateCreated = added; - } - else - { - Logger.LogWarning("Invalid Added value found: {Value}", val); - } - - break; + item.DateCreated = dateCreated; } + break; case "originaltitle": { var val = reader.ReadElementContentAsString(); @@ -373,9 +363,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers { var val = reader.ReadElementContentAsString(); if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count) - && Guid.TryParse(nfoConfiguration.UserId, out var guid)) + && Guid.TryParse(nfoConfiguration.UserId, out var playCountUserId)) { - var user = _userManager.GetUserById(guid); + var user = _userManager.GetUserById(playCountUserId); userData = _userDataManager.GetUserData(user, item); userData.PlayCount = count; _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); @@ -385,26 +375,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "lastplayed": + if (reader.TryReadDateTime(Logger, out var lastPlayed) + && Guid.TryParse(nfoConfiguration.UserId, out var lastPlayedUserId)) { - var val = reader.ReadElementContentAsString(); - if (Guid.TryParse(nfoConfiguration.UserId, out var guid)) - { - if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added)) - { - var user = _userManager.GetUserById(guid); - userData = _userDataManager.GetUserData(user, item); - userData.LastPlayedDate = added; - _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); - } - else - { - Logger.LogWarning("Invalid lastplayed value found: {Value}", val); - } - } - - break; + var user = _userManager.GetUserById(lastPlayedUserId); + userData = _userDataManager.GetUserData(user, item); + userData.LastPlayedDate = lastPlayed; + _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); } + break; case "countrycode": { var val = reader.ReadElementContentAsString(); @@ -641,34 +621,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers case "formed": case "premiered": case "releasedate": + if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var releaseDate)) { - var formatString = nfoConfiguration.ReleaseDateFormat; - - var val = reader.ReadElementContentAsString(); - - if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850) - { - item.PremiereDate = date; - item.ProductionYear = date.Year; - } - - break; + item.PremiereDate = releaseDate; + item.ProductionYear = releaseDate.Year; } + break; case "enddate": + if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var endDate)) { - var formatString = nfoConfiguration.ReleaseDateFormat; - - var val = reader.ReadElementContentAsString(); - - if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850) - { - item.EndDate = date; - } - - break; + item.EndDate = endDate; } + break; case "genre": { var val = reader.ReadElementContentAsString(); From 8a7a1cc72337a5190ac093a471c2bf3369022be2 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 6 Oct 2023 14:53:05 -0400 Subject: [PATCH 61/75] Add ReadNormalizedString to XmlReaderExtensions --- .../Extensions/XmlReaderExtensions.cs | 12 + .../Parsers/BaseItemXmlParser.cs | 225 +++--------------- .../Parsers/PlaylistXmlParser.cs | 9 +- .../Parsers/BaseNfoParser.cs | 159 ++++--------- .../Parsers/EpisodeNfoParser.cs | 14 +- .../Parsers/MovieNfoParser.cs | 29 +-- .../Parsers/SeasonNfoParser.cs | 14 +- .../Parsers/SeriesNfoParser.cs | 22 +- 8 files changed, 112 insertions(+), 372 deletions(-) diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index cb239e3145..661133b771 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -14,6 +14,18 @@ namespace MediaBrowser.Controller.Extensions; /// public static class XmlReaderExtensions { + /// + /// Reads a trimmed string from the current node. + /// + /// The . + /// The trimmed content. + public static string ReadNormalizedString(this XmlReader reader) + { + ArgumentNullException.ThrowIfNull(reader); + + return reader.ReadElementContentAsString().Trim(); + } + /// /// Parses a from the current node. /// diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index b77b2b43e4..d115bc2496 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -137,21 +137,11 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "OriginalTitle": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrEmpty(val)) - { - item.OriginalTitle = val; - } - + item.OriginalTitle = reader.ReadNormalizedString(); break; - } - case "LocalTitle": - item.Name = reader.ReadElementContentAsString(); + item.Name = reader.ReadNormalizedString(); break; - case "CriticRating": { var text = reader.ReadElementContentAsString(); @@ -165,63 +155,26 @@ namespace MediaBrowser.LocalMetadata.Parsers } case "SortTitle": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.ForcedSortName = val; - } - + item.ForcedSortName = reader.ReadNormalizedString(); break; - } - case "Overview": case "Description": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.Overview = val; - } - + item.Overview = reader.ReadNormalizedString(); break; - } - case "Language": - { - var val = reader.ReadElementContentAsString(); - - item.PreferredMetadataLanguage = val; - + item.PreferredMetadataLanguage = reader.ReadNormalizedString(); break; - } - case "CountryCode": - { - var val = reader.ReadElementContentAsString(); - - item.PreferredMetadataCountryCode = val; - + item.PreferredMetadataCountryCode = reader.ReadNormalizedString(); break; - } - case "PlaceOfBirth": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) + var placeOfBirth = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(placeOfBirth) && item is Person person) { - if (item is Person person) - { - person.ProductionLocations = new[] { val }; - } + person.ProductionLocations = new[] { placeOfBirth }; } break; - } - case "LockedFields": { var val = reader.ReadElementContentAsString(); @@ -263,10 +216,7 @@ namespace MediaBrowser.LocalMetadata.Parsers { if (!reader.IsEmptyElement) { - using (var subtree = reader.ReadSubtree()) - { - FetchFromCountriesNode(subtree); - } + reader.Skip(); } else { @@ -278,29 +228,11 @@ namespace MediaBrowser.LocalMetadata.Parsers case "ContentRating": case "MPAARating": - { - var rating = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(rating)) - { - item.OfficialRating = rating; - } - + item.OfficialRating = reader.ReadNormalizedString(); break; - } - case "CustomRating": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.CustomRating = val; - } - + item.CustomRating = reader.ReadNormalizedString(); break; - } - case "RunningTime": { var text = reader.ReadElementContentAsString(); @@ -317,16 +249,13 @@ namespace MediaBrowser.LocalMetadata.Parsers } case "AspectRatio": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val) && item is IHasAspectRatio hasAspectRatio) + var aspectRatio = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio) { - hasAspectRatio.AspectRatio = val; + hasAspectRatio.AspectRatio = aspectRatio; } break; - } case "LockData": { @@ -376,32 +305,21 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "Trailer": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) + var trailer = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(trailer)) { - item.AddTrailerUrl(val); + item.AddTrailerUrl(trailer); } break; - } - case "DisplayOrder": - { - var val = reader.ReadElementContentAsString(); - - if (item is IHasDisplayOrder hasDisplayOrder) + var displayOrder = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(displayOrder) && item is IHasDisplayOrder hasDisplayOrder) { - if (!string.IsNullOrWhiteSpace(val)) - { - hasDisplayOrder.DisplayOrder = val; - } + hasDisplayOrder.DisplayOrder = displayOrder; } break; - } - case "Trailers": { if (!reader.IsEmptyElement) @@ -468,8 +386,8 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "CollectionNumber": - var tmdbCollection = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(tmdbCollection)) + var tmdbCollection = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(tmdbCollection)) { item.SetProviderId(MetadataProvider.TmdbCollection, tmdbCollection); } @@ -672,41 +590,6 @@ namespace MediaBrowser.LocalMetadata.Parsers item.Shares = list.ToArray(); } - private void FetchFromCountriesNode(XmlReader reader) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Country": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - } - - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } - /// /// Fetches from taglines node. /// @@ -725,17 +608,8 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Tagline": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.Tagline = val; - } - + item.Tagline = reader.ReadNormalizedString(); break; - } - default: reader.Skip(); break; @@ -766,17 +640,13 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Genre": - { - var genre = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(genre)) + var genre = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(genre)) { item.AddGenre(genre); } break; - } - default: reader.Skip(); break; @@ -804,17 +674,13 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Tag": - { - var tag = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(tag)) + var tag = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(tag)) { tags.Add(tag); } break; - } - default: reader.Skip(); break; @@ -880,17 +746,13 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Trailer": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) + var trailer = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(trailer)) { - item.AddTrailerUrl(val); + item.AddTrailerUrl(trailer); } break; - } - default: reader.Skip(); break; @@ -921,17 +783,13 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Studio": - { - var studio = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(studio)) + var studio = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(studio)) { item.AddStudio(studio); } break; - } - default: reader.Skip(); break; @@ -964,17 +822,11 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Path": - { - linkedItem.Path = reader.ReadElementContentAsString(); + linkedItem.Path = reader.ReadNormalizedString(); break; - } - case "ItemId": - { - linkedItem.LibraryItemId = reader.ReadElementContentAsString(); + linkedItem.LibraryItemId = reader.ReadNormalizedString(); break; - } - default: reader.Skip(); break; @@ -1015,11 +867,8 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "UserId": - { - item.UserId = reader.ReadElementContentAsString(); + item.UserId = reader.ReadNormalizedString(); break; - } - case "CanEdit": { item.CanEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); @@ -1027,10 +876,8 @@ namespace MediaBrowser.LocalMetadata.Parsers } default: - { reader.Skip(); break; - } } } else diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs index 88b190f2b4..879a3616bd 100644 --- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Xml; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using Microsoft.Extensions.Logging; @@ -30,12 +31,8 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "PlaylistMediaType": - { - item.PlaylistMediaType = reader.ReadElementContentAsString(); - + item.PlaylistMediaType = reader.ReadNormalizedString(); break; - } - case "PlaylistItems": if (!reader.IsEmptyElement) @@ -94,10 +91,8 @@ namespace MediaBrowser.LocalMetadata.Parsers } default: - { reader.Skip(); break; - } } } else diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 6e02add775..af405d7559 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -276,27 +276,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "originaltitle": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrEmpty(val)) - { - item.OriginalTitle = val; - } - - break; - } - + item.OriginalTitle = reader.ReadNormalizedString(); + break; case "name": case "title": case "localtitle": - item.Name = reader.ReadElementContentAsString(); + item.Name = reader.ReadNormalizedString(); break; - case "sortname": - item.SortName = reader.ReadElementContentAsString(); + item.SortName = reader.ReadNormalizedString(); break; - case "criticrating": { var text = reader.ReadElementContentAsString(); @@ -310,40 +299,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "sorttitle": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.ForcedSortName = val; - } - - break; - } - + item.ForcedSortName = reader.ReadNormalizedString(); + break; case "biography": case "plot": case "review": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.Overview = val; - } - - break; - } - + item.Overview = reader.ReadNormalizedString(); + break; case "language": - { - var val = reader.ReadElementContentAsString(); - - item.PreferredMetadataLanguage = val; - - break; - } - + item.PreferredMetadataLanguage = reader.ReadNormalizedString(); + break; case "watched": { var val = reader.ReadElementContentAsBoolean(); @@ -386,14 +351,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "countrycode": - { - var val = reader.ReadElementContentAsString(); - - item.PreferredMetadataCountryCode = val; - - break; - } - + item.PreferredMetadataCountryCode = reader.ReadNormalizedString(); + break; case "lockedfields": { var val = reader.ReadElementContentAsString(); @@ -415,9 +374,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "tagline": - item.Tagline = reader.ReadElementContentAsString(); + item.Tagline = reader.ReadNormalizedString(); break; - case "country": { var val = reader.ReadElementContentAsString(); @@ -434,29 +392,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "mpaa": - { - var rating = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(rating)) - { - item.OfficialRating = rating; - } - - break; - } - + item.OfficialRating = reader.ReadNormalizedString(); + break; case "customrating": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.CustomRating = val; - } - - break; - } - + item.CustomRating = reader.ReadNormalizedString(); + break; case "runtime": { var text = reader.ReadElementContentAsString(); @@ -470,18 +410,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "aspectratio": + var aspectRatio = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio) { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val) - && item is IHasAspectRatio hasAspectRatio) - { - hasAspectRatio.AspectRatio = val; - } - - break; + hasAspectRatio.AspectRatio = aspectRatio; } + break; case "lockdata": { var val = reader.ReadElementContentAsString(); @@ -495,17 +430,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "studio": + var studio = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(studio)) { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.AddStudio(val); - } - - break; + item.AddStudio(studio); } + break; case "director": foreach (var director in reader.GetPersonArray(PersonKind.Director)) { @@ -552,31 +483,24 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "trailer": + var trailer = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(trailer)) { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - val = val.Replace("plugin://plugin.video.youtube/?action=play_video&videoid=", BaseNfoSaver.YouTubeWatchUrl, StringComparison.OrdinalIgnoreCase); - - item.AddTrailerUrl(val); - } - - break; + item.AddTrailerUrl(trailer.Replace( + "plugin://plugin.video.youtube/?action=play_video&videoid=", + BaseNfoSaver.YouTubeWatchUrl, + StringComparison.OrdinalIgnoreCase)); } + break; case "displayorder": + var displayOrder = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(displayOrder) && item is IHasDisplayOrder hasDisplayOrder) { - var val = reader.ReadElementContentAsString(); - - if (item is IHasDisplayOrder hasDisplayOrder && !string.IsNullOrWhiteSpace(val)) - { - hasDisplayOrder.DisplayOrder = val; - } - - break; + hasDisplayOrder.DisplayOrder = displayOrder; } + break; case "year": { var val = reader.ReadElementContentAsString(); @@ -656,16 +580,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers case "style": case "tag": + var tag = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(tag)) { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - item.AddTag(val); - } - - break; + item.AddTag(tag); } + break; case "fileinfo": { if (!reader.IsEmptyElement) diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index d2f349ad7a..e7676eb3a1 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Xml; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using Microsoft.Extensions.Logging; @@ -237,17 +238,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "showtitle": - { - var showtitle = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(showtitle)) - { - item.SeriesName = showtitle; - } - - break; - } - + item.SeriesName = reader.ReadNormalizedString(); + break; default: base.FetchDataFromXmlNode(reader, itemResult); break; diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs index ecfed6873c..16ea5e3eae 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs @@ -5,6 +5,7 @@ using System.Xml; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -113,31 +114,23 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "artist": + var artist = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(artist) && item is MusicVideo artistVideo) { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie) - { - var list = movie.Artists.ToList(); - list.Add(val); - movie.Artists = list.ToArray(); - } - - break; + var list = artistVideo.Artists.ToList(); + list.Add(artist); + artistVideo.Artists = list.ToArray(); } + break; case "album": + var album = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(album) && item is MusicVideo albumVideo) { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie) - { - movie.Album = val; - } - - break; + albumVideo.Album = album; } + break; default: base.FetchDataFromXmlNode(reader, itemResult); break; diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs index 51d5f932bc..486a2588a1 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Xml; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using Microsoft.Extensions.Logging; @@ -56,17 +57,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "seasonname": - { - var name = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(name)) - { - item.Name = name; - } - - break; - } - + item.Name = reader.ReadNormalizedString(); + break; default: base.FetchDataFromXmlNode(reader, itemResult); break; diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs index f22b861eba..dbcfe7997a 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs @@ -1,9 +1,9 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.Xml; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -76,23 +76,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "airs_dayofweek": - { - item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString()); - break; - } - + item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString()); + break; case "airs_time": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.AirTime = val; - } - - break; - } - + item.AirTime = reader.ReadNormalizedString(); + break; case "status": { var status = reader.ReadElementContentAsString(); From 0e51ffa16983e6133d6b6ea295d84b0121241d79 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 6 Oct 2023 15:35:26 -0400 Subject: [PATCH 62/75] Add TryReadInt to XmlReaderExtensions --- .../Extensions/XmlReaderExtensions.cs | 13 ++ .../Parsers/BaseItemXmlParser.cs | 23 +--- .../Parsers/BaseNfoParser.cs | 41 ++---- .../Parsers/EpisodeNfoParser.cs | 120 +++--------------- .../Parsers/SeasonNfoParser.cs | 15 +-- 5 files changed, 54 insertions(+), 158 deletions(-) diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index 661133b771..1b3fa9d239 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -26,6 +26,19 @@ public static class XmlReaderExtensions return reader.ReadElementContentAsString().Trim(); } + /// + /// Reads an int from the current node. + /// + /// The . + /// The parsed int. + /// A value indicating whether the parsing succeeded. + public static bool TryReadInt(this XmlReader reader, out int value) + { + ArgumentNullException.ThrowIfNull(reader); + + return int.TryParse(reader.ReadElementContentAsString(), CultureInfo.InvariantCulture, out value); + } + /// /// Parses a from the current node. /// diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index d115bc2496..0181c864d5 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -234,20 +234,16 @@ namespace MediaBrowser.LocalMetadata.Parsers item.CustomRating = reader.ReadNormalizedString(); break; case "RunningTime": - { - var text = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(text)) + var runtimeText = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(runtimeText)) { - if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) + if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) { item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; } } break; - } - case "AspectRatio": var aspectRatio = reader.ReadNormalizedString(); if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio) @@ -256,7 +252,6 @@ namespace MediaBrowser.LocalMetadata.Parsers } break; - case "LockData": { var val = reader.ReadElementContentAsString(); @@ -336,20 +331,12 @@ namespace MediaBrowser.LocalMetadata.Parsers } case "ProductionYear": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) + if (reader.TryReadInt(out var productionYear) && productionYear > 1850) { - if (int.TryParse(val, out var productionYear) && productionYear > 1850) - { - item.ProductionYear = productionYear; - } + item.ProductionYear = productionYear; } break; - } - case "Rating": case "IMDBrating": { diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index af405d7559..eb7c021426 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -325,20 +325,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "playcount": + if (reader.TryReadInt(out var count) + && Guid.TryParse(nfoConfiguration.UserId, out var playCountUserId)) { - var val = reader.ReadElementContentAsString(); - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count) - && Guid.TryParse(nfoConfiguration.UserId, out var playCountUserId)) - { - var user = _userManager.GetUserById(playCountUserId); - userData = _userDataManager.GetUserData(user, item); - userData.PlayCount = count; - _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); - } - - break; + var user = _userManager.GetUserById(playCountUserId); + userData = _userDataManager.GetUserData(user, item); + userData.PlayCount = count; + _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); } + break; case "lastplayed": if (reader.TryReadDateTime(Logger, out var lastPlayed) && Guid.TryParse(nfoConfiguration.UserId, out var lastPlayedUserId)) @@ -398,17 +394,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers item.CustomRating = reader.ReadNormalizedString(); break; case "runtime": + var runtimeText = reader.ReadElementContentAsString(); + if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) { - var text = reader.ReadElementContentAsString(); - - if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) - { - item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; - } - - break; + item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; } + break; case "aspectratio": var aspectRatio = reader.ReadNormalizedString(); if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio) @@ -502,17 +494,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "year": + if (reader.TryReadInt(out var productionYear) && productionYear > 1850) { - var val = reader.ReadElementContentAsString(); - - if (int.TryParse(val, out var productionYear) && productionYear > 1850) - { - item.ProductionYear = productionYear; - } - - break; + item.ProductionYear = productionYear; } + break; case "rating": { var rating = reader.ReadElementContentAsString(); diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index e7676eb3a1..79966f8b80 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using System.IO; using System.Threading; using System.Xml; @@ -113,130 +112,49 @@ namespace MediaBrowser.XbmcMetadata.Parsers switch (reader.Name) { case "season": + if (reader.TryReadInt(out var seasonNumber)) { - var number = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(number)) - { - if (int.TryParse(number, out var num)) - { - item.ParentIndexNumber = num; - } - } - - break; + item.ParentIndexNumber = seasonNumber; } + break; case "episode": + if (reader.TryReadInt(out var episodeNumber)) { - var number = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(number)) - { - if (int.TryParse(number, out var num)) - { - item.IndexNumber = num; - } - } - - break; + item.IndexNumber = episodeNumber; } + break; case "episodenumberend": + if (reader.TryReadInt(out var episodeNumberEnd)) { - var number = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(number)) - { - if (int.TryParse(number, out var num)) - { - item.IndexNumberEnd = num; - } - } - - break; + item.IndexNumberEnd = episodeNumberEnd; } + break; case "airsbefore_episode": + case "displayepisode": + if (reader.TryReadInt(out var airsBeforeEpisode)) { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be problematic, force us culture - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval)) - { - item.AirsBeforeEpisodeNumber = rval; - } - } - - break; + item.AirsBeforeEpisodeNumber = airsBeforeEpisode; } + break; case "airsafter_season": + if (reader.TryReadInt(out var airsAfterSeason)) { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be problematic, force us culture - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval)) - { - item.AirsAfterSeasonNumber = rval; - } - } - - break; + item.AirsAfterSeasonNumber = airsAfterSeason; } + break; case "airsbefore_season": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be problematic, force us culture - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval)) - { - item.AirsBeforeSeasonNumber = rval; - } - } - - break; - } - case "displayseason": + if (reader.TryReadInt(out var airsBeforeSeason)) { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be problematic, force us culture - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval)) - { - item.AirsBeforeSeasonNumber = rval; - } - } - - break; - } - - case "displayepisode": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be problematic, force us culture - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval)) - { - item.AirsBeforeEpisodeNumber = rval; - } - } - - break; + item.AirsBeforeSeasonNumber = airsBeforeSeason; } + break; case "showtitle": item.SeriesName = reader.ReadNormalizedString(); break; diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs index 486a2588a1..e13f0d9976 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs @@ -1,4 +1,3 @@ -using System.Globalization; using System.Xml; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; @@ -42,20 +41,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers switch (reader.Name) { case "seasonnumber": + if (reader.TryReadInt(out var seasonNumber)) { - var number = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(number)) - { - if (int.TryParse(number, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num)) - { - item.IndexNumber = num; - } - } - - break; + item.IndexNumber = seasonNumber; } + break; case "seasonname": item.Name = reader.ReadNormalizedString(); break; From 1d0ecd3188adbe2e7b3eb0b74a9d786b3e1b86dc Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 6 Oct 2023 16:18:33 -0400 Subject: [PATCH 63/75] More miscellaneous cleanup --- .../Parsers/BaseItemXmlParser.cs | 14 +- .../Parsers/BaseNfoParser.cs | 424 +++++++----------- 2 files changed, 159 insertions(+), 279 deletions(-) diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index 0181c864d5..f980c6c4aa 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -253,17 +253,8 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "LockData": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.IsLocked = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - + item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); break; - } - case "Network": foreach (var name in reader.GetStringArray()) { @@ -857,11 +848,8 @@ namespace MediaBrowser.LocalMetadata.Parsers item.UserId = reader.ReadNormalizedString(); break; case "CanEdit": - { item.CanEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); break; - } - default: reader.Skip(); break; diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index eb7c021426..22210d8697 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -262,9 +262,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers protected virtual void FetchDataFromXmlNode(XmlReader reader, MetadataResult itemResult) { var item = itemResult.Item; - var nfoConfiguration = _config.GetNfoConfiguration(); - UserItemData? userData = null; + UserItemData? userData; switch (reader.Name) { @@ -287,17 +286,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers item.SortName = reader.ReadNormalizedString(); break; case "criticrating": + var criticRatingText = reader.ReadElementContentAsString(); + if (float.TryParse(criticRatingText, CultureInfo.InvariantCulture, out var value)) { - var text = reader.ReadElementContentAsString(); - - if (float.TryParse(text, CultureInfo.InvariantCulture, out var value)) - { - item.CriticRating = value; - } - - break; + item.CriticRating = value; } + break; case "sorttitle": item.ForcedSortName = reader.ReadNormalizedString(); break; @@ -310,20 +305,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers item.PreferredMetadataLanguage = reader.ReadNormalizedString(); break; case "watched": + var played = reader.ReadElementContentAsBoolean(); + if (!string.IsNullOrWhiteSpace(nfoConfiguration.UserId)) { - var val = reader.ReadElementContentAsBoolean(); - - if (!string.IsNullOrWhiteSpace(nfoConfiguration.UserId)) - { - var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId)); - userData = _userDataManager.GetUserData(user, item); - userData.Played = val; - _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); - } - - break; + var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId)); + userData = _userDataManager.GetUserData(user, item); + userData.Played = played; + _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); } + break; case "playcount": if (reader.TryReadInt(out var count) && Guid.TryParse(nfoConfiguration.UserId, out var playCountUserId)) @@ -410,17 +401,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "lockdata": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.IsLocked = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - - break; - } - + item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); + break; case "studio": var studio = reader.ReadNormalizedString(); if (!string.IsNullOrEmpty(studio)) @@ -501,33 +483,17 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "rating": + var rating = reader.ReadElementContentAsString().Replace(',', '.'); + // All external meta is saving this as '.' for decimal I believe...but just to be sure + if (float.TryParse(rating, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var communityRating)) { - var rating = reader.ReadElementContentAsString(); - - // All external meta is saving this as '.' for decimal I believe...but just to be sure - if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val)) - { - item.CommunityRating = val; - } - - break; + item.CommunityRating = communityRating; } + break; case "ratings": - { - if (!reader.IsEmptyElement) - { - using var subtree = reader.ReadSubtree(); - FetchFromRatingsNode(subtree, item); - } - else - { - reader.Read(); - } - - break; - } - + FetchFromRatingsNode(reader, item); + break; case "aired": case "formed": case "premiered": @@ -575,46 +541,26 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "fileinfo": - { - if (!reader.IsEmptyElement) - { - using (var subtree = reader.ReadSubtree()) - { - FetchFromFileInfoNode(subtree, item); - } - } - else - { - reader.Read(); - } - - break; - } - + FetchFromFileInfoNode(reader, item); + break; case "uniqueid": + if (reader.IsEmptyElement) { - if (reader.IsEmptyElement) - { - reader.Read(); - break; - } - - var provider = reader.GetAttribute("type"); - var id = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(provider) && !string.IsNullOrWhiteSpace(id)) - { - item.SetProviderId(provider, id); - } - + reader.Read(); break; } - case "thumb": + var provider = reader.GetAttribute("type"); + var providerId = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(provider) && !string.IsNullOrWhiteSpace(providerId)) { - FetchThumbNode(reader, itemResult, "thumb"); - break; + item.SetProviderId(provider, providerId); } + break; + case "thumb": + FetchThumbNode(reader, itemResult, "thumb"); + break; case "fanart": { if (reader.IsEmptyElement) @@ -719,242 +665,188 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } - private void FetchFromFileInfoNode(XmlReader reader, T item) + private void FetchFromFileInfoNode(XmlReader parentReader, T item) { + if (parentReader.IsEmptyElement) + { + parentReader.Read(); + return; + } + + using var reader = parentReader.ReadSubtree(); reader.MoveToContent(); reader.Read(); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) { - if (reader.NodeType == XmlNodeType.Element) + if (reader.NodeType != XmlNodeType.Element) { - switch (reader.Name) - { - case "streamdetails": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - using (var subtree = reader.ReadSubtree()) - { - FetchFromStreamDetailsNode(subtree, item); - } - - break; - } - - default: - reader.Skip(); - break; - } + reader.Read(); + continue; } - else + + switch (reader.Name) { - reader.Read(); + case "streamdetails": + FetchFromStreamDetailsNode(reader, item); + break; + default: + reader.Skip(); + break; } } } - private void FetchFromStreamDetailsNode(XmlReader reader, T item) + private void FetchFromStreamDetailsNode(XmlReader parentReader, T item) { + if (parentReader.IsEmptyElement) + { + parentReader.Read(); + return; + } + + using var reader = parentReader.ReadSubtree(); reader.MoveToContent(); reader.Read(); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) { - if (reader.NodeType == XmlNodeType.Element) + if (reader.NodeType != XmlNodeType.Element) { - switch (reader.Name) - { - case "video": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - using (var subtree = reader.ReadSubtree()) - { - FetchFromVideoNode(subtree, item); - } - - break; - } - - case "subtitle": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - using (var subtree = reader.ReadSubtree()) - { - FetchFromSubtitleNode(subtree, item); - } - - break; - } - - default: - reader.Skip(); - break; - } + reader.Read(); + continue; } - else + + switch (reader.Name) { - reader.Read(); + case "video": + FetchFromVideoNode(reader, item); + break; + case "subtitle": + FetchFromSubtitleNode(reader, item); + break; + default: + reader.Skip(); + break; } } } - private void FetchFromVideoNode(XmlReader reader, T item) + private void FetchFromVideoNode(XmlReader parentReader, T item) { + if (parentReader.IsEmptyElement) + { + parentReader.Read(); + return; + } + + using var reader = parentReader.ReadSubtree(); reader.MoveToContent(); reader.Read(); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) { - if (reader.NodeType == XmlNodeType.Element) + if (reader.NodeType != XmlNodeType.Element || item is not Video video) { - switch (reader.Name) - { - case "format3d": - { - var val = reader.ReadElementContentAsString(); - - var video = item as Video; - - if (video is not null) - { - if (string.Equals("HSBS", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.HalfSideBySide; - } - else if (string.Equals("HTAB", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.HalfTopAndBottom; - } - else if (string.Equals("FTAB", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.FullTopAndBottom; - } - else if (string.Equals("FSBS", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.FullSideBySide; - } - else if (string.Equals("MVC", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.MVC; - } - } - - break; - } - - case "aspect": - { - var val = reader.ReadElementContentAsString(); - - if (item is Video video) - { - video.AspectRatio = val; - } - - break; - } - - case "width": - { - var val = reader.ReadElementContentAsInt(); - - if (item is Video video) - { - video.Width = val; - } - - break; - } - - case "height": - { - var val = reader.ReadElementContentAsInt(); - - if (item is Video video) - { - video.Height = val; - } - - break; - } - - case "durationinseconds": - { - var val = reader.ReadElementContentAsInt(); - - if (item is Video video) - { - video.RunTimeTicks = new TimeSpan(0, 0, val).Ticks; - } - - break; - } - - default: - reader.Skip(); - break; - } + reader.Read(); + continue; } - else + + switch (reader.Name) { - reader.Read(); + case "format3d": + var format = reader.ReadElementContentAsString(); + if (string.Equals("HSBS", format, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.HalfSideBySide; + } + else if (string.Equals("HTAB", format, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.HalfTopAndBottom; + } + else if (string.Equals("FTAB", format, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.FullTopAndBottom; + } + else if (string.Equals("FSBS", format, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.FullSideBySide; + } + else if (string.Equals("MVC", format, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.MVC; + } + + break; + case "aspect": + video.AspectRatio = reader.ReadNormalizedString(); + break; + case "width": + video.Width = reader.ReadElementContentAsInt(); + break; + case "height": + video.Height = reader.ReadElementContentAsInt(); + break; + case "durationinseconds": + video.RunTimeTicks = new TimeSpan(0, 0, reader.ReadElementContentAsInt()).Ticks; + break; + default: + reader.Skip(); + break; } } } - private void FetchFromSubtitleNode(XmlReader reader, T item) + private void FetchFromSubtitleNode(XmlReader parentReader, T item) { + if (parentReader.IsEmptyElement) + { + parentReader.Read(); + return; + } + + using var reader = parentReader.ReadSubtree(); reader.MoveToContent(); reader.Read(); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) { - if (reader.NodeType == XmlNodeType.Element) + if (reader.NodeType != XmlNodeType.Element) { - switch (reader.Name) - { - case "language": - _ = reader.ReadElementContentAsString(); - if (item is Video video) - { - video.HasSubtitles = true; - } - - break; - - default: - reader.Skip(); - break; - } + reader.Read(); + continue; } - else + + switch (reader.Name) { - reader.Read(); + case "language": + _ = reader.ReadElementContentAsString(); + if (item is Video video) + { + video.HasSubtitles = true; + } + + break; + default: + reader.Skip(); + break; } } } - private void FetchFromRatingsNode(XmlReader reader, T item) + private void FetchFromRatingsNode(XmlReader parentReader, T item) { + if (parentReader.IsEmptyElement) + { + parentReader.Read(); + return; + } + + using var reader = parentReader.ReadSubtree(); reader.MoveToContent(); reader.Read(); From 1dd6442e89ee93127b475e820cca64c804f178ea Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 6 Oct 2023 16:43:50 -0400 Subject: [PATCH 64/75] Use extension methods in GetPersonFromXmlNode --- .../Extensions/XmlReaderExtensions.cs | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index 1b3fa9d239..6be760b2fa 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -122,16 +122,11 @@ public static class XmlReaderExtensions { case "name": case "Name": - name = subtree.ReadElementContentAsString(); + name = subtree.ReadNormalizedString(); break; case "role": case "Role": - var roleValue = subtree.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(roleValue)) - { - role = roleValue; - } - + role = subtree.ReadNormalizedString(); break; case "type": case "Type": @@ -140,23 +135,14 @@ public static class XmlReaderExtensions case "order": case "sortorder": case "SortOrder": - if (int.TryParse( - subtree.ReadElementContentAsString(), - NumberStyles.Integer, - CultureInfo.InvariantCulture, - out var intVal)) + if (subtree.TryReadInt(out var sortOrderVal)) { - sortOrder = intVal; + sortOrder = sortOrderVal; } break; case "thumb": - var thumb = subtree.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(thumb)) - { - imageUrl = thumb; - } - + imageUrl = subtree.ReadNormalizedString(); break; default: subtree.Skip(); From 40e1c5f4c6901469c1c7f3f763f83d1b38e5e979 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 6 Oct 2023 16:56:50 -0400 Subject: [PATCH 65/75] Remove logger parameter from XmlReaderExtensions.TryReadDateTime --- .../Extensions/XmlReaderExtensions.cs | 22 +++++-------------- .../Parsers/BaseItemXmlParser.cs | 2 +- .../Parsers/BaseNfoParser.cs | 4 ++-- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index 6be760b2fa..aa097714a5 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Xml; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Extensions; @@ -43,26 +42,17 @@ public static class XmlReaderExtensions /// Parses a from the current node. /// /// The . - /// The to use on failure. /// The parsed . /// A value indicating whether the parsing succeeded. - public static bool TryReadDateTime(this XmlReader reader, ILogger logger, out DateTime value) + public static bool TryReadDateTime(this XmlReader reader, out DateTime value) { ArgumentNullException.ThrowIfNull(reader); - ArgumentNullException.ThrowIfNull(logger); - - var text = reader.ReadElementContentAsString(); - if (DateTime.TryParse( - text, - CultureInfo.InvariantCulture, - DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, - out value)) - { - return true; - } - logger.LogWarning("Invalid date: {Date}", text); - return false; + return DateTime.TryParse( + reader.ReadElementContentAsString(), + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out value); } /// diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index f980c6c4aa..8a870e0d9b 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -130,7 +130,7 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Added": - if (reader.TryReadDateTime(Logger, out var dateCreated)) + if (reader.TryReadDateTime(out var dateCreated)) { item.DateCreated = dateCreated; } diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 22210d8697..47a127950d 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -268,7 +268,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers switch (reader.Name) { case "dateadded": - if (reader.TryReadDateTime(Logger, out var dateCreated)) + if (reader.TryReadDateTime(out var dateCreated)) { item.DateCreated = dateCreated; } @@ -327,7 +327,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "lastplayed": - if (reader.TryReadDateTime(Logger, out var lastPlayed) + if (reader.TryReadDateTime(out var lastPlayed) && Guid.TryParse(nfoConfiguration.UserId, out var lastPlayedUserId)) { var user = _userManager.GetUserById(lastPlayedUserId); From c38fbece0328e26d9d1c7a6772cba87081c122eb Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 6 Oct 2023 16:57:36 -0400 Subject: [PATCH 66/75] Remove unnecessary Trim() from GetPersonFromXmlNode --- MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index aa097714a5..2742f21e36 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -147,7 +147,7 @@ public static class XmlReaderExtensions return new PersonInfo { - Name = name.Trim(), + Name = name, Role = role, Type = type, SortOrder = sortOrder, From 7fc804e4b85a851a6b78c08602b1e1f3fb35b93f Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@users.noreply.github.com> Date: Sun, 8 Oct 2023 14:05:55 +0200 Subject: [PATCH 67/75] Add full version tag for renovate (#10370) --- .github/workflows/repo-stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index 4eb0cf0996..2b11641166 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8 + - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} days-before-stale: 120 From c707baed8366cd44ea80713cf93e5f92971815bf Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 5 Oct 2023 23:57:11 +0200 Subject: [PATCH 68/75] Jellyfin.Drawing minor improvements Reduce duplicate/dead code --- .../Drawing/IImageProcessor.cs | 11 ---- .../Drawing/ImageProcessingOptions.cs | 3 +- .../Encoder/MediaEncoder.cs | 10 +--- .../Drawing/ImageFormatExtensions.cs | 17 ++++++ .../MediaInfo/EmbeddedImageProvider.cs | 6 -- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 14 +---- src/Jellyfin.Drawing/ImageProcessor.cs | 56 +------------------ .../Drawing/ImageFormatExtensionsTests.cs | 13 +++++ 8 files changed, 37 insertions(+), 93 deletions(-) diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index cdc3d52b9b..0d1e2a5a07 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; @@ -70,14 +69,6 @@ namespace MediaBrowser.Controller.Drawing string? GetImageCacheTag(User user); - /// - /// Processes the image. - /// - /// The options. - /// To stream. - /// Task. - Task ProcessImage(ImageProcessingOptions options, Stream toStream); - /// /// Processes the image. /// @@ -97,7 +88,5 @@ namespace MediaBrowser.Controller.Drawing /// The options. /// The library name to draw onto the collage. void CreateImageCollage(ImageCollageOptions options, string? libraryName); - - bool SupportsTransparency(string path); } } diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs index 7912c5e87e..953cfe698e 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -119,7 +119,8 @@ namespace MediaBrowser.Controller.Drawing private bool IsFormatSupported(string originalImagePath) { var ext = Path.GetExtension(originalImagePath); - return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, "." + outputFormat, StringComparison.OrdinalIgnoreCase)); + ext = ext.Replace(".jpeg", ".jpg", StringComparison.OrdinalIgnoreCase); + return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, outputFormat.GetExtension(), StringComparison.OrdinalIgnoreCase)); } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4bff196658..26f47a18f6 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -650,15 +650,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { ArgumentException.ThrowIfNullOrEmpty(inputPath); - var outputExtension = targetFormat switch - { - ImageFormat.Bmp => ".bmp", - ImageFormat.Gif => ".gif", - ImageFormat.Jpg => ".jpg", - ImageFormat.Png => ".png", - ImageFormat.Webp => ".webp", - _ => ".jpg" - }; + var outputExtension = targetFormat?.GetExtension() ?? ".jpg"; var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension); Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); diff --git a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs index 68a5c25345..1bb24112ec 100644 --- a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs +++ b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs @@ -24,4 +24,21 @@ public static class ImageFormatExtensions ImageFormat.Webp => "image/webp", _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat)) }; + + /// + /// Returns the correct extension for this . + /// + /// This . + /// The is an invalid enumeration value. + /// The correct extension for this . + public static string GetExtension(this ImageFormat format) + => format switch + { + ImageFormat.Bmp => ".bmp", + ImageFormat.Gif => ".gif", + ImageFormat.Jpg => ".jpg", + ImageFormat.Png => ".png", + ImageFormat.Webp => ".webp", + _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat)) + }; } diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs index c24f4e2fcc..0bfee07fd6 100644 --- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs @@ -204,16 +204,10 @@ namespace MediaBrowser.Providers.MediaInfo ? Path.GetExtension(attachmentStream.FileName) : MimeTypes.ToExtension(attachmentStream.MimeType); - if (string.IsNullOrEmpty(extension)) - { - extension = ".jpg"; - } - ImageFormat format = extension switch { ".bmp" => ImageFormat.Bmp, ".gif" => ImageFormat.Gif, - ".jpg" => ImageFormat.Jpg, ".png" => ImageFormat.Png, ".webp" => ImageFormat.Webp, _ => ImageFormat.Jpg diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 416f602179..03f90da8eb 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -200,20 +200,10 @@ public class SkiaEncoder : IImageEncoder { if (!orientation.HasValue) { - return SKEncodedOrigin.TopLeft; + return SKEncodedOrigin.Default; } - return orientation.Value switch - { - ImageOrientation.TopRight => SKEncodedOrigin.TopRight, - ImageOrientation.RightTop => SKEncodedOrigin.RightTop, - ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom, - ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop, - ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom, - ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight, - ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft, - _ => SKEncodedOrigin.TopLeft - }; + return (SKEncodedOrigin)orientation.Value; } /// diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index 4f16e294bc..65a8f4e832 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -107,22 +107,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable /// public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation; - /// - public async Task ProcessImage(ImageProcessingOptions options, Stream toStream) - { - var file = await ProcessImage(options).ConfigureAwait(false); - using var fileStream = AsyncFile.OpenRead(file.Path); - await fileStream.CopyToAsync(toStream).ConfigureAwait(false); - } - /// public IReadOnlyCollection GetSupportedImageOutputFormats() => _imageEncoder.SupportedOutputFormats; - /// - public bool SupportsTransparency(string path) - => _transparentImageTypes.Contains(Path.GetExtension(path)); - /// public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options) { @@ -224,7 +212,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable } } - return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); + return (cacheFilePath, outputFormat.GetMimeType(), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); } catch (Exception ex) { @@ -262,17 +250,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable return ImageFormat.Jpg; } - private string GetMimeType(ImageFormat format, string path) - => format switch - { - ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"), - ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"), - ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"), - ImageFormat.Png => MimeTypes.GetMimeType("i.png"), - ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"), - _ => MimeTypes.GetMimeType(path) - }; - /// /// Gets the cache file path based on a set of parameters. /// @@ -374,7 +351,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable filename.Append(",v="); filename.Append(Version); - return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant()); + return GetCachePath(ResizedImageCachePath, filename.ToString(), format.GetExtension()); } /// @@ -471,35 +448,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable return Task.FromResult((originalImagePath, dateModified)); } - // TODO _mediaEncoder.ConvertImage is not implemented - // if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat)) - // { - // try - // { - // string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); - // - // string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png"; - // var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension); - // - // var file = _fileSystem.GetFileInfo(outputPath); - // if (!file.Exists) - // { - // await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false); - // dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath); - // } - // else - // { - // dateModified = file.LastWriteTimeUtc; - // } - // - // originalImagePath = outputPath; - // } - // catch (Exception ex) - // { - // _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath); - // } - // } - return Task.FromResult((originalImagePath, dateModified)); } diff --git a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs index a5bdb42d89..198ad5a274 100644 --- a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs +++ b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs @@ -30,4 +30,17 @@ public static class ImageFormatExtensionsTests [InlineData((ImageFormat)5)] public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format) => Assert.Throws(() => format.GetMimeType()); + + [Theory] + [MemberData(nameof(GetAllImageFormats))] + public static void GetExtension_Valid_Valid(ImageFormat format) + => Assert.Null(Record.Exception(() => format.GetExtension())); + + [Theory] + [InlineData((ImageFormat)int.MinValue)] + [InlineData((ImageFormat)int.MaxValue)] + [InlineData((ImageFormat)(-1))] + [InlineData((ImageFormat)5)] + public static void GetExtension_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format) + => Assert.Throws(() => format.GetExtension()); } From 8466e53b8f0e885e75e3f3f5e9c81fceb0a61379 Mon Sep 17 00:00:00 2001 From: tellmeY18 Date: Sat, 7 Oct 2023 21:49:16 +0000 Subject: [PATCH 69/75] Translated using Weblate (Malayalam) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ml/ --- Emby.Server.Implementations/Localization/Core/ml.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json index 0620fbcdb0..0b50fa5298 100644 --- a/Emby.Server.Implementations/Localization/Core/ml.json +++ b/Emby.Server.Implementations/Localization/Core/ml.json @@ -121,5 +121,7 @@ "TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.", "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക", "HearingImpaired": "കേൾവി തകരാറുകൾ", - "External": "പുറമേയുള്ള" + "External": "പുറമേയുള്ള", + "TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്‌ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്‌സ്‌ട്രാക്‌റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.", + "TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ" } From 75ee990e1d91d527974054e4130d9d27d0f7f2d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 08:10:21 -0600 Subject: [PATCH 70/75] Update github/codeql-action action to v2.22.1 (#10376) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8e764b0194..4c9d68d112 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0 + uses: github/codeql-action/init@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0 + uses: github/codeql-action/autobuild@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0 + uses: github/codeql-action/analyze@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 From aa073748c00fe2b0508fde88d900143acec09e36 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Mon, 9 Oct 2023 23:12:41 +0800 Subject: [PATCH 71/75] Drop experimental status of flac-in-MP4 for FFmpeg 6+ Signed-off-by: nyanmisaka --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 760fefbf51..44afdb6b11 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -45,6 +45,8 @@ public class DynamicHlsController : BaseJellyfinApiController private const string DefaultEventEncoderPreset = "superfast"; private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls; + private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0); + private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IDlnaManager _dlnaManager; @@ -1705,13 +1707,14 @@ public class DynamicHlsController : BaseJellyfinApiController var audioCodec = _encodingHelper.GetAudioEncoder(state); var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); - // dts, flac, opus and truehd are experimental in mp4 muxer + // opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer var strictArgs = string.Empty; var actualOutputAudioCodec = state.ActualOutputAudioCodec; - if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) - || string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase) + if (string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase) || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase) - || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)) + || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase) + || (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) + && _mediaEncoder.EncoderVersion < _minFFmpegFlacInMp4)) { strictArgs = " -strict -2"; } From 4757ce105bedb075c6663d92bc1e93f87c4779f2 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 10 Oct 2023 00:18:50 +0200 Subject: [PATCH 72/75] Use Process.WaitForExitAsync added in .NET 5 --- .../Extensions/ProcessExtensions.cs | 60 ++----------------- .../Attachments/AttachmentExtractor.cs | 44 +++++--------- .../Encoder/MediaEncoder.cs | 22 ++++--- .../Subtitles/SubtitleEncoder.cs | 46 +++++--------- 4 files changed, 46 insertions(+), 126 deletions(-) diff --git a/MediaBrowser.Common/Extensions/ProcessExtensions.cs b/MediaBrowser.Common/Extensions/ProcessExtensions.cs index c3a7cb394e..bb8ab130df 100644 --- a/MediaBrowser.Common/Extensions/ProcessExtensions.cs +++ b/MediaBrowser.Common/Extensions/ProcessExtensions.cs @@ -15,65 +15,13 @@ namespace MediaBrowser.Common.Extensions /// /// The process to wait for. /// The duration to wait before cancelling waiting for the task. - /// True if the task exited normally, false if the timeout elapsed before the process exited. - /// If is not set to true for the process. - public static async Task WaitForExitAsync(this Process process, TimeSpan timeout) + /// A task that will complete when the process has exited, cancellation has been requested, or an error occurs. + /// The timeout ended. + public static async Task WaitForExitAsync(this Process process, TimeSpan timeout) { using (var cancelTokenSource = new CancellationTokenSource(timeout)) { - return await WaitForExitAsync(process, cancelTokenSource.Token).ConfigureAwait(false); - } - } - - /// - /// Asynchronously wait for the process to exit. - /// - /// The process to wait for. - /// A to observe while waiting for the process to exit. - /// True if the task exited normally, false if cancelled before the process exited. - public static async Task WaitForExitAsync(this Process process, CancellationToken cancelToken) - { - if (!process.EnableRaisingEvents) - { - throw new InvalidOperationException("EnableRisingEvents must be enabled to async wait for a task to exit."); - } - - // Add an event handler for the process exit event - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - process.Exited += (_, _) => tcs.TrySetResult(true); - - // Return immediately if the process has already exited - if (process.HasExitedSafe()) - { - return true; - } - - // Register with the cancellation token then await - using (var cancelRegistration = cancelToken.Register(() => tcs.TrySetResult(process.HasExitedSafe()))) - { - return await tcs.Task.ConfigureAwait(false); - } - } - - /// - /// Gets a value indicating whether the associated process has been terminated using - /// . This is safe to call even if there is no operating system process - /// associated with the . - /// - /// The process to check the exit status for. - /// - /// True if the operating system process referenced by the component has - /// terminated, or if there is no associated operating system process; otherwise, false. - /// - private static bool HasExitedSafe(this Process process) - { - try - { - return process.HasExited; - } - catch (InvalidOperationException) - { - return true; + await process.WaitForExitAsync(cancelTokenSource.Token).ConfigureAwait(false); } } } diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 0ec0c84d41..299f294b29 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -174,22 +174,16 @@ namespace MediaBrowser.MediaEncoding.Attachments process.Start(); - var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false); - - if (!ranToCompletion) + try { - try - { - _logger.LogWarning("Killing ffmpeg attachment extraction process"); - process.Kill(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error killing attachment extraction process"); - } + await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + exitCode = process.ExitCode; + } + catch (OperationCanceledException) + { + process.Kill(true); + exitCode = -1; } - - exitCode = ranToCompletion ? process.ExitCode : -1; } var failed = false; @@ -322,22 +316,16 @@ namespace MediaBrowser.MediaEncoding.Attachments process.Start(); - var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false); - - if (!ranToCompletion) + try { - try - { - _logger.LogWarning("Killing ffmpeg attachment extraction process"); - process.Kill(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error killing attachment extraction process"); - } + await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + exitCode = process.ExitCode; + } + catch (OperationCanceledException) + { + process.Kill(true); + exitCode = -1; } - - exitCode = ranToCompletion ? process.ExitCode : -1; } var failed = false; diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4bff196658..0eaf9748f6 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -760,11 +760,15 @@ namespace MediaBrowser.MediaEncoding.Encoder timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTimeout; } - ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false); - - if (!ranToCompletion) + try + { + await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false); + ranToCompletion = true; + } + catch (OperationCanceledException) { - StopProcess(processWrapper, 1000); + process.Kill(true); + ranToCompletion = false; } } finally @@ -999,7 +1003,7 @@ namespace MediaBrowser.MediaEncoding.Encoder return true; } - private class ProcessWrapper : IDisposable + private sealed class ProcessWrapper : IDisposable { private readonly MediaEncoder _mediaEncoder; @@ -1042,13 +1046,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _mediaEncoder._runningProcesses.Remove(this); } - try - { - process.Dispose(); - } - catch - { - } + process.Dispose(); } public void Dispose() diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index a41e0b7e98..21fa4468ed 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -420,23 +420,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles throw; } - var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); - - if (!ranToCompletion) + try { - try - { - _logger.LogInformation("Killing ffmpeg subtitle conversion process"); - - process.Kill(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error killing subtitle conversion process"); - } + await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); + exitCode = process.ExitCode; + } + catch (OperationCanceledException) + { + process.Kill(true); + exitCode = -1; } - - exitCode = ranToCompletion ? process.ExitCode : -1; } var failed = false; @@ -574,23 +567,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles throw; } - var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); - - if (!ranToCompletion) + try { - try - { - _logger.LogWarning("Killing ffmpeg subtitle extraction process"); - - process.Kill(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error killing subtitle extraction process"); - } + await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); + exitCode = process.ExitCode; + } + catch (OperationCanceledException) + { + process.Kill(true); + exitCode = -1; } - - exitCode = ranToCompletion ? process.ExitCode : -1; } var failed = false; From 305405c9a1ac4a37ac9e8eda44899c33cf76eda3 Mon Sep 17 00:00:00 2001 From: scampower3 <81431263+scampower3@users.noreply.github.com> Date: Tue, 10 Oct 2023 19:12:09 +0800 Subject: [PATCH 73/75] Combine Title and Overview for multi-episodes files for NFO file (#10080) --- .../Parsers/EpisodeNfoParser.cs | 41 ++++++++++++++++++- .../Parsers/EpisodeNfoProviderTests.cs | 6 +-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index d2f349ad7a..432b89c313 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.IO; +using System.Text; using System.Threading; using System.Xml; using MediaBrowser.Common.Configuration; @@ -81,7 +82,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers } // Extract the last episode number from nfo + // Retrieves all title and plot tags from the rest of the nfo and concatenates them with the first episode // This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag + var name = new StringBuilder(item.Item.Name); + var overview = new StringBuilder(item.Item.Overview); while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1) { xml = xmlFile.Substring(0, index + srch.Length); @@ -92,12 +96,44 @@ namespace MediaBrowser.XbmcMetadata.Parsers { reader.MoveToContent(); - if (reader.ReadToDescendant("episode") && int.TryParse(reader.ReadElementContentAsString(), out var num)) + while (!reader.EOF && reader.ReadState == ReadState.Interactive) { - item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num); + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "name": + case "title": + case "localtitle": + name.Append(" / ").Append(reader.ReadElementContentAsString()); + break; + case "episode": + { + if (int.TryParse(reader.ReadElementContentAsString(), out var num)) + { + item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num); + } + + break; + } + + case "biography": + case "plot": + case "review": + overview.Append(" / ").Append(reader.ReadElementContentAsString()); + break; + } + } + + reader.Read(); } } } + + item.Item.Name = name.ToString(); + item.Item.Overview = overview.ToString(); } catch (XmlException) { @@ -172,6 +208,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; } + case "displayafterseason": case "airsafter_season": { var val = reader.ReadElementContentAsString(); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs index f63bc0e1bc..c0d06116b5 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Threading; using Jellyfin.Data.Enums; @@ -114,11 +114,11 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers _parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None); var item = result.Item; - Assert.Equal("Rising (1)", item.Name); + Assert.Equal("Rising (1) / Rising (2)", item.Name); Assert.Equal(1, item.IndexNumber); Assert.Equal(2, item.IndexNumberEnd); Assert.Equal(1, item.ParentIndexNumber); - Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.", item.Overview); + Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy. / Sheppard tries to convince Weir to mount a rescue mission to free Colonel Sumner, Teyla, and the others captured by the Wraith.", item.Overview); Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate); Assert.Equal(2004, item.ProductionYear); } From 502c25750670db92f75e2027febd4e5b6cd6c34b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 13:07:13 +0000 Subject: [PATCH 74/75] Update dependency Microsoft.AspNetCore.Authorization to v7.0.12 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8cf3eae2a5..55b03cdcee 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,7 +25,7 @@ - + From 74f61fbd79ef2e2ad4a986f5f886581b2827de07 Mon Sep 17 00:00:00 2001 From: lonebyte <61915324+lonebyte@users.noreply.github.com> Date: Tue, 10 Oct 2023 22:48:52 +0200 Subject: [PATCH 75/75] Fix HLS playback of m4a files with mjpeg stream (#10069) --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 7bf366e5d0..42c94c29d3 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1721,14 +1721,17 @@ public class DynamicHlsController : BaseJellyfinApiController if (!state.IsOutputVideo) { + var audioTranscodeParams = string.Empty; + + // -vn to drop any video streams + audioTranscodeParams += "-vn"; + if (EncodingHelper.IsCopyCodec(audioCodec)) { - return "-acodec copy" + bitStreamArgs + strictArgs; + return audioTranscodeParams + " -acodec copy" + bitStreamArgs + strictArgs; } - var audioTranscodeParams = string.Empty; - - audioTranscodeParams += "-acodec " + audioCodec + bitStreamArgs + strictArgs; + audioTranscodeParams += " -acodec " + audioCodec + bitStreamArgs + strictArgs; var audioBitrate = state.OutputAudioBitrate; var audioChannels = state.OutputAudioChannels; @@ -1756,7 +1759,6 @@ public class DynamicHlsController : BaseJellyfinApiController audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); } - audioTranscodeParams += " -vn"; return audioTranscodeParams; }