Initial commit

This commit is contained in:
Mrixs committed 2026-10-14 23:04:51 +03:00
commit 02a6dabcfb
62 files changed
+15751

No files matched your search

+262
View File
@@ -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)
}