From 488a8142f9524e9683bd81289d05b2a9bb0430b2 Mon Sep 17 00:00:00 2001 From: Deluan Date: Thu, 20 Nov 2025 22:17:42 -0500 Subject: [PATCH] refactor(deezer): move JWT token handling to a separate file for better organization Signed-off-by: Deluan --- core/agents/deezer/client.go | 76 -------------------------- core/agents/deezer/client_auth.go | 88 +++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 76 deletions(-) create mode 100644 core/agents/deezer/client_auth.go diff --git a/core/agents/deezer/client.go b/core/agents/deezer/client.go index 39a3303b6..32d93bad6 100644 --- a/core/agents/deezer/client.go +++ b/core/agents/deezer/client.go @@ -11,8 +11,6 @@ import ( "net/url" "strconv" "strings" - "sync" - "time" "github.com/microcosm-cc/bluemonday" "github.com/navidrome/navidrome/log" @@ -29,28 +27,6 @@ type httpDoer interface { Do(req *http.Request) (*http.Response, error) } -type jwtToken struct { - token string - expiresAt time.Time - mu sync.RWMutex -} - -func (j *jwtToken) get() (string, bool) { - j.mu.RLock() - defer j.mu.RUnlock() - if time.Now().Before(j.expiresAt) { - return j.token, true - } - return "", false -} - -func (j *jwtToken) set(token string, expiresIn time.Duration) { - j.mu.Lock() - defer j.mu.Unlock() - j.token = token - j.expiresAt = time.Now().Add(expiresIn) -} - type client struct { httpDoer httpDoer language string @@ -152,58 +128,6 @@ const pipeAPIURL = "https://pipe.deezer.com/api" var strictPolicy = bluemonday.StrictPolicy() -func (c *client) getJWT(ctx context.Context) (string, error) { - // Check if we have a valid cached token - if token, valid := c.jwt.get(); valid { - return token, nil - } - - // Fetch a new anonymous token - req, err := http.NewRequestWithContext(ctx, "GET", authBaseURL+"/login/anonymous?jo=p&rto=c", nil) - if err != nil { - return "", err - } - req.Header.Set("Accept", "application/json") - - resp, err := c.httpDoer.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return "", fmt.Errorf("deezer: failed to get JWT token: %s", resp.Status) - } - - data, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - type authResponse struct { - JWT string `json:"jwt"` - RefreshToken string `json:"refresh_token"` - } - - var result authResponse - if err := json.Unmarshal(data, &result); err != nil { - return "", fmt.Errorf("deezer: failed to parse auth response: %w", err) - } - - if result.JWT == "" { - return "", errors.New("deezer: no JWT token in response") - } - // Cache the token for 50 minutes (tokens expire in 1 hour). - // The 10-minute buffer helps handle clock skew, network delays, or timing issues, - // ensuring we refresh the token before it actually expires. - // Note: c.jwt is assumed to be thread-safe. - // Cache the token for 50 minutes (tokens expire in 1 hour) - c.jwt.set(result.JWT, 50*time.Minute) - log.Trace(ctx, "Fetched new Deezer JWT token") - - return result.JWT, nil -} - func (c *client) getArtistBio(ctx context.Context, artistID int) (string, error) { jwt, err := c.getJWT(ctx) if err != nil { diff --git a/core/agents/deezer/client_auth.go b/core/agents/deezer/client_auth.go new file mode 100644 index 000000000..f0f655605 --- /dev/null +++ b/core/agents/deezer/client_auth.go @@ -0,0 +1,88 @@ +package deezer + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "sync" + "time" + + "github.com/navidrome/navidrome/log" +) + +type jwtToken struct { + token string + expiresAt time.Time + mu sync.RWMutex +} + +func (j *jwtToken) get() (string, bool) { + j.mu.RLock() + defer j.mu.RUnlock() + if time.Now().Before(j.expiresAt) { + return j.token, true + } + return "", false +} + +func (j *jwtToken) set(token string, expiresIn time.Duration) { + j.mu.Lock() + defer j.mu.Unlock() + j.token = token + j.expiresAt = time.Now().Add(expiresIn) +} + +func (c *client) getJWT(ctx context.Context) (string, error) { + // Check if we have a valid cached token + if token, valid := c.jwt.get(); valid { + return token, nil + } + + // Fetch a new anonymous token + req, err := http.NewRequestWithContext(ctx, "GET", authBaseURL+"/login/anonymous?jo=p&rto=c", nil) + if err != nil { + return "", err + } + req.Header.Set("Accept", "application/json") + + resp, err := c.httpDoer.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return "", fmt.Errorf("deezer: failed to get JWT token: %s", resp.Status) + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + type authResponse struct { + JWT string `json:"jwt"` + RefreshToken string `json:"refresh_token"` + } + + var result authResponse + if err := json.Unmarshal(data, &result); err != nil { + return "", fmt.Errorf("deezer: failed to parse auth response: %w", err) + } + + if result.JWT == "" { + return "", errors.New("deezer: no JWT token in response") + } + // Cache the token for 50 minutes (tokens expire in 1 hour). + // The 10-minute buffer helps handle clock skew, network delays, or timing issues, + // ensuring we refresh the token before it actually expires. + // Note: c.jwt is assumed to be thread-safe. + // Cache the token for 50 minutes (tokens expire in 1 hour) + c.jwt.set(result.JWT, 50*time.Minute) + log.Trace(ctx, "Fetched new Deezer JWT token") + + return result.JWT, nil +}