diff options
| author | scr4bble <scr4bble@users.noreply.github.com> | 2026-09-27 16:30:36 -0400 |
|---|---|---|
| committer | Cody Robibero <cody@robibe.ro> | 2026-09-27 16:30:36 -0400 |
| commit | 4e73688f7dbefa3e8e9170e8477fbdd6d036101e (patch) | |
| tree | 5f16751402c212159d9262e50e0721cc90d7b81f | |
| parent | 169a3e5d34a26ce7bc6f3d4a4513ab3bdc99e8e6 (diff) | |
Backport pull request #18059 from jellyfin/release-12.z
Fix transcoding settings migration failure caused by null EncoderPreset
Original-merge: 3e27c7ef7f7ec20199bd0493cd8eba42390b22a7
Merged-by: crobibero <cody@robibe.ro>
Backported-by: Cody Robibero <cody@robibe.ro>
4 files changed, 199 insertions, 2 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f5f844bcbc..63ba6bf52d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -242,6 +242,8 @@ - [m0g3r](https://github.com/m0g3r) - [martin-77](https://github.com/martin-77) - [Oggeb1](https://github.com/Oggeb1) + - [scr4bble](https://github.com/scr4bble) + - [Mateus Bandeira](https://github.com/mateusbandeiraa) # Emby Contributors diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs index 0141b43c96..a5006c9bfa 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs @@ -79,8 +79,8 @@ public class MigrateEncodingOptions : IMigrationRoutine tonemappingRange = parsedTonemappingRange; } - var encoderPreset = EncoderPreset.superfast; - if (Enum.TryParse<EncoderPreset>(oldConfig.TonemappingRange, true, out var parsedEncoderPreset)) + var encoderPreset = EncoderPreset.auto; + if (Enum.TryParse<EncoderPreset>(oldConfig.EncoderPreset, true, out var parsedEncoderPreset)) { encoderPreset = parsedEncoderPreset; } diff --git a/Jellyfin.Server/Migrations/Routines/20260915104305_FixNullEncoderPreset.cs b/Jellyfin.Server/Migrations/Routines/20260915104305_FixNullEncoderPreset.cs new file mode 100644 index 0000000000..f8e63f1920 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/20260915104305_FixNullEncoderPreset.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Jellyfin.Server.Migrations.Stages; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines; + +/// <summary> +/// Replaces the nullable encoder preset written by older versions with the default preset. +/// </summary> +[JellyfinMigration("2026-09-15T10:43:05", nameof(FixNullEncoderPreset), Stage = JellyfinMigrationStageTypes.PreInitialisation)] +internal class FixNullEncoderPreset : IAsyncMigrationRoutine +{ + private static readonly XNamespace _xsiNamespace = "http://www.w3.org/2001/XMLSchema-instance"; + + private readonly IApplicationPaths _applicationPaths; + private readonly ILogger<FixNullEncoderPreset> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="FixNullEncoderPreset"/> class. + /// </summary> + /// <param name="applicationPaths">The application paths.</param> + /// <param name="logger">The logger.</param> + public FixNullEncoderPreset(IApplicationPaths applicationPaths, ILogger<FixNullEncoderPreset> logger) + { + _applicationPaths = applicationPaths; + _logger = logger; + } + + /// <inheritdoc /> + public Task PerformAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var path = Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "encoding.xml"); + if (!File.Exists(path)) + { + return Task.CompletedTask; + } + + XDocument document; + try + { + document = XDocument.Load(path, LoadOptions.PreserveWhitespace); + } + catch (XmlException ex) + { + _logger.LogWarning(ex, "Could not parse encoding configuration; skipping null encoder preset migration"); + return Task.CompletedTask; + } + + var encoderPreset = document.Root?.Element(nameof(EncodingOptions.EncoderPreset)); + if (encoderPreset is null) + { + return Task.CompletedTask; + } + + var nilAttribute = encoderPreset.Attribute(_xsiNamespace + "nil"); + if (!string.Equals(nilAttribute?.Value, "true", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(encoderPreset.Value)) + { + return Task.CompletedTask; + } + + nilAttribute?.Remove(); + encoderPreset.Value = nameof(EncoderPreset.auto); + document.Save(path, SaveOptions.DisableFormatting); + _logger.LogInformation("Replaced null encoder preset with auto in encoding configuration"); + return Task.CompletedTask; + } +} diff --git a/tests/Jellyfin.Server.Tests/Migrations/FixNullEncoderPresetTests.cs b/tests/Jellyfin.Server.Tests/Migrations/FixNullEncoderPresetTests.cs new file mode 100644 index 0000000000..a6ebc82425 --- /dev/null +++ b/tests/Jellyfin.Server.Tests/Migrations/FixNullEncoderPresetTests.cs @@ -0,0 +1,118 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using System.Xml.Serialization; +using Emby.Server.Implementations; +using Jellyfin.Server.Migrations.PreStartupRoutines; +using Jellyfin.Server.Migrations.Routines; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Jellyfin.Server.Tests.Migrations; + +public sealed class FixNullEncoderPresetTests : IDisposable +{ + private readonly string _configurationDirectory; + private readonly string _encodingConfigurationPath; + private readonly FixNullEncoderPreset _migration; + + public FixNullEncoderPresetTests() + { + _configurationDirectory = Directory.CreateTempSubdirectory("jellyfin-migration-test-").FullName; + _encodingConfigurationPath = Path.Combine(_configurationDirectory, "encoding.xml"); + + var applicationPaths = new Mock<IApplicationPaths>(); + applicationPaths.SetupGet(paths => paths.ConfigurationDirectoryPath).Returns(_configurationDirectory); + _migration = new FixNullEncoderPreset(applicationPaths.Object, NullLogger<FixNullEncoderPreset>.Instance); + } + + [Fact] + public async Task PerformAsync_ReplacesNullEncoderPresetWithAuto() + { + const string Configuration = """ + <?xml version="1.0" encoding="utf-8"?> + <EncodingOptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <HardwareAccelerationType>qsv</HardwareAccelerationType> + <EncoderPreset xsi:nil="true" /> + <EnableHardwareEncoding>true</EnableHardwareEncoding> + </EncodingOptions> + """; + await File.WriteAllTextAsync(_encodingConfigurationPath, Configuration, TestContext.Current.CancellationToken); + + await _migration.PerformAsync(TestContext.Current.CancellationToken); + + var serializer = new XmlSerializer(typeof(EncodingOptions)); + using var reader = File.OpenRead(_encodingConfigurationPath); + var encodingOptions = Assert.IsType<EncodingOptions>(serializer.Deserialize(reader)); + Assert.Equal(EncoderPreset.auto, encodingOptions.EncoderPreset); + Assert.Equal(HardwareAccelerationType.qsv, encodingOptions.HardwareAccelerationType); + Assert.True(encodingOptions.EnableHardwareEncoding); + } + + [Fact] + public async Task PerformAsync_LeavesValidEncoderPresetUnchanged() + { + const string Configuration = """ + <?xml version="1.0" encoding="utf-8"?> + <EncodingOptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <EncoderPreset>fast</EncoderPreset> + </EncodingOptions> + """; + await File.WriteAllTextAsync(_encodingConfigurationPath, Configuration, TestContext.Current.CancellationToken); + + await _migration.PerformAsync(TestContext.Current.CancellationToken); + + Assert.Equal(Configuration, await File.ReadAllTextAsync(_encodingConfigurationPath, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task PerformAsync_LeavesMalformedConfigurationUnchanged() + { + const string Configuration = "<EncodingOptions><EncoderPreset xsi:nil=\"true\" />"; + await File.WriteAllTextAsync(_encodingConfigurationPath, Configuration, TestContext.Current.CancellationToken); + + await _migration.PerformAsync(TestContext.Current.CancellationToken); + + Assert.Equal(Configuration, await File.ReadAllTextAsync(_encodingConfigurationPath, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task MigrateEncodingOptions_UsesAutoForNullEncoderPreset() + { + const string Configuration = """ + <?xml version="1.0" encoding="utf-8"?> + <EncodingOptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <HardwareAccelerationType>qsv</HardwareAccelerationType> + <TonemappingRange>tv</TonemappingRange> + <EncoderPreset xsi:nil="true" /> + <EnableHardwareEncoding>true</EnableHardwareEncoding> + </EncodingOptions> + """; + await File.WriteAllTextAsync(_encodingConfigurationPath, Configuration, TestContext.Current.CancellationToken); + var applicationPaths = new ServerApplicationPaths( + _configurationDirectory, + _configurationDirectory, + _configurationDirectory, + _configurationDirectory, + _configurationDirectory); + + var migration = new MigrateEncodingOptions(applicationPaths, NullLoggerFactory.Instance); + migration.Perform(); + + var serializer = new XmlSerializer(typeof(EncodingOptions)); + using var reader = File.OpenRead(_encodingConfigurationPath); + var encodingOptions = Assert.IsType<EncodingOptions>(serializer.Deserialize(reader)); + Assert.Equal(EncoderPreset.auto, encodingOptions.EncoderPreset); + Assert.Equal(HardwareAccelerationType.qsv, encodingOptions.HardwareAccelerationType); + Assert.True(encodingOptions.EnableHardwareEncoding); + } + + public void Dispose() + { + Directory.Delete(_configurationDirectory, true); + } +} |
