package musicbrainz import ( "context" "fmt" "net/url" "naviwatcher/internal/database" ) // includedTypes contains release-group primary types that should be included // when no more specific type classification applies. var includedTypes = map[string]bool{ "Album": true, "Single": true, "EP": true, } // GetArtistReleaseGroups fetches all release groups for a given artist from MusicBrainz. // It queries the artist's release groups via the MusicBrainz Web Service API, // parses the XML response, and applies status and type filtering. // // The method handles pagination automatically by following offset parameters // until all release groups are fetched. func (c *MusicBrainzClient) GetArtistReleaseGroups(ctx context.Context, artistMBID string) ([]ReleaseGroup, error) { var allGroups []ReleaseGroup offset := 0 limit := 100 // MusicBrainz max limit per request for { params := url.Values{} params.Set("artist", artistMBID) params.Set("limit", fmt.Sprintf("%d", limit)) params.Set("offset", fmt.Sprintf("%d", offset)) path := "/release-group?" + params.Encode() body, err := c.doGet(ctx, path) if err != nil { return nil, fmt.Errorf("fetch release groups for artist %s: %w", artistMBID, err) } parsed, err := ParseReleaseGroups(body) if err != nil { return nil, fmt.Errorf("parse release groups for artist %s: %w", artistMBID, err) } allGroups = append(allGroups, parsed.ReleaseGroups...) // If we've fetched all results, we've reached the end. // Also break on empty page to prevent infinite loop if API // returns fewer items than advertised by count. if len(parsed.ReleaseGroups) == 0 || offset+len(parsed.ReleaseGroups) >= parsed.Count { break } // Check context cancellation between pages for responsive shutdown. if err := ctx.Err(); err != nil { return nil, fmt.Errorf("fetch release groups for artist %s: %w", artistMBID, err) } offset += limit } return allGroups, nil } // FilterOptions holds per-artist type filtering preferences. type FilterOptions struct { IgnoreSingles bool IgnoreCompilations bool } // FilterReleaseGroups applies type filtering to a list of release groups. // It includes only Album/Single/EP primary types, or release groups whose // secondary type list contains Single/EP/Compilation (e.g. an "Album" that is // also a "Compilation"). The IgnoreSingles / IgnoreCompilations toggles drop // release groups classified as such via either primary or secondary type. // // Release groups carry no status in ws/2, so there is no status filtering. func FilterReleaseGroups(groups []ReleaseGroup, opts FilterOptions) []ReleaseGroup { var filtered []ReleaseGroup for _, rg := range groups { if !IsTypeIncluded(rg.Type) && !hasSecondaryType(rg, "Single", "EP", "Compilation") { continue } if opts.IgnoreSingles && (rg.Type == "Single" || hasSecondaryType(rg, "Single")) { continue } if opts.IgnoreCompilations && (rg.Type == "Compilation" || hasSecondaryType(rg, "Compilation")) { continue } filtered = append(filtered, rg) } return filtered } // hasSecondaryType reports whether any of the release group's secondary types // matches one of the provided values. func hasSecondaryType(rg ReleaseGroup, wanted ...string) bool { for _, s := range rg.SecondaryTypes { for _, w := range wanted { if s == w { return true } } } return false } // IsTypeIncluded returns true if the given primary type is in the base // included set (Album/Single/EP). func IsTypeIncluded(releaseType string) bool { return includedTypes[releaseType] } // ToExternalRelease converts a ReleaseGroup to an ExternalRelease for database // persistence. artistID is the canonical artist key from artist_settings (the // Navidrome artist ID), which is what external_releases.artist_id references and // what the scanner joins on. The MusicBrainz release-group's own ArtistID (an // MBID) must NOT be stored here, because artist_settings is keyed by the // Navidrome ID and the foreign key / join would otherwise never match. func (rg *ReleaseGroup) ToExternalRelease(artistID string) *database.ExternalRelease { // Only the primary type is persisted; secondary types are used transiently // for filtering above and are not stored in the external_releases schema. return &database.ExternalRelease{ RGID: rg.ID, ArtistID: artistID, Title: rg.Title, Type: rg.Type, ReleaseDate: rg.ReleaseDate, } }