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 {
|
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{
|
folder := filepath.Join(m.Config.Session.Downloads.Folder, naming.CleanName(name, naming.Config{
|
||||||
RestrictCharacters: m.Config.Session.Filepaths.RestrictCharacters,
|
RestrictCharacters: m.Config.Session.Filepaths.RestrictCharacters,
|
||||||
TruncateTo: m.Config.Session.Filepaths.TruncateTo,
|
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) {
|
func TestApplyQobuzArtistFiltersRepeats(t *testing.T) {
|
||||||
albums := []collectionAlbum{
|
albums := []collectionAlbum{
|
||||||
{ID: "a1", Title: "Album X", BitDepth: 16, Sampling: 44.1, Explicit: false},
|
{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}
|
resp["tracks"] = map[string]any{"items": items}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
case "artist":
|
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)
|
resp, err := c.getArtistAlbums(ctx, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -218,7 +224,7 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s
|
|||||||
albums = append(albums, itm)
|
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:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported deezer media type: %s", mediaType)
|
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 {
|
if err != nil {
|
||||||
return "", err
|
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{}
|
out := map[string]any{}
|
||||||
if err = json.Unmarshal(raw, &out); err != nil {
|
if err = json.Unmarshal(raw, &out); err != nil {
|
||||||
return "", err
|
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")
|
token := findStringByKey(nestedMap(out, "results"), "TOKEN")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return "", errors.New("mobile_auth returned empty 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 {
|
if err != nil {
|
||||||
return "", err
|
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{}
|
out := map[string]any{}
|
||||||
if err = json.Unmarshal(raw, &out); err != nil {
|
if err = json.Unmarshal(raw, &out); err != nil {
|
||||||
return "", err
|
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"]))
|
sid := strings.TrimSpace(stringFromAny(out["results"]))
|
||||||
if sid == "" {
|
if sid == "" {
|
||||||
return "", errors.New("api_checkToken returned empty 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() }()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
raw, _ := io.ReadAll(resp.Body)
|
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{}
|
out := map[string]any{}
|
||||||
if json.Unmarshal(raw, &out) != nil {
|
if json.Unmarshal(raw, &out) != nil {
|
||||||
return errors.New("invalid jwt refresh response")
|
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 != "" {
|
if jwt := strings.TrimSpace(stringFromAny(out["jwt"])); jwt != "" {
|
||||||
c.jwt = jwt
|
c.jwt = jwt
|
||||||
}
|
}
|
||||||
@@ -905,10 +941,23 @@ func (c *Client) refreshLicenseFromPipe(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
defer func() { _ = resp.Body.Close() }()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
raw, _ := io.ReadAll(resp.Body)
|
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{}
|
out := map[string]any{}
|
||||||
if json.Unmarshal(raw, &out) != nil {
|
if json.Unmarshal(raw, &out) != nil {
|
||||||
return errors.New("invalid pipe response")
|
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")
|
token := findStringByKey(out, "token")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return errors.New("pipe response missing license token")
|
return errors.New("pipe response missing license token")
|
||||||
|
|||||||
@@ -42,29 +42,32 @@ func TestSearchTrack(t *testing.T) {
|
|||||||
func TestGetMetadataArtistPaginatesAlbums(t *testing.T) {
|
func TestGetMetadataArtistPaginatesAlbums(t *testing.T) {
|
||||||
callCount := 0
|
callCount := 0
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/artist/9/albums" {
|
switch r.URL.Path {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
case "/artist/9":
|
||||||
return
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": 9, "name": "Lost Frequencies"})
|
||||||
}
|
case "/artist/9/albums":
|
||||||
callCount++
|
callCount++
|
||||||
index := r.URL.Query().Get("index")
|
index := r.URL.Query().Get("index")
|
||||||
limit := r.URL.Query().Get("limit")
|
limit := r.URL.Query().Get("limit")
|
||||||
if limit != "100" {
|
if limit != "100" {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"message": "bad limit"}})
|
_ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"message": "bad limit"}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch index {
|
switch index {
|
||||||
case "0":
|
case "0":
|
||||||
items := make([]any, 0, 100)
|
items := make([]any, 0, 100)
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
items = append(items, map[string]any{"id": i + 1, "title": "Album"})
|
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)
|
||||||
}
|
}
|
||||||
_ = 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:
|
default:
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
@@ -86,6 +89,9 @@ func TestGetMetadataArtistPaginatesAlbums(t *testing.T) {
|
|||||||
if len(items) != 101 {
|
if len(items) != 101 {
|
||||||
t.Fatalf("albums len = %d, want 101", len(items))
|
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 {
|
if callCount != 2 {
|
||||||
t.Fatalf("call count = %d, want 2", callCount)
|
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)
|
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