diff --git a/CHANGELOG.md b/CHANGELOG.md index e507a0fd..de8f16bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New `delete` command added for deleting one, many, or all custom formats from Radarr or Sonarr. - Exclusions are now supported under `reset_unmatched_scores`. This is used to prevent score resets to specific custom formats. See [the docs][except] for more info. +- New `score_set` property available to each profile defined under the top-level `quality_profiles` + list. This allows different kinds of pre-defined scores to be chosen from the guide, without + having to explicitly override scores in your YAML. ### Changed diff --git a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhase.cs b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhase.cs index 8a18ca77..21e1d50a 100644 --- a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhase.cs @@ -61,18 +61,20 @@ public class QualityProfileConfigPhase }; } - AddCustomFormatScoreData(profileCfs.CfScores, profile, cf); + AddCustomFormatScoreData(profileCfs, profile, cf); } return allProfiles.Values.ToList(); } private void AddCustomFormatScoreData( - ICollection existingScoreData, - QualityProfileScoreConfig profile, + ProcessedQualityProfileData profile, + QualityProfileScoreConfig scoreConfig, CustomFormatData cf) { - var scoreToUse = profile.Score ?? cf.DefaultScore ?? cf.TrashScore; + var existingScoreData = profile.CfScores; + + var scoreToUse = DetermineScore(profile.Profile, scoreConfig, cf); if (scoreToUse is null) { _log.Information("No score in guide or config for CF {Name} ({TrashId})", cf.Name, cf.TrashId); @@ -87,7 +89,7 @@ public class QualityProfileConfigPhase _log.Warning( "Custom format {Name} ({TrashId}) is duplicated in quality profile {ProfileName} with a score " + "of {NewScore}, which is different from the original score of {OriginalScore}", - cf.Name, cf.TrashId, profile.Name, scoreToUse, existingScore); + cf.Name, cf.TrashId, scoreConfig.Name, scoreToUse, existingScore); } else { @@ -99,4 +101,27 @@ public class QualityProfileConfigPhase existingScoreData.Add(new ProcessedQualityProfileScore(cf.TrashId, cf.Name, cf.Id, scoreToUse.Value)); } + + private int? DetermineScore( + QualityProfileConfig profile, + QualityProfileScoreConfig scoreConfig, + CustomFormatData cf) + { + if (scoreConfig.Score is not null) + { + return scoreConfig.Score; + } + + if (profile.ScoreSet is not null) + { + if (cf.TrashScores.TryGetValue(profile.ScoreSet, out var scoreFromSet)) + { + return scoreFromSet; + } + + _log.Debug("CF {CfName} has no Score Set with name '{ScoreSetName}'", cf.Name, profile.ScoreSet); + } + + return cf.DefaultScore; + } } diff --git a/src/Recyclarr.TrashLib/Config/Parsing/ConfigYamlDataObjects.cs b/src/Recyclarr.TrashLib/Config/Parsing/ConfigYamlDataObjects.cs index 400d8ebc..716d353c 100644 --- a/src/Recyclarr.TrashLib/Config/Parsing/ConfigYamlDataObjects.cs +++ b/src/Recyclarr.TrashLib/Config/Parsing/ConfigYamlDataObjects.cs @@ -65,6 +65,7 @@ public record QualityProfileConfigYaml public int? MinFormatScore { get; init; } public QualitySortAlgorithm? QualitySort { get; init; } public IReadOnlyCollection? Qualities { get; init; } + public string? ScoreSet { get; init; } } [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] diff --git a/src/Recyclarr.TrashLib/Config/Services/ServiceConfiguration.cs b/src/Recyclarr.TrashLib/Config/Services/ServiceConfiguration.cs index 0f20d2ce..317ad153 100644 --- a/src/Recyclarr.TrashLib/Config/Services/ServiceConfiguration.cs +++ b/src/Recyclarr.TrashLib/Config/Services/ServiceConfiguration.cs @@ -72,6 +72,7 @@ public record QualityProfileConfig public string? UpgradeUntilQuality { get; init; } public int? UpgradeUntilScore { get; init; } public int? MinFormatScore { get; init; } + public string? ScoreSet { get; init; } public ResetUnmatchedScoresConfig ResetUnmatchedScores { get; init; } = new(); public QualitySortAlgorithm QualitySort { get; init; } public IReadOnlyCollection Qualities { get; init; } = diff --git a/src/Recyclarr.TrashLib/Models/CustomFormatData.cs b/src/Recyclarr.TrashLib/Models/CustomFormatData.cs index e3dd4afb..f03d4121 100644 --- a/src/Recyclarr.TrashLib/Models/CustomFormatData.cs +++ b/src/Recyclarr.TrashLib/Models/CustomFormatData.cs @@ -35,13 +35,9 @@ public record CustomFormatData [JsonNoSerialize] public string TrashId { get; init; } = ""; - [JsonProperty("trash_score")] - [JsonNoSerialize] - public int? TrashScore { get; init; } - [JsonProperty("trash_scores")] [JsonNoSerialize] - public Dictionary TrashScores { get; init; } = new(); + public Dictionary TrashScores { get; init; } = new(StringComparer.InvariantCultureIgnoreCase); [JsonIgnore] public int? DefaultScore => TrashScores.TryGetValue("default", out var score) ? score : null; diff --git a/src/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhaseTest.cs b/src/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhaseTest.cs index c7b92d22..11eff187 100644 --- a/src/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhaseTest.cs +++ b/src/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhaseTest.cs @@ -173,4 +173,50 @@ public class QualityProfileConfigPhaseTest }, o => o.Excluding(x => x.ShouldCreate)); } + + [Test, AutoMockData] + public void All_cfs_use_score_set( + [Frozen] ProcessedCustomFormatCache cache, + QualityProfileConfigPhase sut) + { + cache.AddCustomFormats(new[] + { + NewCf.DataWithScores("", "id1", 1, ("default", 101), ("set1", 102)), + NewCf.DataWithScores("", "id2", 2, ("default", 201), ("set2", 202)) + }); + + var config = NewConfig.Radarr() with + { + CustomFormats = new[] + { + new CustomFormatConfig + { + TrashIds = new[] {"id1", "id2"}, + QualityProfiles = new[] + { + new QualityProfileScoreConfig {Name = "test_profile"} + } + } + }, + QualityProfiles = new[] + { + new QualityProfileConfig + { + Name = "test_profile", + ScoreSet = "set1" + } + } + }; + + var result = sut.Execute(config); + + result.Should().BeEquivalentTo(new[] + { + NewQp.Processed("test_profile", ("id1", 1, 102), ("id2", 2, 201)) with + { + Profile = config.QualityProfiles.First() + } + }, + o => o.Excluding(x => x.ShouldCreate)); + } } diff --git a/src/tests/Recyclarr.TrashLib.TestLibrary/NewCf.cs b/src/tests/Recyclarr.TrashLib.TestLibrary/NewCf.cs index 7f88ba7d..9fdfaa60 100644 --- a/src/tests/Recyclarr.TrashLib.TestLibrary/NewCf.cs +++ b/src/tests/Recyclarr.TrashLib.TestLibrary/NewCf.cs @@ -15,6 +15,21 @@ public static class NewCf }; } + public static CustomFormatData DataWithScores( + string name, + string trashId, + int id, + params (string ScoreSet, int Score)[] scores) + { + return new CustomFormatData + { + Id = id, + Name = name, + TrashId = trashId, + TrashScores = scores.ToDictionary(x => x.ScoreSet, x => x.Score) + }; + } + public static CustomFormatData Data(string name, string trashId, int id = 0) { return new CustomFormatData