263 lines
6.8 KiB
Go
263 lines
6.8 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
|
)
|
|
|
|
// Default values
|
|
const (
|
|
DefaultBaseURL = "https://api.music.yandex.net"
|
|
DefaultTimeout = 5 * time.Second
|
|
UserAgent = "Yandex-Music-API"
|
|
DefaultClientHeader = "YandexMusicAndroid/24023621"
|
|
)
|
|
|
|
// RequestConfig holds configuration for the request client
|
|
type RequestConfig struct {
|
|
BaseURL string
|
|
Token string
|
|
ProxyURL string
|
|
Timeout time.Duration
|
|
Headers map[string]string
|
|
JSONBackend utils.JSONBackend
|
|
Language string
|
|
Device string
|
|
ReportUnknownFields bool
|
|
Strict bool
|
|
OnSchemaMismatch utils.SchemaMismatchHandler
|
|
}
|
|
|
|
// Request handles HTTP requests to Yandex Music API
|
|
type Request struct {
|
|
config RequestConfig
|
|
client *http.Client
|
|
schemaTracker *utils.SchemaMismatchTracker
|
|
}
|
|
|
|
// NewRequest creates a new Request instance
|
|
func NewRequest(config RequestConfig) *Request {
|
|
if config.BaseURL == "" {
|
|
config.BaseURL = DefaultBaseURL
|
|
}
|
|
if config.Timeout == 0 {
|
|
config.Timeout = DefaultTimeout
|
|
}
|
|
if config.Headers == nil {
|
|
config.Headers = make(map[string]string)
|
|
}
|
|
if config.JSONBackend == nil {
|
|
config.JSONBackend = utils.GetDefaultJSONBackend()
|
|
}
|
|
|
|
// Set default headers
|
|
if _, ok := config.Headers["X-Yandex-Music-Client"]; !ok {
|
|
config.Headers["X-Yandex-Music-Client"] = DefaultClientHeader
|
|
}
|
|
|
|
// Configure HTTP client
|
|
httpClient := &http.Client{
|
|
Timeout: config.Timeout,
|
|
}
|
|
|
|
// Configure proxy if provided
|
|
if config.ProxyURL != "" {
|
|
proxyURL, err := url.Parse(config.ProxyURL)
|
|
if err == nil {
|
|
httpClient.Transport = &http.Transport{
|
|
Proxy: http.ProxyURL(proxyURL),
|
|
}
|
|
}
|
|
}
|
|
|
|
r := &Request{
|
|
config: config,
|
|
client: httpClient,
|
|
schemaTracker: utils.NewSchemaMismatchTracker(),
|
|
}
|
|
|
|
// Set token if provided
|
|
if config.Token != "" {
|
|
r.SetToken(config.Token)
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// SetToken updates the authorization token
|
|
func (r *Request) SetToken(token string) {
|
|
r.config.Token = token
|
|
if token != "" {
|
|
r.config.Headers["Authorization"] = "OAuth " + token
|
|
} else {
|
|
delete(r.config.Headers, "Authorization")
|
|
}
|
|
}
|
|
|
|
// SetLanguage sets the Accept-Language header
|
|
func (r *Request) SetLanguage(lang string) {
|
|
r.config.Language = lang
|
|
r.config.Headers["Accept-Language"] = lang
|
|
}
|
|
|
|
// Get performs a GET request
|
|
func (r *Request) Get(ctx context.Context, path string, params map[string]string, result any) error {
|
|
return r.doRequest(ctx, "GET", path, params, nil, result)
|
|
}
|
|
|
|
// Post performs a POST request
|
|
func (r *Request) Post(ctx context.Context, path string, body any, result any) error {
|
|
return r.doRequest(ctx, "POST", path, nil, body, result)
|
|
}
|
|
|
|
// Put performs a PUT request
|
|
func (r *Request) Put(ctx context.Context, path string, body any, result any) error {
|
|
return r.doRequest(ctx, "PUT", path, nil, body, result)
|
|
}
|
|
|
|
// Delete performs a DELETE request
|
|
func (r *Request) Delete(ctx context.Context, path string, body any, result any) error {
|
|
return r.doRequest(ctx, "DELETE", path, nil, body, result)
|
|
}
|
|
|
|
// doRequest performs the actual HTTP request
|
|
func (r *Request) doRequest(ctx context.Context, method, path string, params map[string]string, body any, result any) error {
|
|
// Build URL
|
|
fullURL := r.config.BaseURL + path
|
|
if len(params) > 0 {
|
|
q := url.Values{}
|
|
for k, v := range params {
|
|
q.Set(k, v)
|
|
}
|
|
fullURL += "?" + q.Encode()
|
|
}
|
|
|
|
// Prepare request body
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
bodyBytes, err := r.config.JSONBackend.Marshal(body)
|
|
if err != nil {
|
|
return utils.NewYandexMusicError("failed to marshal request body", err)
|
|
}
|
|
bodyReader = bytes.NewReader(bodyBytes)
|
|
}
|
|
|
|
// Create HTTP request
|
|
req, err := http.NewRequestWithContext(ctx, method, fullURL, bodyReader)
|
|
if err != nil {
|
|
return utils.NewYandexMusicError("failed to create request", err)
|
|
}
|
|
|
|
// Set headers
|
|
req.Header.Set("User-Agent", UserAgent)
|
|
for k, v := range r.config.Headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
// Execute request
|
|
resp, err := r.client.Do(req)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "timeout") {
|
|
return utils.NewYandexMusicError("request timeout", utils.ErrTimeout)
|
|
}
|
|
return utils.NewYandexMusicError("network error", utils.ErrNetwork)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read response
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return utils.NewYandexMusicError("failed to read response", err)
|
|
}
|
|
|
|
// Handle error status codes
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return r.handleErrorResponse(resp.StatusCode, respBody)
|
|
}
|
|
|
|
// Parse response
|
|
if result != nil {
|
|
apiResp, err := utils.DeJSON(respBody)
|
|
if err != nil {
|
|
return utils.NewYandexMusicError("failed to parse response", err)
|
|
}
|
|
|
|
if apiResp.HasError() {
|
|
return utils.NewYandexMusicError(apiResp.GetError(), utils.ErrBadRequest)
|
|
}
|
|
|
|
// Unmarshal result into target
|
|
if apiResp.Result != nil {
|
|
if err := r.config.JSONBackend.Unmarshal(apiResp.Result, result); err != nil {
|
|
return utils.NewYandexMusicError("failed to unmarshal result", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleErrorResponse handles HTTP error responses
|
|
func (r *Request) handleErrorResponse(statusCode int, body []byte) error {
|
|
var msg string
|
|
apiResp, err := utils.DeJSON(body)
|
|
if err == nil && apiResp != nil {
|
|
msg = apiResp.GetError()
|
|
} else {
|
|
msg = string(body)
|
|
}
|
|
|
|
switch statusCode {
|
|
case http.StatusUnauthorized, http.StatusForbidden:
|
|
return utils.NewYandexMusicError(msg, utils.ErrUnauthorized)
|
|
case http.StatusBadRequest:
|
|
return utils.NewYandexMusicError(msg, utils.ErrBadRequest)
|
|
case http.StatusNotFound:
|
|
return utils.NewYandexMusicError(msg, utils.ErrNotFound)
|
|
case http.StatusConflict, http.StatusRequestEntityTooLarge:
|
|
return utils.NewYandexMusicError(msg, utils.ErrNetwork)
|
|
case http.StatusBadGateway:
|
|
return utils.NewYandexMusicError("Bad Gateway", utils.ErrNetwork)
|
|
default:
|
|
return utils.NewYandexMusicError(fmt.Sprintf("%s (%d): %s", msg, statusCode, string(body)), utils.ErrNetwork)
|
|
}
|
|
}
|
|
|
|
// Download downloads a file from a URL
|
|
func (r *Request) Download(ctx context.Context, urlStr string) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
|
if err != nil {
|
|
return nil, utils.NewYandexMusicError("failed to create download request", err)
|
|
}
|
|
|
|
req.Header.Set("User-Agent", UserAgent)
|
|
for k, v := range r.config.Headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
|
|
resp, err := r.client.Do(req)
|
|
if err != nil {
|
|
return nil, utils.NewYandexMusicError("download failed", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, r.handleErrorResponse(resp.StatusCode, body)
|
|
}
|
|
|
|
return io.ReadAll(resp.Body)
|
|
}
|