Initial commit
This commit is contained in:
commit
02a6dabcfb
62 files changed
+15751
No files matched your search
@@ -0,0 +1,88 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetAccountSettings gets account settings (current user's settings from /account/settings)
|
||||
func (c *Client) GetAccountSettings(ctx context.Context) (*models.UserSettings, error) {
|
||||
var result models.UserSettings
|
||||
if err := c.request.Get(ctx, "/account/settings", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// UpdateAccountSettings updates account settings
|
||||
func (c *Client) UpdateAccountSettings(ctx context.Context, settings *models.AccountSettings) (*models.AccountSettings, error) {
|
||||
var result models.AccountSettings
|
||||
if err := c.request.Put(ctx, "/account/settings", settings, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetSubscription gets user subscription info
|
||||
func (c *Client) GetSubscription(ctx context.Context) (*models.Subscription, error) {
|
||||
var result models.Subscription
|
||||
if err := c.request.Get(ctx, "/account/subscription", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetPlusInfo gets Plus subscription info
|
||||
func (c *Client) GetPlusInfo(ctx context.Context) (*models.PlusInfo, error) {
|
||||
var result models.PlusInfo
|
||||
if err := c.request.Get(ctx, "/account/plus", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetPromoCodeStatus gets promo code status
|
||||
func (c *Client) GetPromoCodeStatus(ctx context.Context, code string) (*models.PromoCodeStatus, error) {
|
||||
params := map[string]string{
|
||||
"code": code,
|
||||
}
|
||||
var result models.PromoCodeStatus
|
||||
if err := c.request.Get(ctx, "/promo-code-status", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ActivatePromoCode activates a promo code
|
||||
func (c *Client) ActivatePromoCode(ctx context.Context, code string) (*models.PromoCodeStatus, error) {
|
||||
params := map[string]string{
|
||||
"code": code,
|
||||
}
|
||||
var result models.PromoCodeStatus
|
||||
if err := c.request.Post(ctx, "/promo-code/activate", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetUserSettings gets user settings
|
||||
func (c *Client) GetUserSettings(ctx context.Context, userID string) (*models.UserSettings, error) {
|
||||
params := map[string]string{
|
||||
"user_id": userID,
|
||||
}
|
||||
var result models.UserSettings
|
||||
if err := c.request.Get(ctx, "/user/settings", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetAccountStatus fetches account status
|
||||
func (c *Client) GetAccountStatus(ctx context.Context) (*Status, error) {
|
||||
var result Status
|
||||
if err := c.request.Get(ctx, "/account/status", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// GetAlbums gets albums by IDs
|
||||
func (c *Client) GetAlbums(ctx context.Context, albumIDs []string) ([]*models.Album, error) {
|
||||
if len(albumIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"album-ids": joinIDs(albumIDs),
|
||||
}
|
||||
|
||||
var result []models.Album
|
||||
if err := c.request.Get(ctx, "/albums", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
albums := make([]*models.Album, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
albums[i] = &result[i]
|
||||
}
|
||||
return albums, nil
|
||||
}
|
||||
|
||||
// GetAlbum gets a single album by ID
|
||||
func (c *Client) GetAlbum(ctx context.Context, albumID string) (*models.Album, error) {
|
||||
albums, err := c.GetAlbums(ctx, []string{albumID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(albums) == 0 {
|
||||
return nil, utils.NewYandexMusicError("album not found", utils.ErrNotFound)
|
||||
}
|
||||
return albums[0], nil
|
||||
}
|
||||
|
||||
// GetAlbumsWithTracks gets albums with tracks expanded
|
||||
func (c *Client) GetAlbumsWithTracks(ctx context.Context, albumIDs []string) ([]*models.Album, error) {
|
||||
if len(albumIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"album-ids": joinIDs(albumIDs),
|
||||
"with-tracks": "true",
|
||||
}
|
||||
|
||||
var result []models.Album
|
||||
if err := c.request.Get(ctx, "/albums", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
albums := make([]*models.Album, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
albums[i] = &result[i]
|
||||
}
|
||||
return albums, nil
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// GetArtists gets artists by IDs
|
||||
func (c *Client) GetArtists(ctx context.Context, artistIDs []string) ([]*models.Artist, error) {
|
||||
if len(artistIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"artist-ids": joinIDs(artistIDs),
|
||||
}
|
||||
|
||||
var result []models.Artist
|
||||
if err := c.request.Get(ctx, "/artists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
artists := make([]*models.Artist, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
artists[i] = &result[i]
|
||||
}
|
||||
return artists, nil
|
||||
}
|
||||
|
||||
// GetArtist gets a single artist by ID
|
||||
func (c *Client) GetArtist(ctx context.Context, artistID string) (*models.Artist, error) {
|
||||
artists, err := c.GetArtists(ctx, []string{artistID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(artists) == 0 {
|
||||
return nil, utils.NewYandexMusicError("artist not found", utils.ErrNotFound)
|
||||
}
|
||||
return artists[0], nil
|
||||
}
|
||||
|
||||
// GetArtistAlbums gets albums by an artist
|
||||
func (c *Client) GetArtistAlbums(ctx context.Context, artistID string, page, pageSize int) ([]*models.Album, error) {
|
||||
params := map[string]string{
|
||||
"artist-id": artistID,
|
||||
"page": fmt.Sprintf("%d", page),
|
||||
"page-size": fmt.Sprintf("%d", pageSize),
|
||||
}
|
||||
|
||||
var result []models.Album
|
||||
if err := c.request.Get(ctx, "/artists/albums", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
albums := make([]*models.Album, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
albums[i] = &result[i]
|
||||
}
|
||||
return albums, nil
|
||||
}
|
||||
|
||||
// GetArtistTracks gets tracks by an artist
|
||||
func (c *Client) GetArtistTracks(ctx context.Context, artistID string, page, pageSize int) ([]*models.Track, error) {
|
||||
params := map[string]string{
|
||||
"artist-id": artistID,
|
||||
"page": fmt.Sprintf("%d", page),
|
||||
"page-size": fmt.Sprintf("%d", pageSize),
|
||||
}
|
||||
|
||||
var result []models.Track
|
||||
if err := c.request.Get(ctx, "/artists/tracks", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tracks := make([]*models.Track, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
tracks[i] = &result[i]
|
||||
}
|
||||
return tracks, nil
|
||||
}
|
||||
|
||||
// Need to add fmt import
|
||||
// "fmt"
|
||||
@@ -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"
|
||||
@@ -0,0 +1,154 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// ClientConfig holds configuration for the Client
|
||||
type ClientConfig struct {
|
||||
Token string
|
||||
BaseURL string
|
||||
ProxyURL string
|
||||
Timeout int
|
||||
Language string
|
||||
ReportUnknownFields bool
|
||||
Strict bool
|
||||
OnSchemaMismatch utils.SchemaMismatchHandler
|
||||
JSONBackend utils.JSONBackend
|
||||
}
|
||||
|
||||
// Client is the main Yandex Music API client
|
||||
type Client struct {
|
||||
config ClientConfig
|
||||
request *Request
|
||||
device string
|
||||
me *Status
|
||||
accountUID *int64
|
||||
}
|
||||
|
||||
// NewClient creates a new Yandex Music client
|
||||
func NewClient(config ClientConfig) *Client {
|
||||
if config.BaseURL == "" {
|
||||
config.BaseURL = DefaultBaseURL
|
||||
}
|
||||
if config.Language == "" {
|
||||
config.Language = "ru"
|
||||
}
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = int(DefaultTimeout.Seconds())
|
||||
}
|
||||
|
||||
device := fmt.Sprintf("os=Go; os_version=; manufacturer=Mrixs; model=Yandex Music API Go; clid=; device_id=go-client; uuid=go-client")
|
||||
|
||||
reqConfig := RequestConfig{
|
||||
BaseURL: config.BaseURL,
|
||||
Token: config.Token,
|
||||
ProxyURL: config.ProxyURL,
|
||||
Timeout: time.Duration(config.Timeout) * time.Second,
|
||||
Language: config.Language,
|
||||
JSONBackend: config.JSONBackend,
|
||||
ReportUnknownFields: config.ReportUnknownFields,
|
||||
Strict: config.Strict,
|
||||
OnSchemaMismatch: config.OnSchemaMismatch,
|
||||
}
|
||||
|
||||
request := NewRequest(reqConfig)
|
||||
|
||||
return &Client{
|
||||
config: config,
|
||||
request: request,
|
||||
device: device,
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the client by fetching account info
|
||||
func (c *Client) Init(ctx context.Context) error {
|
||||
status, err := c.GetAccountStatus(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.me = status
|
||||
if status.Account != nil {
|
||||
c.accountUID = &status.Account.UID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRequest returns the underlying request handler
|
||||
func (c *Client) GetRequest() models.RequestInterface {
|
||||
return c.request
|
||||
}
|
||||
|
||||
// GetToken returns the current token
|
||||
func (c *Client) GetToken() string {
|
||||
return c.config.Token
|
||||
}
|
||||
|
||||
// SetToken updates the token
|
||||
func (c *Client) SetToken(token string) {
|
||||
c.config.Token = token
|
||||
c.request.SetToken(token)
|
||||
}
|
||||
|
||||
// GetDevice returns the device string
|
||||
func (c *Client) GetDevice() string {
|
||||
return c.device
|
||||
}
|
||||
|
||||
// GetMe returns the account status
|
||||
func (c *Client) GetMe() *Status {
|
||||
return c.me
|
||||
}
|
||||
|
||||
// GetAccountUID returns the account UID
|
||||
func (c *Client) GetAccountUID() *int64 {
|
||||
return c.accountUID
|
||||
}
|
||||
|
||||
// Context returns a context for requests (can be overridden for timeouts)
|
||||
func (c *Client) Context() context.Context {
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
// WithContext returns a new client with a custom context
|
||||
func (c *Client) WithContext(ctx context.Context) *Client {
|
||||
// In Go, we typically pass context to each method rather than storing it
|
||||
return c
|
||||
}
|
||||
|
||||
// DeviceAuth performs OAuth Device Flow authentication
|
||||
func (c *Client) DeviceAuth(ctx context.Context, onCode func(*DeviceCode)) (*OAuthToken, error) {
|
||||
return c.request.DeviceAuth(ctx, onCode)
|
||||
}
|
||||
|
||||
// RefreshToken refreshes the access token
|
||||
func (c *Client) RefreshToken(ctx context.Context, refreshToken string) (*OAuthToken, error) {
|
||||
return c.request.RefreshToken(ctx, refreshToken)
|
||||
}
|
||||
|
||||
// Status represents account status
|
||||
type Status struct {
|
||||
Account *Account `json:"account"`
|
||||
// Other fields...
|
||||
}
|
||||
|
||||
// Account represents a user account
|
||||
type Account struct {
|
||||
UID int64 `json:"uid"`
|
||||
Login string `json:"login"`
|
||||
DisplayName string `json:"display_name"`
|
||||
// Other fields...
|
||||
}
|
||||
|
||||
// Track represents a music track (minimal for now)
|
||||
type Track struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Duration int `json:"duration_ms"`
|
||||
// Other fields...
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetClips gets clips
|
||||
func (c *Client) GetClips(ctx context.Context, page, pageSize int) ([]*models.Clip, error) {
|
||||
params := map[string]string{
|
||||
"page": "1",
|
||||
"page_size": "20",
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result []models.Clip
|
||||
if err := c.request.Get(ctx, "/clips", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clips := make([]*models.Clip, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
clips[i] = &result[i]
|
||||
}
|
||||
return clips, nil
|
||||
}
|
||||
|
||||
// GetClipsWillLike gets clips the user might like
|
||||
func (c *Client) GetClipsWillLike(ctx context.Context) (*models.ClipsWillLike, error) {
|
||||
var result models.ClipsWillLike
|
||||
if err := c.request.Get(ctx, "/clips/will-like", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetArtistClips gets clips by an artist
|
||||
func (c *Client) GetArtistClips(ctx context.Context, artistID string) (*models.ArtistClipData, error) {
|
||||
params := map[string]string{
|
||||
"artist_id": artistID,
|
||||
}
|
||||
|
||||
var result models.ArtistClipData
|
||||
if err := c.request.Get(ctx, "/artists/clips", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetShots gets shots
|
||||
func (c *Client) GetShots(ctx context.Context, page, pageSize int) ([]*models.Shot, error) {
|
||||
params := map[string]string{
|
||||
"page": "1",
|
||||
"page_size": "20",
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result []models.Shot
|
||||
if err := c.request.Get(ctx, "/shots", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
shots := make([]*models.Shot, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
shots[i] = &result[i]
|
||||
}
|
||||
return shots, nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetConcerts gets concerts
|
||||
func (c *Client) GetConcerts(ctx context.Context, page, pageSize int) (*models.ConcertFeed, error) {
|
||||
params := map[string]string{
|
||||
"page": "1",
|
||||
"page_size": "20",
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.ConcertFeed
|
||||
if err := c.request.Get(ctx, "/concerts", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetConcertFeed gets concert feed
|
||||
func (c *Client) GetConcertFeed(ctx context.Context) (*models.ConcertFeed, error) {
|
||||
var result models.ConcertFeed
|
||||
if err := c.request.Get(ctx, "/concerts/feed", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetConcertInfo gets concert info
|
||||
func (c *Client) GetConcertInfo(ctx context.Context, concertID string) (*models.ConcertInfo, error) {
|
||||
params := map[string]string{
|
||||
"concert_id": concertID,
|
||||
}
|
||||
|
||||
var result models.ConcertInfo
|
||||
if err := c.request.Get(ctx, "/concerts/info", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetConcertLocations gets concert locations
|
||||
func (c *Client) GetConcertLocations(ctx context.Context) (*models.ConcertLocations, error) {
|
||||
var result models.ConcertLocations
|
||||
if err := c.request.Get(ctx, "/concerts/locations", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetConcertTabs gets concert tabs
|
||||
func (c *Client) GetConcertTabs(ctx context.Context, concertID string) ([]*models.ConcertTabConfig, error) {
|
||||
params := map[string]string{
|
||||
"concert_id": concertID,
|
||||
}
|
||||
|
||||
var result []models.ConcertTabConfig
|
||||
if err := c.request.Get(ctx, "/concerts/tabs", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tabs := make([]*models.ConcertTabConfig, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
tabs[i] = &result[i]
|
||||
}
|
||||
return tabs, nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetFeed gets the main feed
|
||||
func (c *Client) GetFeed(ctx context.Context) (*models.Feed, error) {
|
||||
var result models.Feed
|
||||
if err := c.request.Get(ctx, "/feed", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetFeedBlocks gets feed blocks
|
||||
func (c *Client) GetFeedBlocks(ctx context.Context) ([]*models.DayBlock, error) {
|
||||
var result []models.DayBlock
|
||||
if err := c.request.Get(ctx, "/feed/blocks", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blocks := make([]*models.DayBlock, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
blocks[i] = &result[i]
|
||||
}
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
// GetFeedGeneratedPlaylist gets a generated playlist by ID from feed
|
||||
func (c *Client) GetFeedGeneratedPlaylist(ctx context.Context, playlistID string) (*models.GeneratedPlaylist, error) {
|
||||
params := map[string]string{
|
||||
"playlist_id": playlistID,
|
||||
}
|
||||
|
||||
var result models.GeneratedPlaylist
|
||||
if err := c.request.Get(ctx, "/feed/generated-playlist", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetFeedDay gets a specific day from the feed
|
||||
func (c *Client) GetFeedDay(ctx context.Context, date string) (*models.Day, error) {
|
||||
params := map[string]string{
|
||||
"date": date,
|
||||
}
|
||||
|
||||
var result models.Day
|
||||
if err := c.request.Get(ctx, "/feed/day", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetGeneratedPlaylists gets generated playlists
|
||||
func (c *Client) GetGeneratedPlaylists(ctx context.Context) ([]*models.GeneratedPlaylist, error) {
|
||||
var result []models.GeneratedPlaylist
|
||||
if err := c.request.Get(ctx, "/generated-playlists", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playlists := make([]*models.GeneratedPlaylist, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
playlists[i] = &result[i]
|
||||
}
|
||||
return playlists, nil
|
||||
}
|
||||
|
||||
// GetLanding gets a landing page
|
||||
func (c *Client) GetLanding(ctx context.Context, landingID string) (*models.Landing, error) {
|
||||
params := map[string]string{
|
||||
"landing_id": landingID,
|
||||
}
|
||||
|
||||
var result models.Landing
|
||||
if err := c.request.Get(ctx, "/landing", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetLandingList gets a list of landings
|
||||
func (c *Client) GetLandingList(ctx context.Context) (*models.LandingList, error) {
|
||||
var result models.LandingList
|
||||
if err := c.request.Get(ctx, "/landing/list", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetLandingBlocks gets landing blocks by block IDs
|
||||
func (c *Client) GetLandingBlocks(ctx context.Context, blockIDs []string) ([]*models.LandingBlock, error) {
|
||||
params := map[string]string{
|
||||
"blocks": joinStrings(blockIDs, ","),
|
||||
}
|
||||
|
||||
var result []models.LandingBlock
|
||||
if err := c.request.Get(ctx, "/landing/blocks", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blocks := make([]*models.LandingBlock, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
blocks[i] = &result[i]
|
||||
}
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
// joinStrings joins a slice of strings with a separator
|
||||
func joinStrings(slice []string, sep string) string {
|
||||
if len(slice) == 0 {
|
||||
return ""
|
||||
}
|
||||
result := slice[0]
|
||||
for _, s := range slice[1:] {
|
||||
result += sep + s
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetMusicHistory gets music history
|
||||
func (c *Client) GetMusicHistory(ctx context.Context) (*models.MusicHistory, error) {
|
||||
var result models.MusicHistory
|
||||
if err := c.request.Get(ctx, "/music-history", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetMusicHistoryTabs gets music history tabs
|
||||
func (c *Client) GetMusicHistoryTabs(ctx context.Context) ([]*models.MusicHistoryTab, error) {
|
||||
var result []models.MusicHistoryTab
|
||||
if err := c.request.Get(ctx, "/music-history/tabs", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tabs := make([]*models.MusicHistoryTab, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
tabs[i] = &result[i]
|
||||
}
|
||||
return tabs, nil
|
||||
}
|
||||
|
||||
// GetMusicHistoryItems gets music history items
|
||||
func (c *Client) GetMusicHistoryItems(ctx context.Context, tabID string, page, pageSize int) (*models.MusicHistoryItems, error) {
|
||||
params := map[string]string{}
|
||||
if tabID != "" {
|
||||
params["tab_id"] = tabID
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.MusicHistoryItems
|
||||
if err := c.request.Get(ctx, "/music-history/items", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetLabels gets all labels
|
||||
func (c *Client) GetLabels(ctx context.Context) ([]*models.Label, error) {
|
||||
var result []models.Label
|
||||
if err := c.request.Get(ctx, "/labels", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
labels := make([]*models.Label, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
labels[i] = &result[i]
|
||||
}
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
// GetLabel gets a label by ID
|
||||
func (c *Client) GetLabel(ctx context.Context, labelID string) (*models.Label, error) {
|
||||
params := map[string]string{
|
||||
"label_id": labelID,
|
||||
}
|
||||
|
||||
var result models.Label
|
||||
if err := c.request.Get(ctx, "/labels", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetLabelAlbums gets albums for a label
|
||||
func (c *Client) GetLabelAlbums(ctx context.Context, labelID string, page, pageSize int) (*models.LabelAlbums, error) {
|
||||
params := map[string]string{
|
||||
"label_id": labelID,
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.LabelAlbums
|
||||
if err := c.request.Get(ctx, "/labels/albums", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetLabelArtists gets artists for a label
|
||||
func (c *Client) GetLabelArtists(ctx context.Context, labelID string, page, pageSize int) (*models.LabelArtists, error) {
|
||||
params := map[string]string{
|
||||
"label_id": labelID,
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.LabelArtists
|
||||
if err := c.request.Get(ctx, "/labels/artists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// LikeTrack likes a track
|
||||
func (c *Client) LikeTrack(ctx context.Context, trackID string) error {
|
||||
params := map[string]string{
|
||||
"track_id": trackID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/tracks/add", params, &result)
|
||||
}
|
||||
|
||||
// UnlikeTrack unlikes a track
|
||||
func (c *Client) UnlikeTrack(ctx context.Context, trackID string) error {
|
||||
params := map[string]string{
|
||||
"track_id": trackID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/tracks/remove", params, &result)
|
||||
}
|
||||
|
||||
// LikeAlbum likes an album
|
||||
func (c *Client) LikeAlbum(ctx context.Context, albumID string) error {
|
||||
params := map[string]string{
|
||||
"album_id": albumID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/albums/add", params, &result)
|
||||
}
|
||||
|
||||
// UnlikeAlbum unlikes an album
|
||||
func (c *Client) UnlikeAlbum(ctx context.Context, albumID string) error {
|
||||
params := map[string]string{
|
||||
"album_id": albumID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/albums/remove", params, &result)
|
||||
}
|
||||
|
||||
// LikeArtist likes an artist
|
||||
func (c *Client) LikeArtist(ctx context.Context, artistID string) error {
|
||||
params := map[string]string{
|
||||
"artist_id": artistID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/artists/add", params, &result)
|
||||
}
|
||||
|
||||
// UnlikeArtist unlikes an artist
|
||||
func (c *Client) UnlikeArtist(ctx context.Context, artistID string) error {
|
||||
params := map[string]string{
|
||||
"artist_id": artistID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/artists/remove", params, &result)
|
||||
}
|
||||
|
||||
// LikePlaylist likes a playlist
|
||||
func (c *Client) LikePlaylist(ctx context.Context, kind int) error {
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/playlists/add", params, &result)
|
||||
}
|
||||
|
||||
// UnlikePlaylist unlikes a playlist
|
||||
func (c *Client) UnlikePlaylist(ctx context.Context, kind int) error {
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/playlists/remove", params, &result)
|
||||
}
|
||||
|
||||
// GetLikedTracks gets user's liked tracks
|
||||
func (c *Client) GetLikedTracks(ctx context.Context) ([]*models.Track, error) {
|
||||
var result []models.Track
|
||||
if err := c.request.Get(ctx, "/users/likes/tracks", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tracks := make([]*models.Track, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
tracks[i] = &result[i]
|
||||
}
|
||||
return tracks, nil
|
||||
}
|
||||
|
||||
// GetLikedAlbums gets user's liked albums
|
||||
func (c *Client) GetLikedAlbums(ctx context.Context) ([]*models.Album, error) {
|
||||
var result []models.Album
|
||||
if err := c.request.Get(ctx, "/users/likes/albums", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
albums := make([]*models.Album, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
albums[i] = &result[i]
|
||||
}
|
||||
return albums, nil
|
||||
}
|
||||
|
||||
// GetLikedArtists gets user's liked artists
|
||||
func (c *Client) GetLikedArtists(ctx context.Context) ([]*models.Artist, error) {
|
||||
var result []models.Artist
|
||||
if err := c.request.Get(ctx, "/users/likes/artists", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
artists := make([]*models.Artist, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
artists[i] = &result[i]
|
||||
}
|
||||
return artists, nil
|
||||
}
|
||||
|
||||
// GetLikedPlaylists gets user's liked playlists
|
||||
func (c *Client) GetLikedPlaylists(ctx context.Context) ([]*models.Playlist, error) {
|
||||
var result []models.Playlist
|
||||
if err := c.request.Get(ctx, "/users/likes/playlists", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playlists := make([]*models.Playlist, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
playlists[i] = &result[i]
|
||||
}
|
||||
return playlists, nil
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetMetatags gets all metatags
|
||||
func (c *Client) GetMetatags(ctx context.Context) ([]*models.Metatag, error) {
|
||||
var result []models.Metatag
|
||||
if err := c.request.Get(ctx, "/metatags", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metatags := make([]*models.Metatag, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
metatags[i] = &result[i]
|
||||
}
|
||||
return metatags, nil
|
||||
}
|
||||
|
||||
// GetMetatagTree gets metatag tree
|
||||
func (c *Client) GetMetatagTree(ctx context.Context) (*models.MetatagTree, error) {
|
||||
var result models.MetatagTree
|
||||
if err := c.request.Get(ctx, "/metatags/tree", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetMetatagArtists gets artists for a metatag
|
||||
func (c *Client) GetMetatagArtists(ctx context.Context, metatagID string, page, pageSize int) (*models.MetatagArtists, error) {
|
||||
params := map[string]string{
|
||||
"metatag_id": metatagID,
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.MetatagArtists
|
||||
if err := c.request.Get(ctx, "/metatags/artists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetMetatagAlbums gets albums for a metatag
|
||||
func (c *Client) GetMetatagAlbums(ctx context.Context, metatagID string, page, pageSize int) (*models.MetatagAlbums, error) {
|
||||
params := map[string]string{
|
||||
"metatag_id": metatagID,
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.MetatagAlbums
|
||||
if err := c.request.Get(ctx, "/metatags/albums", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetMetatagPlaylists gets playlists for a metatag
|
||||
func (c *Client) GetMetatagPlaylists(ctx context.Context, metatagID string, page, pageSize int) (*models.MetatagPlaylists, error) {
|
||||
params := map[string]string{
|
||||
"metatag_id": metatagID,
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.MetatagPlaylists
|
||||
if err := c.request.Get(ctx, "/metatags/playlists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetGenres gets all genres
|
||||
func (c *Client) GetGenres(ctx context.Context) ([]*models.Genre, error) {
|
||||
var result []models.Genre
|
||||
if err := c.request.Get(ctx, "/genres", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
genres := make([]*models.Genre, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
genres[i] = &result[i]
|
||||
}
|
||||
return genres, nil
|
||||
}
|
||||
|
||||
// GetCharts gets charts
|
||||
func (c *Client) GetCharts(ctx context.Context) ([]*models.Chart, error) {
|
||||
var result []models.Chart
|
||||
if err := c.request.Get(ctx, "/charts", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
charts := make([]*models.Chart, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
charts[i] = &result[i]
|
||||
}
|
||||
return charts, nil
|
||||
}
|
||||
|
||||
// GetChartInfo gets chart info
|
||||
func (c *Client) GetChartInfo(ctx context.Context, chartID string) (*models.ChartInfo, error) {
|
||||
params := map[string]string{
|
||||
"chart_id": chartID,
|
||||
}
|
||||
|
||||
var result models.ChartInfo
|
||||
if err := c.request.Get(ctx, "/charts/info", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetChartItems gets chart items
|
||||
func (c *Client) GetChartItems(ctx context.Context, chartID string, page, pageSize int) (*models.ChartItems, error) {
|
||||
params := map[string]string{
|
||||
"chart_id": chartID,
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.ChartItems
|
||||
if err := c.request.Get(ctx, "/charts/items", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetChartMenu gets chart menu
|
||||
func (c *Client) GetChartMenu(ctx context.Context, chartID string) (*models.ChartInfoMenu, error) {
|
||||
params := map[string]string{
|
||||
"chart_id": chartID,
|
||||
}
|
||||
|
||||
var result models.ChartInfoMenu
|
||||
if err := c.request.Get(ctx, "/charts/menu", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetPins gets user's pins
|
||||
func (c *Client) GetPins(ctx context.Context) (*models.PinsList, error) {
|
||||
var result models.PinsList
|
||||
if err := c.request.Get(ctx, "/pins", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// AddPin adds a pin
|
||||
func (c *Client) AddPin(ctx context.Context, pinType, entityID string) (*models.Pin, error) {
|
||||
params := map[string]string{
|
||||
"type": pinType,
|
||||
"entity_id": entityID,
|
||||
}
|
||||
|
||||
var result models.Pin
|
||||
if err := c.request.Post(ctx, "/pins", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// RemovePin removes a pin
|
||||
func (c *Client) RemovePin(ctx context.Context, pinID string) error {
|
||||
params := map[string]string{
|
||||
"pin_id": pinID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Delete(ctx, "/pins", params, &result)
|
||||
}
|
||||
|
||||
// GetPresaves gets user's presaves
|
||||
func (c *Client) GetPresaves(ctx context.Context) (*models.Presaves, error) {
|
||||
var result models.Presaves
|
||||
if err := c.request.Get(ctx, "/presaves", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// playlistResponse creates a JSON response for a playlist list
|
||||
func playlistResponse(playlists []map[string]any) []byte {
|
||||
resp := map[string]any{
|
||||
"result": playlists,
|
||||
}
|
||||
data, _ := json.Marshal(resp)
|
||||
return data
|
||||
}
|
||||
|
||||
// singlePlaylistResponse creates a JSON response for a single playlist
|
||||
func singlePlaylistResponse(playlist map[string]any) []byte {
|
||||
resp := map[string]any{
|
||||
"result": playlist,
|
||||
}
|
||||
data, _ := json.Marshal(resp)
|
||||
return data
|
||||
}
|
||||
|
||||
// errorResponse creates an error JSON response
|
||||
func errorResponse(errMsg string) []byte {
|
||||
resp := map[string]any{
|
||||
"error": errMsg,
|
||||
}
|
||||
data, _ := json.Marshal(resp)
|
||||
return data
|
||||
}
|
||||
|
||||
func TestClient_GetPlaylists(t *testing.T) {
|
||||
// Sample playlist data
|
||||
playlists := []map[string]any{
|
||||
{
|
||||
"kind": 1,
|
||||
"title": "Playlist 1",
|
||||
"description": "First playlist",
|
||||
"cover_uri": "https://example.com/cover1.jpg",
|
||||
"created": "2023-01-01T00:00:00+03:00",
|
||||
"modified": "2023-01-02T00:00:00+03:00",
|
||||
"track_count": 10,
|
||||
"duration_ms": 1800000,
|
||||
"likes_count": 5,
|
||||
"is_public": true,
|
||||
"revision": 1,
|
||||
"uuid": "uuid-1",
|
||||
"tags": []string{"rock", "pop"},
|
||||
"access_type": "full",
|
||||
"available": true,
|
||||
"content_warning": "",
|
||||
"genre": "rock",
|
||||
"owner_uid": 12345,
|
||||
"shared": false,
|
||||
"snapshot_id": "snap-1",
|
||||
"owner": map[string]any{
|
||||
"uid": 12345,
|
||||
"login": "user1",
|
||||
"display_name": "User One",
|
||||
"cover_uri": "https://example.com/user1.jpg",
|
||||
"sex": "m",
|
||||
"public_playlists": 10,
|
||||
"followers_count": 100,
|
||||
"followings_count": 50,
|
||||
"has_playlists": true,
|
||||
"available": true,
|
||||
},
|
||||
"tracks": []any{},
|
||||
"similar_playlists": []any{},
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"title": "Playlist 2",
|
||||
"description": "Second playlist",
|
||||
"cover_uri": "https://example.com/cover2.jpg",
|
||||
"created": "2023-02-01T00:00:00+03:00",
|
||||
"modified": "2023-02-02T00:00:00+03:00",
|
||||
"track_count": 5,
|
||||
"duration_ms": 900000,
|
||||
"likes_count": 3,
|
||||
"is_public": false,
|
||||
"revision": 2,
|
||||
"uuid": "uuid-2",
|
||||
"tags": []string{"jazz"},
|
||||
"access_type": "full",
|
||||
"available": true,
|
||||
"content_warning": "",
|
||||
"genre": "jazz",
|
||||
"owner_uid": 12345,
|
||||
"shared": false,
|
||||
"snapshot_id": "snap-2",
|
||||
"owner": map[string]any{
|
||||
"uid": 12345,
|
||||
"login": "user1",
|
||||
"display_name": "User One",
|
||||
"cover_uri": "https://example.com/user1.jpg",
|
||||
"sex": "m",
|
||||
"public_playlists": 10,
|
||||
"followers_count": 100,
|
||||
"followings_count": 50,
|
||||
"has_playlists": true,
|
||||
"available": true,
|
||||
},
|
||||
"tracks": []any{},
|
||||
"similar_playlists": []any{},
|
||||
},
|
||||
}
|
||||
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/playlists" {
|
||||
t.Errorf("Expected path /playlists, got %s", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("kinds") != "1,2" {
|
||||
t.Errorf("Expected kinds=1,2, got %s", r.URL.Query().Get("kinds"))
|
||||
}
|
||||
if r.Header.Get("Authorization") != "OAuth test-token" {
|
||||
t.Errorf("Expected Authorization header, got %s", r.Header.Get("Authorization"))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(playlistResponse(playlists))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
result, err := client.GetPlaylists(context.Background(), []int{1, 2})
|
||||
if err != nil {
|
||||
t.Fatalf("GetPlaylists failed: %v", err)
|
||||
}
|
||||
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("Expected 2 playlists, got %d", len(result))
|
||||
}
|
||||
|
||||
if result[0].Kind != 1 {
|
||||
t.Errorf("Expected first playlist kind 1, got %d", result[0].Kind)
|
||||
}
|
||||
if result[0].Title != "Playlist 1" {
|
||||
t.Errorf("Expected first playlist title 'Playlist 1', got '%s'", result[0].Title)
|
||||
}
|
||||
if result[0].Description != "First playlist" {
|
||||
t.Errorf("Expected first playlist description 'First playlist', got '%s'", result[0].Description)
|
||||
}
|
||||
if result[0].TrackCount != 10 {
|
||||
t.Errorf("Expected first playlist track_count 10, got %d", result[0].TrackCount)
|
||||
}
|
||||
if result[0].Owner == nil || result[0].Owner.UID != 12345 {
|
||||
t.Errorf("Expected owner UID 12345")
|
||||
}
|
||||
if result[0].Owner.Login != "user1" {
|
||||
t.Errorf("Expected owner login 'user1', got '%s'", result[0].Owner.Login)
|
||||
}
|
||||
|
||||
if result[1].Kind != 2 {
|
||||
t.Errorf("Expected second playlist kind 2, got %d", result[1].Kind)
|
||||
}
|
||||
if result[1].Title != "Playlist 2" {
|
||||
t.Errorf("Expected second playlist title 'Playlist 2', got '%s'", result[1].Title)
|
||||
}
|
||||
if result[1].IsPublic != false {
|
||||
t.Errorf("Expected second playlist is_public false, got %v", result[1].IsPublic)
|
||||
}
|
||||
|
||||
// Verify client is set on playlists
|
||||
for _, p := range result {
|
||||
if p.GetClient() != client {
|
||||
t.Error("Expected client to be set on playlist")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetPlaylists_Empty(t *testing.T) {
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Error("Server should not be called for empty kinds")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
result, err := client.GetPlaylists(context.Background(), []int{})
|
||||
if err != nil {
|
||||
t.Fatalf("GetPlaylists failed: %v", err)
|
||||
}
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil for empty kinds, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetPlaylist(t *testing.T) {
|
||||
playlist := map[string]any{
|
||||
"kind": 42,
|
||||
"title": "My Playlist",
|
||||
"description": "A test playlist",
|
||||
"cover_uri": "https://example.com/cover.jpg",
|
||||
"created": "2023-01-01T00:00:00+03:00",
|
||||
"modified": "2023-01-02T00:00:00+03:00",
|
||||
"track_count": 15,
|
||||
"duration_ms": 2700000,
|
||||
"likes_count": 10,
|
||||
"is_public": true,
|
||||
"revision": 5,
|
||||
"uuid": "uuid-42",
|
||||
"tags": []string{"rock", "metal"},
|
||||
"access_type": "full",
|
||||
"available": true,
|
||||
"content_warning": "",
|
||||
"genre": "rock",
|
||||
"owner_uid": 999,
|
||||
"shared": false,
|
||||
"snapshot_id": "snap-42",
|
||||
"owner": map[string]any{
|
||||
"uid": 999,
|
||||
"login": "testuser",
|
||||
"display_name": "Test User",
|
||||
"cover_uri": "https://example.com/user.jpg",
|
||||
"sex": "f",
|
||||
"public_playlists": 5,
|
||||
"followers_count": 200,
|
||||
"followings_count": 100,
|
||||
"has_playlists": true,
|
||||
"available": true,
|
||||
},
|
||||
"tracks": []any{},
|
||||
"similar_playlists": []any{},
|
||||
}
|
||||
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/playlists" {
|
||||
t.Errorf("Expected path /playlists, got %s", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("kinds") != "42" {
|
||||
t.Errorf("Expected kinds=42, got %s", r.URL.Query().Get("kinds"))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(playlistResponse([]map[string]any{playlist}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
result, err := client.GetPlaylist(context.Background(), 42)
|
||||
if err != nil {
|
||||
t.Fatalf("GetPlaylist failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected playlist, got nil")
|
||||
}
|
||||
|
||||
if result.Kind != 42 {
|
||||
t.Errorf("Expected kind 42, got %d", result.Kind)
|
||||
}
|
||||
if result.Title != "My Playlist" {
|
||||
t.Errorf("Expected title 'My Playlist', got '%s'", result.Title)
|
||||
}
|
||||
if result.Description != "A test playlist" {
|
||||
t.Errorf("Expected description 'A test playlist', got '%s'", result.Description)
|
||||
}
|
||||
if result.TrackCount != 15 {
|
||||
t.Errorf("Expected track_count 15, got %d", result.TrackCount)
|
||||
}
|
||||
if result.Revision != 5 {
|
||||
t.Errorf("Expected revision 5, got %d", result.Revision)
|
||||
}
|
||||
if result.Owner == nil || result.Owner.Login != "testuser" {
|
||||
t.Errorf("Expected owner login 'testuser'")
|
||||
}
|
||||
if result.GetClient() != client {
|
||||
t.Error("Expected client to be set on playlist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetPlaylist_NotFound(t *testing.T) {
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(playlistResponse([]map[string]any{}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
result, err := client.GetPlaylist(context.Background(), 999)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for not found playlist")
|
||||
}
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil playlist, got %v", result)
|
||||
}
|
||||
// Check that it's a not found error
|
||||
if err.Error() != "playlist not found" && err.Error() != "playlist not found: not found" {
|
||||
t.Errorf("Expected 'playlist not found' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetPlaylist_Error(t *testing.T) {
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(errorResponse("Internal server error"))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
_, err := client.GetPlaylist(context.Background(), 1)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
if err.Error() == "" {
|
||||
t.Errorf("Expected non-empty error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_CreatePlaylist(t *testing.T) {
|
||||
createdPlaylist := map[string]any{
|
||||
"kind": 100,
|
||||
"title": "New Playlist",
|
||||
"description": "",
|
||||
"cover_uri": "",
|
||||
"created": "2024-01-01T00:00:00+03:00",
|
||||
"modified": "2024-01-01T00:00:00+03:00",
|
||||
"track_count": 0,
|
||||
"duration_ms": 0,
|
||||
"likes_count": 0,
|
||||
"is_public": true,
|
||||
"revision": 1,
|
||||
"uuid": "new-uuid-100",
|
||||
"tags": []any{},
|
||||
"access_type": "full",
|
||||
"available": true,
|
||||
"content_warning": "",
|
||||
"genre": "",
|
||||
"owner_uid": 12345,
|
||||
"shared": false,
|
||||
"snapshot_id": "snap-new-100",
|
||||
"owner": map[string]any{
|
||||
"uid": 12345,
|
||||
"login": "me",
|
||||
"display_name": "Me",
|
||||
"cover_uri": "",
|
||||
"sex": "",
|
||||
"public_playlists": 1,
|
||||
"followers_count": 0,
|
||||
"followings_count": 0,
|
||||
"has_playlists": true,
|
||||
"available": true,
|
||||
},
|
||||
"tracks": []any{},
|
||||
"similar_playlists": []any{},
|
||||
}
|
||||
|
||||
var capturedBody map[string]any
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("Expected POST method, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/users/me/playlists" {
|
||||
t.Errorf("Expected path /users/me/playlists, got %s", r.URL.Path)
|
||||
}
|
||||
// Read and parse request body
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
if err := decoder.Decode(&capturedBody); err != nil {
|
||||
t.Errorf("Failed to decode request body: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(singlePlaylistResponse(createdPlaylist))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
result, err := client.CreatePlaylist(context.Background(), "New Playlist", "public")
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePlaylist failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected playlist, got nil")
|
||||
}
|
||||
|
||||
if result.Kind != 100 {
|
||||
t.Errorf("Expected kind 100, got %d", result.Kind)
|
||||
}
|
||||
if result.Title != "New Playlist" {
|
||||
t.Errorf("Expected title 'New Playlist', got '%s'", result.Title)
|
||||
}
|
||||
if result.IsPublic != true {
|
||||
t.Errorf("Expected is_public true, got %v", result.IsPublic)
|
||||
}
|
||||
if result.GetClient() != client {
|
||||
t.Error("Expected client to be set on playlist")
|
||||
}
|
||||
|
||||
// Verify the request body
|
||||
if capturedBody == nil {
|
||||
t.Fatal("Request body not captured")
|
||||
}
|
||||
if capturedBody["title"] != "New Playlist" {
|
||||
t.Errorf("Expected title 'New Playlist' in body, got %v", capturedBody["title"])
|
||||
}
|
||||
if capturedBody["visibility"] != "public" {
|
||||
t.Errorf("Expected visibility 'public' in body, got %v", capturedBody["visibility"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_CreatePlaylist_Private(t *testing.T) {
|
||||
createdPlaylist := map[string]any{
|
||||
"kind": 101,
|
||||
"title": "Private Playlist",
|
||||
"description": "",
|
||||
"cover_uri": "",
|
||||
"created": "2024-01-01T00:00:00+03:00",
|
||||
"modified": "2024-01-01T00:00:00+03:00",
|
||||
"track_count": 0,
|
||||
"duration_ms": 0,
|
||||
"likes_count": 0,
|
||||
"is_public": false,
|
||||
"revision": 1,
|
||||
"uuid": "new-uuid-101",
|
||||
"tags": []any{},
|
||||
"access_type": "full",
|
||||
"available": true,
|
||||
"content_warning": "",
|
||||
"genre": "",
|
||||
"owner_uid": 12345,
|
||||
"shared": false,
|
||||
"snapshot_id": "snap-new-101",
|
||||
"owner": map[string]any{
|
||||
"uid": 12345,
|
||||
"login": "me",
|
||||
"display_name": "Me",
|
||||
"cover_uri": "",
|
||||
"sex": "",
|
||||
"public_playlists": 1,
|
||||
"followers_count": 0,
|
||||
"followings_count": 0,
|
||||
"has_playlists": true,
|
||||
"available": true,
|
||||
},
|
||||
"tracks": []any{},
|
||||
"similar_playlists": []any{},
|
||||
}
|
||||
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(singlePlaylistResponse(createdPlaylist))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
result, err := client.CreatePlaylist(context.Background(), "Private Playlist", "private")
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePlaylist failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected playlist, got nil")
|
||||
}
|
||||
|
||||
if result.Title != "Private Playlist" {
|
||||
t.Errorf("Expected title 'Private Playlist', got '%s'", result.Title)
|
||||
}
|
||||
if result.IsPublic != false {
|
||||
t.Errorf("Expected is_public false for private playlist, got %v", result.IsPublic)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_CreatePlaylist_Error(t *testing.T) {
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(errorResponse("unauthorized"))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
_, err := client.CreatePlaylist(context.Background(), "Test", "public")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
// Check that it's an unauthorized error
|
||||
if !utils.IsUnauthorizedError(err) {
|
||||
t.Errorf("Expected unauthorized error, got: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// GetPlaylists gets playlists by IDs (kinds)
|
||||
func (c *Client) GetPlaylists(ctx context.Context, kinds []int) ([]*models.Playlist, error) {
|
||||
if len(kinds) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Convert kinds to string
|
||||
kindsStr := ""
|
||||
for i, k := range kinds {
|
||||
if i > 0 {
|
||||
kindsStr += ","
|
||||
}
|
||||
kindsStr += fmt.Sprintf("%d", k)
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"kinds": kindsStr,
|
||||
}
|
||||
|
||||
var result []models.Playlist
|
||||
if err := c.request.Get(ctx, "/playlists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playlists := make([]*models.Playlist, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
playlists[i] = &result[i]
|
||||
}
|
||||
return playlists, nil
|
||||
}
|
||||
|
||||
// GetPlaylist gets a single playlist by kind
|
||||
func (c *Client) GetPlaylist(ctx context.Context, kind int) (*models.Playlist, error) {
|
||||
playlists, err := c.GetPlaylists(ctx, []int{kind})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(playlists) == 0 {
|
||||
return nil, utils.NewYandexMusicError("playlist not found", utils.ErrNotFound)
|
||||
}
|
||||
return playlists[0], nil
|
||||
}
|
||||
|
||||
// GetUserPlaylists gets playlists for a user
|
||||
func (c *Client) GetUserPlaylists(ctx context.Context, userID string) ([]*models.Playlist, error) {
|
||||
params := map[string]string{
|
||||
"user-id": userID,
|
||||
}
|
||||
|
||||
var result []models.Playlist
|
||||
if err := c.request.Get(ctx, "/users/playlists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playlists := make([]*models.Playlist, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
playlists[i] = &result[i]
|
||||
}
|
||||
return playlists, nil
|
||||
}
|
||||
|
||||
// CreatePlaylist creates a new playlist
|
||||
func (c *Client) CreatePlaylist(ctx context.Context, title string, visibility string) (*models.Playlist, error) {
|
||||
params := map[string]string{
|
||||
"title": title,
|
||||
"visibility": visibility, // "public" or "private"
|
||||
}
|
||||
|
||||
var result models.Playlist
|
||||
if err := c.request.Post(ctx, "/users/me/playlists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// EditPlaylist edits an existing playlist
|
||||
func (c *Client) EditPlaylist(ctx context.Context, kind int, title, description, visibility string) (*models.Playlist, error) {
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
}
|
||||
if title != "" {
|
||||
params["title"] = title
|
||||
}
|
||||
if description != "" {
|
||||
params["description"] = description
|
||||
}
|
||||
if visibility != "" {
|
||||
params["visibility"] = visibility
|
||||
}
|
||||
|
||||
var result models.Playlist
|
||||
if err := c.request.Put(ctx, "/users/me/playlists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// DeletePlaylist deletes a playlist
|
||||
func (c *Client) DeletePlaylist(ctx context.Context, kind int) error {
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Delete(ctx, "/users/me/playlists", params, &result)
|
||||
}
|
||||
|
||||
// AddTracksToPlaylist adds tracks to a playlist
|
||||
func (c *Client) AddTracksToPlaylist(ctx context.Context, kind int, trackIDs []string, revision int) (*models.Playlist, error) {
|
||||
if len(trackIDs) == 0 {
|
||||
return nil, utils.NewYandexMusicError("no tracks provided", utils.ErrBadRequest)
|
||||
}
|
||||
|
||||
tracksStr := ""
|
||||
for i, id := range trackIDs {
|
||||
if i > 0 {
|
||||
tracksStr += ","
|
||||
}
|
||||
tracksStr += id
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
"track_ids": tracksStr,
|
||||
"revision": fmt.Sprintf("%d", revision),
|
||||
}
|
||||
|
||||
var result models.Playlist
|
||||
if err := c.request.Post(ctx, "/users/me/playlists/tracks", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// RemoveTracksFromPlaylist removes tracks from a playlist
|
||||
func (c *Client) RemoveTracksFromPlaylist(ctx context.Context, kind int, trackIDs []string, revision int) (*models.Playlist, error) {
|
||||
if len(trackIDs) == 0 {
|
||||
return nil, utils.NewYandexMusicError("no tracks provided", utils.ErrBadRequest)
|
||||
}
|
||||
|
||||
tracksStr := ""
|
||||
for i, id := range trackIDs {
|
||||
if i > 0 {
|
||||
tracksStr += ","
|
||||
}
|
||||
tracksStr += id
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
"track_ids": tracksStr,
|
||||
"revision": fmt.Sprintf("%d", revision),
|
||||
}
|
||||
|
||||
var result models.Playlist
|
||||
if err := c.request.Delete(ctx, "/users/me/playlists/tracks", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ReorderPlaylistTracks reorders tracks in a playlist
|
||||
func (c *Client) ReorderPlaylistTracks(ctx context.Context, kind int, from, to, revision int) (*models.Playlist, error) {
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
"from": fmt.Sprintf("%d", from),
|
||||
"to": fmt.Sprintf("%d", to),
|
||||
"revision": fmt.Sprintf("%d", revision),
|
||||
}
|
||||
|
||||
var result models.Playlist
|
||||
if err := c.request.Put(ctx, "/users/me/playlists/tracks/reorder", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetPlaylistRecommendations gets playlist recommendations
|
||||
func (c *Client) GetPlaylistRecommendations(ctx context.Context, kind int) ([]*models.Playlist, error) {
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
}
|
||||
|
||||
var result []models.Playlist
|
||||
if err := c.request.Get(ctx, "/playlists/recommendations", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playlists := make([]*models.Playlist, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
playlists[i] = &result[i]
|
||||
}
|
||||
return playlists, nil
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetQueue gets the current queue
|
||||
func (c *Client) GetQueue(ctx context.Context) (*models.Queue, error) {
|
||||
var result models.Queue
|
||||
if err := c.request.Get(ctx, "/queue", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// CreateQueue creates a new queue
|
||||
func (c *Client) CreateQueue(ctx context.Context, trackID string, queueType string) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"track_id": trackID,
|
||||
"queue_type": queueType, // "radio", "album", "playlist", etc.
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetQueueItem gets a queue item
|
||||
func (c *Client) GetQueueItem(ctx context.Context, queueID string) (*models.QueueItem, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
}
|
||||
|
||||
var result models.QueueItem
|
||||
if err := c.request.Get(ctx, "/queue/item", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// AddToQueue adds a track to the queue
|
||||
func (c *Client) AddToQueue(ctx context.Context, queueID, trackID string, position int) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"track_id": trackID,
|
||||
"position": fmt.Sprintf("%d", position),
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue/add", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// RemoveFromQueue removes a track from the queue
|
||||
func (c *Client) RemoveFromQueue(ctx context.Context, queueID, trackID string) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"track_id": trackID,
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Delete(ctx, "/queue/remove", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ReorderQueue reorders the queue
|
||||
func (c *Client) ReorderQueue(ctx context.Context, queueID string, from, to int) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"from": fmt.Sprintf("%d", from),
|
||||
"to": fmt.Sprintf("%d", to),
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Put(ctx, "/queue/reorder", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ClearQueue clears the queue
|
||||
func (c *Client) ClearQueue(ctx context.Context, queueID string) error {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Delete(ctx, "/queue/clear", params, &result)
|
||||
}
|
||||
|
||||
// NextTrack moves to the next track in the queue
|
||||
func (c *Client) NextTrack(ctx context.Context, queueID string) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue/next", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// PreviousTrack moves to the previous track in the queue
|
||||
func (c *Client) PreviousTrack(ctx context.Context, queueID string) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue/prev", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// Seek seeks to a specific position in the current track (in milliseconds)
|
||||
func (c *Client) Seek(ctx context.Context, queueID string, positionMs int) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"position": fmt.Sprintf("%d", positionMs),
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue/seek", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// SetRepeatMode sets the repeat mode for the queue
|
||||
// mode can be "off", "one", or "all"
|
||||
func (c *Client) SetRepeatMode(ctx context.Context, queueID, mode string) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"mode": mode,
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue/repeat", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// SetShuffle enables or disables shuffle for the queue
|
||||
func (c *Client) SetShuffle(ctx context.Context, queueID string, enabled bool) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"enabled": fmt.Sprintf("%t", enabled),
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue/shuffle", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// SetVolume sets the volume for the queue (0-100)
|
||||
func (c *Client) SetVolume(ctx context.Context, queueID string, volume int) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"volume": fmt.Sprintf("%d", volume),
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue/volume", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetCurrentTrack gets the currently playing track from the queue
|
||||
func (c *Client) GetCurrentTrack(ctx context.Context, queueID string) (*models.QueueItem, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
}
|
||||
|
||||
var result models.QueueItem
|
||||
if err := c.request.Get(ctx, "/queue/current", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetQueueHistory gets the queue history (recently played tracks)
|
||||
func (c *Client) GetQueueHistory(ctx context.Context, queueID string, limit int) ([]*models.QueueItem, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"limit": fmt.Sprintf("%d", limit),
|
||||
}
|
||||
|
||||
var result []models.QueueItem
|
||||
if err := c.request.Get(ctx, "/queue/history", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]*models.QueueItem, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
items[i] = &result[i]
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetPlayContexts gets play contexts
|
||||
func (c *Client) GetPlayContexts(ctx context.Context) ([]*models.PlayContext, error) {
|
||||
var result []models.PlayContext
|
||||
if err := c.request.Get(ctx, "/play-contexts", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contexts := make([]*models.PlayContext, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
contexts[i] = &result[i]
|
||||
}
|
||||
return contexts, nil
|
||||
}
|
||||
|
||||
// GetPlayContextsData gets play contexts data
|
||||
func (c *Client) GetPlayContextsData(ctx context.Context) (*models.PlayContextsData, error) {
|
||||
var result models.PlayContextsData
|
||||
if err := c.request.Get(ctx, "/play-contexts/data", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetRadioStations gets radio stations
|
||||
func (c *Client) GetRadioStations(ctx context.Context) ([]*models.RadioStation, error) {
|
||||
var result []models.RadioStation
|
||||
if err := c.request.Get(ctx, "/radio/stations", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stations := make([]*models.RadioStation, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
stations[i] = &result[i]
|
||||
}
|
||||
return stations, nil
|
||||
}
|
||||
|
||||
// GetRadioStation gets a radio station by ID
|
||||
func (c *Client) GetRadioStation(ctx context.Context, stationID string) (*models.RadioStation, error) {
|
||||
params := map[string]string{
|
||||
"station_id": stationID,
|
||||
}
|
||||
|
||||
var result models.RadioStation
|
||||
if err := c.request.Get(ctx, "/radio/station", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetStationTracks gets tracks for a radio station
|
||||
func (c *Client) GetStationTracks(ctx context.Context, stationID string, limit int) (*models.StationTracksResult, error) {
|
||||
params := map[string]string{
|
||||
"station_id": stationID,
|
||||
}
|
||||
if limit > 0 {
|
||||
params["limit"] = fmt.Sprintf("%d", limit)
|
||||
}
|
||||
|
||||
var result models.StationTracksResult
|
||||
if err := c.request.Get(ctx, "/radio/station/tracks", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetRotorStations gets rotor stations (generative)
|
||||
func (c *Client) GetRotorStations(ctx context.Context) ([]*models.RotorStation, error) {
|
||||
var result []models.RotorStation
|
||||
if err := c.request.Get(ctx, "/rotor/stations", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stations := make([]*models.RotorStation, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
stations[i] = &result[i]
|
||||
}
|
||||
return stations, nil
|
||||
}
|
||||
|
||||
// GetRotorStation gets a rotor station by ID
|
||||
func (c *Client) GetRotorStation(ctx context.Context, stationID string) (*models.RotorStation, error) {
|
||||
params := map[string]string{
|
||||
"station_id": stationID,
|
||||
}
|
||||
|
||||
var result models.RotorStation
|
||||
if err := c.request.Get(ctx, "/rotor/station", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetRotorStationTracks gets tracks for a rotor station
|
||||
func (c *Client) GetRotorStationTracks(ctx context.Context, stationID string, limit int) (*models.RotorStationTracksResult, error) {
|
||||
params := map[string]string{
|
||||
"station_id": stationID,
|
||||
}
|
||||
if limit > 0 {
|
||||
params["limit"] = fmt.Sprintf("%d", limit)
|
||||
}
|
||||
|
||||
var result models.RotorStationTracksResult
|
||||
if err := c.request.Get(ctx, "/rotor/station/tracks", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// StartRotorSession starts a rotor session
|
||||
func (c *Client) StartRotorSession(ctx context.Context, stationID string) (*models.RotorSession, error) {
|
||||
params := map[string]string{
|
||||
"station_id": stationID,
|
||||
}
|
||||
|
||||
var result models.RotorSession
|
||||
if err := c.request.Post(ctx, "/rotor/session/start", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetRotorSession gets rotor session info
|
||||
func (c *Client) GetRotorSession(ctx context.Context, sessionID string) (*models.RotorSession, error) {
|
||||
params := map[string]string{
|
||||
"session_id": sessionID,
|
||||
}
|
||||
|
||||
var result models.RotorSession
|
||||
if err := c.request.Get(ctx, "/rotor/session", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// RotorSessionFeedback sends feedback for a rotor session track
|
||||
func (c *Client) RotorSessionFeedback(ctx context.Context, sessionID, trackID, feedbackType string) error {
|
||||
params := map[string]string{
|
||||
"session_id": sessionID,
|
||||
"track_id": trackID,
|
||||
"feedback": feedbackType, // "like", "dislike", "skip"
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/rotor/session/feedback", params, &result)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// Search performs a multi-type search
|
||||
func (c *Client) Search(ctx context.Context, query string, searchType string, page, pageSize int) (*models.SearchResult, error) {
|
||||
params := map[string]string{
|
||||
"text": query,
|
||||
"type": searchType,
|
||||
"page": fmt.Sprintf("%d", page),
|
||||
"page_size": fmt.Sprintf("%d", pageSize),
|
||||
}
|
||||
|
||||
var result models.SearchResult
|
||||
if err := c.request.Get(ctx, "/search", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set client on all results
|
||||
if result.Tracks != nil {
|
||||
for i := range result.Tracks.Results {
|
||||
result.Tracks.Results[i].SetClient(c)
|
||||
}
|
||||
}
|
||||
if result.Albums != nil {
|
||||
for i := range result.Albums.Results {
|
||||
result.Albums.Results[i].SetClient(c)
|
||||
}
|
||||
}
|
||||
if result.Artists != nil {
|
||||
for i := range result.Artists.Results {
|
||||
result.Artists.Results[i].SetClient(c)
|
||||
}
|
||||
}
|
||||
if result.Playlists != nil {
|
||||
for i := range result.Playlists.Results {
|
||||
result.Playlists.Results[i].SetClient(c)
|
||||
}
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// SearchTracks searches for tracks
|
||||
func (c *Client) SearchTracks(ctx context.Context, query string, page, pageSize int) (*models.SearchTracksResult, error) {
|
||||
result, err := c.Search(ctx, query, "track", page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Tracks, nil
|
||||
}
|
||||
|
||||
// SearchAlbums searches for albums
|
||||
func (c *Client) SearchAlbums(ctx context.Context, query string, page, pageSize int) (*models.SearchAlbumsResult, error) {
|
||||
result, err := c.Search(ctx, query, "album", page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Albums, nil
|
||||
}
|
||||
|
||||
// SearchArtists searches for artists
|
||||
func (c *Client) SearchArtists(ctx context.Context, query string, page, pageSize int) (*models.SearchArtistsResult, error) {
|
||||
result, err := c.Search(ctx, query, "artist", page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Artists, nil
|
||||
}
|
||||
|
||||
// SearchPlaylists searches for playlists
|
||||
func (c *Client) SearchPlaylists(ctx context.Context, query string, page, pageSize int) (*models.SearchPlaylistsResult, error) {
|
||||
result, err := c.Search(ctx, query, "playlist", page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Playlists, nil
|
||||
}
|
||||
|
||||
// SearchVideos searches for videos
|
||||
func (c *Client) SearchVideos(ctx context.Context, query string, page, pageSize int) (*models.SearchVideosResult, error) {
|
||||
result, err := c.Search(ctx, query, "video", page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Videos, nil
|
||||
}
|
||||
|
||||
// SearchUsers searches for users
|
||||
func (c *Client) SearchUsers(ctx context.Context, query string, page, pageSize int) (*models.SearchUsersResult, error) {
|
||||
result, err := c.Search(ctx, query, "user", page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Users, nil
|
||||
}
|
||||
|
||||
// SearchSuggestions gets search suggestions
|
||||
func (c *Client) SearchSuggestions(ctx context.Context, query string) ([]string, error) {
|
||||
params := map[string]string{
|
||||
"text": query,
|
||||
"type": "suggest",
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Suggestions []string `json:"suggestions"`
|
||||
}
|
||||
if err := c.request.Get(ctx, "/search/suggest", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Suggestions, nil
|
||||
}
|
||||
|
||||
// SearchBanners gets search banners
|
||||
func (c *Client) SearchBanners(ctx context.Context, query string) ([]models.SearchBanner, error) {
|
||||
params := map[string]string{
|
||||
"text": query,
|
||||
"type": "banner",
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Banners []models.SearchBanner `json:"banners"`
|
||||
}
|
||||
if err := c.request.Get(ctx, "/search/banner", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Banners, nil
|
||||
}
|
||||
|
||||
// SearchTags gets search tags
|
||||
func (c *Client) SearchTags(ctx context.Context, query string) ([]models.Tag, error) {
|
||||
params := map[string]string{
|
||||
"text": query,
|
||||
"type": "tag",
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Tags []models.Tag `json:"tags"`
|
||||
}
|
||||
if err := c.request.Get(ctx, "/search/tag", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Tags, nil
|
||||
}
|
||||
|
||||
// SearchResult represents search results
|
||||
// This is defined in models/search.go
|
||||
@@ -0,0 +1,73 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetSettings gets user settings
|
||||
func (c *Client) GetSettings(ctx context.Context) (*models.AccountSettings, error) {
|
||||
var result models.AccountSettings
|
||||
if err := c.request.Get(ctx, "/account/settings", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// UpdateSettings updates user settings
|
||||
func (c *Client) UpdateSettings(ctx context.Context, settings *models.AccountSettings) (*models.AccountSettings, error) {
|
||||
var result models.AccountSettings
|
||||
if err := c.request.Put(ctx, "/account/settings", settings, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetExperiments gets experiments
|
||||
func (c *Client) GetExperiments(ctx context.Context) (*models.Experiments, error) {
|
||||
var result models.Experiments
|
||||
if err := c.request.Get(ctx, "/experiments", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetExperimentDetails gets experiment details
|
||||
func (c *Client) GetExperimentDetails(ctx context.Context, experimentID string) (*models.ExperimentDetail, error) {
|
||||
params := map[string]string{
|
||||
"experiment_id": experimentID,
|
||||
}
|
||||
var result models.ExperimentDetail
|
||||
if err := c.request.Get(ctx, "/experiments/details", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetPermissions gets user permissions
|
||||
func (c *Client) GetPermissions(ctx context.Context) (*models.Permissions, error) {
|
||||
var result models.Permissions
|
||||
if err := c.request.Get(ctx, "/permissions", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetPermissionAlerts gets permission alerts
|
||||
func (c *Client) GetPermissionAlerts(ctx context.Context) (*models.PermissionAlerts, error) {
|
||||
var result models.PermissionAlerts
|
||||
if err := c.request.Get(ctx, "/permission-alerts", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetContentRestrictions gets content restrictions
|
||||
func (c *Client) GetContentRestrictions(ctx context.Context) (*models.ContentRestrictions, error) {
|
||||
var result models.ContentRestrictions
|
||||
if err := c.request.Get(ctx, "/content-restrictions", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// GetTrackSupplement gets supplement for a track
|
||||
// GET /tracks/{track_id}/supplement
|
||||
func (c *Client) GetTrackSupplement(ctx context.Context, trackID string) (*models.Supplement, error) {
|
||||
path := fmt.Sprintf("/tracks/%s/supplement", trackID)
|
||||
|
||||
var result models.Supplement
|
||||
if err := c.request.Get(ctx, path, nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetTrackLyrics gets lyrics for a track (new endpoint with signing)
|
||||
// GET /tracks/{track_id}/lyrics
|
||||
func (c *Client) GetTrackLyrics(ctx context.Context, trackID string, format string) (*models.TrackLyrics, error) {
|
||||
path := fmt.Sprintf("/tracks/%s/lyrics", trackID)
|
||||
|
||||
// Generate signature for the request
|
||||
sign := utils.GetSignRequest(trackID, utils.DefaultSignKey)
|
||||
|
||||
params := map[string]string{
|
||||
"format": format,
|
||||
"timeStamp": strconv.FormatInt(sign.Timestamp, 10),
|
||||
"sign": sign.Value,
|
||||
}
|
||||
|
||||
var result models.TrackLyrics
|
||||
if err := c.request.Get(ctx, path, params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetTrackLyricsText fetches the full lyrics text from the download URL
|
||||
func (c *Client) GetTrackLyricsText(ctx context.Context, trackLyrics *models.TrackLyrics) (string, error) {
|
||||
data, err := c.request.Download(ctx, trackLyrics.DownloadURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// GetLyrics gets lyrics info for a track (legacy endpoint)
|
||||
// GET /tracks/lyrics?track_id={track_id}
|
||||
func (c *Client) GetLyrics(ctx context.Context, trackID string) (*models.LyricsInfo, error) {
|
||||
params := map[string]string{
|
||||
"track_id": trackID,
|
||||
}
|
||||
|
||||
var result models.LyricsInfo
|
||||
if err := c.request.Get(ctx, "/tracks/lyrics", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetVideoSupplement gets video supplement for a track (legacy endpoint)
|
||||
// GET /tracks/video-supplement?track_id={track_id}
|
||||
func (c *Client) GetVideoSupplement(ctx context.Context, trackID string) (*models.VideoSupplement, error) {
|
||||
params := map[string]string{
|
||||
"track_id": trackID,
|
||||
}
|
||||
|
||||
var result models.VideoSupplement
|
||||
if err := c.request.Get(ctx, "/tracks/video-supplement", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetLyricsByTrackID is an alias for GetTrackLyrics for backward compatibility
|
||||
// GET /tracks/{track_id}/lyrics
|
||||
func (c *Client) GetLyricsByTrackID(ctx context.Context, trackID string, format string) (*models.TrackLyrics, error) {
|
||||
return c.GetTrackLyrics(ctx, trackID, format)
|
||||
}
|
||||
|
||||
// LyricsFormat represents the lyrics format
|
||||
type LyricsFormat string
|
||||
|
||||
const (
|
||||
// LyricsFormatText is plain text format
|
||||
LyricsFormatText LyricsFormat = "TEXT"
|
||||
// LyricsFormatLRC is LRC format with timestamps
|
||||
LyricsFormatLRC LyricsFormat = "LRC"
|
||||
)
|
||||
|
||||
// GetTrackLyricsWithOptions gets lyrics with options
|
||||
func (c *Client) GetTrackLyricsWithOptions(ctx context.Context, trackID string, format LyricsFormat) (*models.TrackLyrics, error) {
|
||||
return c.GetTrackLyrics(ctx, trackID, string(format))
|
||||
}
|
||||
|
||||
// SupplementOptions holds options for supplement requests
|
||||
type SupplementOptions struct {
|
||||
TrackID string
|
||||
}
|
||||
|
||||
// GetSupplement gets track supplement with options
|
||||
func (c *Client) GetSupplement(ctx context.Context, opts SupplementOptions) (*models.Supplement, error) {
|
||||
return c.GetTrackSupplement(ctx, opts.TrackID)
|
||||
}
|
||||
|
||||
// LyricsOptions holds options for lyrics requests
|
||||
type LyricsOptions struct {
|
||||
TrackID string
|
||||
Format LyricsFormat
|
||||
}
|
||||
|
||||
// GetLyricsWithOptions gets track lyrics with options
|
||||
func (c *Client) GetLyricsWithOptions(ctx context.Context, opts LyricsOptions) (*models.TrackLyrics, error) {
|
||||
format := string(opts.Format)
|
||||
if format == "" {
|
||||
format = string(LyricsFormatText)
|
||||
}
|
||||
return c.GetTrackLyrics(ctx, opts.TrackID, format)
|
||||
}
|
||||
|
||||
// Sign represents a request signature
|
||||
type Sign struct {
|
||||
Timestamp int64
|
||||
Value string
|
||||
}
|
||||
|
||||
// GetSignRequest creates a signature for a track request (exported for external use)
|
||||
func GetSignRequest(trackID string) Sign {
|
||||
h := utils.GetSignRequest(trackID, utils.DefaultSignKey)
|
||||
|
||||
return Sign{
|
||||
Timestamp: h.Timestamp,
|
||||
Value: h.Value,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
)
|
||||
|
||||
// testServer creates a test HTTP server with the given handler
|
||||
func testServer(handler http.HandlerFunc) *httptest.Server {
|
||||
return httptest.NewServer(handler)
|
||||
}
|
||||
|
||||
// newTestClient creates a client pointing to the test server
|
||||
func newTestClient(serverURL string) *Client {
|
||||
return NewClient(ClientConfig{
|
||||
BaseURL: serverURL,
|
||||
Token: "test-token",
|
||||
Language: "en",
|
||||
Timeout: 5,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// GetTracks gets tracks by IDs
|
||||
func (c *Client) GetTracks(ctx context.Context, trackIDs []string) ([]*models.Track, error) {
|
||||
if len(trackIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"track-ids": joinIDs(trackIDs),
|
||||
}
|
||||
|
||||
var result []models.Track
|
||||
if err := c.request.Get(ctx, "/tracks", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tracks := make([]*models.Track, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
tracks[i] = &result[i]
|
||||
}
|
||||
return tracks, nil
|
||||
}
|
||||
|
||||
// GetTrack gets a single track by ID
|
||||
func (c *Client) GetTrack(ctx context.Context, trackID string) (*models.Track, error) {
|
||||
tracks, err := c.GetTracks(ctx, []string{trackID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tracks) == 0 {
|
||||
return nil, utils.NewYandexMusicError("track not found", utils.ErrNotFound)
|
||||
}
|
||||
return tracks[0], nil
|
||||
}
|
||||
|
||||
// GetTrackDownloadInfo gets download info for a track
|
||||
func (c *Client) GetTrackDownloadInfo(ctx context.Context, trackID, quality string) ([]models.DownloadInfo, error) {
|
||||
// This requires signing - use the sign utility
|
||||
// For now, return basic implementation
|
||||
params := map[string]string{
|
||||
"track_id": trackID,
|
||||
}
|
||||
|
||||
var result []models.DownloadInfo
|
||||
if err := c.request.Get(ctx, fmt.Sprintf("/tracks/%s/download-info", trackID), params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DownloadTrack downloads a track
|
||||
func (c *Client) DownloadTrack(ctx context.Context, trackID, format string, bitrate int) ([]byte, error) {
|
||||
info, err := c.GetTrackDownloadInfo(ctx, trackID, format)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find matching download info
|
||||
var url string
|
||||
for _, di := range info {
|
||||
if di.Codec == format && (bitrate == 0 || di.Bitrate == bitrate) {
|
||||
url = di.URL
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if url == "" && len(info) > 0 {
|
||||
url = info[0].URL
|
||||
}
|
||||
|
||||
if url == "" {
|
||||
return nil, utils.NewYandexMusicError("no download URL available", utils.ErrNotFound)
|
||||
}
|
||||
|
||||
return c.request.Download(ctx, url)
|
||||
}
|
||||
|
||||
// SearchResult represents search results
|
||||
type SearchResult struct {
|
||||
Tracks *SearchTracksResult `json:"tracks"`
|
||||
}
|
||||
|
||||
// SearchTracksResult represents track search results
|
||||
type SearchTracksResult struct {
|
||||
Total int `json:"total"`
|
||||
Results []models.Track `json:"results"`
|
||||
}
|
||||
|
||||
func joinIDs(ids []string) string {
|
||||
result := ""
|
||||
for i, id := range ids {
|
||||
if i > 0 {
|
||||
result += ","
|
||||
}
|
||||
result += id
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
func TestClient_GetTracks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
trackIDs []string
|
||||
mockResponse []byte
|
||||
mockError error
|
||||
expectError bool
|
||||
expectedTracks int
|
||||
checkTrack func(*testing.T, *models.Track)
|
||||
}{
|
||||
{
|
||||
name: "single track",
|
||||
trackIDs: []string{"12345"},
|
||||
mockResponse: []byte(`{
|
||||
"result": [
|
||||
{
|
||||
"id": "12345",
|
||||
"title": "Test Track",
|
||||
"duration_ms": 180000,
|
||||
"storage_id": "67890",
|
||||
"cover_uri": "https://example.com/cover.jpg",
|
||||
"genre": "rock",
|
||||
"version": "album",
|
||||
"explicit": false,
|
||||
"available": true,
|
||||
"available_for_premium_users": true,
|
||||
"r128": -12.5,
|
||||
"track_sharing_flag": "allowed",
|
||||
"artists": [{"id": "1", "name": "Artist 1", "cover_uri": "https://example.com/artist.jpg"}],
|
||||
"albums": [{"id": "789", "title": "Test Album", "cover_uri": "https://example.com/album.jpg", "year": 2023}],
|
||||
"major": {"id": "major1", "name": "Major Label", "description": "Big label"},
|
||||
"lyrics": {"id": "lyr1", "text": "Test lyrics", "available": true, "has_text": true}
|
||||
}
|
||||
]
|
||||
}`),
|
||||
expectedTracks: 1,
|
||||
checkTrack: func(t *testing.T, tr *models.Track) {
|
||||
if tr.ID != "12345" {
|
||||
t.Errorf("Expected ID '12345', got '%s'", tr.ID)
|
||||
}
|
||||
if tr.Title != "Test Track" {
|
||||
t.Errorf("Expected Title 'Test Track', got '%s'", tr.Title)
|
||||
}
|
||||
if tr.DurationMs != 180000 {
|
||||
t.Errorf("Expected DurationMs 180000, got %d", tr.DurationMs)
|
||||
}
|
||||
if len(tr.Artists) != 1 {
|
||||
t.Errorf("Expected 1 artist, got %d", len(tr.Artists))
|
||||
}
|
||||
if tr.Major == nil || tr.Major.Name != "Major Label" {
|
||||
t.Errorf("Expected Major 'Major Label'")
|
||||
}
|
||||
if tr.Lyrics == nil || tr.Lyrics.Text != "Test lyrics" {
|
||||
t.Errorf("Expected Lyrics 'Test lyrics'")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple tracks",
|
||||
trackIDs: []string{"12345", "67890"},
|
||||
mockResponse: []byte(`{
|
||||
"result": [
|
||||
{"id": "12345", "title": "Track 1", "duration_ms": 100000, "storage_id": "1", "cover_uri": "", "genre": "", "version": "", "explicit": false, "available": true, "available_for_premium_users": true, "r128": 0, "track_sharing_flag": "", "artists": [], "albums": []},
|
||||
{"id": "67890", "title": "Track 2", "duration_ms": 200000, "storage_id": "2", "cover_uri": "", "genre": "", "version": "", "explicit": false, "available": true, "available_for_premium_users": true, "r128": 0, "track_sharing_flag": "", "artists": [], "albums": []}
|
||||
]
|
||||
}`),
|
||||
expectedTracks: 2,
|
||||
checkTrack: func(t *testing.T, tr *models.Track) {
|
||||
if tr.ID != "12345" && tr.ID != "67890" {
|
||||
t.Errorf("Unexpected track ID: %s", tr.ID)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty track IDs",
|
||||
trackIDs: []string{},
|
||||
mockResponse: nil,
|
||||
expectedTracks: 0,
|
||||
},
|
||||
{
|
||||
name: "nil track IDs",
|
||||
trackIDs: nil,
|
||||
mockResponse: nil,
|
||||
expectedTracks: 0,
|
||||
},
|
||||
{
|
||||
name: "API error",
|
||||
trackIDs: []string{"12345"},
|
||||
mockError: utils.NewYandexMusicError("not found", utils.ErrNotFound),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "empty result",
|
||||
trackIDs: []string{"nonexistent"},
|
||||
mockResponse: []byte(`{"result": []}`),
|
||||
expectedTracks: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var server *httptest.Server
|
||||
if tt.mockError != nil || tt.mockResponse != nil {
|
||||
server = testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/tracks" {
|
||||
t.Errorf("Expected path '/tracks', got '%s'", r.URL.Path)
|
||||
}
|
||||
if tt.mockError != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"error": "not found"}`))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(tt.mockResponse)
|
||||
})
|
||||
defer server.Close()
|
||||
}
|
||||
|
||||
var client *Client
|
||||
if server != nil {
|
||||
client = newTestClient(server.URL)
|
||||
} else {
|
||||
client = newTestClient("http://localhost:9999")
|
||||
}
|
||||
|
||||
tracks, err := client.GetTracks(context.Background(), tt.trackIDs)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(tracks) != tt.expectedTracks {
|
||||
t.Errorf("Expected %d tracks, got %d", tt.expectedTracks, len(tracks))
|
||||
return
|
||||
}
|
||||
|
||||
if tt.checkTrack != nil {
|
||||
for _, track := range tracks {
|
||||
tt.checkTrack(t, track)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify client is set on tracks
|
||||
for _, track := range tracks {
|
||||
if track.GetClient() == nil {
|
||||
t.Error("Client not set on track")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetTrack(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
trackID string
|
||||
mockResponse []byte
|
||||
mockError error
|
||||
expectError bool
|
||||
checkTrack func(*testing.T, *models.Track)
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
trackID: "12345",
|
||||
mockResponse: []byte(`{
|
||||
"result": [
|
||||
{
|
||||
"id": "12345",
|
||||
"title": "Test Track",
|
||||
"duration_ms": 180000,
|
||||
"storage_id": "67890",
|
||||
"cover_uri": "https://example.com/cover.jpg",
|
||||
"genre": "rock",
|
||||
"version": "album",
|
||||
"explicit": false,
|
||||
"available": true,
|
||||
"available_for_premium_users": true,
|
||||
"r128": -12.5,
|
||||
"track_sharing_flag": "allowed",
|
||||
"artists": [{"id": "1", "name": "Artist 1", "cover_uri": "https://example.com/artist.jpg"}],
|
||||
"albums": [{"id": "789", "title": "Test Album", "cover_uri": "https://example.com/album.jpg", "year": 2023}],
|
||||
"major": {"id": "major1", "name": "Major Label", "description": "Big label"},
|
||||
"lyrics": {"id": "lyr1", "text": "Test lyrics", "available": true, "has_text": true}
|
||||
}
|
||||
]
|
||||
}`),
|
||||
checkTrack: func(t *testing.T, tr *models.Track) {
|
||||
if tr.ID != "12345" {
|
||||
t.Errorf("Expected ID '12345', got '%s'", tr.ID)
|
||||
}
|
||||
if tr.Title != "Test Track" {
|
||||
t.Errorf("Expected Title 'Test Track', got '%s'", tr.Title)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
trackID: "nonexistent",
|
||||
mockResponse: []byte(`{"result": []}`),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "API error",
|
||||
trackID: "12345",
|
||||
mockError: utils.NewYandexMusicError("unauthorized", utils.ErrUnauthorized),
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var server *httptest.Server
|
||||
if tt.mockError != nil || tt.mockResponse != nil {
|
||||
server = testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/tracks" {
|
||||
t.Errorf("Expected path '/tracks', got '%s'", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("track-ids") != tt.trackID {
|
||||
t.Errorf("Expected track-ids '%s', got '%s'", tt.trackID, r.URL.Query().Get("track-ids"))
|
||||
}
|
||||
if tt.mockError != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"error": "unauthorized"}`))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(tt.mockResponse)
|
||||
})
|
||||
defer server.Close()
|
||||
}
|
||||
|
||||
var client *Client
|
||||
if server != nil {
|
||||
client = newTestClient(server.URL)
|
||||
} else {
|
||||
client = newTestClient("http://localhost:9999")
|
||||
}
|
||||
|
||||
track, err := client.GetTrack(context.Background(), tt.trackID)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if track == nil {
|
||||
t.Fatal("Expected track, got nil")
|
||||
}
|
||||
|
||||
if tt.checkTrack != nil {
|
||||
tt.checkTrack(t, track)
|
||||
}
|
||||
|
||||
if track.GetClient() == nil {
|
||||
t.Error("Client not set on track")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SearchTracks(t *testing.T) {
|
||||
// Create test server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/search" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
if query.Get("type") != "track" {
|
||||
t.Errorf("Expected type=track, got %s", query.Get("type"))
|
||||
}
|
||||
|
||||
page := query.Get("page")
|
||||
pageSize := query.Get("page_size")
|
||||
|
||||
response := map[string]interface{}{
|
||||
"result": map[string]interface{}{
|
||||
"tracks": map[string]interface{}{
|
||||
"total": 2,
|
||||
"results": []map[string]interface{}{
|
||||
{
|
||||
"id": "1",
|
||||
"title": "Track 1",
|
||||
"duration_ms": 100000,
|
||||
"storage_id": "1",
|
||||
"cover_uri": "",
|
||||
"genre": "",
|
||||
"version": "",
|
||||
"explicit": false,
|
||||
"available": true,
|
||||
"available_for_premium_users": true,
|
||||
"r128": 0,
|
||||
"track_sharing_flag": "",
|
||||
"artists": []interface{}{},
|
||||
"albums": []interface{}{},
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"title": "Track 2",
|
||||
"duration_ms": 200000,
|
||||
"storage_id": "2",
|
||||
"cover_uri": "",
|
||||
"genre": "",
|
||||
"version": "",
|
||||
"explicit": false,
|
||||
"available": true,
|
||||
"available_for_premium_users": true,
|
||||
"r128": 0,
|
||||
"track_sharing_flag": "",
|
||||
"artists": []interface{}{},
|
||||
"albums": []interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Verify pagination
|
||||
if page != "1" || pageSize != "20" {
|
||||
t.Errorf("Unexpected pagination: page=%s, page_size=%s", page, pageSize)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Create client with test server URL
|
||||
config := ClientConfig{
|
||||
BaseURL: server.URL,
|
||||
Token: "test-token",
|
||||
}
|
||||
client := NewClient(config)
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := client.SearchTracks(ctx, "test query", 1, 20)
|
||||
if err != nil {
|
||||
t.Fatalf("SearchTracks failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected search result, got nil")
|
||||
}
|
||||
|
||||
if result.Total != 2 {
|
||||
t.Errorf("Expected total 2, got %d", result.Total)
|
||||
}
|
||||
|
||||
if len(result.Results) != 2 {
|
||||
t.Errorf("Expected 2 results, got %d", len(result.Results))
|
||||
}
|
||||
|
||||
for i, track := range result.Results {
|
||||
if track.GetClient() == nil {
|
||||
t.Errorf("Track %d: Client not set", i)
|
||||
}
|
||||
if track.ID != "1" && track.ID != "2" {
|
||||
t.Errorf("Track %d: Unexpected ID %s", i, track.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SearchTracks_Mocked(t *testing.T) {
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || r.URL.Path != "/search" {
|
||||
t.Errorf("Unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
return
|
||||
}
|
||||
if r.URL.Query().Get("type") != "track" {
|
||||
t.Errorf("Expected type=track, got %s", r.URL.Query().Get("type"))
|
||||
}
|
||||
|
||||
mockSearchResult := map[string]interface{}{
|
||||
"result": map[string]interface{}{
|
||||
"tracks": map[string]interface{}{
|
||||
"total": 2,
|
||||
"results": []map[string]interface{}{
|
||||
{
|
||||
"id": "1",
|
||||
"title": "Track 1",
|
||||
"duration_ms": 100000,
|
||||
"storage_id": "1",
|
||||
"cover_uri": "",
|
||||
"genre": "",
|
||||
"version": "",
|
||||
"explicit": false,
|
||||
"available": true,
|
||||
"available_for_premium_users": true,
|
||||
"r128": 0,
|
||||
"track_sharing_flag": "",
|
||||
"artists": []interface{}{},
|
||||
"albums": []interface{}{},
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"title": "Track 2",
|
||||
"duration_ms": 200000,
|
||||
"storage_id": "2",
|
||||
"cover_uri": "",
|
||||
"genre": "",
|
||||
"version": "",
|
||||
"explicit": false,
|
||||
"available": true,
|
||||
"available_for_premium_users": true,
|
||||
"r128": 0,
|
||||
"track_sharing_flag": "",
|
||||
"artists": []interface{}{},
|
||||
"albums": []interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(mockSearchResult)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := client.SearchTracks(ctx, "test query", 1, 20)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("SearchTracks failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected search result, got nil")
|
||||
}
|
||||
|
||||
if result.Total != 2 {
|
||||
t.Errorf("Expected total 2, got %d", result.Total)
|
||||
}
|
||||
|
||||
if len(result.Results) != 2 {
|
||||
t.Errorf("Expected 2 results, got %d", len(result.Results))
|
||||
}
|
||||
|
||||
for i, track := range result.Results {
|
||||
if track.GetClient() == nil {
|
||||
t.Errorf("Track %d: Client not set", i)
|
||||
}
|
||||
}
|
||||
|
||||
// Test error case
|
||||
errorServer := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"error": "network error"}`))
|
||||
})
|
||||
defer errorServer.Close()
|
||||
|
||||
errorClient := newTestClient(errorServer.URL)
|
||||
_, err = errorClient.SearchTracks(ctx, "test", 1, 20)
|
||||
if err == nil {
|
||||
t.Error("Expected error for network failure")
|
||||
}
|
||||
if !utils.IsNetworkError(err) {
|
||||
t.Errorf("Expected network error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinIDs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
expected string
|
||||
}{
|
||||
{"empty", []string{}, ""},
|
||||
{"single", []string{"123"}, "123"},
|
||||
{"multiple", []string{"123", "456", "789"}, "123,456,789"},
|
||||
{"with empty", []string{"123", "", "456"}, "123,,456"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := joinIDs(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("joinIDs(%v) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user