paginate artist album fetching for deezer and qobuz

This commit is contained in:
2026-04-21 11:39:04 +02:00
parent 9ebddc8316
commit 4f86751ff4
4 changed files with 203 additions and 3 deletions

View File

@@ -203,7 +203,7 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s
resp["tracks"] = map[string]any{"items": items} resp["tracks"] = map[string]any{"items": items}
return resp, nil return resp, nil
case "artist": case "artist":
resp, err := c.apiGet(ctx, "/artist/"+item+"/albums", nil) resp, err := c.getArtistAlbums(ctx, item)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -224,6 +224,35 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s
} }
} }
func (c *Client) getArtistAlbums(ctx context.Context, artistID string) (map[string]any, error) {
const pageSize = 100
index := 0
total := -1
all := make([]any, 0)
for {
params := url.Values{}
params.Set("limit", strconv.Itoa(pageSize))
params.Set("index", strconv.Itoa(index))
resp, err := c.apiGet(ctx, "/artist/"+strings.TrimSpace(artistID)+"/albums", params)
if err != nil {
return nil, err
}
data, _ := resp["data"].([]any)
all = append(all, data...)
if total < 0 {
total = intFromAny(resp["total"])
}
if len(data) < pageSize {
break
}
index += len(data)
if total > 0 && index >= total {
break
}
}
return map[string]any{"data": all, "total": total}, nil
}
func (c *Client) GetDownloadable(ctx context.Context, item string, _ int) (*provider.Downloadable, error) { func (c *Client) GetDownloadable(ctx context.Context, item string, _ int) (*provider.Downloadable, error) {
if strings.TrimSpace(c.license) == "" { if strings.TrimSpace(c.license) == "" {
if strings.TrimSpace(c.arl) != "" { if strings.TrimSpace(c.arl) != "" {

View File

@@ -39,6 +39,58 @@ func TestSearchTrack(t *testing.T) {
} }
} }
func TestGetMetadataArtistPaginatesAlbums(t *testing.T) {
callCount := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/artist/9/albums" {
w.WriteHeader(http.StatusNotFound)
return
}
callCount++
index := r.URL.Query().Get("index")
limit := r.URL.Query().Get("limit")
if limit != "100" {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"message": "bad limit"}})
return
}
switch index {
case "0":
items := make([]any, 0, 100)
for i := 0; i < 100; i++ {
items = append(items, map[string]any{"id": i + 1, "title": "Album"})
}
_ = json.NewEncoder(w).Encode(map[string]any{"data": items, "total": 101})
case "100":
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"id": 101, "title": "Album 101"}}, "total": 101})
default:
w.WriteHeader(http.StatusBadRequest)
}
}))
defer ts.Close()
cfgData := config.DefaultConfigData()
c := New(&config.Config{File: cfgData, Session: cfgData})
c.loggedIn = true
origBase := baseURL
baseURL = ts.URL
defer func() { baseURL = origBase }()
meta, err := c.GetMetadata(context.Background(), "9", "artist")
if err != nil {
t.Fatalf("GetMetadata() error = %v", err)
}
albumsObj, _ := meta["albums"].(map[string]any)
items, _ := albumsObj["items"].([]any)
if len(items) != 101 {
t.Fatalf("albums len = %d, want 101", len(items))
}
if callCount != 2 {
t.Fatalf("call count = %d, want 2", callCount)
}
}
func TestGetDownloadableNativeCipher(t *testing.T) { func TestGetDownloadableNativeCipher(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { switch r.URL.Path {

View File

@@ -122,6 +122,9 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s
if mediaType == "label" { if mediaType == "label" {
return c.getLabel(ctx, item) return c.getLabel(ctx, item)
} }
if mediaType == "artist" {
return c.getArtist(ctx, item)
}
params := url.Values{} params := url.Values{}
params.Set("app_id", c.cfg.Session.Qobuz.AppID) params.Set("app_id", c.cfg.Session.Qobuz.AppID)
@@ -130,8 +133,6 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s
params.Set("offset", "0") params.Set("offset", "0")
switch mediaType { switch mediaType {
case "artist":
params.Set("extra", "albums")
case "playlist": case "playlist":
params.Set("extra", "tracks") params.Set("extra", "tracks")
case "label": case "label":
@@ -358,6 +359,77 @@ func (c *Client) getLabel(ctx context.Context, labelID string) (map[string]any,
return resp, nil return resp, nil
} }
func (c *Client) getArtist(ctx context.Context, artistID string) (map[string]any, error) {
pageLimit := 500
params := url.Values{}
params.Set("app_id", c.cfg.Session.Qobuz.AppID)
params.Set("artist_id", artistID)
params.Set("limit", strconv.Itoa(pageLimit))
params.Set("offset", "0")
params.Set("extra", "albums")
resp, status, err := c.apiRequest(ctx, "artist/get", params, c.authHeaders())
if err != nil {
return nil, err
}
if status != http.StatusOK {
return nil, fmt.Errorf("artist/get failed: status=%d", status)
}
albumsObj, ok := mapValue(resp["albums"])
if !ok {
return resp, nil
}
items, ok := albumsObj["items"].([]any)
if !ok {
return resp, nil
}
total, _ := intValue(resp["albums_count"])
if total <= 0 {
total, _ = intValue(albumsObj["total"])
}
if total <= pageLimit && len(items) < pageLimit {
return resp, nil
}
for offset := pageLimit; ; offset += pageLimit {
if total > 0 && offset >= total {
break
}
pageParams := url.Values{}
pageParams.Set("app_id", c.cfg.Session.Qobuz.AppID)
pageParams.Set("artist_id", artistID)
pageParams.Set("limit", strconv.Itoa(pageLimit))
pageParams.Set("offset", strconv.Itoa(offset))
pageParams.Set("extra", "albums")
pageResp, pageStatus, pageErr := c.apiRequest(ctx, "artist/get", pageParams, c.authHeaders())
if pageErr != nil {
return nil, pageErr
}
if pageStatus != http.StatusOK {
return nil, fmt.Errorf("artist/get pagination failed: status=%d offset=%d", pageStatus, offset)
}
pageAlbums, ok := mapValue(pageResp["albums"])
if !ok {
break
}
pageItems, ok := pageAlbums["items"].([]any)
if !ok || len(pageItems) == 0 {
break
}
items = append(items, pageItems...)
if len(pageItems) < pageLimit {
break
}
}
albumsObj["items"] = items
resp["albums"] = albumsObj
return resp, nil
}
func (c *Client) authHeaders() map[string]string { func (c *Client) authHeaders() map[string]string {
headers := map[string]string{"X-App-Id": c.cfg.Session.Qobuz.AppID} headers := map[string]string{"X-App-Id": c.cfg.Session.Qobuz.AppID}
if c.uat != "" { if c.uat != "" {

View File

@@ -157,6 +157,53 @@ func TestGetLabelPagination(t *testing.T) {
} }
} }
func TestGetArtistPagination(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
offset := r.URL.Query().Get("offset")
if offset == "" {
offset = "0"
}
resp := map[string]any{}
switch offset {
case "0":
resp = map[string]any{
"albums_count": 620,
"albums": map[string]any{"items": makeItems(0, 500)},
}
case "500":
resp = map[string]any{"albums": map[string]any{"items": makeItems(500, 620)}}
default:
w.WriteHeader(http.StatusNotFound)
_ = json.NewEncoder(w).Encode(map[string]any{"message": "not found"})
return
}
_ = json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
c := newTestClient(t)
c.loggedIn = true
c.baseURL = ts.URL
raw, err := c.GetMetadata(context.Background(), "artist-id", "artist")
if err != nil {
t.Fatalf("GetMetadata() error = %v", err)
}
albums, ok := mapValue(raw["albums"])
if !ok {
t.Fatalf("albums missing")
}
items, ok := albums["items"].([]any)
if !ok {
t.Fatalf("items missing")
}
if len(items) != 620 {
t.Fatalf("len(items) = %d, want 620", len(items))
}
}
func newTestClient(t *testing.T) *Client { func newTestClient(t *testing.T) *Client {
t.Helper() t.Helper()
d := config.DefaultConfigData() d := config.DefaultConfigData()