feat(listenbrainz): add SimilarRecordings client method
This commit is contained in:
@@ -180,3 +180,56 @@ func buildPayload(listenType string, listens []Listen) payload {
|
||||
}
|
||||
return payload{ListenType: listenType, Payload: entries}
|
||||
}
|
||||
|
||||
// LB algorithm parameter for /explore/similar-recordings/. Hardcoded for v1
|
||||
// — matches LB's documented default.
|
||||
const lbSimilarRecordingsAlgorithm = "session_based_days_7500_session_30_contribution_5_threshold_15_limit_100_filter_True_skip_30"
|
||||
|
||||
// SimilarRecording is one entry in the /explore/similar-recordings response.
|
||||
type SimilarRecording struct {
|
||||
MBID string `json:"recording_mbid"`
|
||||
Score float64 `json:"score"`
|
||||
}
|
||||
|
||||
// SimilarRecordings fetches up to `limit` similar recordings for the given
|
||||
// recording MBID. Public endpoint — no token required. Returns the same
|
||||
// typed errors as SubmitListens.
|
||||
func (c *Client) SimilarRecordings(ctx context.Context, mbid string, limit int) ([]SimilarRecording, error) {
|
||||
base := c.BaseURL
|
||||
if base == "" {
|
||||
base = defaultBaseURL
|
||||
}
|
||||
url := fmt.Sprintf("%s/1/explore/similar-recordings/%s?algorithm=%s&count=%d",
|
||||
base, mbid, lbSimilarRecordingsAlgorithm, limit)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listenbrainz: build similar-recordings 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 []SimilarRecording
|
||||
if derr := json.NewDecoder(resp.Body).Decode(&out); derr != nil {
|
||||
return nil, fmt.Errorf("%w: decode similar-recordings: %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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,3 +162,98 @@ func TestClient_SubmitListens_OmitsEmptyMBIDs(t *testing.T) {
|
||||
t.Errorf("body should omit empty recording_mbid: %s", seenBody)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SimilarRecordings_Success(t *testing.T) {
|
||||
c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte(`[
|
||||
{"recording_mbid": "11111111-1111-1111-1111-111111111111", "score": 0.95},
|
||||
{"recording_mbid": "22222222-2222-2222-2222-222222222222", "score": 0.80}
|
||||
]`))
|
||||
})
|
||||
defer srv.Close()
|
||||
out, err := c.SimilarRecordings(context.Background(), "aaaa", 100)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if len(out) != 2 {
|
||||
t.Fatalf("got %d results, want 2", len(out))
|
||||
}
|
||||
if out[0].MBID != "11111111-1111-1111-1111-111111111111" || out[0].Score != 0.95 {
|
||||
t.Errorf("first result wrong: %+v", out[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SimilarRecordings_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.SimilarRecordings(context.Background(), "abc", 50)
|
||||
if !strings.Contains(seenURL, "algorithm=") {
|
||||
t.Errorf("URL missing algorithm param: %q", seenURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SimilarRecordings_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.SimilarRecordings(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_SimilarRecordings_401_ReturnsErrAuth(t *testing.T) {
|
||||
c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
})
|
||||
defer srv.Close()
|
||||
_, err := c.SimilarRecordings(context.Background(), "abc", 100)
|
||||
if !errors.Is(err, ErrAuth) {
|
||||
t.Errorf("err = %v, want ErrAuth", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SimilarRecordings_400_ReturnsErrPermanent(t *testing.T) {
|
||||
c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
})
|
||||
defer srv.Close()
|
||||
_, err := c.SimilarRecordings(context.Background(), "abc", 100)
|
||||
if !errors.Is(err, ErrPermanent) {
|
||||
t.Errorf("err = %v, want ErrPermanent", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SimilarRecordings_503_ReturnsErrTransient(t *testing.T) {
|
||||
c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
})
|
||||
defer srv.Close()
|
||||
_, err := c.SimilarRecordings(context.Background(), "abc", 100)
|
||||
if !errors.Is(err, ErrTransient) {
|
||||
t.Errorf("err = %v, want ErrTransient", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SimilarRecordings_429_ReturnsRetryAfter(t *testing.T) {
|
||||
c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Retry-After", "120")
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
})
|
||||
defer srv.Close()
|
||||
_, err := c.SimilarRecordings(context.Background(), "abc", 100)
|
||||
var ra *RetryAfterError
|
||||
if !errors.As(err, &ra) {
|
||||
t.Fatalf("err = %v, want *RetryAfterError", err)
|
||||
}
|
||||
if ra.Wait != 120*time.Second {
|
||||
t.Errorf("Wait = %v, want 120s", ra.Wait)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user