package musicbrainz import ( "context" "fmt" "net/url" "naviwatcher/internal/database" "naviwatcher/internal/normalize" ) // excludedStatuses contains release-group statuses that should be filtered out. var excludedStatuses = map[string]bool{ "Bootleg": true, "Promotion": true, "Pseudo-Release": true, } // includedTypes contains release-group types that should be included. var includedTypes = map[string]bool{ "Album": true, "Single": true, "EP": true, "Compilation": 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 := buildPath("/release-group", params) 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 status and type filtering to a list of release groups. // It excludes Bootleg, Promotion, and Pseudo-Release statuses. // It includes only Album, Single, EP, and Compilation types, unless the type // is disabled via FilterOptions. func FilterReleaseGroups(groups []ReleaseGroup, opts FilterOptions) []ReleaseGroup { var filtered []ReleaseGroup for _, rg := range groups { if IsStatusExcluded(rg.Status) { continue } if !IsTypeIncluded(rg.Type) { continue } if opts.IgnoreSingles && rg.Type == "Single" { continue } if opts.IgnoreCompilations && rg.Type == "Compilation" { continue } filtered = append(filtered, rg) } return filtered } // IsStatusExcluded returns true if the given status should be excluded. func IsStatusExcluded(status string) bool { return excludedStatuses[status] } // IsTypeIncluded returns true if the given type is in the base included set. func IsTypeIncluded(releaseType string) bool { return includedTypes[releaseType] } // NormalizeString normalizes a string for fuzzy matching. // It delegates to the shared normalize package; see normalize.NormalizeString // for the full normalization contract. func NormalizeString(s string) string { return normalize.NormalizeString(s) } // NormalizeArtistName normalizes an artist name for comparison. // It delegates to the shared normalize package; see // normalize.NormalizeArtistName for the full normalization contract. func NormalizeArtistName(name string) string { return normalize.NormalizeArtistName(name) } // ToExternalRelease converts a ReleaseGroup to an ExternalRelease // for database persistence. func (rg *ReleaseGroup) ToExternalRelease() *database.ExternalRelease { return &database.ExternalRelease{ RGID: rg.ID, ArtistID: rg.ArtistID, Title: rg.Title, Type: rg.Type, ReleaseDate: rg.ReleaseDate, } }