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
This commit is contained in:
Claude 2026-02-02 12:49:33 +00:00
commit 1ca809e4ed
No known key found for this signature in database
5 changed files with 250 additions and 0 deletions

View file

@ -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 {

View file

@ -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."
}

View file

@ -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."
}

View file

@ -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 {

View file

@ -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())
}