diff --git a/internal/provider/deezer/client.go b/internal/provider/deezer/client.go index ba702e3..033b679 100644 --- a/internal/provider/deezer/client.go +++ b/internal/provider/deezer/client.go @@ -175,7 +175,7 @@ func (c *Client) GetDownloadable(ctx context.Context, item string, _ int) (*prov return d, nil } if !c.cfg.Session.Deezer.LowerQualityIfNotAvailable { - return nil, dlErr + return nil, fmt.Errorf("deezer full-quality mode failed and fallback is disabled: %w", dlErr) } } preview := strings.TrimSpace(stringFromAny(meta["preview"])) @@ -352,8 +352,15 @@ func (c *Client) apiGet(ctx context.Context, path string, params url.Values) (ma if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("deezer api failed: status=%d body=%s", resp.StatusCode, string(body)) } - if e := stringFromAny(out["error"]); e != "" { - return nil, fmt.Errorf("deezer api error: %s", e) + if errObj, ok := out["error"].(map[string]any); ok { + msg := strings.TrimSpace(stringFromAny(errObj["message"])) + if msg == "" { + msg = strings.TrimSpace(stringFromAny(errObj["type"])) + } + if msg == "" { + msg = "unknown deezer error" + } + return nil, fmt.Errorf("deezer api error: %s", msg) } return out, nil } @@ -375,7 +382,7 @@ func enrichTrack(track map[string]any) { track["media_number"] = d } } - if v := stringFromAny(track["explicit_lyrics"]); v == "true" { + if boolFromAny(track["explicit_lyrics"]) { track["explicit"] = true } } @@ -441,6 +448,11 @@ func intFromAny(v any) int { } } +func boolFromAny(v any) bool { + b, ok := v.(bool) + return ok && b +} + func runCommand(ctx context.Context, name string, args ...string) ([]byte, error) { cmd := exec.CommandContext(ctx, name, args...) b, err := cmd.CombinedOutput() diff --git a/internal/provider/deezer/client_test.go b/internal/provider/deezer/client_test.go index dc12a88..cc9f7d2 100644 --- a/internal/provider/deezer/client_test.go +++ b/internal/provider/deezer/client_test.go @@ -3,8 +3,10 @@ package deezer import ( "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" + "strings" "testing" "streamrip-go/internal/config" @@ -64,3 +66,87 @@ func TestGetDownloadableUsesPreview(t *testing.T) { t.Fatalf("unexpected downloadable: %+v", d) } } + +func TestGetMetadataSetsExplicitFromBool(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/track/9": + _ = json.NewEncoder(w).Encode(map[string]any{ + "id": 9, + "title": "X", + "explicit_lyrics": true, + "artist": map[string]any{"name": "Artist"}, + }) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + + cfgData := config.DefaultConfigData() + c := New(&config.Config{File: cfgData, Session: cfgData}) + c.loggedIn = true + orig := baseURL + baseURL = ts.URL + defer func() { baseURL = orig }() + + meta, err := c.GetMetadata(context.Background(), "9", "track") + if err != nil { + t.Fatalf("GetMetadata() error = %v", err) + } + if explicit, _ := meta["explicit"].(bool); !explicit { + t.Fatalf("expected explicit=true, got %#v", meta["explicit"]) + } +} + +func TestSearchReturnsStructuredAPIError(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/search/track" { + _ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"message": "invalid query"}}) + return + } + w.WriteHeader(http.StatusNotFound) + })) + defer ts.Close() + + cfgData := config.DefaultConfigData() + c := New(&config.Config{File: cfgData, Session: cfgData}) + c.loggedIn = true + orig := baseURL + baseURL = ts.URL + defer func() { baseURL = orig }() + + _, err := c.Search(context.Background(), "track", "", 5) + if err == nil || !strings.Contains(err.Error(), "invalid query") { + t.Fatalf("expected structured deezer error, got %v", err) + } +} + +func TestGetDownloadableErrorsWhenFullQualityFailsAndFallbackDisabled(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/track/42" { + _ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "title": "X", "preview": "https://cdn.example/p.mp3"}) + return + } + w.WriteHeader(http.StatusNotFound) + })) + defer ts.Close() + + cfgData := config.DefaultConfigData() + cfgData.Deezer.UseDeezloader = true + cfgData.Deezer.LowerQualityIfNotAvailable = false + c := New(&config.Config{File: cfgData, Session: cfgData}) + c.loggedIn = true + c.bin = "definitely-not-a-real-yt-dlp-bin" + c.run = func(context.Context, string, ...string) ([]byte, error) { + return nil, fmt.Errorf("unexpected run call") + } + orig := baseURL + baseURL = ts.URL + defer func() { baseURL = orig }() + + _, err := c.GetDownloadable(context.Background(), "42", 2) + if err == nil || !strings.Contains(err.Error(), "full-quality mode failed") { + t.Fatalf("expected full-quality failure error, got %v", err) + } +}