Initial commit
This commit is contained in:
commit
02a6dabcfb
62 files changed
+15751
No files matched your search
@@ -0,0 +1,198 @@
|
||||
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
|
||||
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
|
||||
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
|
||||
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 Yandex OAuth
|
||||
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 for the OAuth token
|
||||
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 a 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"
|
||||
Reference in new issue
Block a user