feat(listenbrainz): add SimilarArtists client method
This commit is contained in:
@@ -233,3 +233,56 @@ func (c *Client) SimilarRecordings(ctx context.Context, mbid string, limit int)
|
||||
return nil, fmt.Errorf("%w: status %d", ErrPermanent, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// LB algorithm parameter for /explore/similar-artists/. Hardcoded for v1
|
||||
// — matches LB's documented default.
|
||||
const lbSimilarArtistsAlgorithm = "session_based_days_7500_session_30_contribution_5_threshold_15_limit_100_filter_True_skip_30"
|
||||
|
||||
// SimilarArtist is one entry in the /explore/similar-artists response.
|
||||
type SimilarArtist struct {
|
||||
MBID string `json:"artist_mbid"`
|
||||
Score float64 `json:"score"`
|
||||
}
|
||||
|
||||
// SimilarArtists fetches up to `limit` similar artists for the given
|
||||
// artist MBID. Public endpoint — no token required. Returns the same
|
||||
// typed errors as SubmitListens.
|
||||
func (c *Client) SimilarArtists(ctx context.Context, mbid string, limit int) ([]SimilarArtist, error) {
|
||||
base := c.BaseURL
|
||||
if base == "" {
|
||||
base = defaultBaseURL
|
||||
}
|
||||
url := fmt.Sprintf("%s/1/explore/similar-artists/%s?algorithm=%s&count=%d",
|
||||
base, mbid, lbSimilarArtistsAlgorithm, limit)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listenbrainz: build similar-artists request: %w", err)
|
||||
}
|
||||
|
||||
httpClient := c.HTTP
|
||||
if httpClient == nil {
|
||||
httpClient = http.DefaultClient
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrTransient, err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
switch {
|
||||
case resp.StatusCode >= 200 && resp.StatusCode < 300:
|
||||
var out []SimilarArtist
|
||||
if derr := json.NewDecoder(resp.Body).Decode(&out); derr != nil {
|
||||
return nil, fmt.Errorf("%w: decode similar-artists: %v", ErrTransient, derr)
|
||||
}
|
||||
return out, nil
|
||||
case resp.StatusCode == http.StatusUnauthorized:
|
||||
return nil, ErrAuth
|
||||
case resp.StatusCode == http.StatusTooManyRequests:
|
||||
return nil, &RetryAfterError{Wait: parseRetryAfter(resp.Header.Get("Retry-After"))}
|
||||
case resp.StatusCode >= 500:
|
||||
return nil, fmt.Errorf("%w: status %d", ErrTransient, resp.StatusCode)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: status %d", ErrPermanent, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,3 +257,98 @@ func TestClient_SimilarRecordings_429_ReturnsRetryAfter(t *testing.T) {
|
||||
t.Errorf("Wait = %v, want 120s", ra.Wait)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SimilarArtists_Success(t *testing.T) {
|
||||
c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte(`[
|
||||
{"artist_mbid": "33333333-3333-3333-3333-333333333333", "score": 0.91},
|
||||
{"artist_mbid": "44444444-4444-4444-4444-444444444444", "score": 0.72}
|
||||
]`))
|
||||
})
|
||||
defer srv.Close()
|
||||
out, err := c.SimilarArtists(context.Background(), "aaaa", 100)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if len(out) != 2 {
|
||||
t.Fatalf("got %d, want 2", len(out))
|
||||
}
|
||||
if out[0].MBID != "33333333-3333-3333-3333-333333333333" || out[0].Score != 0.91 {
|
||||
t.Errorf("first result wrong: %+v", out[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SimilarArtists_AlgorithmParamSet(t *testing.T) {
|
||||
var seenURL string
|
||||
c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) {
|
||||
seenURL = r.URL.String()
|
||||
_, _ = w.Write([]byte(`[]`))
|
||||
})
|
||||
defer srv.Close()
|
||||
_, _ = c.SimilarArtists(context.Background(), "abc", 50)
|
||||
if !strings.Contains(seenURL, "algorithm=") {
|
||||
t.Errorf("URL missing algorithm param: %q", seenURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SimilarArtists_LimitParamSet(t *testing.T) {
|
||||
var seenURL string
|
||||
c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) {
|
||||
seenURL = r.URL.String()
|
||||
_, _ = w.Write([]byte(`[]`))
|
||||
})
|
||||
defer srv.Close()
|
||||
_, _ = c.SimilarArtists(context.Background(), "abc", 50)
|
||||
if !strings.Contains(seenURL, "count=50") && !strings.Contains(seenURL, "limit=50") {
|
||||
t.Errorf("URL missing count/limit param: %q", seenURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SimilarArtists_401_ReturnsErrAuth(t *testing.T) {
|
||||
c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
})
|
||||
defer srv.Close()
|
||||
_, err := c.SimilarArtists(context.Background(), "abc", 100)
|
||||
if !errors.Is(err, ErrAuth) {
|
||||
t.Errorf("err = %v, want ErrAuth", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SimilarArtists_400_ReturnsErrPermanent(t *testing.T) {
|
||||
c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
})
|
||||
defer srv.Close()
|
||||
_, err := c.SimilarArtists(context.Background(), "abc", 100)
|
||||
if !errors.Is(err, ErrPermanent) {
|
||||
t.Errorf("err = %v, want ErrPermanent", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SimilarArtists_503_ReturnsErrTransient(t *testing.T) {
|
||||
c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
})
|
||||
defer srv.Close()
|
||||
_, err := c.SimilarArtists(context.Background(), "abc", 100)
|
||||
if !errors.Is(err, ErrTransient) {
|
||||
t.Errorf("err = %v, want ErrTransient", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SimilarArtists_429_ReturnsRetryAfter(t *testing.T) {
|
||||
c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Retry-After", "60")
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
})
|
||||
defer srv.Close()
|
||||
_, err := c.SimilarArtists(context.Background(), "abc", 100)
|
||||
var ra *RetryAfterError
|
||||
if !errors.As(err, &ra) {
|
||||
t.Fatalf("err = %v, want *RetryAfterError", err)
|
||||
}
|
||||
if ra.Wait != 60*time.Second {
|
||||
t.Errorf("Wait = %v, want 60s", ra.Wait)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user