Files
NaviWatcher/internal/musicbrainz/api.go

140 lines
4.1 KiB
Go

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,
}
}