From 1ca809e4ed35da17285cbdc00972a0f8aea6c422 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 12:49:33 +0000 Subject: [PATCH] feat(agents): add artist biography and album info for Tidal agent Implement additional metadata retrieval methods: - GetArtistBiography: Fetch artist bio from /artists/{id}/bio endpoint - GetAlbumInfo: Fetch album metadata including review/description from /albums/{id}/review endpoint New client methods: - getArtistBio: Get artist biography text - getAlbumReview: Get album review/description text The album info includes the album name, Tidal URL, and description (when available from the review endpoint). https://claude.ai/code/session_01P4bEnAgYS5dHuZBdsJ2XGy --- adapters/tidal/client.go | 64 ++++++++ .../tests/fixtures/tidal.album.review.json | 3 + .../tests/fixtures/tidal.artist.bio.json | 3 + adapters/tidal/tidal.go | 37 +++++ adapters/tidal/tidal_test.go | 143 ++++++++++++++++++ 5 files changed, 250 insertions(+) create mode 100644 adapters/tidal/tests/fixtures/tidal.album.review.json create mode 100644 adapters/tidal/tests/fixtures/tidal.artist.bio.json diff --git a/adapters/tidal/client.go b/adapters/tidal/client.go index 4c7d3cb2e..a4e13505b 100644 --- a/adapters/tidal/client.go +++ b/adapters/tidal/client.go @@ -250,6 +250,70 @@ func (c *client) searchTracks(ctx context.Context, trackName, artistName string, return result.Tracks, nil } +func (c *client) getArtistBio(ctx context.Context, artistID string) (string, error) { + token, err := c.getToken(ctx) + if err != nil { + return "", fmt.Errorf("failed to get token: %w", err) + } + + params := url.Values{} + params.Add("countryCode", "US") + + req, err := http.NewRequestWithContext(ctx, "GET", apiBaseURL+"/artists/"+artistID+"/bio", nil) + if err != nil { + return "", err + } + req.URL.RawQuery = params.Encode() + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Accept", "application/vnd.tidal.v1+json") + req.Header.Set("Content-Type", "application/vnd.tidal.v1+json") + + var result struct { + Text string `json:"text"` + } + err = c.makeRequest(req, &result) + if err != nil { + return "", err + } + + if result.Text == "" { + return "", ErrNotFound + } + return result.Text, nil +} + +func (c *client) getAlbumReview(ctx context.Context, albumID string) (string, error) { + token, err := c.getToken(ctx) + if err != nil { + return "", fmt.Errorf("failed to get token: %w", err) + } + + params := url.Values{} + params.Add("countryCode", "US") + + req, err := http.NewRequestWithContext(ctx, "GET", apiBaseURL+"/albums/"+albumID+"/review", nil) + if err != nil { + return "", err + } + req.URL.RawQuery = params.Encode() + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Accept", "application/vnd.tidal.v1+json") + req.Header.Set("Content-Type", "application/vnd.tidal.v1+json") + + var result struct { + Text string `json:"text"` + } + err = c.makeRequest(req, &result) + if err != nil { + return "", err + } + + if result.Text == "" { + return "", ErrNotFound + } + return result.Text, nil +} + func (c *client) getTrackRadio(ctx context.Context, trackID string, limit int) ([]TrackResource, error) { token, err := c.getToken(ctx) if err != nil { diff --git a/adapters/tidal/tests/fixtures/tidal.album.review.json b/adapters/tidal/tests/fixtures/tidal.album.review.json new file mode 100644 index 000000000..d6f77deb4 --- /dev/null +++ b/adapters/tidal/tests/fixtures/tidal.album.review.json @@ -0,0 +1,3 @@ +{ + "text": "Random Access Memories is the fourth studio album by French electronic music duo Daft Punk. It was released on 17 May 2013 through Columbia Records. The album pays tribute to the late 1970s and early 1980s era of music." +} diff --git a/adapters/tidal/tests/fixtures/tidal.artist.bio.json b/adapters/tidal/tests/fixtures/tidal.artist.bio.json new file mode 100644 index 000000000..bb3d5bc31 --- /dev/null +++ b/adapters/tidal/tests/fixtures/tidal.artist.bio.json @@ -0,0 +1,3 @@ +{ + "text": "Daft Punk was a French electronic music duo formed in Paris in 1993. The duo consisted of musicians Thomas Bangalter and Guy-Manuel de Homem-Christo. They achieved popularity in the late 1990s as part of the French house movement." +} diff --git a/adapters/tidal/tidal.go b/adapters/tidal/tidal.go index bf734057d..f29a5547f 100644 --- a/adapters/tidal/tidal.go +++ b/adapters/tidal/tidal.go @@ -129,6 +129,43 @@ func (t *tidalAgent) GetArtistURL(ctx context.Context, id, name, mbid string) (s return tidalArtistURLBase + artist.ID, nil } +func (t *tidalAgent) GetArtistBiography(ctx context.Context, id, name, mbid string) (string, error) { + artist, err := t.searchArtist(ctx, name) + if err != nil { + return "", err + } + + bio, err := t.client.getArtistBio(ctx, artist.ID) + if err != nil { + if errors.Is(err, ErrNotFound) { + return "", agents.ErrNotFound + } + log.Error(ctx, "Error getting artist bio from Tidal", "artist", name, err) + return "", err + } + + return bio, nil +} + +func (t *tidalAgent) GetAlbumInfo(ctx context.Context, name, artist, mbid string) (*agents.AlbumInfo, error) { + album, err := t.searchAlbum(ctx, name, artist) + if err != nil { + return nil, err + } + + // Try to get album review/description + description, err := t.client.getAlbumReview(ctx, album.ID) + if err != nil && !errors.Is(err, ErrNotFound) { + log.Warn(ctx, "Error getting album review from Tidal", "album", name, err) + } + + return &agents.AlbumInfo{ + Name: album.Attributes.Title, + Description: description, + URL: "https://tidal.com/browse/album/" + album.ID, + }, nil +} + func (t *tidalAgent) GetAlbumImages(ctx context.Context, name, artist, mbid string) ([]agents.ExternalImage, error) { album, err := t.searchAlbum(ctx, name, artist) if err != nil { diff --git a/adapters/tidal/tidal_test.go b/adapters/tidal/tidal_test.go index 969191627..1439bfecb 100644 --- a/adapters/tidal/tidal_test.go +++ b/adapters/tidal/tidal_test.go @@ -213,6 +213,123 @@ var _ = Describe("tidalAgent", func() { }) }) + Describe("GetArtistBiography", func() { + var agent *tidalAgent + var httpClient *mockHttpClient + + BeforeEach(func() { + httpClient = newMockHttpClient() + agent = &tidalAgent{ + ds: &tests.MockDataStore{}, + client: newClient("test-id", "test-secret", httpClient), + } + }) + + It("returns artist biography", func() { + // Mock token response + httpClient.tokenResponse = &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{"access_token":"test-token","token_type":"Bearer","expires_in":86400}`)), + } + + // Mock search response + fSearch, _ := os.Open("tests/fixtures/tidal.search.artist.json") + httpClient.searchResponse = &http.Response{Body: fSearch, StatusCode: 200} + + // Mock bio response + fBio, _ := os.Open("tests/fixtures/tidal.artist.bio.json") + httpClient.artistBioResponse = &http.Response{Body: fBio, StatusCode: 200} + + bio, err := agent.GetArtistBiography(ctx, "", "Daft Punk", "") + + Expect(err).ToNot(HaveOccurred()) + Expect(bio).To(ContainSubstring("French electronic music duo")) + }) + + It("returns ErrNotFound when bio is empty", func() { + // Mock token response + httpClient.tokenResponse = &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{"access_token":"test-token","token_type":"Bearer","expires_in":86400}`)), + } + + // Mock search response + fSearch, _ := os.Open("tests/fixtures/tidal.search.artist.json") + httpClient.searchResponse = &http.Response{Body: fSearch, StatusCode: 200} + + // Mock empty bio response + httpClient.artistBioResponse = &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{"text":""}`)), + } + + _, err := agent.GetArtistBiography(ctx, "", "Daft Punk", "") + + Expect(err).To(MatchError(agents.ErrNotFound)) + }) + }) + + Describe("GetAlbumInfo", func() { + var agent *tidalAgent + var httpClient *mockHttpClient + + BeforeEach(func() { + httpClient = newMockHttpClient() + agent = &tidalAgent{ + ds: &tests.MockDataStore{}, + client: newClient("test-id", "test-secret", httpClient), + } + }) + + It("returns album info with description", func() { + // Mock token response + httpClient.tokenResponse = &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{"access_token":"test-token","token_type":"Bearer","expires_in":86400}`)), + } + + // Mock album search response + fAlbum, _ := os.Open("tests/fixtures/tidal.search.album.json") + httpClient.albumSearchResponse = &http.Response{Body: fAlbum, StatusCode: 200} + + // Mock album review response + fReview, _ := os.Open("tests/fixtures/tidal.album.review.json") + httpClient.albumReviewResponse = &http.Response{Body: fReview, StatusCode: 200} + + info, err := agent.GetAlbumInfo(ctx, "Random Access Memories", "Daft Punk", "") + + Expect(err).ToNot(HaveOccurred()) + Expect(info.Name).To(Equal("Random Access Memories")) + Expect(info.Description).To(ContainSubstring("fourth studio album")) + Expect(info.URL).To(Equal("https://tidal.com/browse/album/28048252")) + }) + + It("returns album info without description when review not available", func() { + // Mock token response + httpClient.tokenResponse = &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{"access_token":"test-token","token_type":"Bearer","expires_in":86400}`)), + } + + // Mock album search response + fAlbum, _ := os.Open("tests/fixtures/tidal.search.album.json") + httpClient.albumSearchResponse = &http.Response{Body: fAlbum, StatusCode: 200} + + // Mock empty album review response + httpClient.albumReviewResponse = &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{"text":""}`)), + } + + info, err := agent.GetAlbumInfo(ctx, "Random Access Memories", "Daft Punk", "") + + Expect(err).ToNot(HaveOccurred()) + Expect(info.Name).To(Equal("Random Access Memories")) + Expect(info.Description).To(BeEmpty()) + Expect(info.URL).To(Equal("https://tidal.com/browse/album/28048252")) + }) + }) + Describe("GetAlbumImages", func() { var agent *tidalAgent var httpClient *mockHttpClient @@ -409,6 +526,8 @@ type mockHttpClient struct { albumSearchResponse *http.Response trackSearchResponse *http.Response artistResponse *http.Response + artistBioResponse *http.Response + albumReviewResponse *http.Response similarResponse *http.Response tracksResponse *http.Response trackRadioResponse *http.Response @@ -478,6 +597,16 @@ func (c *mockHttpClient) Do(req *http.Request) (*http.Response, error) { // Handle artist request if req.URL.Host == "openapi.tidal.com" && len(req.URL.Path) > 9 && req.URL.Path[:9] == "/artists/" { + // Check if it's a bio request + if len(req.URL.Path) > 13 && req.URL.Path[len(req.URL.Path)-4:] == "/bio" { + if c.artistBioResponse != nil { + return c.artistBioResponse, nil + } + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{"text":""}`)), + }, nil + } // Check if it's a similar artists or tracks request if len(req.URL.Path) > 17 && req.URL.Path[len(req.URL.Path)-8:] == "/similar" { if c.similarResponse != nil { @@ -508,5 +637,19 @@ func (c *mockHttpClient) Do(req *http.Request) (*http.Response, error) { }, nil } + // Handle album request + if req.URL.Host == "openapi.tidal.com" && len(req.URL.Path) > 8 && req.URL.Path[:8] == "/albums/" { + // Check if it's a review request + if len(req.URL.Path) > 15 && req.URL.Path[len(req.URL.Path)-7:] == "/review" { + if c.albumReviewResponse != nil { + return c.albumReviewResponse, nil + } + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{"text":""}`)), + }, nil + } + } + panic("URL not mocked: " + req.URL.String()) }