Files
yandex-music-api-go/internal/client/auth.go
T
MrixsandClaude Code 0456ca0665 Add comprehensive godoc documentation to all exported symbols
- Document all exported types, functions, methods, and constants across 52 files
- Add package-level doc comments to client, models, utils, and ynison packages
- Document HTTP method and endpoint path for each API client method
- Document model struct fields and DeJSON/DeList/ToJSON/ToMap methods
- Fix duplicate package doc comment in internal/models/base.go

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-18 08:57:43 +03:00

203 lines
6.4 KiB
Go

package client
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
)
// DeviceCode represents the device authorization response from the Yandex OAuth device flow.
type DeviceCode struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURL string `json:"verification_url"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
// OAuthToken represents an OAuth token response from the Yandex OAuth token endpoint.
type OAuthToken struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
// DeviceAuth performs OAuth Device Flow authentication.
// It requests a device code, invokes the onCode callback with it, then polls for the token.
// The onCode callback receives the DeviceCode which contains the user_code and verification URL.
func (r *Request) DeviceAuth(ctx context.Context, onCode func(*DeviceCode)) (*OAuthToken, error) {
// Step 1: Request device code
deviceCode, err := r.requestDeviceCode(ctx)
if err != nil {
return nil, err
}
// Call callback with device code
if onCode != nil {
onCode(deviceCode)
}
// Step 2: Poll for token
return r.pollForToken(ctx, deviceCode)
}
// requestDeviceCode requests a device code from the Yandex OAuth device code endpoint.
func (r *Request) requestDeviceCode(ctx context.Context) (*DeviceCode, error) {
// Use Yandex OAuth endpoint for device auth
data := url.Values{}
data.Set("client_id", "23cabbbdc6cd418abb4b39c32c41195d")
data.Set("scope", "login:info login:email yandex_music")
req, err := http.NewRequestWithContext(ctx, "POST", "https://oauth.yandex.ru/device/code", strings.NewReader(data.Encode()))
if err != nil {
return nil, utils.NewYandexMusicError("failed to create device code request", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", UserAgent)
resp, err := r.client.Do(req)
if err != nil {
return nil, utils.NewYandexMusicError("failed to request device code", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, utils.NewYandexMusicError("failed to read device code response", err)
}
if resp.StatusCode != http.StatusOK {
return nil, utils.NewYandexMusicError(fmt.Sprintf("device code request failed: %s", string(body)), utils.ErrBadRequest)
}
var deviceCode DeviceCode
if err := json.Unmarshal(body, &deviceCode); err != nil {
return nil, utils.NewYandexMusicError("failed to parse device code", err)
}
return &deviceCode, nil
}
// pollForToken polls the Yandex OAuth token endpoint until the user authorizes the device code.
// It returns the OAuthToken once authorization is complete, or an error if the code expires or is denied.
func (r *Request) pollForToken(ctx context.Context, deviceCode *DeviceCode) (*OAuthToken, error) {
interval := deviceCode.Interval
if interval == 0 {
interval = 5
}
deadline := time.Now().Add(time.Duration(deviceCode.ExpiresIn) * time.Second)
ticker := time.NewTicker(time.Duration(interval) * time.Second)
defer ticker.Stop()
data := url.Values{}
data.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
data.Set("client_id", "23cabbbdc6cd418abb4b39c32c41195d")
data.Set("device_code", deviceCode.DeviceCode)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
if time.Now().After(deadline) {
return nil, utils.NewYandexMusicError("device authorization timed out", utils.ErrTimeout)
}
req, err := http.NewRequestWithContext(ctx, "POST", "https://oauth.yandex.ru/token", strings.NewReader(data.Encode()))
if err != nil {
return nil, utils.NewYandexMusicError("failed to create token request", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", UserAgent)
resp, err := r.client.Do(req)
if err != nil {
continue // Retry on network error
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
var token OAuthToken
if err := json.Unmarshal(body, &token); err != nil {
return nil, utils.NewYandexMusicError("failed to parse token", err)
}
return &token, nil
}
// Check for specific errors
var errResp map[string]string
json.Unmarshal(body, &errResp)
if errResp["error"] == "authorization_pending" {
continue // Keep polling
}
if errResp["error"] == "slow_down" {
interval *= 2
ticker.Reset(time.Duration(interval) * time.Second)
continue
}
if errResp["error"] == "expired_token" {
return nil, utils.NewYandexMusicError("device code expired", utils.ErrTimeout)
}
if errResp["error"] == "access_denied" {
return nil, utils.NewYandexMusicError("access denied by user", utils.ErrUnauthorized)
}
}
}
}
// RefreshToken refreshes an access token using the provided refresh token.
// It makes a POST request to the Yandex OAuth token endpoint with grant_type=refresh_token.
func (r *Request) RefreshToken(ctx context.Context, refreshToken string) (*OAuthToken, error) {
data := url.Values{}
data.Set("grant_type", "refresh_token")
data.Set("client_id", "23cabbbdc6cd418abb4b39c32c41195d")
data.Set("refresh_token", refreshToken)
req, err := http.NewRequestWithContext(ctx, "POST", "https://oauth.yandex.ru/token", strings.NewReader(data.Encode()))
if err != nil {
return nil, utils.NewYandexMusicError("failed to create refresh request", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", UserAgent)
resp, err := r.client.Do(req)
if err != nil {
return nil, utils.NewYandexMusicError("failed to refresh token", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, utils.NewYandexMusicError("failed to read refresh response", err)
}
if resp.StatusCode != http.StatusOK {
return nil, utils.NewYandexMusicError(fmt.Sprintf("token refresh failed: %s", string(body)), utils.ErrUnauthorized)
}
var token OAuthToken
if err := json.Unmarshal(body, &token); err != nil {
return nil, utils.NewYandexMusicError("failed to parse refreshed token", err)
}
return &token, nil
}
// Missing imports - need to add at top of file
// "io"
// "net/http"