diff --git a/internal/spotify/connect_mapping.go b/internal/spotify/connect_mapping.go index cccd149..3d0b0d8 100644 --- a/internal/spotify/connect_mapping.go +++ b/internal/spotify/connect_mapping.go @@ -22,6 +22,18 @@ func extractSearchItems(payload map[string]any, kind string) ([]Item, int) { } func extractItemFromPayload(payload map[string]any, kind string) (Item, bool) { + if kind == "track" { + if m, ok := getMap(payload, "data", "trackUnion"); ok { + if item, ok := extractItem(m, kind); ok { + return item, true + } + } + if m, ok := getMap(payload, "data", "track"); ok { + if item, ok := extractItem(m, kind); ok { + return item, true + } + } + } items := collectItemsByKind(payload, kind) if len(items) == 0 { return Item{}, false @@ -99,6 +111,11 @@ func extractItem(value any, kind string) (Item, bool) { if !ok { return Item{}, false } + if kind == "track" { + if inner, ok := m["track"].(map[string]any); ok { + m = inner + } + } uri := getString(m, "uri") if uri == "" && kind != "" { if id := getString(m, "id"); id != "" { @@ -217,25 +234,106 @@ func findFirstName(value any) string { func extractArtistNames(value any) []string { artists := []string{} - walkMap(value, func(m map[string]any) { - if list, ok := m["artists"].([]any); ok { - for _, entry := range list { - if name := findFirstName(entry); name != "" { - artists = append(artists, name) - } - } + m, ok := value.(map[string]any) + if !ok { + return nil + } + if list, ok := m["artists"].([]any); ok { + appendArtistNames(&artists, list) + } + if group, ok := m["artists"].(map[string]any); ok { + if list, ok := group["items"].([]any); ok { + appendArtistNames(&artists, list) } - }) + if list, ok := group["nodes"].([]any); ok { + appendArtistNames(&artists, list) + } + if list, ok := group["edges"].([]any); ok { + appendArtistNames(&artists, list) + } + } + if group, ok := m["firstArtist"].(map[string]any); ok { + if list, ok := group["items"].([]any); ok { + appendArtistNames(&artists, list) + } + if list, ok := group["nodes"].([]any); ok { + appendArtistNames(&artists, list) + } + if list, ok := group["edges"].([]any); ok { + appendArtistNames(&artists, list) + } + } + if group, ok := m["otherArtists"].(map[string]any); ok { + if list, ok := group["items"].([]any); ok { + appendArtistNames(&artists, list) + } + if list, ok := group["nodes"].([]any); ok { + appendArtistNames(&artists, list) + } + if list, ok := group["edges"].([]any); ok { + appendArtistNames(&artists, list) + } + } if len(artists) == 0 { - if m, ok := value.(map[string]any); ok { - if name := getString(m, "artistName"); name != "" { - artists = append(artists, name) - } + if name := getString(m, "artistName"); name != "" { + artists = append(artists, name) } } return dedupeStrings(artists) } +func appendArtistNames(artists *[]string, entries []any) { + for _, entry := range entries { + if name := artistNameFromValue(entry); name != "" { + *artists = append(*artists, name) + continue + } + } +} + +func artistNameFromValue(value any) string { + m, ok := value.(map[string]any) + if !ok { + return "" + } + if profile, ok := m["profile"].(map[string]any); ok { + if name := getString(profile, "name"); name != "" { + return name + } + } + if node, ok := m["node"].(map[string]any); ok { + if name := artistNameFromValue(node); name != "" { + return name + } + } + if artist, ok := m["artist"].(map[string]any); ok { + if name := artistNameFromValue(artist); name != "" { + return name + } + } + name := getString(m, "name") + if name == "" { + return "" + } + if len(m) == 1 || isArtistMap(m) || getString(m, "id") != "" { + return name + } + return "" +} + +func isArtistMap(m map[string]any) bool { + if uri := getString(m, "uri"); strings.HasPrefix(uri, "spotify:artist:") { + return true + } + if typ := getString(m, "type"); typ == "artist" { + return true + } + if _, ok := m["profile"]; ok { + return true + } + return false +} + func extractAlbumName(value any) string { var album string walkMap(value, func(m map[string]any) { diff --git a/internal/spotify/connect_mapping_test.go b/internal/spotify/connect_mapping_test.go index 24580ca..41040ad 100644 --- a/internal/spotify/connect_mapping_test.go +++ b/internal/spotify/connect_mapping_test.go @@ -62,6 +62,144 @@ func TestExtractItemFallbacks(t *testing.T) { } } +func TestExtractItemArtistsContainers(t *testing.T) { + raw := map[string]any{ + "uri": "spotify:track:abc", + "name": "Song", + "artists": map[string]any{ + "items": []any{ + map[string]any{"name": "Artist One"}, + map[string]any{"name": "Artist Two"}, + }, + }, + } + item, ok := extractItem(raw, "track") + if !ok { + t.Fatalf("expected item") + } + if len(item.Artists) != 2 || item.Artists[0] != "Artist One" || item.Artists[1] != "Artist Two" { + t.Fatalf("unexpected artists: %#v", item.Artists) + } +} + +func TestExtractItemArtistsEdges(t *testing.T) { + raw := map[string]any{ + "uri": "spotify:track:abc", + "name": "Song", + "artists": map[string]any{ + "edges": []any{ + map[string]any{"node": map[string]any{"name": "Artist One"}}, + map[string]any{"node": map[string]any{"name": "Artist Two"}}, + }, + }, + } + item, ok := extractItem(raw, "track") + if !ok { + t.Fatalf("expected item") + } + if len(item.Artists) != 2 || item.Artists[0] != "Artist One" || item.Artists[1] != "Artist Two" { + t.Fatalf("unexpected artists: %#v", item.Artists) + } +} + +func TestExtractItemFirstArtistItems(t *testing.T) { + raw := map[string]any{ + "uri": "spotify:track:abc", + "name": "Song", + "firstArtist": map[string]any{ + "items": []any{ + map[string]any{"profile": map[string]any{"name": "Artist One"}}, + }, + }, + } + item, ok := extractItem(raw, "track") + if !ok { + t.Fatalf("expected item") + } + if len(item.Artists) != 1 || item.Artists[0] != "Artist One" { + t.Fatalf("unexpected artists: %#v", item.Artists) + } +} + +func TestExtractItemOtherArtistsItems(t *testing.T) { + raw := map[string]any{ + "uri": "spotify:track:abc", + "name": "Song", + "firstArtist": map[string]any{ + "items": []any{ + map[string]any{"profile": map[string]any{"name": "Artist One"}}, + }, + }, + "otherArtists": map[string]any{ + "items": []any{ + map[string]any{"profile": map[string]any{"name": "Artist Two"}}, + }, + }, + } + item, ok := extractItem(raw, "track") + if !ok { + t.Fatalf("expected item") + } + if len(item.Artists) != 2 || item.Artists[0] != "Artist One" || item.Artists[1] != "Artist Two" { + t.Fatalf("unexpected artists: %#v", item.Artists) + } +} +func TestExtractItemFromPayloadPrefersTrackUnion(t *testing.T) { + payload := map[string]any{ + "data": map[string]any{ + "trackUnion": map[string]any{ + "uri": "spotify:track:primary", + "name": "Primary", + "artists": []any{ + map[string]any{"name": "Main Artist"}, + }, + }, + "track": map[string]any{ + "uri": "spotify:track:secondary", + "name": "Secondary", + "artists": []any{ + map[string]any{"name": "Wrong Artist"}, + }, + }, + "other": map[string]any{ + "items": []any{ + map[string]any{ + "uri": "spotify:track:secondary", + "name": "Secondary", + "artists": []any{ + map[string]any{"name": "Wrong Artist"}, + }, + }, + }, + }, + }, + } + item, ok := extractItemFromPayload(payload, "track") + if !ok { + t.Fatalf("expected item") + } + if item.ID != "primary" || len(item.Artists) != 1 || item.Artists[0] != "Main Artist" { + t.Fatalf("unexpected item: %#v", item) + } +} + +func TestExtractItemArtistsIDName(t *testing.T) { + raw := map[string]any{ + "uri": "spotify:track:abc", + "name": "Song", + "artists": []any{ + map[string]any{"id": "ar1", "name": "Artist One"}, + }, + } + item, ok := extractItem(raw, "track") + if !ok { + t.Fatalf("expected item") + } + if len(item.Artists) != 1 || item.Artists[0] != "Artist One" { + t.Fatalf("unexpected artists: %#v", item.Artists) + } +} + func TestExtractSearchItemsFallback(t *testing.T) { payload := map[string]any{ "data": map[string]any{