paginate tidal artist album fetches across all pages

This commit is contained in:
2026-04-21 17:33:25 +02:00
parent 654bed17e2
commit 6dbf6a222d
2 changed files with 101 additions and 21 deletions

View File

@@ -310,31 +310,49 @@ func (c *Client) fetchArtistAlbums(ctx context.Context, artistID string) ([]map[
out := make([]map[string]any, 0) out := make([]map[string]any, 0)
seen := map[string]struct{}{} seen := map[string]struct{}{}
for _, p := range paths { for _, p := range paths {
resp, status, err := c.apiRequest(ctx, p.path, p.params, c.baseURL) offset := 0
if err != nil { for {
return nil, err params := url.Values{}
} for k, values := range p.params {
if status != http.StatusOK { for _, v := range values {
return nil, fmt.Errorf("tidal artist albums failed: status=%d", status) params.Add(k, v)
} }
items, _ := resp["items"].([]any)
for _, raw := range items {
itm, ok := raw.(map[string]any)
if !ok {
continue
} }
if wrapped, ok := itm["item"].(map[string]any); ok { params.Set("offset", strconv.Itoa(offset))
itm = wrapped
resp, status, err := c.apiRequest(ctx, p.path, params, c.baseURL)
if err != nil {
return nil, err
} }
id := stringify(itm["id"]) if status != http.StatusOK {
if id == "" { return nil, fmt.Errorf("tidal artist albums failed: status=%d offset=%d", status, offset)
continue
} }
if _, dup := seen[id]; dup { items, _ := resp["items"].([]any)
continue if len(items) == 0 {
break
} }
seen[id] = struct{}{} for _, raw := range items {
out = append(out, itm) itm, ok := raw.(map[string]any)
if !ok {
continue
}
if wrapped, ok := itm["item"].(map[string]any); ok {
itm = wrapped
}
id := stringify(itm["id"])
if id == "" {
continue
}
if _, dup := seen[id]; dup {
continue
}
seen[id] = struct{}{}
out = append(out, itm)
}
if len(items) < 100 {
break
}
offset += 100
} }
} }
return out, nil return out, nil

View File

@@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strconv"
"testing" "testing"
"streamrip-go/internal/config" "streamrip-go/internal/config"
@@ -98,3 +99,64 @@ func TestBestHLSVariantURLFallsBackToMaster(t *testing.T) {
t.Fatalf("url = %q, want %q", got, master) t.Fatalf("url = %q, want %q", got, master)
} }
} }
func TestGetMetadataArtistPaginatesAlbums(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v1/sessions":
_ = json.NewEncoder(w).Encode(map[string]any{"countryCode": "US", "userId": 123})
case "/v1/artists/9":
_ = json.NewEncoder(w).Encode(map[string]any{"id": 9, "name": "Artist X"})
case "/v1/artists/9/albums":
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
filter := r.URL.Query().Get("filter")
if filter == "" {
if offset == 0 {
items := make([]any, 0, 100)
for i := 0; i < 100; i++ {
items = append(items, map[string]any{"id": i + 1})
}
_ = json.NewEncoder(w).Encode(map[string]any{"items": items})
return
}
if offset == 100 {
_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{map[string]any{"id": 101}}})
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{}})
return
}
if filter == "EPSANDSINGLES" {
if offset == 0 {
_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{map[string]any{"id": 101}, map[string]any{"id": 102}}})
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{}})
return
}
w.WriteHeader(http.StatusBadRequest)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
cfgData := config.DefaultConfigData()
cfgData.Tidal.AccessToken = "token"
cfgData.Tidal.CountryCode = "US"
c := New(&config.Config{File: cfgData, Session: cfgData})
c.baseURL = ts.URL + "/v1"
if err := c.Login(context.Background()); err != nil {
t.Fatalf("login err = %v", err)
}
meta, err := c.GetMetadata(context.Background(), "9", "artist")
if err != nil {
t.Fatalf("GetMetadata() err = %v", err)
}
albumsObj, _ := meta["albums"].(map[string]any)
items, _ := albumsObj["items"].([]map[string]any)
if len(items) != 102 {
t.Fatalf("albums len = %d, want 102", len(items))
}
}