diff --git a/internal/provider/beatport/client.go b/internal/provider/beatport/client.go index dc0f126..a258306 100644 --- a/internal/provider/beatport/client.go +++ b/internal/provider/beatport/client.go @@ -480,14 +480,16 @@ func (c *Client) rawRequest(ctx context.Context, method, endpoint string, params func (c *Client) normalizeRelease(raw map[string]any, tracks []any) map[string]any { id := jsonutil.StringFromAny(raw["id"]) - artist := c.releaseArtistName(raw["artists"]) + artist := c.releaseArtistNameForTracks(raw["artists"], tracks) + trackRelease := cloneMap(raw) + trackRelease["album_artist_name"] = artist items := make([]any, 0, len(tracks)) for _, entry := range tracks { track, ok := entry.(map[string]any) if !ok { continue } - track["release"] = raw + track["release"] = trackRelease items = append(items, c.normalizeTrack(track)) } return map[string]any{ @@ -510,7 +512,10 @@ func (c *Client) normalizeTrack(raw map[string]any) map[string]any { trackArtistNames := artistNames(raw["artists"]) artistName := displayArtistNames(trackArtistNames) artistID := firstArtistID(raw["artists"]) - albumArtist := c.releaseArtistName(release["artists"]) + albumArtist := strings.TrimSpace(jsonutil.StringFromAny(release["album_artist_name"])) + if albumArtist == "" { + albumArtist = c.releaseArtistName(release["artists"]) + } if albumArtist == "" { albumArtist = artistName } @@ -693,6 +698,71 @@ func (c *Client) releaseArtistName(v any) string { return displayArtistNames(artistNames(v)) } +func (c *Client) releaseArtistNameForTracks(releaseArtists any, tracks []any) string { + common := commonReleaseArtists(releaseArtists, tracks) + if len(common) > 0 { + return displayArtistNames(artistNames(common)) + } + return c.releaseArtistName(releaseArtists) +} + +func commonReleaseArtists(releaseArtists any, tracks []any) []any { + items := sliceAny(releaseArtists) + if len(items) == 0 || len(tracks) == 0 { + return nil + } + counts := map[string]int{} + trackCount := 0 + for _, rawTrack := range tracks { + track, ok := rawTrack.(map[string]any) + if !ok { + continue + } + trackCount++ + seen := map[string]bool{} + for _, rawArtist := range sliceAny(track["artists"]) { + if key := artistIdentity(rawArtist); key != "" { + seen[key] = true + } + } + for key := range seen { + counts[key]++ + } + } + if trackCount == 0 { + return nil + } + common := make([]any, 0) + for _, rawArtist := range items { + if key := artistIdentity(rawArtist); key != "" && counts[key] == trackCount { + common = append(common, rawArtist) + } + } + return common +} + +func artistIdentity(raw any) string { + m, ok := raw.(map[string]any) + if !ok { + return "" + } + if id := strings.TrimSpace(jsonutil.StringFromAny(m["id"])); id != "" { + return "id:" + id + } + if name := strings.TrimSpace(jsonutil.StringFromAny(m["name"])); name != "" { + return "name:" + strings.ToLower(name) + } + return "" +} + +func cloneMap(in map[string]any) map[string]any { + out := make(map[string]any, len(in)) + for k, v := range in { + out[k] = v + } + return out +} + func firstArtistID(v any) string { items := sliceAny(v) if len(items) == 0 { diff --git a/internal/provider/beatport/client_test.go b/internal/provider/beatport/client_test.go index 4ed98ad..389344e 100644 --- a/internal/provider/beatport/client_test.go +++ b/internal/provider/beatport/client_test.go @@ -114,7 +114,7 @@ func TestAlbumMetadataCollapsesManyReleaseArtists(t *testing.T) { writeJSON(t, w, map[string]any{ "id": 7, "name": "Compilation", - "track_count": 1, + "track_count": 2, "artists": []any{ map[string]any{"id": 1, "name": "A"}, map[string]any{"id": 2, "name": "B"}, @@ -125,12 +125,10 @@ func TestAlbumMetadataCollapsesManyReleaseArtists(t *testing.T) { case "/catalog/releases/7/tracks/": writeJSON(t, w, map[string]any{ "next": nil, - "results": []any{map[string]any{ - "id": 42, - "name": "Track Name", - "number": 1, - "artists": []any{map[string]any{"id": 1, "name": "A"}}, - }}, + "results": []any{ + map[string]any{"id": 42, "name": "Track A", "number": 1, "artists": []any{map[string]any{"id": 1, "name": "A"}}}, + map[string]any{"id": 43, "name": "Track B", "number": 2, "artists": []any{map[string]any{"id": 2, "name": "B"}}}, + }, }) default: t.Fatalf("unexpected path %q", r.URL.Path) @@ -154,6 +152,52 @@ func TestAlbumMetadataCollapsesManyReleaseArtists(t *testing.T) { } } +func TestAlbumMetadataUsesArtistPresentOnEveryTrack(t *testing.T) { + c, closeServer := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/catalog/releases/7/": + writeJSON(t, w, map[string]any{ + "id": 7, + "name": "Artist Album", + "track_count": 3, + "artists": []any{ + map[string]any{"id": 1, "name": "Main"}, + map[string]any{"id": 2, "name": "Guest A"}, + map[string]any{"id": 3, "name": "Guest B"}, + map[string]any{"id": 4, "name": "Guest C"}, + }, + }) + case "/catalog/releases/7/tracks/": + writeJSON(t, w, map[string]any{ + "next": nil, + "results": []any{ + map[string]any{"id": 42, "name": "Track A", "number": 1, "artists": []any{map[string]any{"id": 1, "name": "Main"}, map[string]any{"id": 2, "name": "Guest A"}}}, + map[string]any{"id": 43, "name": "Track B", "number": 2, "artists": []any{map[string]any{"id": 1, "name": "Main"}, map[string]any{"id": 3, "name": "Guest B"}}}, + map[string]any{"id": 44, "name": "Track C", "number": 3, "artists": []any{map[string]any{"id": 1, "name": "Main"}, map[string]any{"id": 4, "name": "Guest C"}}}, + }, + }) + default: + t.Fatalf("unexpected path %q", r.URL.Path) + } + })) + defer closeServer() + + meta, err := c.GetMetadata(context.Background(), "7", "album") + if err != nil { + t.Fatalf("GetMetadata() error = %v", err) + } + artist := meta["artist"].(map[string]any) + if artist["name"] != "Main" { + t.Fatalf("album artist = %q, want Main", artist["name"]) + } + track := meta["tracks"].(map[string]any)["items"].([]any)[0].(map[string]any) + album := track["album"].(map[string]any) + albumArtist := album["artist"].(map[string]any) + if albumArtist["name"] != "Main" { + t.Fatalf("track album artist = %q, want Main", albumArtist["name"]) + } +} + func TestReleaseArtistCollapseCanBeDisabled(t *testing.T) { cfg := &config.Config{Session: config.DefaultConfigData(), File: config.DefaultConfigData()} cfg.Session.Beatport.VariousArtistsThreshold = -1