musicbrainz-provider #2

Merged
Mrixs merged 72 commits from musicbrainz-provider into master 2026-08-05 20:19:16 +00:00
5 changed files with 49 additions and 10 deletions
Showing only changes of commit c2f3258186 - Show all commits

View File

@@ -108,6 +108,7 @@ Based on the specification (docs/Specification.md), the application follows a mo
- Implements the comparison algorithm (0.85 similarity threshold) - Implements the comparison algorithm (0.85 similarity threshold)
- Handles removal of special characters, years, and bracketed keywords - Handles removal of special characters, years, and bracketed keywords
- Compares local albums vs. external discographies - Compares local albums vs. external discographies
- Applies per-artist ignore_singles/ignore_compilations filters at scan time for immediate responsiveness to setting changes
Shared normalization lives in `internal/normalize` (`NormalizeString`, `NormalizeArtistName`) — this is the single source of truth for string normalization, reused by both `internal/musicbrainz` and `internal/scanner`. Do NOT add local copies of normalization logic elsewhere. Shared normalization lives in `internal/normalize` (`NormalizeString`, `NormalizeArtistName`) — this is the single source of truth for string normalization, reused by both `internal/musicbrainz` and `internal/scanner`. Do NOT add local copies of normalization logic elsewhere.

View File

@@ -81,17 +81,17 @@ This creates two filtering points:
- [x] Must pass before next task - [x] Must pass before next task
### Task 3: Document the Data Flow ### Task 3: Document the Data Flow
- [ ] Update documentation to clearly explain how ignore_singles/ignore_compilations settings propagate through the system - [x] Update documentation to clearly explain how ignore_singles/ignore_compilations settings propagate through the system
- [ ] Add comments to key functions explaining the filtering flow - [x] Add comments to key functions explaining the filtering flow
- [ ] Ensure CLAUDE.md accurately reflects the current implementation - [x] Ensure CLAUDE.md accurately reflects the current implementation
- [ ] Create diagrams or flowcharts if helpful for understanding - [x] Create diagrams or flowcharts if helpful for understanding
- [ ] Must pass before next task - [x] Must pass before next task
### Task 4: Final Verification ### Task 4: Final Verification
- [ ] Run full test suite to ensure all changes work correctly - [x] Run full test suite to ensure all changes work correctly
- [ ] Verify no breaking changes were introduced - [x] Verify no breaking changes were introduced
- [ ] Confirm that the implementation handles the use case described in the memory file - [x] Confirm that the implementation handles the use case described in the memory file
- [ ] Update this plan with completion status - [x] Update this plan with completion status
## Post-Completion ## Post-Completion
*Items requiring manual intervention or external systems - no checkboxes, informational only* *Items requiring manual intervention or external systems - no checkboxes, informational only*

View File

@@ -57,6 +57,15 @@ func SyncArtistDiscography(
// ignore_singles / ignore_compilations take effect without waiting for cache // ignore_singles / ignore_compilations take effect without waiting for cache
// expiry. (Status/type inclusion was already applied when the rows were first // expiry. (Status/type inclusion was already applied when the rows were first
// synced and stored, so only the toggles can change.) // synced and stored, so only the toggles can change.)
//
// The MusicBrainz sync path applies filtering at store-time (when caching
// release groups from the API), while the scanner path applies filtering at
// read-time (when retrieving cached data). This dual-path approach ensures:
// 1. Storage efficiency: filtered results are stored, reducing database size
// 2. Real-time responsiveness: changes to ignore_singles/ignore_compilations
// take effect immediately without waiting for cache expiry
// 3. Consistency: both paths use the same filtering logic via
// musicbrainz.ApplyTypeToggles
if fresh { if fresh {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("sync artist discography: %w", err) return nil, fmt.Errorf("sync artist discography: %w", err)

View File

@@ -25,6 +25,20 @@ type MissingRelease struct {
// the dashboard, artist page, and Telegram digest — rather than waiting for the // the dashboard, artist page, and Telegram digest — rather than waiting for the
// artist's MusicBrainz cache to expire and the rows to be pruned on the next // artist's MusicBrainz cache to expire and the rows to be pruned on the next
// cache-miss re-sync. // cache-miss re-sync.
//
// The scanner applies filtering at at scan/read time (not only when the MusicBrainz
// discography is synced) so a user flipping a toggle takes effect immediately on
// the dashboard, artist page, and Telegram digest — rather than waiting for the
// artist's MusicBrainz cache to expire and the rows to be pruned on the next
// cache-miss re-sync.
//
// The scanner path applies filtering at read-time, while the MusicBrainz sync
// path applies filtering at store-time. This dual-path approach ensures:
// 1. Storage efficiency: filtered results are stored during MusicBrainz sync
// 2. Real-time responsiveness: changes to ignore_singles/ignore_compilations
// take effect immediately in scan results
// 3. Consistency: both paths use the same filtering logic via
// musicbrainz.ApplyTypeToggles
type TypeFilter struct { type TypeFilter struct {
IgnoreSingles bool IgnoreSingles bool
IgnoreCompilations bool IgnoreCompilations bool
@@ -34,6 +48,10 @@ type TypeFilter struct {
// A release counts as a Single/Compilation via either its primary Type or its // A release counts as a Single/Compilation via either its primary Type or its
// secondary types, matching musicbrainz.FilterReleaseGroups so both the // secondary types, matching musicbrainz.FilterReleaseGroups so both the
// cache-miss (store-time) and read-time paths agree. // cache-miss (store-time) and read-time paths agree.
//
// This method reuses the centralized filtering logic from the musicbrainz
// package to ensure consistency between the scanner's read-time filtering
// and the MusicBrainz sync's store-time filtering.
func (f TypeFilter) suppressed(ext database.ExternalRelease) bool { func (f TypeFilter) suppressed(ext database.ExternalRelease) bool {
// Use the centralized filtering logic from musicbrainz package // Use the centralized filtering logic from musicbrainz package
opts := musicbrainz.FilterOptions{ opts := musicbrainz.FilterOptions{
@@ -55,6 +73,10 @@ func (f TypeFilter) suppressed(ext database.ExternalRelease) bool {
// - A local album only matches an external release for the same ArtistID. // - A local album only matches an external release for the same ArtistID.
// - An external release is "missing" when none of the local albums (same // - An external release is "missing" when none of the local albums (same
// ArtistID) IsMatch at the given threshold. // ArtistID) IsMatch at the given threshold.
//
// The filter.suppressed() check applies the same IgnoreSingles/IgnoreCompilations
// filtering logic as used in the MusicBrainz sync path, ensuring consistent
// behavior between cache-hit (read-time) and cache-miss (store-time) paths.
func FindMissingReleases(local []database.LocalAlbum, external []database.ExternalRelease, threshold float64, filter TypeFilter) []MissingRelease { func FindMissingReleases(local []database.LocalAlbum, external []database.ExternalRelease, threshold float64, filter TypeFilter) []MissingRelease {
// Resolve the threshold exactly as ScanArtist/ScanAll do, so the exported // Resolve the threshold exactly as ScanArtist/ScanAll do, so the exported
// primitive honors the same zero-means-default contract rather than treating // primitive honors the same zero-means-default contract rather than treating

View File

@@ -31,6 +31,9 @@ func ScanArtist(ctx context.Context, db *database.DB, artistID string, threshold
// Apply the artist's type toggles at read time so ignore_singles / // Apply the artist's type toggles at read time so ignore_singles /
// ignore_compilations changes take effect immediately, without waiting for // ignore_compilations changes take effect immediately, without waiting for
// the MusicBrainz cache to expire and prune rows on the next re-sync. // the MusicBrainz cache to expire and prune rows on the next re-sync.
// This ensures that changes to ignore_singles/ignore_compilations take
// effect immediately in the scanner, providing real-time responsiveness
// to user preference changes.
settings, err := database.GetArtistSettings(db, artistID) settings, err := database.GetArtistSettings(db, artistID)
if err != nil { if err != nil {
// If artist settings don't exist, use empty filter (no filtering) // If artist settings don't exist, use empty filter (no filtering)
@@ -57,6 +60,10 @@ func ScanArtist(ctx context.Context, db *database.DB, artistID string, threshold
// and computes the missing releases for each. Results are concatenated into a // and computes the missing releases for each. Results are concatenated into a
// single slice across all artists. // single slice across all artists.
// //
// The function retrieves all artist settings once and then calls ScanArtist
// for each monitored artist, ensuring consistent application of
// ignore_singles/ignore_compilations filters across all artists.
//
// ctx.Err() is checked between artists; if cancellation occurs mid-iteration, // ctx.Err() is checked between artists; if cancellation occurs mid-iteration,
// scanning stops early and the accumulated results so far are returned along // scanning stops early and the accumulated results so far are returned along
// with the cancellation error. threshold follows the same contract as // with the cancellation error. threshold follows the same contract as