feat: Support for "score sets" in quality profiles

A score set is a collection of scores defined by a single custom format
JSON data file in the TRaSH Guides. Score sets provide a way to define
"themes" for scores that get used across multiple custom formats.

This feature adds the `score_sets` property to the top-level
`quality_profiles` objects.
json-serializing-nullable-fields-issue
Robert Dailey 9 months ago
parent f9ba985d1f
commit 4f52201ede

@ -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

@ -61,18 +61,20 @@ public class QualityProfileConfigPhase
};
}
AddCustomFormatScoreData(profileCfs.CfScores, profile, cf);
AddCustomFormatScoreData(profileCfs, profile, cf);
}
return allProfiles.Values.ToList();
}
private void AddCustomFormatScoreData(
ICollection<ProcessedQualityProfileScore> 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;
}
}

@ -65,6 +65,7 @@ public record QualityProfileConfigYaml
public int? MinFormatScore { get; init; }
public QualitySortAlgorithm? QualitySort { get; init; }
public IReadOnlyCollection<QualityProfileQualityConfigYaml>? Qualities { get; init; }
public string? ScoreSet { get; init; }
}
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]

@ -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<QualityProfileQualityConfig> Qualities { get; init; } =

@ -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<string, int> TrashScores { get; init; } = new();
public Dictionary<string, int> TrashScores { get; init; } = new(StringComparer.InvariantCultureIgnoreCase);
[JsonIgnore]
public int? DefaultScore => TrashScores.TryGetValue("default", out var score) ? score : null;

@ -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));
}
}

@ -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

Loading…
Cancel
Save