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:
@@ -35,6 +35,7 @@ type Client struct {
|
||||
http *http.Client
|
||||
limiter *ratelimit.Limiter
|
||||
baseURL string
|
||||
fetchCfg func(ctx context.Context) (string, []string, error)
|
||||
loggedIn bool
|
||||
secret string
|
||||
uat string
|
||||
@@ -42,10 +43,11 @@ type Client struct {
|
||||
|
||||
func New(cfg *config.Config) *Client {
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
http: netutil.NewHTTPClient(30*time.Second, cfg.Session.Downloads.VerifySSL),
|
||||
limiter: ratelimit.New(cfg.Session.Downloads.RequestsPerMinute),
|
||||
baseURL: baseURL,
|
||||
cfg: cfg,
|
||||
http: netutil.NewHTTPClient(30*time.Second, cfg.Session.Downloads.VerifySSL),
|
||||
limiter: ratelimit.New(cfg.Session.Downloads.RequestsPerMinute),
|
||||
baseURL: baseURL,
|
||||
fetchCfg: nil,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,37 +61,44 @@ func (c *Client) LoggedIn() bool {
|
||||
|
||||
func (c *Client) Login(ctx context.Context) error {
|
||||
q := &c.cfg.Session.Qobuz
|
||||
q.EmailOrUserID = strings.TrimSpace(q.EmailOrUserID)
|
||||
q.PasswordOrToken = strings.TrimSpace(q.PasswordOrToken)
|
||||
if q.EmailOrUserID == "" || q.PasswordOrToken == "" {
|
||||
return errMissingCredentials
|
||||
}
|
||||
|
||||
if q.AppID == "" || len(q.Secrets) == 0 {
|
||||
appID, secrets, err := c.fetchAppIDAndSecrets(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
refreshed := false
|
||||
if err := c.ensureAppCredentials(ctx, q); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loginOnce := func() (map[string]any, int, error) {
|
||||
headers := map[string]string{"X-App-Id": q.AppID}
|
||||
params := url.Values{}
|
||||
params.Set("app_id", q.AppID)
|
||||
if q.UseAuthToken {
|
||||
params.Set("user_id", q.EmailOrUserID)
|
||||
params.Set("user_auth_token", q.PasswordOrToken)
|
||||
} else {
|
||||
params.Set("email", q.EmailOrUserID)
|
||||
params.Set("password", q.PasswordOrToken)
|
||||
}
|
||||
q.AppID = appID
|
||||
q.Secrets = secrets
|
||||
c.cfg.File.Qobuz.AppID = appID
|
||||
c.cfg.File.Qobuz.Secrets = append([]string(nil), secrets...)
|
||||
_ = c.cfg.SaveFile()
|
||||
return c.apiRequest(ctx, "user/login", params, headers)
|
||||
}
|
||||
|
||||
headers := map[string]string{"X-App-Id": q.AppID}
|
||||
params := url.Values{}
|
||||
params.Set("app_id", q.AppID)
|
||||
if q.UseAuthToken {
|
||||
params.Set("user_id", q.EmailOrUserID)
|
||||
params.Set("user_auth_token", q.PasswordOrToken)
|
||||
} else {
|
||||
params.Set("email", q.EmailOrUserID)
|
||||
params.Set("password", q.PasswordOrToken)
|
||||
}
|
||||
|
||||
resp, status, err := c.apiRequest(ctx, "user/login", params, headers)
|
||||
resp, status, err := loginOnce()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != http.StatusOK && !refreshed {
|
||||
if refreshErr := c.refreshAppCredentials(ctx, q); refreshErr == nil {
|
||||
refreshed = true
|
||||
resp, status, err = loginOnce()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
return fmt.Errorf("qobuz login failed: status=%d body=%v", status, resp)
|
||||
}
|
||||
@@ -99,8 +108,15 @@ func (c *Client) Login(ctx context.Context) error {
|
||||
return fmt.Errorf("qobuz login missing user_auth_token")
|
||||
}
|
||||
|
||||
headers["X-User-Auth-Token"] = uat
|
||||
headers := map[string]string{"X-App-Id": q.AppID, "X-User-Auth-Token": uat}
|
||||
validSecret, err := c.getValidSecret(ctx, q.Secrets, headers)
|
||||
if err != nil && !refreshed {
|
||||
if refreshErr := c.refreshAppCredentials(ctx, q); refreshErr == nil {
|
||||
refreshed = true
|
||||
headers["X-App-Id"] = q.AppID
|
||||
validSecret, err = c.getValidSecret(ctx, q.Secrets, headers)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -112,6 +128,31 @@ func (c *Client) Login(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) ensureAppCredentials(ctx context.Context, q *config.QobuzConfig) error {
|
||||
q.AppID = strings.TrimSpace(q.AppID)
|
||||
if q.AppID != "" && len(q.Secrets) > 0 {
|
||||
return nil
|
||||
}
|
||||
return c.refreshAppCredentials(ctx, q)
|
||||
}
|
||||
|
||||
func (c *Client) refreshAppCredentials(ctx context.Context, q *config.QobuzConfig) error {
|
||||
fetch := c.fetchCfg
|
||||
if fetch == nil {
|
||||
fetch = c.fetchAppIDAndSecrets
|
||||
}
|
||||
appID, secrets, err := fetch(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q.AppID = strings.TrimSpace(appID)
|
||||
q.Secrets = append([]string(nil), secrets...)
|
||||
c.cfg.File.Qobuz.AppID = q.AppID
|
||||
c.cfg.File.Qobuz.Secrets = append([]string(nil), secrets...)
|
||||
_ = c.cfg.SaveFile()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[string]any, error) {
|
||||
if !c.loggedIn {
|
||||
return nil, errNotLoggedIn
|
||||
@@ -215,14 +256,12 @@ func (c *Client) GetDownloadable(ctx context.Context, item string, quality int)
|
||||
}
|
||||
|
||||
streamURL, _ := resp["url"].(string)
|
||||
streamURL = strings.TrimSpace(streamURL)
|
||||
if streamURL == "" {
|
||||
return nil, fmt.Errorf("track is not streamable")
|
||||
}
|
||||
|
||||
ext := "mp3"
|
||||
if quality > 1 {
|
||||
ext = "flac"
|
||||
}
|
||||
ext := qobuzDownloadExtension(resp, quality, streamURL)
|
||||
|
||||
return &provider.Downloadable{
|
||||
URL: streamURL,
|
||||
@@ -231,6 +270,41 @@ func (c *Client) GetDownloadable(ctx context.Context, item string, quality int)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func qobuzDownloadExtension(resp map[string]any, quality int, streamURL string) string {
|
||||
if parsed, err := url.Parse(strings.TrimSpace(streamURL)); err == nil {
|
||||
p := strings.ToLower(parsed.Path)
|
||||
if strings.HasSuffix(p, ".flac") {
|
||||
return "flac"
|
||||
}
|
||||
if strings.HasSuffix(p, ".mp3") {
|
||||
return "mp3"
|
||||
}
|
||||
}
|
||||
|
||||
mimeType, _ := resp["mime_type"].(string)
|
||||
mimeType = strings.ToLower(strings.TrimSpace(mimeType))
|
||||
if strings.Contains(mimeType, "flac") {
|
||||
return "flac"
|
||||
}
|
||||
if strings.Contains(mimeType, "mpeg") || strings.Contains(mimeType, "mp3") {
|
||||
return "mp3"
|
||||
}
|
||||
|
||||
if formatID, ok := intValue(resp["format_id"]); ok {
|
||||
if formatID == 5 {
|
||||
return "mp3"
|
||||
}
|
||||
if formatID > 5 {
|
||||
return "flac"
|
||||
}
|
||||
}
|
||||
|
||||
if quality > 1 {
|
||||
return "flac"
|
||||
}
|
||||
return "mp3"
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user