From 1ace0fb13d56cf10ebac83fb1eff8eccc8fe4f6a Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Wed, 5 May 2021 01:24:41 -0500 Subject: [PATCH] fix(radarr): quality definition now sets unlimited For max and preferred values --- .../RadarrQualityDataTest.cs | 193 ++++++++++++++++++ src/Trash/Command/ServiceCommand.cs | 7 +- .../Objects/RadarrQualityDefinitionItem.cs | 4 +- .../QualityDefinition/RadarrQualityData.cs | 44 +++- .../RadarrQualityDefinitionUpdater.cs | 21 +- 5 files changed, 253 insertions(+), 16 deletions(-) create mode 100644 src/Trash.Tests/Radarr/QualityDefinition/RadarrQualityDataTest.cs diff --git a/src/Trash.Tests/Radarr/QualityDefinition/RadarrQualityDataTest.cs b/src/Trash.Tests/Radarr/QualityDefinition/RadarrQualityDataTest.cs new file mode 100644 index 00000000..3d6b70b4 --- /dev/null +++ b/src/Trash.Tests/Radarr/QualityDefinition/RadarrQualityDataTest.cs @@ -0,0 +1,193 @@ +using FluentAssertions; +using NUnit.Framework; +using Trash.Radarr.QualityDefinition; + +namespace Trash.Tests.Radarr.QualityDefinition +{ + [TestFixture] + [Parallelizable(ParallelScope.All)] + public class RadarrQualityDataTest + { + private static readonly object[] ToleranceTestValues = + { + new object[] {-RadarrQualityData.Tolerance - 0.01m, true}, + new object[] {-RadarrQualityData.Tolerance, false}, + new object[] {-RadarrQualityData.Tolerance / 2, false}, + new object[] {RadarrQualityData.Tolerance / 2, false}, + new object[] {RadarrQualityData.Tolerance, false}, + new object[] {RadarrQualityData.Tolerance + 0.01m, true} + }; + + [TestCaseSource(nameof(ToleranceTestValues))] + public void PreferredOutsideTolerance_WithVariousTolerance_ReturnsExpectedResult(decimal offset, + bool expectedResult) + { + const decimal testVal = 100; + var data = new RadarrQualityData {Preferred = testVal}; + data.PreferredOutsideTolerance(testVal + offset) + .Should().Be(expectedResult); + } + + [TestCaseSource(nameof(ToleranceTestValues))] + public void MaxOutsideTolerance_WithVariousTolerance_ReturnsExpectedResult(decimal offset, bool expectedResult) + { + const decimal testVal = 100; + var data = new RadarrQualityData {Max = testVal}; + data.MaxOutsideTolerance(testVal + offset) + .Should().Be(expectedResult); + } + + [TestCaseSource(nameof(ToleranceTestValues))] + public void MinOutsideTolerance_WithVariousTolerance_ReturnsExpectedResult(decimal offset, bool expectedResult) + { + const decimal testVal = 0; + var data = new RadarrQualityData {Min = testVal}; + data.MinOutsideTolerance(testVal + offset) + .Should().Be(expectedResult); + } + + private static readonly object[] InterpolatedPreferredTestParams = + { + new[] + { + 400m, + 1.0m, + RadarrQualityData.PreferredUnlimitedThreshold + }, + new[] + { + RadarrQualityData.PreferredUnlimitedThreshold, + 1.0m, + RadarrQualityData.PreferredUnlimitedThreshold + }, + new[] + { + RadarrQualityData.PreferredUnlimitedThreshold - 1m, + 1.0m, + RadarrQualityData.PreferredUnlimitedThreshold - 1m + }, + new[] + { + 10m, + 0m, + 0m + }, + new[] + { + 100m, + 0.5m, + 50m + } + }; + + [TestCaseSource(nameof(InterpolatedPreferredTestParams))] + public void InterpolatedPreferred_VariousValues_ExpectedResults(decimal max, decimal ratio, + decimal expectedResult) + { + var data = new RadarrQualityData {Min = 0, Max = max}; + data.InterpolatedPreferred(ratio).Should().Be(expectedResult); + } + + [Test] + public void AnnotatedMax_OutsideThreshold_EqualsSameValueWithUnlimited() + { + const decimal testVal = RadarrQualityData.MaxUnlimitedThreshold; + var data = new RadarrQualityData {Max = testVal}; + data.AnnotatedMax.Should().Be($"{testVal} (Unlimited)"); + } + + [Test] + public void AnnotatedMax_WithinThreshold_EqualsSameStringValue() + { + const decimal testVal = RadarrQualityData.MaxUnlimitedThreshold - 1; + var data = new RadarrQualityData {Max = testVal}; + data.AnnotatedMax.Should().Be($"{testVal}"); + } + + [Test] + public void AnnotatedMin_NoThreshold_EqualsSameValue() + { + const decimal testVal = 10m; + var data = new RadarrQualityData {Max = testVal}; + data.AnnotatedMax.Should().Be($"{testVal}"); + } + + [Test] + public void AnnotatedPreferred_OutsideThreshold_EqualsSameValueWithUnlimited() + { + const decimal testVal = RadarrQualityData.PreferredUnlimitedThreshold; + var data = new RadarrQualityData {Preferred = testVal}; + data.AnnotatedPreferred.Should().Be($"{testVal} (Unlimited)"); + } + + [Test] + public void AnnotatedPreferred_WithinThreshold_EqualsSameStringValue() + { + const decimal testVal = RadarrQualityData.PreferredUnlimitedThreshold - 1; + var data = new RadarrQualityData {Preferred = testVal}; + data.AnnotatedPreferred.Should().Be($"{testVal}"); + } + + [Test] + public void Max_AboveThreshold_EqualsSameValue() + { + const decimal testVal = RadarrQualityData.MaxUnlimitedThreshold + 1; + var data = new RadarrQualityData {Max = testVal}; + data.Max.Should().Be(testVal); + } + + [Test] + public void MaxForApi_AboveThreshold_EqualsNull() + { + const decimal testVal = RadarrQualityData.MaxUnlimitedThreshold + 1; + var data = new RadarrQualityData {Max = testVal}; + data.MaxForApi.Should().Be(null); + } + + [Test] + public void MaxForApi_HighestWithinThreshold_EqualsSameValue() + { + const decimal testVal = RadarrQualityData.MaxUnlimitedThreshold - 0.1m; + var data = new RadarrQualityData {Max = testVal}; + data.MaxForApi.Should().Be(testVal).And.Be(data.Max); + } + + [Test] + public void MaxForApi_LowestWithinThreshold_EqualsSameValue() + { + var data = new RadarrQualityData {Max = 0}; + data.MaxForApi.Should().Be(0); + } + + [Test] + public void Preferred_AboveThreshold_EqualsSameValue() + { + const decimal testVal = RadarrQualityData.PreferredUnlimitedThreshold + 1; + var data = new RadarrQualityData {Preferred = testVal}; + data.Preferred.Should().Be(testVal); + } + + [Test] + public void PreferredForApi_AboveThreshold_EqualsNull() + { + const decimal testVal = RadarrQualityData.PreferredUnlimitedThreshold + 1; + var data = new RadarrQualityData {Preferred = testVal}; + data.PreferredForApi.Should().Be(null); + } + + [Test] + public void PreferredForApi_HighestWithinThreshold_EqualsSameValue() + { + const decimal testVal = RadarrQualityData.PreferredUnlimitedThreshold - 0.1m; + var data = new RadarrQualityData {Preferred = testVal}; + data.PreferredForApi.Should().Be(testVal).And.Be(data.Preferred); + } + + [Test] + public void PreferredForApi_LowestWithinThreshold_EqualsSameValue() + { + var data = new RadarrQualityData {Preferred = 0}; + data.PreferredForApi.Should().Be(0); + } + } +} diff --git a/src/Trash/Command/ServiceCommand.cs b/src/Trash/Command/ServiceCommand.cs index 5b750ec0..6be07730 100644 --- a/src/Trash/Command/ServiceCommand.cs +++ b/src/Trash/Command/ServiceCommand.cs @@ -86,8 +86,13 @@ namespace Trash.Command // This is important. If any DTOs are missing members, say, if Radarr or Sonarr adds one in a future // version, this needs to fail to indicate that a software change is required. Otherwise, we lose // state between when we request settings, and re-apply them again with a few properties modified. - MissingMemberHandling = MissingMemberHandling.Error + MissingMemberHandling = MissingMemberHandling.Error, + + // This makes sure that null properties, such as maxSize and preferredSize in Radarr + // Quality Definitions, do not get written out to JSON request bodies. + NullValueHandling = NullValueHandling.Ignore }; + settings.JsonSerializer = new NewtonsoftJsonSerializer(jsonSettings); }); } diff --git a/src/Trash/Radarr/Api/Objects/RadarrQualityDefinitionItem.cs b/src/Trash/Radarr/Api/Objects/RadarrQualityDefinitionItem.cs index 9cd7e6c4..325619d3 100644 --- a/src/Trash/Radarr/Api/Objects/RadarrQualityDefinitionItem.cs +++ b/src/Trash/Radarr/Api/Objects/RadarrQualityDefinitionItem.cs @@ -20,7 +20,7 @@ namespace Trash.Radarr.Api.Objects public string Title { get; set; } = ""; public int Weight { get; set; } public decimal MinSize { get; set; } - public decimal MaxSize { get; set; } - public decimal PreferredSize { get; set; } + public decimal? MaxSize { get; set; } + public decimal? PreferredSize { get; set; } } } diff --git a/src/Trash/Radarr/QualityDefinition/RadarrQualityData.cs b/src/Trash/Radarr/QualityDefinition/RadarrQualityData.cs index 4b160792..6fa1d075 100644 --- a/src/Trash/Radarr/QualityDefinition/RadarrQualityData.cs +++ b/src/Trash/Radarr/QualityDefinition/RadarrQualityData.cs @@ -1,10 +1,52 @@ -namespace Trash.Radarr.QualityDefinition +using System; +using System.Globalization; +using System.Text; + +namespace Trash.Radarr.QualityDefinition { public class RadarrQualityData { + public const decimal Tolerance = 0.1m; + public const decimal MaxUnlimitedThreshold = 400; + public const decimal PreferredUnlimitedThreshold = 395; + public string Name { get; set; } = ""; public decimal Min { get; set; } public decimal Max { get; set; } public decimal Preferred { get; set; } + + public decimal? MaxForApi => Max < MaxUnlimitedThreshold ? Max : null; + public decimal MinForApi => Min; + public decimal? PreferredForApi => Preferred < PreferredUnlimitedThreshold ? Preferred : null; + + public string AnnotatedMin => Min.ToString(CultureInfo.InvariantCulture); + public string AnnotatedMax => AnnotatedValue(Max, MaxUnlimitedThreshold); + public string AnnotatedPreferred => AnnotatedValue(Preferred, PreferredUnlimitedThreshold); + + public decimal InterpolatedPreferred(decimal ratio) + { + var cappedMax = Math.Min(Max, PreferredUnlimitedThreshold); + return Math.Round(Min + (cappedMax - Min) * ratio, 1); + } + + private static string AnnotatedValue(decimal value, decimal threshold) + { + var builder = new StringBuilder(value.ToString(CultureInfo.InvariantCulture)); + if (value >= threshold) + { + builder.Append(" (Unlimited)"); + } + + return builder.ToString(); + } + + public bool MinOutsideTolerance(decimal other) => + Math.Abs(other - Min) > Tolerance; + + public bool MaxOutsideTolerance(decimal? other) => + Math.Abs((other ?? MaxUnlimitedThreshold) - Max) > Tolerance; + + public bool PreferredOutsideTolerance(decimal? other) => + Math.Abs((other ?? PreferredUnlimitedThreshold) - Preferred) > Tolerance; } } diff --git a/src/Trash/Radarr/QualityDefinition/RadarrQualityDefinitionUpdater.cs b/src/Trash/Radarr/QualityDefinition/RadarrQualityDefinitionUpdater.cs index 860cdd66..5fb1b9fb 100644 --- a/src/Trash/Radarr/QualityDefinition/RadarrQualityDefinitionUpdater.cs +++ b/src/Trash/Radarr/QualityDefinition/RadarrQualityDefinitionUpdater.cs @@ -26,13 +26,13 @@ namespace Trash.Radarr.QualityDefinition private static void PrintQualityPreview(IEnumerable quality) { Console.WriteLine(""); - const string format = "{0,-20} {1,-10} {2,-10} {3,-10}"; + const string format = "{0,-20} {1,-10} {2,-15} {3,-15}"; Console.WriteLine(format, "Quality", "Min", "Max", "Preferred"); Console.WriteLine(format, "-------", "---", "---", "---------"); foreach (var q in quality) { - Console.WriteLine(format, q.Name, q.Min, q.Max, q.Preferred); + Console.WriteLine(format, q.Name, q.AnnotatedMin, q.AnnotatedMax, q.AnnotatedPreferred); } Console.WriteLine(""); @@ -59,8 +59,7 @@ namespace Trash.Radarr.QualityDefinition // Apply a calculated preferred size foreach (var quality in selectedQuality) { - quality.Preferred = - Math.Round(quality.Min + (quality.Max - quality.Min) * config.QualityDefinition.PreferredRatio, 1); + quality.Preferred = quality.InterpolatedPreferred(config.QualityDefinition.PreferredRatio); } if (args.Preview) @@ -83,11 +82,9 @@ namespace Trash.Radarr.QualityDefinition { static bool QualityIsDifferent(RadarrQualityDefinitionItem a, RadarrQualityData b) { - const decimal tolerance = 0.1m; - return - Math.Abs(a.MaxSize - b.Max) > tolerance || - Math.Abs(a.MinSize - b.Min) > tolerance || - Math.Abs(a.PreferredSize - b.Preferred) > tolerance; + return b.MinOutsideTolerance(a.MinSize) || + b.MaxOutsideTolerance(a.MaxSize) || + b.PreferredOutsideTolerance(a.PreferredSize); } var newQuality = new List(); @@ -106,9 +103,9 @@ namespace Trash.Radarr.QualityDefinition } // Not using the original list again, so it's OK to modify the definition reftype objects in-place. - entry.MinSize = qualityData.Min; - entry.MaxSize = qualityData.Max; - entry.PreferredSize = qualityData.Preferred; + entry.MinSize = qualityData.MinForApi; + entry.MaxSize = qualityData.MaxForApi; + entry.PreferredSize = qualityData.PreferredForApi; newQuality.Add(entry); Log.Debug("Setting Quality " +