fix: Merge CFs using Replace instead of Add

json-serializing-nullable-fields-issue
Robert Dailey 8 months ago
parent 5881b611cf
commit 9d351f99ed

@ -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 - 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 these nodes may now be empty. This is mostly to make commenting out parts of configuration
templates easier. 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 ## [5.4.2] - 2023-09-14

@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Recyclarr.Common.Extensions;
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging; namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
@ -24,13 +25,13 @@ public abstract class ServiceConfigMerger<T> where T : ServiceConfigYaml
{ {
return a with 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), QualityProfiles = MergeQualityProfiles(a.QualityProfiles, b.QualityProfiles),
QualityDefinition = Combine(a.QualityDefinition, b.QualityDefinition, QualityDefinition = Combine(a.QualityDefinition, b.QualityDefinition,
(x, y) => x with (a1, b1) => a1 with
{ {
Type = y.Type ?? x.Type, Type = b1.Type ?? a1.Type,
PreferredRatio = y.PreferredRatio ?? x.PreferredRatio PreferredRatio = b1.PreferredRatio ?? a1.PreferredRatio
}), }),
DeleteOldCustomFormats = DeleteOldCustomFormats =
@ -41,6 +42,48 @@ public abstract class ServiceConfigMerger<T> where T : ServiceConfigYaml
}; };
} }
private sealed record FlattenedCfs(string? ProfileName, int? Score, IReadOnlyCollection<string> TrashIds);
private static IReadOnlyCollection<CustomFormatConfigYaml> MergeCustomFormats(
IReadOnlyCollection<CustomFormatConfigYaml> a,
IReadOnlyCollection<CustomFormatConfigYaml> 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<FlattenedCfs> FlattenCfs(IEnumerable<CustomFormatConfigYaml> 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<QualityProfileConfigYaml>? MergeQualityProfiles( private static IReadOnlyCollection<QualityProfileConfigYaml>? MergeQualityProfiles(
IReadOnlyCollection<QualityProfileConfigYaml>? a, IReadOnlyCollection<QualityProfileConfigYaml>? a,
IReadOnlyCollection<QualityProfileConfigYaml>? b) IReadOnlyCollection<QualityProfileConfigYaml>? b)

@ -263,7 +263,17 @@ public class ServiceConfigMergerTest
TrashIds = new[] {"id1", "id2"}, TrashIds = new[] {"id1", "id2"},
QualityProfiles = new[] 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 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 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}
}
}
}
}); });
} }

@ -164,9 +164,21 @@ public class IncludePostProcessorIntegrationTest : ConfigIntegrationFixture
Include = null, Include = null,
CustomFormats = new[] CustomFormats = new[]
{ {
new CustomFormatConfigYaml {TrashIds = new[] {"496f355514737f7d83bf7aa4d24f8169"}}, new CustomFormatConfigYaml
new CustomFormatConfigYaml {TrashIds = new[] {"2f22d89048b01681dde8afe203bf2e95"}}, {
new CustomFormatConfigYaml {TrashIds = new[] {"240770601cc226190c367ef59aba7463"}} TrashIds = new[]
{
"496f355514737f7d83bf7aa4d24f8169",
"240770601cc226190c367ef59aba7463"
}
},
new CustomFormatConfigYaml
{
TrashIds = new[]
{
"2f22d89048b01681dde8afe203bf2e95"
}
}
}, },
QualityDefinition = new QualitySizeConfigYaml QualityDefinition = new QualitySizeConfigYaml
{ {

Loading…
Cancel
Save