7.5 KiB
Fix Code Review Findings
Overview
Fix the MAJOR and MINOR issues identified in the max-effort code review of the Notifier+WebUI+Sync branch. The most critical issues are filter inconsistencies across cache-hit, cache-miss, and read-time paths that cause releases to incorrectly appear/disappear from the dashboard depending on MusicBrainz cache state.
Problem: Three separate filter implementations (musicbrainz.FilterReleaseGroups, musicbrainz.SyncArtistDiscography cache-hit path, scanner.TypeFilter.suppressed) have divergent logic for IgnoreSingles/IgnoreCompilations toggles — specifically, whether EP secondary type counts as a Single.
Context (from review)
-
Files involved:
internal/musicbrainz/api.go—FilterReleaseGroups,hasSliceType(variadic)internal/musicbrainz/sync.go—SyncArtistDiscographycache-hit filter (line 93-115)internal/scanner/diff.go—TypeFilter.suppressed,hasType(exact match)internal/database/external_releases.go—ArtistCacheFreshlexicographic time comparisoninternal/musicbrainz/sync.go— stale notification pruning (parameter limit)internal/scanner/scan.go—ScanArtistmissingErrArtistNotFoundhandlinginternal/notifier/scheduler.go—NotifyOnceRGID-only map keyinternal/database/artist_settings.go—SaveArtistSettingssubquery inefficiency
-
Key pattern: Centralized filter logic should exist in one place; all three paths should delegate to it.
-
Dependencies:
hasSliceTypeinapi.gois the canonical implementation (handlesSingle+EPforIgnoreSingles).
Development Approach
- Testing approach: TDD — write tests before implementation for each fix
- Complete each task fully (code + tests passing) before the next
- CRITICAL: every task MUST include new/updated tests for code changes
- All tests must pass before starting next task (
go test ./...) - Update this plan if scope changes during implementation
Testing Strategy
- Unit tests for every modified function (success + error cases)
- Integration-style tests for filter behavior across cache boundaries (using in-memory DB + stubbed MB client)
- No e2e framework in project; handler tests cover equivalent surface
Progress Tracking
- Mark completed items with
[x]immediately when done - Add newly discovered tasks with
➕prefix - Document issues/blockers with
⚠️prefix
Implementation Steps
Task 1: Centralize filter logic into shared helper
- Create
internal/musicbrainz/filter.gowithApplyTypeToggles(releases []ExternalRelease, opts FilterOptions) []ExternalReleasethat implements the canonical logic:IgnoreSingles→ filter whereType=="Single" OR hasSliceType(SecondaryTypes, "Single", "EP");IgnoreCompilations→ filter whereType=="Compilation" OR hasSliceType(SecondaryTypes, "Compilation") - Move
hasSliceTypeandFilterOptionsstruct to the new file (or keep in api.go and import) - Write tests for
ApplyTypeToggles: table-driven covering Single, EP, Compilation, Album with various SecondaryTypes combinations - Run tests - must pass before task 2
Task 2: Update musicbrainz.FilterReleaseGroups to use centralized helper
- Refactor
FilterReleaseGroupsinapi.goto callApplyTypeToggles(or inline the shared logic if keeping in same package) - Ensure existing
api_test.gotests still pass (filter behavior unchanged for cache-miss path) - Run tests - must pass before task 3
Task 3: Fix sync.go cache-hit path to use centralized filter
- Update
SyncArtistDiscographycache-hit branch to call the shared filter helper instead of inline logic - Ensure
optsfromgetArtistFilterOptionsis passed correctly - Write test in
sync_test.gothat verifies cache-hit path produces identical filter results as cache-miss path for sameFilterOptionsand release data - Run tests - must pass before task 4
Task 4: Fix scanner diff.go TypeFilter.suppressed to use centralized filter
- Update
TypeFilter.suppressedindiff.goto use the same logic asApplyTypeToggles(i.e., treatEPin SecondaryTypes as a Single whenIgnoreSingles=true) - Since scanner is separate package, either: (a) export
ApplyTypeTogglesfrom musicbrainz and import, or (b) duplicate the minimal logic with a comment referencing the canonical source. Choose (a) for DRY. - Update
scanner/diff.goto importmusicbrainzand use the shared filter - Write tests in
diff_test.goverifying scanner filter matches musicbrainz filter for all release type combinations - Run tests - must pass before task 5
Task 5: Fix ArtistCacheFresh lexicographic time comparison
- Change
external_releases.cached_atfrom TEXT to INTEGER (unix epoch seconds) via migration010_cached_at_to_integer - Update
FormatCachedAtto returntime.Time.Unix()(int64) - Update
ArtistCacheFreshquery to comparecached_at >= ?as integers - Update
SaveExternalReleaseand sync insert to store integer timestamp - Write tests: verify cache freshness check works across format change; test migration on existing DB
- Run tests - must pass before task 6
Task 6: Batch stale notification pruning to avoid SQLite parameter limit
- Modify stale notification deletion in
sync.go(lines 150-185) to process in chunks of 500 parameters - Write test with >1000 synthetic release groups to verify no parameter-limit error
- Run tests - must pass before task 7
Task 7: Handle ErrArtistNotFound in ScanArtist gracefully
- In
ScanArtist, wrapGetArtistSettingscall; ifErrArtistNotFound, use emptyTypeFilter(no filtering) instead of returning error - Write test: create external_releases row for non-existent artist_id, verify ScanArtist succeeds and returns missing releases (with default no-filter behavior)
- Run tests - must pass before task 8
Task 8: Fix NotifyOnce map key to use composite ArtistID+RGID
- Change
missingByRGIDmap key fromm.RGIDtom.ArtistID + "|" + m.RGID(or use a struct key) - Update lookup from
unnotifiedslice similarly - Add comment documenting that RGID is globally unique in MusicBrainz (UUID) so single-key is theoretically safe, but composite is defensive
- Write test verifying composite key works and doesn't break existing behavior
- Run tests - must pass before task 9
Task 9: Remove SaveArtistSettings INSERT subquery inefficiency (minor)
- Rewrite
SaveArtistSettingsto use two statements: INSERT with explicit values, then UPDATE on conflict (or use UPSERT with COALESCE for all columns including last_synced) - Ensure
last_syncedis preserved on update viaCOALESCE(excluded.last_synced, artist_settings.last_synced) - Write test verifying
last_syncedpreserved on update - Run tests - must pass before task 10
Task 10: Verify acceptance criteria and full test suite
- Run
go test ./...— all pass - Run
go vet ./...— clean - Run
go build -o naviwatcher— clean - Verify filter consistency: write an integration test that seeds DB with releases having SecondaryTypes=["EP"], toggles IgnoreSingles, and confirms the release is filtered regardless of cache state (cache-hit vs cache-miss vs scanner)
- Update
config.yaml.exampleif any new config fields added - Run tests - must pass
Task 11: Update documentation
- Update README.md if any new behavior or config documented
- Note the filter centralization pattern in CLAUDE.md if new pattern established