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...) // Stop when a page is empty (no more results) or when the page // returned fewer items than the request limit — a reliable end-of-data // signal. We intentionally do NOT trust parsed.Count for the cutoff: // MusicBrainz occasionally reports an inaccurate count, which would // prematurely truncate an artist's discography and hide missing // releases. The empty-page check also prevents an infinite loop if the // API keeps returning a non-empty page past the reported count. if len(parsed.ReleaseGroups) == 0 || len(parsed.ReleaseGroups) < limit { 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 } // 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. // 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 { // The primary type and the secondary types are both persisted so that the // cache-hit path in SyncArtistDiscography can re-apply the same // IgnoreSingles / IgnoreCompilations rules (which consider secondary types) // as the cache-miss path, keeping results stable across cache refreshes. return &database.ExternalRelease{ RGID: rg.ID, ArtistID: artistID, Title: rg.Title, ReleaseDate: rg.ReleaseDate, Type: rg.Type, SecondaryTypes: rg.SecondaryTypes, } } // 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 { // First filter by base allowed types (Album/Single/EP) or those with Single/EP/Compilation as secondary type var preFiltered []ReleaseGroup for _, rg := range groups { if !isTypeIncluded(rg.Type) && !hasSliceType(rg.SecondaryTypes, "Single", "EP", "Compilation") { continue } preFiltered = append(preFiltered, rg) } // Then apply the IgnoreSingles/IgnoreCompilations toggles using the centralized logic return ApplyTypeTogglesToReleaseGroups(preFiltered, opts) } // 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] }