mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
harden deezer auth errors and mixed playlist preflight
This commit is contained in:
@@ -728,6 +728,20 @@ func (m *Main) ripPlaylist(ctx context.Context, p provider.Client, source, playl
|
||||
}
|
||||
|
||||
func (m *Main) ripPlaylistMixed(ctx context.Context, playlistID, name string, refs []PlaylistTrackRef) error {
|
||||
requiredSources := map[string]struct{}{}
|
||||
for _, ref := range refs {
|
||||
s := strings.TrimSpace(ref.Source)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
requiredSources[s] = struct{}{}
|
||||
}
|
||||
for source := range requiredSources {
|
||||
if err := m.requireSourceDownloadAuth(source); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
folder := filepath.Join(m.Config.Session.Downloads.Folder, naming.CleanName(name, naming.Config{
|
||||
RestrictCharacters: m.Config.Session.Filepaths.RestrictCharacters,
|
||||
TruncateTo: m.Config.Session.Filepaths.TruncateTo,
|
||||
|
||||
@@ -535,6 +535,16 @@ func TestRipAlbumRequiresDeezerARL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRipPlaylistMixedRequiresDeezerAuth(t *testing.T) {
|
||||
d := config.DefaultConfigData()
|
||||
m := &Main{Config: &config.Config{File: d, Session: d}}
|
||||
|
||||
err := m.ripPlaylistMixed(context.Background(), "mix1", "Mix", []PlaylistTrackRef{{Source: "deezer", ID: "1"}})
|
||||
if err == nil || !strings.Contains(err.Error(), "deezer") {
|
||||
t.Fatalf("expected deezer auth error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyQobuzArtistFiltersRepeats(t *testing.T) {
|
||||
albums := []collectionAlbum{
|
||||
{ID: "a1", Title: "Album X", BitDepth: 16, Sampling: 44.1, Explicit: false},
|
||||
|
||||
@@ -203,6 +203,12 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s
|
||||
resp["tracks"] = map[string]any{"items": items}
|
||||
return resp, nil
|
||||
case "artist":
|
||||
name := strings.TrimSpace(item)
|
||||
if artistMeta, artistErr := c.apiGet(ctx, "/artist/"+item, nil); artistErr == nil {
|
||||
if n := strings.TrimSpace(stringFromAny(artistMeta["name"])); n != "" {
|
||||
name = n
|
||||
}
|
||||
}
|
||||
resp, err := c.getArtistAlbums(ctx, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -218,7 +224,7 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s
|
||||
albums = append(albums, itm)
|
||||
}
|
||||
}
|
||||
return map[string]any{"name": "", "albums": map[string]any{"items": albums}}, nil
|
||||
return map[string]any{"name": name, "albums": map[string]any{"items": albums}}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported deezer media type: %s", mediaType)
|
||||
}
|
||||
@@ -727,10 +733,20 @@ func (c *Client) mobileAuth(ctx context.Context) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("mobile_auth failed: status=%d body=%s", resp.StatusCode, string(raw))
|
||||
}
|
||||
out := map[string]any{}
|
||||
if err = json.Unmarshal(raw, &out); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if errObj, ok := out["error"].(map[string]any); ok && len(errObj) > 0 {
|
||||
msg := firstNonEmpty(stringFromAny(errObj["message"]), stringFromAny(errObj["type"]))
|
||||
if msg == "" {
|
||||
msg = "mobile_auth returned an error"
|
||||
}
|
||||
return "", errors.New(msg)
|
||||
}
|
||||
token := findStringByKey(nestedMap(out, "results"), "TOKEN")
|
||||
if token == "" {
|
||||
return "", errors.New("mobile_auth returned empty token")
|
||||
@@ -764,10 +780,20 @@ func (c *Client) apiCheckToken(ctx context.Context, authToken string) (string, e
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("api_checkToken failed: status=%d body=%s", resp.StatusCode, string(raw))
|
||||
}
|
||||
out := map[string]any{}
|
||||
if err = json.Unmarshal(raw, &out); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if errObj, ok := out["error"].(map[string]any); ok && len(errObj) > 0 {
|
||||
msg := firstNonEmpty(stringFromAny(errObj["message"]), stringFromAny(errObj["type"]))
|
||||
if msg == "" {
|
||||
msg = "api_checkToken returned an error"
|
||||
}
|
||||
return "", errors.New(msg)
|
||||
}
|
||||
sid := strings.TrimSpace(stringFromAny(out["results"]))
|
||||
if sid == "" {
|
||||
return "", errors.New("api_checkToken returned empty sid")
|
||||
@@ -861,10 +887,20 @@ func (c *Client) refreshJWT(ctx context.Context) error {
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("jwt refresh failed: status=%d body=%s", resp.StatusCode, string(raw))
|
||||
}
|
||||
out := map[string]any{}
|
||||
if json.Unmarshal(raw, &out) != nil {
|
||||
return errors.New("invalid jwt refresh response")
|
||||
}
|
||||
if errObj, ok := out["error"].(map[string]any); ok && len(errObj) > 0 {
|
||||
msg := firstNonEmpty(stringFromAny(errObj["message"]), stringFromAny(errObj["type"]))
|
||||
if msg == "" {
|
||||
msg = "jwt refresh returned an error"
|
||||
}
|
||||
return errors.New(msg)
|
||||
}
|
||||
if jwt := strings.TrimSpace(stringFromAny(out["jwt"])); jwt != "" {
|
||||
c.jwt = jwt
|
||||
}
|
||||
@@ -905,10 +941,23 @@ func (c *Client) refreshLicenseFromPipe(ctx context.Context) error {
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("pipe license refresh failed: status=%d body=%s", resp.StatusCode, string(raw))
|
||||
}
|
||||
out := map[string]any{}
|
||||
if json.Unmarshal(raw, &out) != nil {
|
||||
return errors.New("invalid pipe response")
|
||||
}
|
||||
if errs, ok := out["errors"].([]any); ok && len(errs) > 0 {
|
||||
msg := ""
|
||||
if em, ok := errs[0].(map[string]any); ok {
|
||||
msg = strings.TrimSpace(stringFromAny(em["message"]))
|
||||
}
|
||||
if msg == "" {
|
||||
msg = "pipe response returned graphql error"
|
||||
}
|
||||
return errors.New(msg)
|
||||
}
|
||||
token := findStringByKey(out, "token")
|
||||
if token == "" {
|
||||
return errors.New("pipe response missing license token")
|
||||
|
||||
@@ -42,10 +42,10 @@ 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
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/artist/9":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"id": 9, "name": "Lost Frequencies"})
|
||||
case "/artist/9/albums":
|
||||
callCount++
|
||||
index := r.URL.Query().Get("index")
|
||||
limit := r.URL.Query().Get("limit")
|
||||
@@ -66,6 +66,9 @@ func TestGetMetadataArtistPaginatesAlbums(t *testing.T) {
|
||||
default:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
@@ -86,6 +89,9 @@ func TestGetMetadataArtistPaginatesAlbums(t *testing.T) {
|
||||
if len(items) != 101 {
|
||||
t.Fatalf("albums len = %d, want 101", len(items))
|
||||
}
|
||||
if got := strings.TrimSpace(stringFromAny(meta["name"])); got != "Lost Frequencies" {
|
||||
t.Fatalf("artist name = %q, want Lost Frequencies", got)
|
||||
}
|
||||
if callCount != 2 {
|
||||
t.Fatalf("call count = %d, want 2", callCount)
|
||||
}
|
||||
@@ -333,3 +339,44 @@ func TestLoginWithRefreshToken(t *testing.T) {
|
||||
t.Fatalf("session refresh token = %q", c.cfg.Session.Deezer.RefreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshJWTHTTPError(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"message": "bad refresh"}})
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cfgData := config.DefaultConfigData()
|
||||
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||
c.refresh = "refresh-token"
|
||||
|
||||
origAuth := authURL
|
||||
authURL = ts.URL
|
||||
defer func() { authURL = origAuth }()
|
||||
|
||||
err := c.refreshJWT(context.Background())
|
||||
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "status=401") {
|
||||
t.Fatalf("expected http status error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshLicenseFromPipeGraphQLError(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"errors": []any{map[string]any{"message": "token expired"}}})
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cfgData := config.DefaultConfigData()
|
||||
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||
c.jwt = "jwt-token"
|
||||
|
||||
origPipe := pipeURL
|
||||
pipeURL = ts.URL
|
||||
defer func() { pipeURL = origPipe }()
|
||||
|
||||
err := c.refreshLicenseFromPipe(context.Background())
|
||||
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "token expired") {
|
||||
t.Fatalf("expected graphql error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user