From 9d351f99ed08d3069f2e1ddfc376e0bab49ef091 Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Sat, 16 Sep 2023 16:41:59 -0500 Subject: [PATCH] fix: Merge CFs using Replace instead of Add --- CHANGELOG.md | 3 + .../ConfigMerging/ServiceConfigMerger.cs | 51 +++++++++++- .../ConfigMerging/ServiceConfigMergerTest.cs | 80 ++++++++++++++++++- .../IncludePostProcessorIntegrationTest.cs | 18 ++++- 4 files changed, 143 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58ce85a5..8336e5bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Relaxed validation rules for `trash_ids` and `quality_profiles` under `custom_formats`. Both of these nodes may now be empty. This is mostly to make commenting out parts of configuration templates easier. +- The merge operation for `custom_formats` is now "Join" (previously "Add"). If, for the same + profile, you "reassign" a different score to a CF, the score now gets updated without having to + remove the CF from `custom_formats` sections in included YAML files. ## [5.4.2] - 2023-09-14 diff --git a/src/Recyclarr.TrashLib.Config/Parsing/PostProcessing/ConfigMerging/ServiceConfigMerger.cs b/src/Recyclarr.TrashLib.Config/Parsing/PostProcessing/ConfigMerging/ServiceConfigMerger.cs index 14d397d4..3eb678ce 100644 --- a/src/Recyclarr.TrashLib.Config/Parsing/PostProcessing/ConfigMerging/ServiceConfigMerger.cs +++ b/src/Recyclarr.TrashLib.Config/Parsing/PostProcessing/ConfigMerging/ServiceConfigMerger.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Recyclarr.Common.Extensions; namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging; @@ -24,13 +25,13 @@ public abstract class ServiceConfigMerger where T : ServiceConfigYaml { return a with { - CustomFormats = Combine(a.CustomFormats, b.CustomFormats, (x, y) => x.Concat(y).ToList()), + CustomFormats = Combine(a.CustomFormats, b.CustomFormats, MergeCustomFormats), QualityProfiles = MergeQualityProfiles(a.QualityProfiles, b.QualityProfiles), QualityDefinition = Combine(a.QualityDefinition, b.QualityDefinition, - (x, y) => x with + (a1, b1) => a1 with { - Type = y.Type ?? x.Type, - PreferredRatio = y.PreferredRatio ?? x.PreferredRatio + Type = b1.Type ?? a1.Type, + PreferredRatio = b1.PreferredRatio ?? a1.PreferredRatio }), DeleteOldCustomFormats = @@ -41,6 +42,48 @@ public abstract class ServiceConfigMerger where T : ServiceConfigYaml }; } + private sealed record FlattenedCfs(string? ProfileName, int? Score, IReadOnlyCollection TrashIds); + + private static IReadOnlyCollection MergeCustomFormats( + IReadOnlyCollection a, + IReadOnlyCollection b) + { + var flattenedA = FlattenCfs(a); + var flattenedB = FlattenCfs(b); + + return flattenedA + // This builds a list of TrashIds in side B that are assigned to matching profiles in A + .Select(x => (A: x, B: flattenedB + .Where(y => y.ProfileName.EqualsIgnoreCase(x.ProfileName)) // Ignore score + .SelectMany(y => y.TrashIds) + .Distinct(StringComparer.InvariantCultureIgnoreCase) + .ToList())) + // Add everything on side A that isn't on side B + .Select(x => new CustomFormatConfigYaml + { + TrashIds = x.A.TrashIds + .Except(x.B, StringComparer.InvariantCultureIgnoreCase) + .ToList(), + QualityProfiles = x.A.ProfileName is not null + ? new[] {new QualityScoreConfigYaml {Name = x.A.ProfileName, Score = x.A.Score}} + : null + }) + .Concat(b) + .ToList(); + + static List FlattenCfs(IEnumerable cfs) + { + return cfs + .Where(x => x.TrashIds is not null) + .SelectMany(x => x is {QualityProfiles.Count: > 0} + ? x.QualityProfiles.Select(y => new FlattenedCfs(y.Name, y.Score, x.TrashIds!)) + : new[] {new FlattenedCfs(null, null, x.TrashIds!)}) + .GroupBy(x => (Name: x.ProfileName, x.Score)) + .Select(x => new FlattenedCfs(x.Key.Name, x.Key.Score, x.SelectMany(y => y.TrashIds).ToList())) + .ToList(); + } + } + private static IReadOnlyCollection? MergeQualityProfiles( IReadOnlyCollection? a, IReadOnlyCollection? b) diff --git a/src/tests/Recyclarr.TrashLib.Config.Tests/Parsing/PostProcessing/ConfigMerging/ServiceConfigMergerTest.cs b/src/tests/Recyclarr.TrashLib.Config.Tests/Parsing/PostProcessing/ConfigMerging/ServiceConfigMergerTest.cs index caa96d4a..714cebaf 100644 --- a/src/tests/Recyclarr.TrashLib.Config.Tests/Parsing/PostProcessing/ConfigMerging/ServiceConfigMergerTest.cs +++ b/src/tests/Recyclarr.TrashLib.Config.Tests/Parsing/PostProcessing/ConfigMerging/ServiceConfigMergerTest.cs @@ -263,7 +263,17 @@ public class ServiceConfigMergerTest TrashIds = new[] {"id1", "id2"}, QualityProfiles = new[] { - new QualityScoreConfigYaml {Name = "c", Score = 100} + new QualityScoreConfigYaml {Name = "c", Score = 100}, + new QualityScoreConfigYaml {Name = "d", Score = 101}, + new QualityScoreConfigYaml {Name = "e", Score = 102} + } + }, + new CustomFormatConfigYaml + { + TrashIds = new[] {"id2"}, + QualityProfiles = new[] + { + new QualityScoreConfigYaml {Name = "f", Score = 100} } } } @@ -288,6 +298,14 @@ public class ServiceConfigMergerTest { new QualityScoreConfigYaml {Name = "e", Score = 300} } + }, + new CustomFormatConfigYaml + { + TrashIds = new[] {"id1"}, + QualityProfiles = new[] + { + new QualityScoreConfigYaml {Name = "c", Score = 50} + } } } }; @@ -298,7 +316,65 @@ public class ServiceConfigMergerTest result.Should().BeEquivalentTo(new SonarrConfigYaml { - CustomFormats = leftConfig.CustomFormats.Concat(rightConfig.CustomFormats).ToList() + CustomFormats = new[] + { + new CustomFormatConfigYaml + { + TrashIds = new[] {"id2"}, + QualityProfiles = new[] + { + new QualityScoreConfigYaml {Name = "c", Score = 100} + } + }, + new CustomFormatConfigYaml + { + TrashIds = new[] {"id1", "id2"}, + QualityProfiles = new[] + { + new QualityScoreConfigYaml {Name = "d", Score = 101} + } + }, + new CustomFormatConfigYaml + { + TrashIds = new[] {"id1", "id2"}, + QualityProfiles = new[] + { + new QualityScoreConfigYaml {Name = "e", Score = 102} + } + }, + new CustomFormatConfigYaml + { + TrashIds = new[] {"id2"}, + QualityProfiles = new[] + { + new QualityScoreConfigYaml {Name = "f", Score = 100} + } + }, + new CustomFormatConfigYaml + { + TrashIds = new[] {"id3", "id4"}, + QualityProfiles = new[] + { + new QualityScoreConfigYaml {Name = "d", Score = 200} + } + }, + new CustomFormatConfigYaml + { + TrashIds = new[] {"id5", "id6"}, + QualityProfiles = new[] + { + new QualityScoreConfigYaml {Name = "e", Score = 300} + } + }, + new CustomFormatConfigYaml + { + TrashIds = new[] {"id1"}, + QualityProfiles = new[] + { + new QualityScoreConfigYaml {Name = "c", Score = 50} + } + } + } }); } diff --git a/src/tests/Recyclarr.TrashLib.Config.Tests/Parsing/PostProcessing/IncludePostProcessorIntegrationTest.cs b/src/tests/Recyclarr.TrashLib.Config.Tests/Parsing/PostProcessing/IncludePostProcessorIntegrationTest.cs index be66e5e0..fbf95507 100644 --- a/src/tests/Recyclarr.TrashLib.Config.Tests/Parsing/PostProcessing/IncludePostProcessorIntegrationTest.cs +++ b/src/tests/Recyclarr.TrashLib.Config.Tests/Parsing/PostProcessing/IncludePostProcessorIntegrationTest.cs @@ -164,9 +164,21 @@ public class IncludePostProcessorIntegrationTest : ConfigIntegrationFixture Include = null, CustomFormats = new[] { - new CustomFormatConfigYaml {TrashIds = new[] {"496f355514737f7d83bf7aa4d24f8169"}}, - new CustomFormatConfigYaml {TrashIds = new[] {"2f22d89048b01681dde8afe203bf2e95"}}, - new CustomFormatConfigYaml {TrashIds = new[] {"240770601cc226190c367ef59aba7463"}} + new CustomFormatConfigYaml + { + TrashIds = new[] + { + "496f355514737f7d83bf7aa4d24f8169", + "240770601cc226190c367ef59aba7463" + } + }, + new CustomFormatConfigYaml + { + TrashIds = new[] + { + "2f22d89048b01681dde8afe203bf2e95" + } + } }, QualityDefinition = new QualitySizeConfigYaml {