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