mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
harden qobuz and downloader reliability edge cases
This commit is contained in:
@@ -2,6 +2,8 @@ package qobuz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -219,3 +221,155 @@ func makeItems(start, end int) []map[string]any {
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func TestLoginRefreshesAppCredentialsWhenSecretInvalid(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/user/login":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"user_auth_token": "uat-token"})
|
||||
case "/track/getFileUrl":
|
||||
if r.Header.Get("X-App-Id") != "new-app" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"message": "bad app"})
|
||||
return
|
||||
}
|
||||
tsValue := r.URL.Query().Get("request_ts")
|
||||
sig := r.URL.Query().Get("request_sig")
|
||||
if sig != qobuzSecretSig(tsValue, "good-secret") {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"message": "bad secret"})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"message": "ok secret"})
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := config.DefaultConfigData()
|
||||
d.Qobuz.EmailOrUserID = "user@example.com"
|
||||
d.Qobuz.PasswordOrToken = "hash"
|
||||
d.Qobuz.AppID = "old-app"
|
||||
d.Qobuz.Secrets = []string{"bad-secret"}
|
||||
cfg := &config.Config{File: d, Session: d}
|
||||
c := New(cfg)
|
||||
c.baseURL = ts.URL
|
||||
c.fetchCfg = func(context.Context) (string, []string, error) {
|
||||
return "new-app", []string{"good-secret"}, nil
|
||||
}
|
||||
|
||||
if err := c.Login(context.Background()); err != nil {
|
||||
t.Fatalf("Login() error = %v", err)
|
||||
}
|
||||
if !c.loggedIn {
|
||||
t.Fatalf("expected logged-in client")
|
||||
}
|
||||
if c.secret != "good-secret" {
|
||||
t.Fatalf("secret = %q, want good-secret", c.secret)
|
||||
}
|
||||
if c.cfg.Session.Qobuz.AppID != "new-app" {
|
||||
t.Fatalf("session app id = %q", c.cfg.Session.Qobuz.AppID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginRetriesAfterRefreshingAppCredentials(t *testing.T) {
|
||||
loginCalls := 0
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/user/login":
|
||||
loginCalls++
|
||||
if r.URL.Query().Get("app_id") != "new-app" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"message": "expired app id"})
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"user_auth_token": "uat-token"})
|
||||
case "/track/getFileUrl":
|
||||
tsValue := r.URL.Query().Get("request_ts")
|
||||
sig := r.URL.Query().Get("request_sig")
|
||||
if r.Header.Get("X-App-Id") == "new-app" && sig == qobuzSecretSig(tsValue, "good-secret") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"message": "ok secret"})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"message": "bad secret"})
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := config.DefaultConfigData()
|
||||
d.Qobuz.EmailOrUserID = "user@example.com"
|
||||
d.Qobuz.PasswordOrToken = "hash"
|
||||
d.Qobuz.AppID = "old-app"
|
||||
d.Qobuz.Secrets = []string{"old-secret"}
|
||||
cfg := &config.Config{File: d, Session: d}
|
||||
c := New(cfg)
|
||||
c.baseURL = ts.URL
|
||||
c.fetchCfg = func(context.Context) (string, []string, error) {
|
||||
return "new-app", []string{"good-secret"}, nil
|
||||
}
|
||||
|
||||
if err := c.Login(context.Background()); err != nil {
|
||||
t.Fatalf("Login() error = %v", err)
|
||||
}
|
||||
if loginCalls < 2 {
|
||||
t.Fatalf("expected login retry after refresh, calls=%d", loginCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQobuzDownloadExtension(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
resp map[string]any
|
||||
quality int
|
||||
url string
|
||||
want string
|
||||
}{
|
||||
{name: "from url flac", resp: map[string]any{}, quality: 1, url: "https://cdn.example/a.flac?token=1", want: "flac"},
|
||||
{name: "from url mp3", resp: map[string]any{}, quality: 4, url: "https://cdn.example/a.mp3", want: "mp3"},
|
||||
{name: "from mime type", resp: map[string]any{"mime_type": "audio/flac"}, quality: 1, url: "https://cdn.example/stream", want: "flac"},
|
||||
{name: "from format id", resp: map[string]any{"format_id": float64(5)}, quality: 4, url: "https://cdn.example/stream", want: "mp3"},
|
||||
{name: "fallback quality", resp: map[string]any{}, quality: 3, url: "https://cdn.example/stream", want: "flac"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := qobuzDownloadExtension(tt.resp, tt.quality, tt.url); got != tt.want {
|
||||
t.Fatalf("%s: qobuzDownloadExtension()=%q want %q", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDownloadableUsesReturnedURLExtension(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/track/getFileUrl" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"url": "https://cdn.example/track.mp3?token=abc"})
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := newTestClient(t)
|
||||
c.loggedIn = true
|
||||
c.secret = "secret"
|
||||
c.baseURL = ts.URL
|
||||
|
||||
d, err := c.GetDownloadable(context.Background(), "19512574", 4)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDownloadable() error = %v", err)
|
||||
}
|
||||
if d.Extension != "mp3" {
|
||||
t.Fatalf("extension = %q, want mp3", d.Extension)
|
||||
}
|
||||
}
|
||||
|
||||
func qobuzSecretSig(requestTS, secret string) string {
|
||||
raw := "trackgetFileUrlformat_id27intentstreamtrack_id19512574" + requestTS + secret
|
||||
hash := md5.Sum([]byte(raw))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user