From a3b8aa8a74851bba1974d3d4351cbed9579a437a Mon Sep 17 00:00:00 2001 From: Vladimir Zagainov Date: Wed, 5 Aug 2026 17:08:31 +0300 Subject: [PATCH] feat: complete Task 2 - update filtering core logic for live/remix filtering --- docs/plans/2026-07-30-live-remix-filtering.md | 8 +- internal/musicbrainz/filter.go | 39 +++++- internal/musicbrainz/filter_test.go | 122 +++++++++++++----- 3 files changed, 133 insertions(+), 36 deletions(-) diff --git a/docs/plans/2026-07-30-live-remix-filtering.md b/docs/plans/2026-07-30-live-remix-filtering.md index 98c24f3..5a2babf 100644 --- a/docs/plans/2026-07-30-live-remix-filtering.md +++ b/docs/plans/2026-07-30-live-remix-filtering.md @@ -49,10 +49,10 @@ - [x] run project tests - must pass before next task ### Task 2: Update Filtering Core Logic -- [ ] Update `FilterOptions` struct in `internal/musicbrainz/filter.go` to include `IgnoreLive` and `IgnoreRemix` -- [ ] Update `ApplyTypeToggles` in `internal/musicbrainz/filter.go` to include logic for "Live" and "Remix" types -- [ ] write unit tests for `ApplyTypeToggles` covering all four toggle types (Single, Compilation, Live, Remix) -- [ ] run project tests - must pass before next task +- [x] Update `FilterOptions` struct in `internal/musicbrainz/filter.go` to include `IgnoreLive` and `IgnoreRemix` +- [x] Update `ApplyTypeToggles` in `internal/musicbrainz/filter.go` to include logic for "Live" and "Remix" types +- [x] write unit tests for `ApplyTypeToggles` covering all four toggle types (Single, Compilation, Live, Remix) +- [x] run project tests - must pass before next task ### Task 3: Update Web UI and Handlers - [ ] Update `ArtistData` or similar view models to include the new filter booleans diff --git a/internal/musicbrainz/filter.go b/internal/musicbrainz/filter.go index 400e7b1..0c3c55d 100644 --- a/internal/musicbrainz/filter.go +++ b/internal/musicbrainz/filter.go @@ -8,6 +8,8 @@ import ( type FilterOptions struct { IgnoreSingles bool IgnoreCompilations bool + IgnoreLive bool + IgnoreRemix bool } // hasSliceType reports whether the slice contains any of the wanted values. @@ -24,8 +26,11 @@ func hasSliceType(types []string, wanted ...string) bool { // ApplyTypeToggles filters releases based on the IgnoreSingles and IgnoreCompilations flags. // Implements canonical filtering logic: -// IgnoreSingles filters: Type == "Single" OR Type == "EP" OR SecondaryTypes contains "Single" OR "EP" -// IgnoreCompilations filters: Type == "Compilation" OR SecondaryTypes contains "Compilation" +// +// IgnoreSingles filters: Type == "Single" OR Type == "EP" OR SecondaryTypes contains "Single" OR "EP" +// IgnoreCompilations filters: Type == "Compilation" OR SecondaryTypes contains "Compilation" +// IgnoreLive filters: Type == "Live" OR SecondaryTypes contains "Live" +// IgnoreRemix filters: Type == "Remix" OR SecondaryTypes contains "Remix" func ApplyTypeToggles(releases []database.ExternalRelease, opts FilterOptions) []database.ExternalRelease { var result []database.ExternalRelease for _, release := range releases { @@ -43,6 +48,20 @@ func ApplyTypeToggles(releases []database.ExternalRelease, opts FilterOptions) [ } } + // Apply IgnoreLive filtering: filter out if Type is Live OR SecondaryTypes contains Live + if opts.IgnoreLive { + if release.Type == "Live" || hasSliceType(release.SecondaryTypes, "Live") { + continue + } + } + + // Apply IgnoreRemix filtering: filter out if Type is Remix OR SecondaryTypes contains Remix + if opts.IgnoreRemix { + if release.Type == "Remix" || hasSliceType(release.SecondaryTypes, "Remix") { + continue + } + } + result = append(result, release) } return result @@ -67,7 +86,21 @@ func ApplyTypeTogglesToReleaseGroups(groups []ReleaseGroup, opts FilterOptions) } } + // Apply IgnoreLive filtering: filter out if Type is Live OR SecondaryTypes contains Live + if opts.IgnoreLive { + if rg.Type == "Live" || hasSliceType(rg.SecondaryTypes, "Live") { + continue + } + } + + // Apply IgnoreRemix filtering: filter out if Type is Remix OR SecondaryTypes contains Remix + if opts.IgnoreRemix { + if rg.Type == "Remix" || hasSliceType(rg.SecondaryTypes, "Remix") { + continue + } + } + result = append(result, rg) } return result -} \ No newline at end of file +} diff --git a/internal/musicbrainz/filter_test.go b/internal/musicbrainz/filter_test.go index a1908b1..cf5b10a 100644 --- a/internal/musicbrainz/filter_test.go +++ b/internal/musicbrainz/filter_test.go @@ -15,37 +15,73 @@ func TestApplyTypeToggles(t *testing.T) { {RGID: "r4", Type: "Album", SecondaryTypes: []string{"Compilation"}}, {RGID: "r5", Type: "EP", SecondaryTypes: []string{}}, {RGID: "r6", Type: "Album", SecondaryTypes: []string{"EP"}}, + {RGID: "r7", Type: "Live", SecondaryTypes: []string{}}, + {RGID: "r8", Type: "Album", SecondaryTypes: []string{"Live"}}, + {RGID: "r9", Type: "Remix", SecondaryTypes: []string{}}, + {RGID: "r10", Type: "Album", SecondaryTypes: []string{"Remix"}}, } tests := []struct { name string - opts musicbrainz.FilterOptions - expectedCounts int - expectedRGIDs []string + opts musicbrainz.FilterOptions + expectedCounts int + expectedRGIDs []string }{ { name: "No filters", - opts: musicbrainz.FilterOptions{IgnoreSingles: false, IgnoreCompilations: false}, - expectedCounts: 6, - expectedRGIDs: []string{"r1", "r2", "r3", "r4", "r5", "r6"}, + opts: musicbrainz.FilterOptions{IgnoreSingles: false, IgnoreCompilations: false, IgnoreLive: false, IgnoreRemix: false}, + expectedCounts: 10, + expectedRGIDs: []string{"r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10"}, }, { name: "Ignore singles only", - opts: musicbrainz.FilterOptions{IgnoreSingles: true, IgnoreCompilations: false}, - expectedCounts: 2, // r3, r4 - expectedRGIDs: []string{"r3", "r4"}, + opts: musicbrainz.FilterOptions{IgnoreSingles: true, IgnoreCompilations: false, IgnoreLive: false, IgnoreRemix: false}, + // Actually: r1(Single), r2(Album+Single), r5(EP), r6(Album+EP) should be filtered out + // Leaving: r3(Compilation), r4(Album+Compilation), r7(Live), r8(Album+Live), r9(Remix), r10(Album+Remix) + expectedCounts: 6, + expectedRGIDs: []string{"r3", "r4", "r7", "r8", "r9", "r10"}, }, { name: "Ignore compilations only", - opts: musicbrainz.FilterOptions{IgnoreSingles: false, IgnoreCompilations: true}, - expectedCounts: 4, // r1, r2, r5, r6 - expectedRGIDs: []string{"r1", "r2", "r5", "r6"}, + opts: musicbrainz.FilterOptions{IgnoreSingles: false, IgnoreCompilations: true, IgnoreLive: false, IgnoreRemix: false}, + // r1, r2, r5, r6, r7, r8, r9, r10 (r3 and r4 filtered out) + expectedCounts: 8, + expectedRGIDs: []string{"r1", "r2", "r5", "r6", "r7", "r8", "r9", "r10"}, }, { - name: "Ignore both", - opts: musicbrainz.FilterOptions{IgnoreSingles: true, IgnoreCompilations: true}, + name: "Ignore live only", + opts: musicbrainz.FilterOptions{IgnoreSingles: false, IgnoreCompilations: false, IgnoreLive: true, IgnoreRemix: false}, + // r1, r2, r3, r4, r5, r6, r9, r10 (r7 and r8 filtered out) + expectedCounts: 8, + expectedRGIDs: []string{"r1", "r2", "r3", "r4", "r5", "r6", "r9", "r10"}, + }, + { + name: "Ignore remix only", + opts: musicbrainz.FilterOptions{IgnoreSingles: false, IgnoreCompilations: false, IgnoreLive: false, IgnoreRemix: true}, + // r1, r2, r3, r4, r5, r6, r7, r8 (r9 and r10 filtered out) + expectedCounts: 8, + expectedRGIDs: []string{"r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8"}, + }, + { + name: "Ignore both singles and compilations", + opts: musicbrainz.FilterOptions{IgnoreSingles: true, IgnoreCompilations: true, IgnoreLive: false, IgnoreRemix: false}, + // Actually: r1, r2, r5, r6 filtered (singles) and r3, r4 filtered (compilations) + // Leaving: r7(Live), r8(Album+Live), r9(Remix), r10(Album+Remix) + expectedCounts: 4, + expectedRGIDs: []string{"r7", "r8", "r9", "r10"}, + }, + { + name: "Ignore live and remix", + opts: musicbrainz.FilterOptions{IgnoreSingles: false, IgnoreCompilations: false, IgnoreLive: true, IgnoreRemix: true}, + // r1, r2, r3, r4, r5, r6 (r7, r8, r9, r10 filtered out) + expectedCounts: 6, + expectedRGIDs: []string{"r1", "r2", "r3", "r4", "r5", "r6"}, + }, + { + name: "Ignore all four types", + opts: musicbrainz.FilterOptions{IgnoreSingles: true, IgnoreCompilations: true, IgnoreLive: true, IgnoreRemix: true}, expectedCounts: 0, - expectedRGIDs: []string{}, + expectedRGIDs: []string{}, }, } @@ -72,35 +108,63 @@ func TestApplyTypeTogglesToReleaseGroups(t *testing.T) { {ID: "g4", Type: "Album", SecondaryTypes: []string{"Compilation"}}, {ID: "g5", Type: "EP", SecondaryTypes: []string{}}, {ID: "g6", Type: "Album", SecondaryTypes: []string{"EP"}}, + {ID: "g7", Type: "Live", SecondaryTypes: []string{}}, + {ID: "g8", Type: "Album", SecondaryTypes: []string{"Live"}}, + {ID: "g9", Type: "Remix", SecondaryTypes: []string{}}, + {ID: "g10", Type: "Album", SecondaryTypes: []string{"Remix"}}, } tests := []struct { name string - opts musicbrainz.FilterOptions - expectedCounts int - expectedIDs []string + opts musicbrainz.FilterOptions + expectedCounts int + expectedIDs []string }{ { name: "No filters", - opts: musicbrainz.FilterOptions{IgnoreSingles: false, IgnoreCompilations: false}, - expectedCounts: 6, - expectedIDs: []string{"g1", "g2", "g3", "g4", "g5", "g6"}, + opts: musicbrainz.FilterOptions{IgnoreSingles: false, IgnoreCompilations: false, IgnoreLive: false, IgnoreRemix: false}, + expectedCounts: 10, + expectedIDs: []string{"g1", "g2", "g3", "g4", "g5", "g6", "g7", "g8", "g9", "g10"}, }, { name: "Ignore singles only", - opts: musicbrainz.FilterOptions{IgnoreSingles: true, IgnoreCompilations: false}, - expectedCounts: 2, // g3, g4 - expectedIDs: []string{"g3", "g4"}, + opts: musicbrainz.FilterOptions{IgnoreSingles: true, IgnoreCompilations: false, IgnoreLive: false, IgnoreRemix: false}, + expectedCounts: 6, + expectedIDs: []string{"g3", "g4", "g7", "g8", "g9", "g10"}, }, { name: "Ignore compilations only", - opts: musicbrainz.FilterOptions{IgnoreSingles: false, IgnoreCompilations: true}, - expectedCounts: 4, // g1, g2, g5, g6 - expectedIDs: []string{"g1", "g2", "g5", "g6"}, + opts: musicbrainz.FilterOptions{IgnoreSingles: false, IgnoreCompilations: true, IgnoreLive: false, IgnoreRemix: false}, + expectedCounts: 8, // g1, g2, g5, g6, g7, g8, g9, g10 (g3 and g4 filtered out) + expectedIDs: []string{"g1", "g2", "g5", "g6", "g7", "g8", "g9", "g10"}, }, { - name: "Ignore both", - opts: musicbrainz.FilterOptions{IgnoreSingles: true, IgnoreCompilations: true}, + name: "Ignore live only", + opts: musicbrainz.FilterOptions{IgnoreSingles: false, IgnoreCompilations: false, IgnoreLive: true, IgnoreRemix: false}, + expectedCounts: 8, // g1, g2, g3, g4, g5, g6, g9, g10 (g7 and g8 filtered out) + expectedIDs: []string{"g1", "g2", "g3", "g4", "g5", "g6", "g9", "g10"}, + }, + { + name: "Ignore remix only", + opts: musicbrainz.FilterOptions{IgnoreSingles: false, IgnoreCompilations: false, IgnoreLive: false, IgnoreRemix: true}, + expectedCounts: 8, // g1, g2, g3, g4, g5, g6, g7, g8 (g9 and g10 filtered out) + expectedIDs: []string{"g1", "g2", "g3", "g4", "g5", "g6", "g7", "g8"}, + }, + { + name: "Ignore both singles and compilations", + opts: musicbrainz.FilterOptions{IgnoreSingles: true, IgnoreCompilations: true, IgnoreLive: false, IgnoreRemix: false}, + expectedCounts: 4, + expectedIDs: []string{"g7", "g8", "g9", "g10"}, + }, + { + name: "Ignore live and remix", + opts: musicbrainz.FilterOptions{IgnoreSingles: false, IgnoreCompilations: false, IgnoreLive: true, IgnoreRemix: true}, + expectedCounts: 6, // g1, g2, g3, g4, g5, g6 (g7, g8, g9, g10 filtered out) + expectedIDs: []string{"g1", "g2", "g3", "g4", "g5", "g6"}, + }, + { + name: "Ignore all four types", + opts: musicbrainz.FilterOptions{IgnoreSingles: true, IgnoreCompilations: true, IgnoreLive: true, IgnoreRemix: true}, expectedCounts: 0, expectedIDs: []string{}, },