package musicbrainz import ( "context" "encoding/json" "fmt" "strings" "github.com/metadata-agregator/internal/domain" ) type Provider struct { client *client } func New() *Provider { return &Provider{ client: newClient(), } } func (p *Provider) Name() string { return "musicbrainz" } func (p *Provider) GetArtist(ctx context.Context, id string) (*domain.Artist, error) { data, err := p.client.lookup(ctx, "artist", id, []string{"genres", "url-rels"}) if err != nil { return nil, fmt.Errorf("lookup artist: %w", err) } mb, err := decode[mbArtist](data) if err != nil { return nil, err } return mapArtist(mb), nil } func (p *Provider) SearchArtists(ctx context.Context, query string, limit, offset int) (*domain.SearchResult[domain.Artist], error) { if limit <= 0 || limit > 100 { limit = 25 } escapedQuery := escapeQuery(query) data, err := p.client.search(ctx, "artist", fmt.Sprintf("artist:%s", escapedQuery), limit, offset) if err != nil { return nil, fmt.Errorf("search artists: %w", err) } var resp struct { Count int `json:"count"` Offset int `json:"offset"` Artists []*mbArtist `json:"artists"` } if err := decodeInto(data, &resp); err != nil { return nil, err } result := &domain.SearchResult[domain.Artist]{ Total: resp.Count, Limit: limit, Offset: offset, } for _, mb := range resp.Artists { if artist := mapArtist(mb); artist != nil { result.Items = append(result.Items, *artist) } } return result, nil } func (p *Provider) GetAlbum(ctx context.Context, id string) (*domain.Album, error) { data, err := p.client.lookup(ctx, "release-group", id, []string{"releases", "artist-credits", "genres"}) if err != nil { return nil, fmt.Errorf("lookup release-group: %w", err) } mb, err := decode[mbReleaseGroup](data) if err != nil { return nil, err } var release *mbRelease if len(mb.Releases) > 0 { release = selectCanonicalRelease(mb.Releases) } return mapAlbum(mb, release), nil } func (p *Provider) SearchAlbums(ctx context.Context, query string, artist string, limit, offset int) (*domain.SearchResult[domain.Album], error) { if limit <= 0 || limit > 100 { limit = 25 } var luceneQuery string if artist != "" && query != "" { luceneQuery = fmt.Sprintf("releasegroup:%s AND artist:%s", escapeQuery(query), escapeQuery(artist)) } else if artist != "" { luceneQuery = fmt.Sprintf("artist:%s", escapeQuery(artist)) } else { luceneQuery = fmt.Sprintf("releasegroup:%s", escapeQuery(query)) } data, err := p.client.search(ctx, "release-group", luceneQuery, limit, offset) if err != nil { return nil, fmt.Errorf("search albums: %w", err) } var resp struct { Count int `json:"count"` Offset int `json:"offset"` ReleaseGroups []*mbReleaseGroup `json:"release-groups"` } if err := decodeInto(data, &resp); err != nil { return nil, err } result := &domain.SearchResult[domain.Album]{ Total: resp.Count, Limit: limit, Offset: offset, } for _, mb := range resp.ReleaseGroups { if album := mapAlbum(mb, nil); album != nil { result.Items = append(result.Items, *album) } } return result, nil } func (p *Provider) GetArtistAlbums(ctx context.Context, artistID string, limit, offset int) (*domain.SearchResult[domain.Album], error) { if limit <= 0 || limit > 100 { limit = 25 } data, err := p.client.browse(ctx, "release-group", "artist", artistID, limit, offset, []string{"artist-credits"}) if err != nil { return nil, fmt.Errorf("browse release-groups: %w", err) } var resp struct { ReleaseGroupCount int `json:"release-group-count"` ReleaseGroupOffset int `json:"release-group-offset"` ReleaseGroups []*mbReleaseGroup `json:"release-groups"` } if err := decodeInto(data, &resp); err != nil { return nil, err } result := &domain.SearchResult[domain.Album]{ Total: resp.ReleaseGroupCount, Limit: limit, Offset: resp.ReleaseGroupOffset, } for _, mb := range resp.ReleaseGroups { if album := mapAlbum(mb, nil); album != nil { result.Items = append(result.Items, *album) } } return result, nil } func (p *Provider) GetTrack(ctx context.Context, id string) (*domain.Track, error) { data, err := p.client.lookup(ctx, "recording", id, []string{"artist-credits", "isrcs", "work-rels"}) if err != nil { return nil, fmt.Errorf("lookup recording: %w", err) } mb, err := decode[mbRecording](data) if err != nil { return nil, err } return mapTrack(mb, 0, 0), nil } func (p *Provider) GetAlbumTracks(ctx context.Context, albumID string) ([]domain.Track, error) { data, err := p.client.browse(ctx, "release", "release-group", albumID, 100, 0, nil) if err != nil { return nil, fmt.Errorf("browse releases: %w", err) } var resp struct { Releases []*mbRelease `json:"releases"` } if err := decodeInto(data, &resp); err != nil { return nil, err } if len(resp.Releases) == 0 { return nil, ErrNotFound } release := selectCanonicalRelease(resp.Releases) releaseData, err := p.client.lookup(ctx, "release", release.ID, []string{"recordings", "artist-credits", "isrcs"}) if err != nil { return nil, fmt.Errorf("lookup release: %w", err) } fullRelease, err := decode[mbRelease](releaseData) if err != nil { return nil, err } var tracks []domain.Track for _, medium := range fullRelease.Media { for _, t := range medium.Tracks { if track := mapTrack(&t.Recording, medium.Position, t.Position); track != nil { tracks = append(tracks, *track) } } } return tracks, nil } func (p *Provider) GetTrackByISRC(ctx context.Context, isrc string) (*domain.Track, error) { data, err := p.client.get(ctx, fmt.Sprintf("isrc/%s", isrc), nil) if err != nil { return nil, fmt.Errorf("lookup isrc: %w", err) } var resp struct { Recordings []*mbRecording `json:"recordings"` } if err := decodeInto(data, &resp); err != nil { return nil, err } if len(resp.Recordings) == 0 { return nil, ErrNotFound } return p.GetTrack(ctx, resp.Recordings[0].ID) } func (p *Provider) GetLabel(ctx context.Context, id string) (*domain.Label, error) { data, err := p.client.lookup(ctx, "label", id, nil) if err != nil { return nil, fmt.Errorf("lookup label: %w", err) } mb, err := decode[mbLabel](data) if err != nil { return nil, err } return mapLabel(mb), nil } func (p *Provider) GetWork(ctx context.Context, id string) (*domain.Work, error) { data, err := p.client.lookup(ctx, "work", id, []string{"artist-rels"}) if err != nil { return nil, fmt.Errorf("lookup work: %w", err) } mb, err := decode[mbWork](data) if err != nil { return nil, err } return mapWork(mb), nil } func selectCanonicalRelease(releases []*mbRelease) *mbRelease { if len(releases) == 0 { return nil } var best *mbRelease bestScore := -1 for _, r := range releases { score := 0 switch r.Status { case "Official": score += 100 case "Promotion": score += 50 } if len(r.Media) > 0 { switch r.Media[0].Format { case "Digital Media": score += 20 case "CD": score += 15 } } if r.Barcode != "" { score += 5 } if score > bestScore { bestScore = score best = r } } return best } func escapeQuery(s string) string { special := []string{`+`, `-`, `&`, `|`, `!`, `(`, `)`, `{`, `}`, `[`, `]`, `^`, `"`, `~`, `*`, `?`, `:`, `/`, `\`} result := s for _, char := range special { result = strings.ReplaceAll(result, char, `\`+char) } return `"` + result + `"` } func decodeInto(data []byte, v any) error { return json.Unmarshal(data, v) }