From 4f86751ff46cb58170c5cd0fe725622270929a15 Mon Sep 17 00:00:00 2001 From: Joren Date: Tue, 21 Apr 2026 11:39:04 +0200 Subject: [PATCH] paginate artist album fetching for deezer and qobuz --- internal/provider/deezer/client.go | 31 +++++++++- internal/provider/deezer/client_test.go | 52 +++++++++++++++++ internal/provider/qobuz/client.go | 76 ++++++++++++++++++++++++- internal/provider/qobuz/client_test.go | 47 +++++++++++++++ 4 files changed, 203 insertions(+), 3 deletions(-) diff --git a/internal/provider/deezer/client.go b/internal/provider/deezer/client.go index 194db37..e5b7067 100644 --- a/internal/provider/deezer/client.go +++ b/internal/provider/deezer/client.go @@ -203,7 +203,7 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s resp["tracks"] = map[string]any{"items": items} return resp, nil case "artist": - resp, err := c.apiGet(ctx, "/artist/"+item+"/albums", nil) + resp, err := c.getArtistAlbums(ctx, item) if err != nil { 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) { if strings.TrimSpace(c.license) == "" { if strings.TrimSpace(c.arl) != "" { diff --git a/internal/provider/deezer/client_test.go b/internal/provider/deezer/client_test.go index ee2dcc0..cbae29f 100644 --- a/internal/provider/deezer/client_test.go +++ b/internal/provider/deezer/client_test.go @@ -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) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { diff --git a/internal/provider/qobuz/client.go b/internal/provider/qobuz/client.go index a8da79b..a90677c 100644 --- a/internal/provider/qobuz/client.go +++ b/internal/provider/qobuz/client.go @@ -122,6 +122,9 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s if mediaType == "label" { return c.getLabel(ctx, item) } + if mediaType == "artist" { + return c.getArtist(ctx, item) + } params := url.Values{} 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") switch mediaType { - case "artist": - params.Set("extra", "albums") case "playlist": params.Set("extra", "tracks") case "label": @@ -358,6 +359,77 @@ func (c *Client) getLabel(ctx context.Context, labelID string) (map[string]any, 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 { headers := map[string]string{"X-App-Id": c.cfg.Session.Qobuz.AppID} if c.uat != "" { diff --git a/internal/provider/qobuz/client_test.go b/internal/provider/qobuz/client_test.go index 5aa7fe3..113d7c0 100644 --- a/internal/provider/qobuz/client_test.go +++ b/internal/provider/qobuz/client_test.go @@ -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 { t.Helper() d := config.DefaultConfigData()