diff --git a/CHANGELOG.md b/CHANGELOG.md index 37843e5d..d325f0e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Media Naming: Sync file naming configuration even if `rename` is not set to `true`. - Quality Profiles: Validation check added for quality groups with less than 2 qualities. - Quality Profiles: Fix "Groups must contain multiple qualities" sync error. +- Quality Profiles: Fix "Must contain all qualities" sync error. ## [6.0.0] - 2023-09-29 diff --git a/src/Recyclarr.Cli/Pipelines/QualityProfile/QualityItemOrganizer.cs b/src/Recyclarr.Cli/Pipelines/QualityProfile/QualityItemOrganizer.cs index 74601531..94510562 100644 --- a/src/Recyclarr.Cli/Pipelines/QualityProfile/QualityItemOrganizer.cs +++ b/src/Recyclarr.Cli/Pipelines/QualityProfile/QualityItemOrganizer.cs @@ -11,8 +11,8 @@ public class QualityItemOrganizer public UpdatedQualities OrganizeItems(QualityProfileDto dto, QualityProfileConfig config) { - var wanted = ProcessWantedItems(dto.Items, config.Qualities); - var unwanted = ProcessUnwantedItems(dto.Items, wanted); + var wanted = GetWantedItems(dto.Items, config.Qualities); + var unwanted = GetUnwantedItems(dto.Items, wanted); var combined = CombineAndSortItems(config.QualitySort, wanted, unwanted); AssignMissingGroupIds(combined); @@ -27,7 +27,7 @@ public class QualityItemOrganizer [SuppressMessage("SonarLint", "S1751", Justification = "'continue' used here is for separating local methods")] - private List ProcessWantedItems( + private List GetWantedItems( IReadOnlyCollection dtoItems, IReadOnlyCollection configQualities) { @@ -78,24 +78,55 @@ public class QualityItemOrganizer return updatedItems; } - private static IEnumerable ProcessUnwantedItems( + private static IEnumerable FilterUnwantedItems( + ProfileItemDto dto, + IReadOnlyCollection wantedItems) + { + // Quality + if (dto.Quality is not null) + { + if (wantedItems.FindQualityByName(dto.Quality.Name) is null) + { + // Not in wanted list, so we keep + return new[] {dto}; + } + } + // Group + else + { + // If this is actually a quality instead of a group, this will effectively be a no-op since the Items + // array will already be empty. + var unwantedQualities = dto.Items + .Where(y => wantedItems.FindQualityByName(y.Quality?.Name) is null); + + // If the group is in the wanted list, then we only want to add qualities inside it that are NOT wanted + if (wantedItems.FindGroupByName(dto.Name) is not null) + { + return unwantedQualities; + } + + // If the group is NOT in the wanted list, keep the group and add its children (if they are not wanted) + return new[] + { + dto with + { + Items = unwantedQualities + .Select(y => y with {Allowed = false}) + .ToList() + } + }; + } + + return Array.Empty(); + } + + private static IEnumerable GetUnwantedItems( IEnumerable dtoItems, IReadOnlyCollection wantedItems) { - // Find remaining items in the DTO that were *not* handled by the user's config. return dtoItems - .Where(x => !ExistsInWantedItems(wantedItems, x)) - .Select(x => x with - { - Allowed = false, - - // If this is actually a quality instead of a group, this will effectively be a no-op since the Items - // array will already be empty. - Items = x.Items - .Where(y => wantedItems.FindQualityByName(y.Quality?.Name) is null) - .Select(y => y with {Allowed = false}) - .ToList() - }) + .SelectMany(x => FilterUnwantedItems(x, wantedItems)) + .Select(x => x with {Allowed = false}) // Find item groups that have less than 2 nested qualities remaining in them. Those get flattened out. // If Count == 0, that gets handled by the `Where()` below. .Select(x => x.Items.Count == 1 ? x.Items.First() : x) @@ -124,13 +155,4 @@ public class QualityItemOrganizer item.Id = nextItemId++; } } - - private static bool ExistsInWantedItems(IEnumerable wantedItems, ProfileItemDto dto) - { - var existingItem = dto.Quality is null - ? wantedItems.FindGroupByName(dto.Name) - : wantedItems.FindQualityByName(dto.Quality.Name); - - return existingItem is not null; - } } diff --git a/src/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/QualityItemOrganizerTest.cs b/src/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/QualityItemOrganizerTest.cs index bd995337..7686596a 100644 --- a/src/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/QualityItemOrganizerTest.cs +++ b/src/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/QualityItemOrganizerTest.cs @@ -207,4 +207,40 @@ public class QualityItemOrganizerTest NewQp.QualityDto(3, "three", false)) }); } + + [Test] + public void Remove_quality_from_existing_group() + { + var config = new QualityProfileConfig + { + Qualities = new[] + { + NewQp.GroupConfig("group1", "one", "two", "three") + } + }; + + var dto = new QualityProfileDto + { + Items = new[] + { + NewQp.GroupDto(1001, "group1", true, + NewQp.QualityDto(1, "one", true), + NewQp.QualityDto(2, "two", true), + NewQp.QualityDto(3, "three", true), + NewQp.QualityDto(4, "four", true)) + } + }; + + var sut = new QualityItemOrganizer(); + var result = sut.OrganizeItems(dto, config); + + result.Items.Should().BeEquivalentTo(new[] + { + NewQp.GroupDto(1001, "group1", true, + NewQp.QualityDto(1, "one", true), + NewQp.QualityDto(2, "two", true), + NewQp.QualityDto(3, "three", true)), + NewQp.QualityDto(4, "four", false) + }); + } }