136 lines
4.6 KiB
Go
136 lines
4.6 KiB
Go
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 {
|
|
// 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,
|
|
Type: rg.Type,
|
|
ReleaseDate: rg.ReleaseDate,
|
|
SecondaryTypes: rg.SecondaryTypes,
|
|
}
|
|
}
|