mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-10 03:17:27 +02:00
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:
parent
89d4d68304
commit
1ca809e4ed
5 changed files with 250 additions and 0 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
3
adapters/tidal/tests/fixtures/tidal.album.review.json
vendored
Normal file
3
adapters/tidal/tests/fixtures/tidal.album.review.json
vendored
Normal 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."
|
||||
}
|
||||
3
adapters/tidal/tests/fixtures/tidal.artist.bio.json
vendored
Normal file
3
adapters/tidal/tests/fixtures/tidal.artist.bio.json
vendored
Normal 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."
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue