Initial commit
This commit is contained in:
commit
02a6dabcfb
62 files changed
+15751
No files matched your search
@@ -0,0 +1,262 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user