mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
fix deezer mobile token and launch parity
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -38,6 +39,11 @@ var (
|
|||||||
"Deezer/9.0.11.4 (Android; 13; Mobile; us) Google Pixel 6",
|
"Deezer/9.0.11.4 (Android; 13; Mobile; us) Google Pixel 6",
|
||||||
"Deezer/9.0.11.4 (Android; 14; Mobile; us) OnePlus IN2023",
|
"Deezer/9.0.11.4 (Android; 14; Mobile; us) OnePlus IN2023",
|
||||||
}
|
}
|
||||||
|
deezerAppVersion = "9.0.11.4"
|
||||||
|
deezerAppLang = "us"
|
||||||
|
deezerBuildID = "android_minSDK26"
|
||||||
|
deezerScreenW = "1080"
|
||||||
|
deezerScreenH = "2134"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@@ -56,12 +62,16 @@ type Client struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *config.Config) *Client {
|
func New(cfg *config.Config) *Client {
|
||||||
|
httpClient := netutil.NewHTTPClient(30*time.Second, cfg.Session.Downloads.VerifySSL)
|
||||||
|
if jar, err := cookiejar.New(nil); err == nil {
|
||||||
|
httpClient.Jar = jar
|
||||||
|
}
|
||||||
return &Client{
|
return &Client{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
http: netutil.NewHTTPClient(30*time.Second, cfg.Session.Downloads.VerifySSL),
|
http: httpClient,
|
||||||
limiter: ratelimit.New(cfg.Session.Downloads.RequestsPerMinute),
|
limiter: ratelimit.New(cfg.Session.Downloads.RequestsPerMinute),
|
||||||
ua: randomDeezerUA(),
|
ua: randomDeezerUA(),
|
||||||
deviceID: randomHexN(16),
|
deviceID: randomHexN(32),
|
||||||
arl: strings.TrimSpace(cfg.Session.Deezer.ARL),
|
arl: strings.TrimSpace(cfg.Session.Deezer.ARL),
|
||||||
refresh: strings.TrimSpace(cfg.Session.Deezer.RefreshToken),
|
refresh: strings.TrimSpace(cfg.Session.Deezer.RefreshToken),
|
||||||
}
|
}
|
||||||
@@ -80,15 +90,6 @@ func (c *Client) Login(ctx context.Context) error {
|
|||||||
c.userID = ""
|
c.userID = ""
|
||||||
email := strings.TrimSpace(c.cfg.Session.Deezer.Email)
|
email := strings.TrimSpace(c.cfg.Session.Deezer.Email)
|
||||||
password := strings.TrimSpace(c.cfg.Session.Deezer.Password)
|
password := strings.TrimSpace(c.cfg.Session.Deezer.Password)
|
||||||
if c.refresh != "" {
|
|
||||||
if err := c.refreshJWT(ctx); err == nil {
|
|
||||||
_ = c.refreshLicenseFromPipe(ctx)
|
|
||||||
if c.license != "" {
|
|
||||||
c.loggedIn = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c.arl != "" {
|
if c.arl != "" {
|
||||||
if err := c.refreshSessionFromARL(ctx); err != nil {
|
if err := c.refreshSessionFromARL(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -97,6 +98,13 @@ func (c *Client) Login(ctx context.Context) error {
|
|||||||
if err := c.loginWithCredentials(ctx, email, password); err != nil {
|
if err := c.loginWithCredentials(ctx, email, password); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else if c.refresh != "" {
|
||||||
|
if err := c.refreshJWT(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.refreshLicenseFromPipe(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return errors.New("deezer login requires deezer.arl, deezer.email+deezer.password, or deezer.refresh_token")
|
return errors.New("deezer login requires deezer.arl, deezer.email+deezer.password, or deezer.refresh_token")
|
||||||
}
|
}
|
||||||
@@ -289,14 +297,22 @@ func (c *Client) GetDownloadable(ctx context.Context, item string, _ int) (*prov
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
trackToken := strings.TrimSpace(jsonutil.StringFromAny(meta["track_token"]))
|
trackToken, err := c.getTrackToken(ctx, item)
|
||||||
if trackToken == "" {
|
if err != nil {
|
||||||
trackToken, err = c.getTrackToken(ctx, item)
|
return nil, err
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
media, err := c.getMediaURL(ctx, trackToken, c.cfg.Session.Deezer.Quality, c.cfg.Session.Deezer.LowerQualityIfNotAvailable)
|
media, err := c.getMediaURL(ctx, trackToken, c.cfg.Session.Deezer.Quality, c.cfg.Session.Deezer.LowerQualityIfNotAvailable)
|
||||||
|
if err != nil {
|
||||||
|
var mediaErr *deezerMediaError
|
||||||
|
if errors.As(err, &mediaErr) && mediaErr.Code == 2004 {
|
||||||
|
if freshToken, tokenErr := c.getTrackTokenFromPipe(ctx, item); tokenErr == nil && strings.TrimSpace(freshToken) != "" && freshToken != trackToken {
|
||||||
|
if retryMedia, retryErr := c.getMediaURL(ctx, freshToken, c.cfg.Session.Deezer.Quality, c.cfg.Session.Deezer.LowerQualityIfNotAvailable); retryErr == nil {
|
||||||
|
media = retryMedia
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -406,10 +422,8 @@ func (c *Client) refreshSessionFromARL(ctx context.Context) error {
|
|||||||
c.userID = findStringByKey(results, "USER_ID")
|
c.userID = findStringByKey(results, "USER_ID")
|
||||||
c.jwt = jsonutil.FirstNonEmpty(c.jwt, findStringByKey(results, "JWT"))
|
c.jwt = jsonutil.FirstNonEmpty(c.jwt, findStringByKey(results, "JWT"))
|
||||||
c.refresh = jsonutil.FirstNonEmpty(c.refresh, findStringByKey(results, "refresh_token"))
|
c.refresh = jsonutil.FirstNonEmpty(c.refresh, findStringByKey(results, "refresh_token"))
|
||||||
if c.sid == "" {
|
if sid, sidErr := c.bootstrapSID(ctx); sidErr == nil && strings.TrimSpace(sid) != "" {
|
||||||
if sid, sidErr := c.bootstrapSID(ctx); sidErr == nil {
|
c.sid = sid
|
||||||
c.sid = sid
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if c.sid != "" && c.userID != "" {
|
if c.sid != "" && c.userID != "" {
|
||||||
_ = c.mobileUserAutolog(ctx)
|
_ = c.mobileUserAutolog(ctx)
|
||||||
@@ -417,7 +431,7 @@ func (c *Client) refreshSessionFromARL(ctx context.Context) error {
|
|||||||
if c.jwt == "" && c.refresh != "" {
|
if c.jwt == "" && c.refresh != "" {
|
||||||
_ = c.refreshJWT(ctx)
|
_ = c.refreshJWT(ctx)
|
||||||
}
|
}
|
||||||
if c.license == "" && c.jwt != "" {
|
if c.jwt != "" {
|
||||||
_ = c.refreshLicenseFromPipe(ctx)
|
_ = c.refreshLicenseFromPipe(ctx)
|
||||||
}
|
}
|
||||||
if c.license == "" {
|
if c.license == "" {
|
||||||
@@ -555,6 +569,36 @@ func (c *Client) loginWithCredentials(ctx context.Context, email, password strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) getTrackToken(ctx context.Context, trackID string) (string, error) {
|
func (c *Client) getTrackToken(ctx context.Context, trackID string) (string, error) {
|
||||||
|
if token, err := c.getTrackTokenFromPipe(ctx, trackID); err == nil && strings.TrimSpace(token) != "" {
|
||||||
|
return token, nil
|
||||||
|
} else if errors.Is(err, errDeezerJWTExpired) {
|
||||||
|
if strings.TrimSpace(c.refresh) != "" {
|
||||||
|
_ = c.refreshJWT(ctx)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(c.jwt) == "" && strings.TrimSpace(c.arl) != "" {
|
||||||
|
_ = c.refreshSessionFromARL(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(c.jwt) == "" {
|
||||||
|
if strings.TrimSpace(c.arl) != "" {
|
||||||
|
_ = c.refreshSessionFromARL(ctx)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(c.jwt) == "" && strings.TrimSpace(c.refresh) != "" {
|
||||||
|
_ = c.refreshJWT(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if token, err := c.getTrackTokenFromPipe(ctx, trackID); err == nil && strings.TrimSpace(token) != "" {
|
||||||
|
return token, nil
|
||||||
|
} else if errors.Is(err, errDeezerJWTExpired) {
|
||||||
|
if strings.TrimSpace(c.refresh) != "" {
|
||||||
|
_ = c.refreshJWT(ctx)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(c.jwt) != "" {
|
||||||
|
if retryToken, retryErr := c.getTrackTokenFromPipe(ctx, trackID); retryErr == nil && strings.TrimSpace(retryToken) != "" {
|
||||||
|
return retryToken, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
resp, err := c.apiGet(ctx, "/track/"+url.PathEscape(strings.TrimSpace(trackID)), nil)
|
resp, err := c.apiGet(ctx, "/track/"+url.PathEscape(strings.TrimSpace(trackID)), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -566,6 +610,78 @@ func (c *Client) getTrackToken(ctx context.Context, trackID string) (string, err
|
|||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) getTrackTokenFromPipe(ctx context.Context, trackID string) (string, error) {
|
||||||
|
if strings.TrimSpace(c.jwt) == "" {
|
||||||
|
return "", errors.New("deezer jwt unavailable for track media token")
|
||||||
|
}
|
||||||
|
if err := c.limiter.Wait(ctx); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
query := `query KmpMpTrackMedia($trackId: String!) { track(trackId: $trackId) { media { __typename ...TrackMediaFields } } } fragment TrackMediaFields on TrackMedia { id version token { payload expiresAt version } estimatedSizes { flac: FLAC mp3_320: MP3_320 mp3_128: MP3_128 mp3_misc: MP3_MISC opus_std: OPUS_STD opus_high: OPUS_HIGH sbc_256: SBC_256 aac_96: AAC_96 aac_64: AAC_64 ac4_ims: AC4_IMS dd_joc: DD_JOC mp4_ra1: MP4_RA1 mp4_ra2: MP4_RA2 mp4_ra3: MP4_RA3 } gain rights { sub { available } ads { available } } }`
|
||||||
|
body := map[string]any{
|
||||||
|
"operationName": "KmpMpTrackMedia",
|
||||||
|
"variables": map[string]any{"trackId": strings.TrimSpace(trackID)},
|
||||||
|
"query": query,
|
||||||
|
"extensions": map[string]any{
|
||||||
|
"clientLibrary": map[string]any{
|
||||||
|
"name": "apollo-kotlin",
|
||||||
|
"version": "4.4.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, pipeURL, bytes.NewReader(encoded))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", c.ua)
|
||||||
|
req.Header.Set("Accept", "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json")
|
||||||
|
req.Header.Set("Accept-Charset", "UTF-8")
|
||||||
|
req.Header.Set("Accept-Language", "en-US")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(c.jwt))
|
||||||
|
|
||||||
|
resp, err := c.http.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
raw, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return "", fmt.Errorf("deezer track media query failed: status=%d body=%s", resp.StatusCode, string(raw))
|
||||||
|
}
|
||||||
|
out := map[string]any{}
|
||||||
|
if err = json.Unmarshal(raw, &out); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if errs, ok := out["errors"].([]any); ok && len(errs) > 0 {
|
||||||
|
msg := ""
|
||||||
|
typ := ""
|
||||||
|
if em, ok := errs[0].(map[string]any); ok {
|
||||||
|
msg = strings.TrimSpace(jsonutil.StringFromAny(em["message"]))
|
||||||
|
typ = strings.TrimSpace(jsonutil.StringFromAny(em["type"]))
|
||||||
|
}
|
||||||
|
if strings.EqualFold(typ, "JwtTokenExpiredError") || strings.Contains(strings.ToLower(msg), "not valid anymore") || strings.Contains(strings.ToLower(msg), "jwt") && strings.Contains(strings.ToLower(msg), "expired") {
|
||||||
|
return "", errDeezerJWTExpired
|
||||||
|
}
|
||||||
|
if msg == "" {
|
||||||
|
msg = "unknown graphql error"
|
||||||
|
}
|
||||||
|
return "", errors.New(msg)
|
||||||
|
}
|
||||||
|
payload := strings.TrimSpace(jsonutil.StringFromAny(jsonutil.NestedMap(jsonutil.NestedMap(jsonutil.NestedMap(jsonutil.NestedMap(out, "data"), "track"), "media"), "token")["payload"]))
|
||||||
|
if payload == "" {
|
||||||
|
return "", errors.New("deezer track media response missing token payload")
|
||||||
|
}
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
type lyricsResult struct {
|
type lyricsResult struct {
|
||||||
Text string
|
Text string
|
||||||
SyncedLRC string
|
SyncedLRC string
|
||||||
@@ -717,6 +833,12 @@ func (c *Client) mobileAuth(ctx context.Context) (string, error) {
|
|||||||
params.Set("output", "3")
|
params.Set("output", "3")
|
||||||
params.Set("method", "mobile_auth")
|
params.Set("method", "mobile_auth")
|
||||||
params.Set("network", randomHexN(32))
|
params.Set("network", randomHexN(32))
|
||||||
|
params.Set("version", deezerAppVersion)
|
||||||
|
params.Set("lang", deezerAppLang)
|
||||||
|
params.Set("buildId", deezerBuildID)
|
||||||
|
params.Set("screenWidth", deezerScreenW)
|
||||||
|
params.Set("screenHeight", deezerScreenH)
|
||||||
|
params.Set("uniq_id", randomHexN(16))
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, gatewayURL+"?"+params.Encode(), nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, gatewayURL+"?"+params.Encode(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -819,13 +941,15 @@ func (c *Client) mobileUserAutolog(ctx context.Context) error {
|
|||||||
"ACCOUNT_ID": c.userID,
|
"ACCOUNT_ID": c.userID,
|
||||||
"arl": c.arl,
|
"arl": c.arl,
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(c.refresh) != "" {
|
||||||
|
payload["refresh_token"] = strings.TrimSpace(c.refresh)
|
||||||
|
}
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Set("api_key", apiKey)
|
params.Set("api_key", apiKey)
|
||||||
params.Set("sid", c.sid)
|
params.Set("sid", c.sid)
|
||||||
params.Set("output", "3")
|
params.Set("output", "3")
|
||||||
params.Set("input", "3")
|
params.Set("input", "3")
|
||||||
params.Set("network", randomHexN(32))
|
params.Set("network", randomHexN(32))
|
||||||
params.Set("arl", c.arl)
|
|
||||||
|
|
||||||
for _, method := range []string{"mobile_userAutolog", "mobile_userAutoLog"} {
|
for _, method := range []string{"mobile_userAutolog", "mobile_userAutoLog"} {
|
||||||
if err := c.limiter.Wait(ctx); err != nil {
|
if err := c.limiter.Wait(ctx); err != nil {
|
||||||
@@ -926,6 +1050,12 @@ func (c *Client) refreshLicenseFromPipe(ctx context.Context) error {
|
|||||||
"operationName": "KmpMpMediaServiceLicenseToken",
|
"operationName": "KmpMpMediaServiceLicenseToken",
|
||||||
"query": "query KmpMpMediaServiceLicenseToken { tokens { mediaServiceLicenseToken { token expirationDate } } }",
|
"query": "query KmpMpMediaServiceLicenseToken { tokens { mediaServiceLicenseToken { token expirationDate } } }",
|
||||||
"variables": map[string]any{},
|
"variables": map[string]any{},
|
||||||
|
"extensions": map[string]any{
|
||||||
|
"clientLibrary": map[string]any{
|
||||||
|
"name": "apollo-kotlin",
|
||||||
|
"version": "4.4.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
b, _ := json.Marshal(body)
|
b, _ := json.Marshal(body)
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, pipeURL, bytes.NewReader(b))
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, pipeURL, bytes.NewReader(b))
|
||||||
@@ -933,7 +1063,9 @@ func (c *Client) refreshLicenseFromPipe(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", c.ua)
|
req.Header.Set("User-Agent", c.ua)
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json")
|
||||||
|
req.Header.Set("Accept-Charset", "UTF-8")
|
||||||
|
req.Header.Set("Accept-Language", "en-US")
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(c.jwt))
|
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(c.jwt))
|
||||||
resp, err := c.http.Do(req)
|
resp, err := c.http.Do(req)
|
||||||
@@ -1096,26 +1228,51 @@ type mediaResult struct {
|
|||||||
Cipher string
|
Cipher string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) getMediaURL(ctx context.Context, trackToken string, quality int, allowFallback bool) (*mediaResult, error) {
|
type deezerMediaError struct {
|
||||||
requestedFormats := buildFormatPriority(quality, allowFallback)
|
Code int
|
||||||
var lastErr error
|
Message string
|
||||||
for _, format := range requestedFormats {
|
|
||||||
result, err := c.getMediaURLForFormat(ctx, trackToken, format)
|
|
||||||
if err == nil {
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
lastErr = err
|
|
||||||
if !allowFallback {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if lastErr != nil {
|
|
||||||
return nil, lastErr
|
|
||||||
}
|
|
||||||
return nil, errors.New("deezer media response contains no playable variants")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) getMediaURLForFormat(ctx context.Context, trackToken, format string) (*mediaResult, error) {
|
func (e *deezerMediaError) Error() string {
|
||||||
|
msg := strings.TrimSpace(e.Message)
|
||||||
|
if strings.Contains(strings.ToLower(msg), "drm") {
|
||||||
|
return "deezer media is DRM protected for this format/account"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("deezer media error %d: %s", e.Code, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getMediaURL(ctx context.Context, trackToken string, quality int, allowFallback bool) (*mediaResult, error) {
|
||||||
|
requestedFormats := buildFormatPriority(quality, allowFallback)
|
||||||
|
result, err := c.getMediaURLWithRequest(ctx, trackToken, requestedFormats)
|
||||||
|
if err == nil {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
var mediaErr *deezerMediaError
|
||||||
|
if errors.As(err, &mediaErr) && mediaErr.Code == 2004 {
|
||||||
|
if refreshErr := c.forceMobileLaunchRefresh(ctx); refreshErr == nil {
|
||||||
|
return c.getMediaURLWithRequest(ctx, trackToken, requestedFormats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) forceMobileLaunchRefresh(ctx context.Context) error {
|
||||||
|
if strings.TrimSpace(c.arl) != "" {
|
||||||
|
c.sid = ""
|
||||||
|
c.jwt = ""
|
||||||
|
c.license = ""
|
||||||
|
return c.refreshSessionFromARL(ctx)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(c.refresh) != "" {
|
||||||
|
if err := c.refreshJWT(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.refreshLicenseFromPipe(ctx)
|
||||||
|
}
|
||||||
|
return errors.New("deezer launch refresh requires arl or refresh token")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getMediaURLWithRequest(ctx context.Context, trackToken string, requestedFormats []string) (*mediaResult, error) {
|
||||||
if strings.TrimSpace(c.license) == "" {
|
if strings.TrimSpace(c.license) == "" {
|
||||||
return nil, errors.New("missing deezer license token")
|
return nil, errors.New("missing deezer license token")
|
||||||
}
|
}
|
||||||
@@ -1123,24 +1280,32 @@ func (c *Client) getMediaURLForFormat(ctx context.Context, trackToken, format st
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formats := make([]map[string]string, 0, 6)
|
||||||
|
for _, format := range []string{"FLAC", "MP3_320", "MP3_128"} {
|
||||||
|
formats = append(formats,
|
||||||
|
map[string]string{"cipher": "BF_CBC_STRIPE", "format": format},
|
||||||
|
map[string]string{"cipher": "NONE", "format": format},
|
||||||
|
)
|
||||||
|
}
|
||||||
reqBody := map[string]any{
|
reqBody := map[string]any{
|
||||||
"license_token": c.license,
|
"license_token": c.license,
|
||||||
"track_tokens": []string{trackToken},
|
"track_tokens": []string{trackToken},
|
||||||
"media": []map[string]any{{
|
"media": []map[string]any{{
|
||||||
"type": "FULL",
|
"type": "FULL",
|
||||||
"formats": []map[string]string{{"cipher": "BF_CBC_STRIPE", "format": format}, {"cipher": "NONE", "format": format}},
|
"formats": formats,
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(reqBody)
|
b, err := json.Marshal(reqBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, mediaURL, strings.NewReader(string(b)))
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, mediaURL, bytes.NewReader(b))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", c.ua)
|
req.Header.Set("User-Agent", c.ua)
|
||||||
req.Header.Set("Accept", "*/*")
|
req.Header.Set("Accept", "*/*")
|
||||||
|
req.Header.Set("Accept-Charset", "UTF-8")
|
||||||
req.Header.Set("Content-Type", "text/plain; charset=UTF-8")
|
req.Header.Set("Content-Type", "text/plain; charset=UTF-8")
|
||||||
|
|
||||||
resp, err := c.http.Do(req)
|
resp, err := c.http.Do(req)
|
||||||
@@ -1180,16 +1345,18 @@ func (c *Client) getMediaURLForFormat(ctx context.Context, trackToken, format st
|
|||||||
}
|
}
|
||||||
if len(parsed.Data[0].Errors) > 0 {
|
if len(parsed.Data[0].Errors) > 0 {
|
||||||
e := parsed.Data[0].Errors[0]
|
e := parsed.Data[0].Errors[0]
|
||||||
if strings.Contains(strings.ToLower(e.Message), "drm") {
|
return nil, &deezerMediaError{Code: e.Code, Message: e.Message}
|
||||||
return nil, errors.New("deezer media is DRM protected for this format/account")
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("deezer media error %d: %s", e.Code, e.Message)
|
|
||||||
}
|
}
|
||||||
for _, m := range parsed.Data[0].Media {
|
for _, want := range requestedFormats {
|
||||||
if len(m.Sources) == 0 || strings.TrimSpace(m.Sources[0].URL) == "" {
|
for _, m := range parsed.Data[0].Media {
|
||||||
continue
|
if !strings.EqualFold(strings.TrimSpace(m.Format), want) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(m.Sources) == 0 || strings.TrimSpace(m.Sources[0].URL) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return &mediaResult{URL: m.Sources[0].URL, Format: m.Format, Cipher: m.Cipher.Type}, nil
|
||||||
}
|
}
|
||||||
return &mediaResult{URL: m.Sources[0].URL, Format: m.Format, Cipher: m.Cipher.Type}, nil
|
|
||||||
}
|
}
|
||||||
return nil, errors.New("deezer media response contains no sources")
|
return nil, errors.New("deezer media response contains no sources")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,23 @@ func TestGetDownloadableNativeCipher(t *testing.T) {
|
|||||||
case "/track/42":
|
case "/track/42":
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "title": "X", "track_token": "tt"})
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "title": "X", "track_token": "tt"})
|
||||||
case "/media":
|
case "/media":
|
||||||
|
if got := strings.TrimSpace(r.Header.Get("Accept-Charset")); got != "UTF-8" {
|
||||||
|
t.Fatalf("accept-charset = %q, want UTF-8", got)
|
||||||
|
}
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
media, _ := payload["media"].([]any)
|
||||||
|
if len(media) != 1 {
|
||||||
|
t.Fatalf("media length = %d, want 1", len(media))
|
||||||
|
}
|
||||||
|
entry, _ := media[0].(map[string]any)
|
||||||
|
formats, _ := entry["formats"].([]any)
|
||||||
|
if len(formats) != 6 {
|
||||||
|
t.Fatalf("formats length = %d, want 6", len(formats))
|
||||||
|
}
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"errors": []any{}, "media": []any{map[string]any{"cipher": map[string]any{"type": "BF_CBC_STRIPE"}, "format": "FLAC", "sources": []any{map[string]any{"url": "https://cdn.example/file"}}}}}}})
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"errors": []any{}, "media": []any{map[string]any{"cipher": map[string]any{"type": "BF_CBC_STRIPE"}, "format": "FLAC", "sources": []any{map[string]any{"url": "https://cdn.example/file"}}}}}}})
|
||||||
default:
|
default:
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
@@ -141,6 +158,66 @@ func TestGetDownloadableNativeCipher(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoginPrefersARLFlowOverRefreshShortcut(t *testing.T) {
|
||||||
|
mobileToken := testMobileToken(t)
|
||||||
|
refreshCalled := false
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/web":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"results": map[string]any{"USER_ID": "42", "JWT": "jwt-from-web", "refresh_token": "refresh-from-web", "license_token": "license-from-web"}})
|
||||||
|
case "/gateway":
|
||||||
|
switch r.URL.Query().Get("method") {
|
||||||
|
case "mobile_auth":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"results": map[string]any{"TOKEN": mobileToken}})
|
||||||
|
case "api_checkToken":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"results": "sid123"})
|
||||||
|
case "mobile_userAutolog":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"results": map[string]any{"JWT": "jwt-from-autolog", "license_token": "license-from-autolog", "refresh_token": "refresh-from-autolog"}})
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
case "/pipe":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"tokens": map[string]any{"mediaServiceLicenseToken": map[string]any{"token": "license-from-pipe"}}}})
|
||||||
|
case "/renew":
|
||||||
|
refreshCalled = true
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
cfgData := config.DefaultConfigData()
|
||||||
|
cfgData.Deezer.ARL = "arl"
|
||||||
|
cfgData.Deezer.RefreshToken = "refresh-token"
|
||||||
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||||
|
|
||||||
|
origGateway := gatewayURL
|
||||||
|
origWeb := webGWLight
|
||||||
|
origPipe := pipeURL
|
||||||
|
origAuth := authURL
|
||||||
|
gatewayURL = ts.URL + "/gateway"
|
||||||
|
webGWLight = ts.URL + "/web"
|
||||||
|
pipeURL = ts.URL + "/pipe"
|
||||||
|
authURL = ts.URL + "/renew"
|
||||||
|
defer func() {
|
||||||
|
gatewayURL = origGateway
|
||||||
|
webGWLight = origWeb
|
||||||
|
pipeURL = origPipe
|
||||||
|
authURL = origAuth
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := c.Login(context.Background()); err != nil {
|
||||||
|
t.Fatalf("Login() error = %v", err)
|
||||||
|
}
|
||||||
|
if refreshCalled {
|
||||||
|
t.Fatalf("expected ARL launch flow without refresh shortcut")
|
||||||
|
}
|
||||||
|
if c.license != "license-from-pipe" {
|
||||||
|
t.Fatalf("license = %q, want license-from-pipe", c.license)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetDownloadableRequiresARL(t *testing.T) {
|
func TestGetDownloadableRequiresARL(t *testing.T) {
|
||||||
cfgData := config.DefaultConfigData()
|
cfgData := config.DefaultConfigData()
|
||||||
cfgData.Deezer.ARL = ""
|
cfgData.Deezer.ARL = ""
|
||||||
@@ -191,6 +268,96 @@ func TestGetDownloadableDRMError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetTrackTokenPrefersPipeToken(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/pipe":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"track": map[string]any{"media": map[string]any{"token": map[string]any{"payload": "pipe-track-token"}}}}})
|
||||||
|
case "/track/42":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "track_token": "api-track-token"})
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
cfgData := config.DefaultConfigData()
|
||||||
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||||
|
c.loggedIn = true
|
||||||
|
c.jwt = "jwt-token"
|
||||||
|
|
||||||
|
origBase := baseURL
|
||||||
|
origPipe := pipeURL
|
||||||
|
baseURL = ts.URL
|
||||||
|
pipeURL = ts.URL + "/pipe"
|
||||||
|
defer func() {
|
||||||
|
baseURL = origBase
|
||||||
|
pipeURL = origPipe
|
||||||
|
}()
|
||||||
|
|
||||||
|
token, err := c.getTrackToken(context.Background(), "42")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getTrackToken() error = %v", err)
|
||||||
|
}
|
||||||
|
if token != "pipe-track-token" {
|
||||||
|
t.Fatalf("token = %q, want pipe-track-token", token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDownloadableUsesPipeTrackToken(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/track/42":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "title": "X", "track_token": "api-track-token"})
|
||||||
|
case "/pipe":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"track": map[string]any{"media": map[string]any{"token": map[string]any{"payload": "pipe-track-token"}}}}})
|
||||||
|
case "/media":
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tokens, _ := payload["track_tokens"].([]any)
|
||||||
|
if len(tokens) == 0 || strings.TrimSpace(jsonutil.StringFromAny(tokens[0])) != "pipe-track-token" {
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"errors": []any{map[string]any{"code": 2004, "message": "The track country differs from the license."}}, "media": []any{}}}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"errors": []any{}, "media": []any{map[string]any{"cipher": map[string]any{"type": "BF_CBC_STRIPE"}, "format": "FLAC", "sources": []any{map[string]any{"url": "https://cdn.example/file"}}}}}}})
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
cfgData := config.DefaultConfigData()
|
||||||
|
cfgData.Deezer.ARL = "arl"
|
||||||
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||||
|
c.loggedIn = true
|
||||||
|
c.arl = "arl"
|
||||||
|
c.license = "license"
|
||||||
|
c.jwt = "jwt"
|
||||||
|
|
||||||
|
origBase := baseURL
|
||||||
|
origMedia := mediaURL
|
||||||
|
origPipe := pipeURL
|
||||||
|
baseURL = ts.URL
|
||||||
|
mediaURL = ts.URL + "/media"
|
||||||
|
pipeURL = ts.URL + "/pipe"
|
||||||
|
defer func() {
|
||||||
|
baseURL = origBase
|
||||||
|
mediaURL = origMedia
|
||||||
|
pipeURL = origPipe
|
||||||
|
}()
|
||||||
|
|
||||||
|
d, err := c.GetDownloadable(context.Background(), "42", 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetDownloadable() error = %v", err)
|
||||||
|
}
|
||||||
|
if d.URL != "https://cdn.example/file" || d.Extension != "flac" {
|
||||||
|
t.Fatalf("unexpected downloadable: %+v", d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetMetadataAddsLyricsFromPipe(t *testing.T) {
|
func TestGetMetadataAddsLyricsFromPipe(t *testing.T) {
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
@@ -292,6 +459,102 @@ func TestLoginWithCredentials(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMobileAuthIncludesAppContextParams(t *testing.T) {
|
||||||
|
mobileToken := testMobileToken(t)
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/gateway" {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.URL.Query().Get("method") != "mobile_auth" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got := strings.TrimSpace(r.URL.Query().Get("version")); got != deezerAppVersion {
|
||||||
|
t.Fatalf("version = %q, want %q", got, deezerAppVersion)
|
||||||
|
}
|
||||||
|
if got := strings.TrimSpace(r.URL.Query().Get("lang")); got != deezerAppLang {
|
||||||
|
t.Fatalf("lang = %q, want %q", got, deezerAppLang)
|
||||||
|
}
|
||||||
|
if got := strings.TrimSpace(r.URL.Query().Get("buildId")); got != deezerBuildID {
|
||||||
|
t.Fatalf("buildId = %q, want %q", got, deezerBuildID)
|
||||||
|
}
|
||||||
|
if got := strings.TrimSpace(r.URL.Query().Get("screenWidth")); got != deezerScreenW {
|
||||||
|
t.Fatalf("screenWidth = %q, want %q", got, deezerScreenW)
|
||||||
|
}
|
||||||
|
if got := strings.TrimSpace(r.URL.Query().Get("screenHeight")); got != deezerScreenH {
|
||||||
|
t.Fatalf("screenHeight = %q, want %q", got, deezerScreenH)
|
||||||
|
}
|
||||||
|
if got := strings.TrimSpace(r.URL.Query().Get("uniq_id")); got == "" {
|
||||||
|
t.Fatalf("uniq_id is empty")
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"results": map[string]any{"TOKEN": mobileToken}})
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
cfgData := config.DefaultConfigData()
|
||||||
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||||
|
|
||||||
|
origGateway := gatewayURL
|
||||||
|
gatewayURL = ts.URL + "/gateway"
|
||||||
|
defer func() { gatewayURL = origGateway }()
|
||||||
|
|
||||||
|
token, err := c.mobileAuth(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mobileAuth() error = %v", err)
|
||||||
|
}
|
||||||
|
if token != mobileToken {
|
||||||
|
t.Fatalf("token = %q, want %q", token, mobileToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMobileUserAutologIncludesRefreshToken(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/gateway" {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if method := r.URL.Query().Get("method"); method != "mobile_userAutolog" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got := strings.TrimSpace(r.URL.Query().Get("arl")); got != "" {
|
||||||
|
t.Fatalf("unexpected arl query parameter: %q", got)
|
||||||
|
}
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got := strings.TrimSpace(jsonutil.StringFromAny(payload["refresh_token"])); got != "refresh-token" {
|
||||||
|
t.Fatalf("refresh_token payload = %q, want refresh-token", got)
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"results": map[string]any{"JWT": "jwt-token", "license_token": "license-token"}})
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
cfgData := config.DefaultConfigData()
|
||||||
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||||
|
c.sid = "sid123"
|
||||||
|
c.userID = "42"
|
||||||
|
c.arl = "arl-token"
|
||||||
|
c.refresh = "refresh-token"
|
||||||
|
|
||||||
|
origGateway := gatewayURL
|
||||||
|
gatewayURL = ts.URL + "/gateway"
|
||||||
|
defer func() { gatewayURL = origGateway }()
|
||||||
|
|
||||||
|
if err := c.mobileUserAutolog(context.Background()); err != nil {
|
||||||
|
t.Fatalf("mobileUserAutolog() error = %v", err)
|
||||||
|
}
|
||||||
|
if c.jwt != "jwt-token" {
|
||||||
|
t.Fatalf("jwt = %q, want jwt-token", c.jwt)
|
||||||
|
}
|
||||||
|
if c.license != "license-token" {
|
||||||
|
t.Fatalf("license = %q, want license-token", c.license)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testMobileToken(t *testing.T) string {
|
func testMobileToken(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
plain := []byte(strings.Repeat("A", 64) + strings.Repeat("B", 16) + strings.Repeat("C", 16))
|
plain := []byte(strings.Repeat("A", 64) + strings.Repeat("B", 16) + strings.Repeat("C", 16))
|
||||||
@@ -382,3 +645,51 @@ func TestRefreshLicenseFromPipeGraphQLError(t *testing.T) {
|
|||||||
t.Fatalf("expected graphql error, got %v", err)
|
t.Fatalf("expected graphql error, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRefreshLicenseFromPipeUsesMobileGraphQLShape(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if got := strings.TrimSpace(r.Header.Get("Authorization")); got != "Bearer jwt-token" {
|
||||||
|
t.Fatalf("authorization = %q, want Bearer jwt-token", got)
|
||||||
|
}
|
||||||
|
if got := r.Header.Get("Accept"); !strings.Contains(got, "application/graphql-response+json") {
|
||||||
|
t.Fatalf("accept header = %q", got)
|
||||||
|
}
|
||||||
|
if got := strings.TrimSpace(r.Header.Get("Accept-Language")); got != "en-US" {
|
||||||
|
t.Fatalf("accept-language = %q, want en-US", got)
|
||||||
|
}
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
op := strings.TrimSpace(jsonutil.StringFromAny(payload["operationName"]))
|
||||||
|
if op != "KmpMpMediaServiceLicenseToken" {
|
||||||
|
t.Fatalf("operationName = %q, want KmpMpMediaServiceLicenseToken", op)
|
||||||
|
}
|
||||||
|
ext, _ := payload["extensions"].(map[string]any)
|
||||||
|
clientLib, _ := ext["clientLibrary"].(map[string]any)
|
||||||
|
if got := strings.TrimSpace(jsonutil.StringFromAny(clientLib["name"])); got != "apollo-kotlin" {
|
||||||
|
t.Fatalf("clientLibrary.name = %q, want apollo-kotlin", got)
|
||||||
|
}
|
||||||
|
if got := strings.TrimSpace(jsonutil.StringFromAny(clientLib["version"])); got != "4.4.2" {
|
||||||
|
t.Fatalf("clientLibrary.version = %q, want 4.4.2", got)
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"tokens": map[string]any{"mediaServiceLicenseToken": map[string]any{"token": "license-token"}}}})
|
||||||
|
}))
|
||||||
|
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 }()
|
||||||
|
|
||||||
|
if err := c.refreshLicenseFromPipe(context.Background()); err != nil {
|
||||||
|
t.Fatalf("refreshLicenseFromPipe() error = %v", err)
|
||||||
|
}
|
||||||
|
if c.license != "license-token" {
|
||||||
|
t.Fatalf("license = %q, want license-token", c.license)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user