Initial commit

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

No files matched your search

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